是一个"可选的"管道操作员惯用语F#

D.F*_*F.F 6 f# pipeline idiomatic

我喜欢使用管道运算符'|>'.但是,当混合使用返回'Option-Typed-values'的函数返回'simple'值的函数时,事情会变得有点混乱,例如:

// foo: int -> int*int
// bar: int*int -> bool
let f (x: string) = x |> int |> foo |> bar
Run Code Online (Sandbox Code Playgroud)

工作,但它可能会抛出'System.FormatException:...'

现在假设我想通过使函数'int'给出一个可选的结果来解决这个问题:

let intOption x = 
    match System.Int32.TryParse x with
    | (true, x) -> Some x
    | (false,_) -> None
Run Code Online (Sandbox Code Playgroud)

现在唯一的问题当然是功能

let g x = x |> intOption |> foo |> bar
Run Code Online (Sandbox Code Playgroud)

由于输入错误,将无法编译.好的,只需定义一个'可选的'管道:

let ( |= ) x f = 
   match x with
   | Some y -> Some (f y)
   | None -> None
Run Code Online (Sandbox Code Playgroud)

现在我可以简单地定义:

let f x = x |> intOption |= foo |= bar
Run Code Online (Sandbox Code Playgroud)

一切都像魅力一样.

好的,问题:这是惯用的F#吗?是否可以接受?风格不好?

备注:当然,给定正确的类型,"| ="运算符允许随意拆分和合并"管道",只关注它们重要的选项:

x |> ...|> divisionOption |= (fun y -> y*y) |=...|>...
Run Code Online (Sandbox Code Playgroud)

小智 10

我认为使用Option.map会更加惯用:

let gx = x |> intOption |> Option.map foo |> Option.map bar


kae*_*fer 5

其他答案尚未涵盖两个方面。

  • F#Option类型的单子运算
  • 明智地使用自定义运算符,而不是通过管线传递到标准函数可以提高可读性

代替像这样的成熟计算表达式MaybeBuilder(),我们可以定义让约束函数为该Option类型提供单子运算。让我们代表操作员的绑定操作>>=

let (>>=) ma f = Option.bind f ma
// val ( >>= ) : ma:'a option -> f:('a -> 'b option) -> 'b option
let ``return`` = Some
// val return : arg0:'a -> 'a option
Run Code Online (Sandbox Code Playgroud)

从这跟随

let (>=>) f g a = f a >>= g
// val ( >=> ) : f:('a -> 'b option) -> g:('b -> 'c option) -> a:'a -> 'c option
let fmap f ma = ma >>= (``return`` << f)
// val fmap : f:('a -> 'b) -> ma:'a option -> 'b option
let join mma = mma >>= id
// val join : mma:'a option option -> 'a option
Run Code Online (Sandbox Code Playgroud)

fmap基本上是Opion.map; join将嵌套实例取消嵌套一级,由Kleisli运算符进行组合>=>是流水线的替代方法。

在轻量级语法中,运算符免于使用嵌套范围增加缩进。当将lambda函数串在一起时,这可能很有用,它允许嵌套同时最多缩进一层。

a_option
|> Option.bind (fun a ->
    f a
    |> Option.bind (fun b ->
        g b 
        |> Option.bind ... ) )
Run Code Online (Sandbox Code Playgroud)

a_option
>>= fun a ->
    f a
>>= fun b ->
    g b
>>= ...
Run Code Online (Sandbox Code Playgroud)


byt*_*ter 5

使用(|>)似乎是一个非常突出的概念的实现,即通过计算链对值进行线程化。然而,由于 F# 运算符的语法限制(优先级和左/右关联性),在实际项目中使用此概念可能有些困难。即:

\n
    \n
  • 每当您使用Option.mapor时Option.bind,就很难使用代码块。该代码只有在以下intOption |> Option.map foo |> Option.map bar情况下才能很好地工作:foobar是命名函数
  • \n
  • 很难让 lambda 保持小且独立;
  • \n
  • 无论如何,代码中都会充满括号(自从 Lisp 时代以来我就不喜欢这样了:)
  • \n
\n

使用几个小函数,“链接”方法可以编写更简洁的代码。
\n注意:对于现实生活中的项目,我强烈建议咨询您的团队,因为新的运算符或扩展方法对于您团队的其他成员来说可能看起来违反直觉。

\n
\n

几乎是现实生活中的应用程序代码。假设您的应用程序使用命令行解析器来转换此命令行:

\n
MyApp.exe -source foo -destination bar -loglevel debug\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 到Map<string, string>包含键/值对的 a 中。

\n

现在,让我们loglevel只关注处理参数,看看代码是如何处理它的:

\n
    \n
  1. 过滤;MapKey="loglevel"注意,可能有零个元素;
  2. \n
  3. 但也可能有多个元素,所以我们需要获取第一个;
  4. \n
  5. 然后我们将一个字符串值解析为您的应用程序特定的enum类型LogLevel。注意,解析可能会失败;
  6. \n
  7. 然后,例如,如果附加了调试器,我们可以任意覆盖该值;
  8. \n
  9. 但同样,在这一点上仍然可能None有价值。让我们设置一些默认值;
  10. \n
  11. 现在我们确定该值是Some,所以只需调用Option.get
  12. \n
\n

这是代码。注释指出了上面列表中的步骤:

\n\n
MyApp.exe -source foo -destination bar -loglevel debug\n
Run Code Online (Sandbox Code Playgroud)\n

"loglevel"在这里,我们看到如何通过“可选”计算链顺序转换键 ( )。每个 lambda 都会为要转换的值引入自己的别名(例如strLogLevel)。

\n
\n

这是要使用的库:

\n
let logLevel =\n    "loglevel"\n    |> args.TryFind                            // (1)\n    |> Option.bind      ^<| Seq.tryPick Some   // (2)\n    |> Option.bind      ^<| fun strLogLevel -> // (3)\n        match System.Enum.TryParse(strLogLevel, true) with\n        | true, v -> Some v\n        | _ -> None\n    |> Option.Or        ^<| fun _ ->           // (4)\n        if System.Diagnostics.Debugger.IsAttached\n            then Some LogLevel.Debug\n            else None\n    |> Option.OrDefault ^<| fun _ ->           // (5)\n        LogLevel.Verbose\n    |> Option.get                              // (6)\n
Run Code Online (Sandbox Code Playgroud)\n