2015-11-19 32 views
4

我喜欢使用管道运算符'|>'很多。然而,混合与函数返回“简单”值的函数返回“选项类型的值”,事情就变得有点凌乱,例如当:是一个“可选”管道操作员惯用的F#

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

的作品,但它可能会抛出一个“System.FormatException:。 ..”

现在假设我想解决的是通过使功能‘诠释’给一个可选的结果:

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

只有现在的问题是当然的功能

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

由于输入错误而无法编译。好吧,简单地定义一个 'optionalized' 管:

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

我没有看到这方面的需要操作员可以使用'|> Option.map F' - 事实上,您可以定义操作就像那样;) - 并且最好的方法是使用'|> Option.bind f''得到单调情况;) – Carsten

+1

使用管道运算符没有什么特别的“习惯用法”,除了它有时有助于类型推断。滥用它(以及任何其他自定义操作符)可能会大大降低代码的可读性。 – Petr

+0

噢,没想到Option.map。所以我想这回答所有问题;有一个核心库函数,其中我的操作符或多或少是一个特例,因此使用inbuild函数肯定会更好...... thx –

回答

8

我想使用Option.map会更惯用:

设GX = X |> intOption |> Option.map FOO |> Option.map酒吧

+0

是的,正如我对Carsten评论中所提到的,我没有想到'Option.map',它当然可以解决问题... thx! –

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

还有其他答案尚未涉及两个方面。

对F#的 Option
  • 明智地使用运营商定制,而不是流水线标准功能的
    • 单子操作可以提高可读性

    而是像MaybeBuilder()一个完全成熟的计算表达式,我们可以定义让对象函数为Option类型提供了monadic操作。让我们通过运营商>>=代表绑定操作:

    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 
    >>= ... 
    
  • 1

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

    • 无论何时您使用Option.mapOption.bind,都很难使用代码块。代码intOption |> Option.map foo |> Option.map bar只有在foobar被命名的函数时才会很好用;
    • 很难保持lambda小而分开;
    • 在任何情况下,代码将出现括号(我不喜欢,因为我的Lisp次:)

    使用的几个小功能,在“链接”的方式让写一个更简洁的代码。
    备注:对于现实生活中的项目,我强烈建议与您的团队协商,因为新的操作员或扩展方法可能对您团队的其他成员不利。


    几乎是一个真实的应用程序代码。说,你的应用程序使用一个命令行解析器,其将这个命令行:

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

    ...到包含键/值对一个Map<string, string>

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

    1. 滤波器MapKey="loglevel";请注意,可能有零个元素;
    2. 但也可能有几个元素,所以我们需要得到第一个;
    3. 然后我们解析出您的应用程序特定enumLogLevel类型的刺值。请注意,解析可能会失败;例如,如果附加了调试器,我们可以任意覆盖该值;
    4. 但是,此时仍有可能存在None值。我们放置一些默认值;
    5. 现在我们确信价值是Some,所以请致电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) 
    

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


    而这里的图书馆使用:

    // A high precedence, right associative backward pipe, more info here: 
    // http://tryfs.net/snippets/snippet-4o 
    let inline (^<|) f a = f a 
    
    /// <summary>Advanced operations on options.</summary> 
    type Microsoft.FSharp.Core.Option<'T> with 
        // Attempts to return Some either from the original value or by calling lambda. 
        // Lambda is required to return a monadic value (Option<'T>) 
        static member Or f (x:Option<'T>) = 
         match x with 
         | None  -> f() 
         | x   -> x 
    
        // Same as above, but for lambdas returning plain types (e.g., `T) 
        static member OrDefault f (x:Option<'T>) = 
         match x with 
         | None  -> f() |> Some 
         | x   -> x