我喜欢使用管道操作员' |>'很多。但是,当混合函数返回简单'具有返回' 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) |=...|>...
答案 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)
其他答案尚未涵盖两个方面。
Option
类型我们可以定义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.map
或Option.bind
时,都很难使用代码块。仅当intOption |> Option.map foo |> Option.map bar
和foo
被命名为函数时,代码bar
才会有效; 使用几个小功能,&#34;链接&#34;方法让我们写一个更简洁的代码 注意:对于现实生活中的项目,我强烈建议您咨询您的团队,因为新的操作员或扩展方法可能会让您的团队其他成员反直觉。
几乎是真实的应用代码。比如说,您的应用程序使用命令行解析器来转换此命令行:
MyApp.exe -source foo -destination bar -loglevel debug
...转换为包含键/值对的Map<string, string>
。
现在,让我们只关注处理loglevel
参数,看看代码是如何处理的:
Map
的{{1}};请注意,可能没有元素; Key="loglevel"
enum
类型。注意,解析可能会失败; LogLevel
价值。让我们放一些默认值; None
,因此请致电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)
)是如何通过一个&#34;可选的&#34;链顺序转换的。计算。每个lambda为要转换的值引入自己的别名(例如,"loglevel"
)。
这是要使用的库:
strLogLevel