我们将继续介绍有关F#中的函数式编程的系列文章。 今天,我们讨论功能的关联性和组成,以及比较组成和管道。 看猫下!

关联性和功能组成
功能关联
假设有一行函数编写在一起。 它们将以什么顺序组合?
例如,此功能是什么意思?
let F xyz = xyz
这是否意味着将函数y
应用于参数z
,然后将结果传递给x
? 即::
let F xyz = x (yz)
还是将函数x
应用于自变量y
,之后将使用自变量z
评估作为结果获得的函数? 即::
let F xyz = (xy) z
- 第二个选项是正确的。
- 函数的使用具有关联性 。
xyz
含义与(xy) z
相同。wxyz
等于((wx) y) z
。- 这看起来不妙。
- 我们已经看到了部分应用程序是如何工作的。
- 如果我们将
x
作为具有两个参数的函数进行讨论,则(xy) z
是部分应用第一个参数的结果,然后将参数z
传递给中间函数。
如果需要正确的关联性,则可以使用括号或竖线。 以下三个条目是等效的:
let F xyz = x (yz) let F xyz = yz |> x // let F xyz = x <| yz //
作为练习,尝试在不进行实际计算的情况下显示这些功能的签名。
功能组成
我们多次提到函数的组成,但是这个术语的真正含义是什么? 乍一看似乎令人恐惧,但实际上,一切都很简单。
假设我们有一个函数“ f”,它将类型“ T1”映射到类型“ T2”。 我们还有一个函数“ g”将类型“ T2”转换为类型“ T3”。 然后,我们可以连接“ f”的输出和“ g”的输入,创建一个新函数,将类型“ T1”转换为类型“ T3”。

例如:
let f (x:int) = float x * 3.0 // f - int->float let g (x:float) = x > 4.0 // g - float->bool
我们可以创建一个新函数“ h”,该函数将“ f”的输出用作“ g”的输入。
let h (x:int) = let y = f(x) g(y) // g
更加紧凑:
let h (x:int) = g ( f(x) ) // h int->bool // h 1 h 2
到目前为止,如此简单。 这很有趣,我们可以定义一个新的函数“ compose”,该函数将函数“ f”和“ g”合并在一起,甚至不知道它们的签名。
let compose fgx = g ( f(x) )
执行之后,您可以看到编译器正确地确定“ f
”是泛型类型'a
到泛型类型'b
的函数,并且' g
'限于类型'b
输入:
val compose : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c
(请注意,仅由于每个函数只有一个输入参数和一个输出,才有可能对操作进行广义组合。在非函数语言中,这种方法是不可能的。)
如我们所见,该定义用于“ >>
”运算符。
let (>>) fgx = g ( f(x) )
由于有了这个定义,可以使用合成在现有功能的基础上构建新功能。
let add1 x = x + 1 let times2 x = x * 2 let add1Times2 x = (>>) add1 times2 x // add1Times2 3
显式记录非常麻烦。 但是您可以使它的使用更容易理解。
首先,您可以删除参数x
,该组合将返回部分应用程序。
let add1Times2 = (>>) add1 times2
其次,因为 >>
是二进制运算符,您可以将其放在中间。
let add1Times2 = add1 >> times2
使用组合可使代码更清晰。
let add1 x = x + 1 let times2 x = x * 2 // let add1Times2 x = times2(add1 x) // let add1Times2 = add1 >> times2
在实践中使用合成运算符
合成运算符(像所有infix运算符一样)比常规函数具有较低的优先级。 这意味着在合成中使用的函数可以不带括号而带有参数。
例如,如果“ add”和“ times”函数具有参数,则可以在编写过程中传递它们。
let add nx = x + n let times nx = x * n let add1Times2 = add 1 >> times 2 let add5Times3 = add 5 >> times 3 // add5Times3 1
只要功能的相应输入和输出匹配,功能就可以使用任何值。 例如,考虑以下代码两次执行一个函数:
let twice f = f >> f // ('a -> 'a) -> ('a -> 'a)
请注意,编译器已推断“ f
”接受并返回相同类型的值。
现在考虑“ +
”功能。 如前所述,输入是int
,但输出实际上是(int->int)
。 因此,“ +
”可用于“ twice
”。 因此,您可以编写:
let add1 = (+) 1 // (int -> int) let add1Twice = twice add1 // (int -> int) // add1Twice 9
另一方面,您不能写:
let addThenMultiply = (+) >> (*)
因为输入“ *”必须是int
,而不是int->int
函数(这是加法的输出)。
但是,如果您更正第一个函数,使其仅返回int
,那么一切都会起作用:
let add1ThenMultiply = (+) 1 >> (*) // (+) 1 (int -> int) 'int' // add1ThenMultiply 2 7
如有必要,还可以通过“ <<
”以相反的顺序执行合成:
let times2Add1 = add 1 << times 2 times2Add1 3
反向组合主要用于使代码更像英语(“类似于英语”)。 例如:
let myList = [] myList |> List.isEmpty |> not // myList |> (not << List.isEmpty) //
组成与 传送带
成分和传送带之间的细微差别可能会让您感到困惑,因为 它们看起来很相似。
首先,查看管道的定义:
let (|>) xf = fx
所有这些使您可以将函数的参数放在其前面,而不是之后。 仅此而已。 如果函数具有多个参数,则输入应为最后一个参数(在当前参数集中,而不是根本不输入)。 前面看到的一个示例:
let doSomething xyz = x+y+z doSomething 1 2 3 // 3 |> doSomething 1 2 //
组成不同,不能替代管道。 在下面的示例中,即使数字3也不是一个函数,所以“输出”不能传递给doSomething
:
3 >> doSomething 1 2 // // f >> g g(f(x)) : doSomething 1 2 ( 3(x) ) // 3 ! // error FS0001: This expression was expected to have type 'a->'b // but here has type int
编译器抱怨值“ 3”必须是一种函数'a->'b
。
将此与组合的定义进行比较,该定义包含3个参数,其中前两个应为函数。
let (>>) fgx = g ( f(x) ) let add nx = x + n let times nx = x * n let add1Times2 = add 1 >> times 2
尝试使用管道而不是合成将导致编译错误。 在下面的示例中,“ add 1
”是(部分)函数int->int
,它不能用作“ times 2
”的第二个参数。
let add1Times2 = add 1 |> times 2 // // x |> f f(x) : let add1Times2 = times 2 (add 1) // add1 'int' // error FS0001: Type mismatch. 'int -> int' does not match 'int'
编译器抱怨“ times 2
”必须接受参数int->int
,即 是一个函数(int->int)->'a
。
其他资源
F#的教程很多,包括那些具有C#或Java经验的人的材料。 当您深入了解F#时,以下链接可能会很有用:
还介绍了其他几种开始学习F#的方法 。
最后,F#社区非常适合初学者。 在Slack上,由F#Software Foundation支持的聊天非常活跃,您可以自由加入初学者室。 我们强烈建议您这样做!
不要忘记访问俄语社区F#的网站 ! 如果您对学习语言有任何疑问,我们将很乐意在聊天室中讨论这些问题:
关于翻译作者
由@kleidemos翻译
在F#开发人员的俄语社区的努力下进行了翻译和编辑更改。 我们也感谢@schvepsss和@shwars为本文准备发表。