是一个 "optionalized" 管道运算符惯用的 F#
is an "optionalized" pipe operator idiomatic F#
我非常喜欢使用管道运算符“|>”。但是,当将 return 'simple' 值的函数与 return '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
由于输入错误无法编译。好吧,简单定义一个'optionalized'管道:
let ( |= ) x f =
match x with
| Some y -> Some (f y)
| None -> None
现在我可以简单地定义:
let f x = x |> intOption |= foo |= bar
一切都很顺利。
好的,问题:这是地道的 F# 吗?可以接受吗?画风不好?
备注:当然,给定正确的类型,'|=' 运算符允许随意拆分和合并 'pipelines' 选项,同时只关心重要的选项:
x |> ...|> divisionOption |= (fun y -> y*y) |=...|>...
我认为使用 Option.map 会更地道:
let g x = x |> intOption |> Option.map foo |> Option.map bar
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
}
其他答案还没有涵盖两个方面。
- 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.map
; join
将嵌套实例取消嵌套一层,Kleisli 运算符 >=>
的组合是流水线的替代方法。
在轻量级语法中,运算符免于增加嵌套范围的缩进。这在将 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
>>= ...
使用 (|>)
似乎是 通过一系列计算 线程化值这一非常突出的概念的实现。但是,由于 F# 运算符的句法限制(优先级和 left/right 关联性),在实际项目中使用此概念可能有些困难。即:
- 无论何时使用
Option.map
或Option.bind
,都很难使用代码块。仅当 foo
和 bar
是命名函数时,代码 intOption |> Option.map foo |> Option.map bar
才能正常工作;
- 很难让 lambda 保持小而独立;
- 在任何情况下,代码都会充满括号(我从 Lisp 时代开始就不喜欢它:)
使用几个小函数,“链接”方法可以编写更简洁的代码。
注意:对于现实生活中的项目,我强烈建议您咨询您的团队,因为新的运算符或扩展方法可能对您团队的其他成员来说有悖常理。
几乎是真实的应用程序代码。比如说,您的应用程序使用命令行解析器来转换此命令行:
MyApp.exe -source foo -destination bar -loglevel debug
…变成包含 key/value 对的 Map<string, string>
。
现在,让我们只关注处理 loglevel
参数,看看代码是如何处理它的:
- 过滤
Map
为 Key="loglevel"
;注意,可能有零个元素;
- 但是可能还有几个个元素,所以我们需要获取第一个;
- 然后我们将一个 sting 值解析为
LogLevel
类型的应用程序特定 enum
。注意,解析可能会失败;
- 然后,例如,如果附加了调试器,我们可以任意覆盖该值;
- 但是,此时仍然可能有
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)
这里我们看到一个键 ("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
我非常喜欢使用管道运算符“|>”。但是,当将 return 'simple' 值的函数与 return '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
由于输入错误无法编译。好吧,简单定义一个'optionalized'管道:
let ( |= ) x f =
match x with
| Some y -> Some (f y)
| None -> None
现在我可以简单地定义:
let f x = x |> intOption |= foo |= bar
一切都很顺利。
好的,问题:这是地道的 F# 吗?可以接受吗?画风不好?
备注:当然,给定正确的类型,'|=' 运算符允许随意拆分和合并 'pipelines' 选项,同时只关心重要的选项:
x |> ...|> divisionOption |= (fun y -> y*y) |=...|>...
我认为使用 Option.map 会更地道:
let g x = x |> intOption |> Option.map foo |> Option.map bar
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
}
其他答案还没有涵盖两个方面。
- F#
Option
类型的单子操作 - 明智地使用自定义运算符而不是流水线化到标准函数可以提高可读性
我们可以定义为 Option
类型提供一元操作的 let-bound 函数,而不是像 MaybeBuilder()
这样的成熟的计算表达式。让我们用运算符 >>=
:
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 ... ) )
对
a_option
>>= fun a ->
f a
>>= fun b ->
g b
>>= ...
使用 (|>)
似乎是 通过一系列计算 线程化值这一非常突出的概念的实现。但是,由于 F# 运算符的句法限制(优先级和 left/right 关联性),在实际项目中使用此概念可能有些困难。即:
- 无论何时使用
Option.map
或Option.bind
,都很难使用代码块。仅当foo
和bar
是命名函数时,代码intOption |> Option.map foo |> Option.map bar
才能正常工作; - 很难让 lambda 保持小而独立;
- 在任何情况下,代码都会充满括号(我从 Lisp 时代开始就不喜欢它:)
使用几个小函数,“链接”方法可以编写更简洁的代码。
注意:对于现实生活中的项目,我强烈建议您咨询您的团队,因为新的运算符或扩展方法可能对您团队的其他成员来说有悖常理。
几乎是真实的应用程序代码。比如说,您的应用程序使用命令行解析器来转换此命令行:
MyApp.exe -source foo -destination bar -loglevel debug
…变成包含 key/value 对的 Map<string, string>
。
现在,让我们只关注处理 loglevel
参数,看看代码是如何处理它的:
- 过滤
Map
为Key="loglevel"
;注意,可能有零个元素; - 但是可能还有几个个元素,所以我们需要获取第一个;
- 然后我们将一个 sting 值解析为
LogLevel
类型的应用程序特定enum
。注意,解析可能会失败; - 然后,例如,如果附加了调试器,我们可以任意覆盖该值;
- 但是,此时仍然可能有
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)
这里我们看到一个键 ("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