F#:Piping vs. Composing vs. ... Composing?

Pri*_*e M 12 f#

我是新手--F#,一般编程和这个社区.我是一名数学家,在我的本科生中接触过计算机科学.我正在尝试在F#中完成一些任务,而"F#Cheat Sheet"展示了在不解释重复的情况下构成函数的三种不同方式.以下是链接中的相关信息,以了解我的意思.

let 关键字还定义了一个名为功能.

let negate x = x * -1 
let square x = x * x 
let print x = printfn "The number is: %d" x

let squareNegateThenPrint x = 
print (negate (square x)) 
Run Code Online (Sandbox Code Playgroud)

管道运算符 |> 用于将函数和参数链接在一起.双重反引号标识符便于提高可读性,尤其是在单元测试中:

let ``square, negate, then print`` x = 
    x |> square |> negate |> print
Run Code Online (Sandbox Code Playgroud)

组合运算符 >> 用于组合函数:

let squareNegateThenPrint' = 
    square >> negate >> print
Run Code Online (Sandbox Code Playgroud)

通过检查和在VS F#中进行交互的功能:

  1. squareNegateThenPrint x
  2. ``square,negate,then print''x
  3. squareNegateThenPrint"

看来这是完成同样事情的3种方法的清单,这里有什么细微差别吗?我相信,给定相同的int它们都会返回相同的int,但是超出它的范围呢?我没看到什么?这三种方法各有哪些优缺点?

2和3都使用"运算符",1似乎是组成函数的通常的"数学"方式,以便从旧函数中创建新函数.我怀疑选项3真正等效于1(在某种意义上,>>运算符被定义square >> negate >> print为实际计算为,print (negate (square x))但代码有利于可读性,因为您按照它们发生的顺序看到函数名称而不是通常的数学顺序符号,并且定义这种方式可以节省一两次键击,因为你不必x在函数名的末尾包含,因为>>运算符可能使左函数自动继承对函数变量的依赖,而不是显式引用变量.

但那么管道方法如何发挥作用呢?管道操作员是一个更通用的操作员,恰好适用于功能组合吗?

此外,我做了相当多的谷歌尝试在发布之前阅读文档,但我没有得到任何地方.我确信如果我继续学习语言,在明年的某个时候我会理解这些差异.但我也相信,在这里有人可以加快这个过程并解释或提供一些很好的例子.最后,我并不精通C#,或者其他任何语言(数学除外),所以对一个总菜鸟的解释,而不仅仅是f#noob赞赏.谢谢!

Fyo*_*kin 13

首先 - 是的,所有这些方法在"逻辑上"和编译到硬件时都是等效的.这是因为|>>>运算符定义为inline.定义看起来大致如下:

let inline (|>) x f = f x
let inline (>>) f g = fun x -> g (f x)
Run Code Online (Sandbox Code Playgroud)

inline关键字的含义是编译器将使用函数体替换对函数的调用,然后编译结果.因此,以下两者:

x |> f |> g
(f >> g) x
Run Code Online (Sandbox Code Playgroud)

将以与以下内容完全相同的方式编译:

g (f x)
Run Code Online (Sandbox Code Playgroud)

然而,在实践中,有一些问题.


一个问题是类型推断及其与类/接口的相互作用.考虑以下:

let b = "abcd" |> (fun x -> x.Length)
let a = (fun x -> x.Length) "abcd"
Run Code Online (Sandbox Code Playgroud)

即使这些定义在逻辑上和编译形式上都是等价的,但第一个定义将编译,而第二个定义则不会.发生这种情况是因为F#中的类型推断从左到右进行而没有双向,因此,在第一个定义中,到编译器到达时x.Length,它已经知道它x是a string,因此它可以正确地解析成员查找.在第二个例子中,编译器不知道是什么x,因为它还没有遇到参数"abcd".


另一个问题与可怕的价值限制有关.简单来说,它表示在语法上(非逻辑上!)一个(与函数相对)的定义不能是通用的.这有一些与可变性有关的模糊原因 - 请参阅链接文章以获得解释.

将此应用于函数组合,考虑下面的代码(注意,这两个fg是通用的功能):

let f x = [x]
let g y = [y]

let h1 = f >> g
let h2 x = x |> f |> g
Run Code Online (Sandbox Code Playgroud)

在这里,h2将编译好,但h1不会,抱怨价值限制.


在实践中,这三种方式之间的选择通常归结为可读性和便利性.这些都不比其他人好.当我编写代码时,我通常会根据自己的喜好选择.

  • 是的,确切地说.另外,如果你是新手,我推荐https://fsharpforfunandprofit.com/ - 为初学者提供大量信息. (2认同)

Aar*_*ach 6

这些都是在不同情况下使用的基本等效概念。没有普遍适用的正确方式或错误方式,但有时您可以利用流水线和组合运算符,这些运算符通过实践和更多地接触 F# 编程模式变得显而易见。

举几个例子,在处理序列时经常使用流水线,因为它允许很长的操作链以一种可读的方式组合在一起,看起来像一种流畅的查询语法。

[0..100]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x / 2)
|> List.sum
Run Code Online (Sandbox Code Playgroud)

对于我们许多一直使用 F# 的人来说,这比List.sum (List.map (fun x -> x / 2) (List.filter (fun x -> x % 2 = 0) [0..100])).

组合通常用于处理高阶函数,例如bind. 例如:

[0..5] 
|> List.tryFind (fun x -> x = 3) 
|> Option.bind ((*) 3 >> Some)
Run Code Online (Sandbox Code Playgroud)

在这里,我们使用tryFind管道在列表上执行 a 并管道Option它返回的类型Option.bind 这需要一个带有签名的函数int -> Option 'b,但是如果我们已经有了一个函数int -> int(例如乘法),我们可以使用它>>来组合该函数Some并传递组合函数到bind. ((*) 3只是部分应用于3乘法函数的 F# 语法,返回一个将任何整数乘以 3 的函数)。

我们可以通过编写 来实现相同的目的Option.bind (fun x -> Some (x * 3)),但是>>操作符和部分应用程序语法为我们节省了一些击键次数。