is an “optionalized” pipe operator idiomatic F#

不打扰是莪最后的温柔 提交于 2019-12-08 21:08:36

问题


I like to use the pipe operator '|>' a lot. However, when mixing functions that return 'simple' values with functions that return 'Option-Typed-values', things become a bit messy e.g.:

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

works, but it might throw a 'System.FormatException:...'

Now assume I want to fix that by making the function 'int' give an optional result:

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

Only problem now is that of course the function

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

won't compile due to typing errors. Ok, simply define an 'optionalized' pipe:

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

now I can simply define:

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

and everything works like a charme.

OK, question: Is that idiomatic F#? Acceptable? Bad style?

Remark: Of course, given the right types the '|=' operator allows to split and merge 'pipelines' with options at will while only care about options where they matter:

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

回答1:


I think using Option.map would be more idiomatic:

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




回答2:


There are two aspects not yet covered by other answers.

  • Monadic operations for F#'s Option type
  • Judicious use of custom operators instead of pipelining to standard functions can improve readability

Instead of a full-blown computation expression like MaybeBuilder(), we can define let-bound functions providing monadic operations for the Option type. Let's represent the bind operation by the operator >>=:

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

From this follows

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 is basically Opion.map; join un-nests a nested instance by one level, and composition by the Kleisli operator >=> is an alternative to pipelining.

In lightweight syntax, operators are exempted from increasing indentation with nested scope. This could be useful when stringing together lambda functions, allowing nesting while still indenting by at most one level.

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:


Using (|>) seems to be an implementation of a very prominent concept of threading a value through a chain of computations. However, due to syntactic limitations of F# operators (precedence and left/right associativity) it may be somewhat difficult to use this concept in real-life projects. Namely:

  • Whenever you use Option.map or Option.bind, it is hard to use code blocks. The code intOption |> Option.map foo |> Option.map bar would work great only if foo and bar are named functions;
  • It is hard to keep lambdas small and separate;
  • In any case, the code will be full of parentheses (which I dislike since my Lisp times :)

Using several small functions, the "chaining" approach lets writing a more succinct code.
Note: For real-life projects, I strongly recommend consulting with your team, because new operators or extension methods may appear counter-intuitive for the rest of your team.


Almost a real-life app code. Say, your app uses a command line parser which transforms this command line:

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

…into a Map<string, string> that contains key/value pairs.

Now, let's focus on processing loglevel parameter only and see how it is being processed by the code:

  1. Filter the Map for Key="loglevel"; note, there may be zero elements;
  2. But there may be also several elements, so we need to get the first one;
  3. Then we parse out a sting value into your app-specific enum of LogLevel type. Note, parsing may fail;
  4. Then we may, for example, arbitrarily override the value if debugger is attached;
  5. But again, at this point there still may be None value. Let's place some default value;
  6. Now we are sure that the value is Some, so just call Option.get.

Here's the code. Comments indicate steps from the list above:

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)

Here we see how a key ("loglevel") is being sequentially transformed through a chain of "optionalized" computations. Each lambda introduces its own alias for the value being transformed (for example, strLogLevel).


And here's the library to use:

// 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



回答4:


Option.map / Option.bind is a really nice simple solution and I think if you have one or two chained functions, it's the preferable way of handling things.

I think it's worth adding that occasionally you could end up with fairly complicated nested Option behaviour, at which point, I think it's worth defining a MaybeBuilder. A really simple example would be:

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()

You can then use this in the syntax:

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


来源:https://stackoverflow.com/questions/33806543/is-an-optionalized-pipe-operator-idiomatic-f

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!