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

时间:2015-11-19 14:23:30

标签: f# pipeline idiomatic

我喜欢使用管道操作员' |>'很多。但是,当混合函数返回简单'具有返回' Option-Typed-values'的函数的值,事情变得有点混乱,例如:

// foo: int -> int*int
// bar: int*int -> bool
let f (x: string) = x |> int |> foo |> bar

有效,但它可能会抛出一个System.FormatException:...'

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

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

现在唯一的问题是功能

let g x = x |> intOption |> foo |> bar
由于输入错误,

无法编译。好的,只需定义一个'可选的'管:

let ( |= ) x f = 
   match x with
   | Some y -> Some (f y)
   | None -> None

现在我可以简单地定义:

let f x = x |> intOption |= foo |= bar

并且一切都像魅力一样。

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

备注:当然,如果选择了正确的类型,' | ='运营商允许拆分和合并管道'随意选择,只关心重要的选项:

x |> ...|> divisionOption |= (fun y -> y*y) |=...|>...

4 个答案:

答案 0 :(得分:8)

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

  

设g x = x |> intOption |> Option.map foo |> Option.map栏

答案 1 :(得分:2)

Option.map / Option.bind是一个非常好的简单解决方案,我认为如果你有一个或两个链式函数,它是处理事物的首选方式。

我认为值得补充的是,偶尔你可能会遇到相当复杂的嵌套选项行为,此时,我认为值得定义一个MaybeBuilder。一个非常简单的例子是:

type MaybeBuilder() =
    member this.Bind(m, f) = 
        Option.bind f m
    member this.Return(x) = 
        Some x
    member this.ReturnFrom(x) = 
        x

let maybe = MaybeBuilder()

然后您可以在语法中使用它:

maybe {
   let! a = intOption x
   let! b = foo a
   let! c = bar b
   return c
}

答案 2 :(得分:2)

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

  • F#' Option类型
  • 的Monadic操作
  • 明智地使用自定义运算符而不是管道传输到标准函数可以提高可读性

我们可以定义let-bound函数,为MaybeBuilder()类型提供monadic操作,而不是像Option这样的完整计算表达式。让我们代表运营商>>= bind 操作:

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

从此开始

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

fmap基本上是Opion.map; join将嵌套实例展开一级,Kleisli运算符>=>的合成是流水线操作的替代方法。

在轻量级语法中,运算符可以免于使用嵌套作用域增加缩进。当将lambda函数串联在一起时,这可能很有用,允许嵌套,同时仍然最多只能缩放一个级别。

a_option
|> Option.bind (fun a ->
    f a
    |> Option.bind (fun b ->
        g b 
        |> Option.bind ... ) )

VS

a_option
>>= fun a ->
    f a
>>= fun b ->
    g b
>>= ...

答案 3 :(得分:1)

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

  • 每当您使用Option.mapOption.bind时,都很难使用代码块。仅当intOption |> Option.map foo |> Option.map barfoo被命名为函数时,代码bar才会有效;
  • 很难将lambda保持小而分开;
  • 在任何情况下,代码都会带有括号(我从Lisp时代开始就不喜欢它)。

使用几个小功能,&#34;链接&#34;方法让我们写一个更简洁的代码 注意:对于现实生活中的项目,我强烈建议您咨询您的团队,因为新的操作员或扩展方法可能会让您的团队其他成员反直觉。

几乎是真实的应用代码。比如说,您的应用程序使用命令行解析器来转换此命令行:

MyApp.exe -source foo -destination bar -loglevel debug

...转换为包含键/值对的Map<string, string>

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

  1. 过滤Map的{​​{1}};请注意,可能没有元素;
  2. 但是可能还有几个元素,所以我们需要得到第一个;
  3. 然后,我们会将特定值解析为特定于应用的Key="loglevel" enum类型。注意,解析可能会失败;
  4. 然后我们可以,例如,如果附加了调试器,则任意覆盖该值;
  5. 但同样,此时仍有LogLevel价值。让我们放一些默认值;
  6. 现在我们确定该值为None,因此请致电Some
  7. 这是代码。评论表明上面列表中的步骤:

    Option.get

    在这里,我们看到一个键(let logLevel = "loglevel" |> args.TryFind // (1) |> Option.bind ^<| Seq.tryPick Some // (2) |> Option.bind ^<| fun strLogLevel -> // (3) match System.Enum.TryParse(strLogLevel, true) with | true, v -> Some v | _ -> None |> Option.Or ^<| fun _ -> // (4) if System.Diagnostics.Debugger.IsAttached then Some LogLevel.Debug else None |> Option.OrDefault ^<| fun _ -> // (5) LogLevel.Verbose |> Option.get // (6) )是如何通过一个&#34;可选的&#34;链顺序转换的。计算。每个lambda为要转换的值引入自己的别名(例如,"loglevel")。

    这是要使用的库:

    strLogLevel