【问题标题】:is an "optionalized" pipe operator idiomatic F#是一个“可选的”管道运算符惯用的 F#
【发布时间】:2016-02-21 18:02:57
【问题描述】:

我很喜欢使用管道运算符'|>'。但是,当将返回“简单”值的函数与返回“选项类型值”的函数混合使用时,事情会变得有点混乱,例如:

// 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) |=...|>...

【问题讨论】:

  • 我不认为需要这个操作符,因为你可以使用|> Option.map f - 事实上你可以定义你的操作符;) - 最好是使用|> Option.bind f 你得到单子案例也是如此;)
  • 在使用管道运算符时没有什么特别“惯用的”,除了它有时有助于类型推断。滥用它(以及任何其他自定义运算符)可能会大大降低代码的可读性。
  • 哦,是的,没想到 Option.map。所以我想这可以回答所有问题;有一个核心库函数,我的操作符或多或少是一个特例,因此使用 inbuild 函数当然更好......thx

标签: f# pipeline idioms


【解决方案1】:

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

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

【讨论】:

  • 是的,正如我在对 Carsten 评论的评论中提到的那样,我没有想到“Option.map”,它确实解决了所有提出的问题......谢谢!
【解决方案2】:

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

我认为值得补充的是,有时您可能会遇到相当复杂的嵌套 Option 行为,此时,我认为值得定义 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
}

【讨论】:

    【解决方案3】:

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

    • F# 的 Option 类型的一元操作
    • 明智地使用自定义运算符而不是流水线到标准函数可以提高可读性

    我们可以定义为Option 类型提供单子操作的let-bound 函数,而不是像MaybeBuilder() 这样的成熟计算表达式。让我们用运算符>>=来表示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.mapjoin 将嵌套实例取消嵌套一层,由 Kleisli 运算符 &gt;=&gt; 组合是流水线的替代方法。

    在轻量级语法中,操作符无需在嵌套范围内增加缩进。这在将 lambda 函数串在一起时可能很有用,允许嵌套同时最多缩进一级。

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

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

    【讨论】:

      【解决方案4】:

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

      • 无论何时使用Option.mapOption.bind,都很难使用代码块。代码 intOption |&gt; Option.map foo |&gt; Option.map bar 只有在 foobar 被命名为函数时才能正常工作;
      • 很难保持 lambda 小而独立;
      • 在任何情况下,代码都会充满括号(自从我的 Lisp 时代以来我不喜欢它:)

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


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

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

      ...转换为包含键/值对的Map&lt;string, string&gt;

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

      1. Key="loglevel"过滤Map;注意,可能有零个元素;
      2. 但也可能有几个元素,所以我们需要获取第一个;
      3. 然后,我们将一个字符串值解析为您的应用程序特定的enum LogLevel 类型。注意,解析可能会失败;
      4. 然后我们可以,例如,如果附加了调试器,则可以任意覆盖该值;
      5. 但同样,此时仍可能存在None 值。让我们设置一些默认值;
      6. 现在我们确定该值为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") 如何通过“可选”计算链顺序转换。每个 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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-07-06
        • 1970-01-01
        • 2010-11-30
        • 1970-01-01
        • 1970-01-01
        • 2020-04-18
        • 1970-01-01
        相关资源
        最近更新 更多