问题
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
orOption.bind
, it is hard to use code blocks. The codeintOption |> Option.map foo |> Option.map bar
would work great only iffoo
andbar
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:
- Filter the
Map
forKey="loglevel"
; note, there may be zero elements; - But there may be also several elements, so we need to get the first one;
- Then we parse out a sting value into your app-specific
enum
ofLogLevel
type. Note, parsing may fail; - Then we may, for example, arbitrarily override the value if debugger is attached;
- But again, at this point there still may be
None
value. Let's place some default value; - Now we are sure that the value is
Some
, so just callOption.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