F#奇妙游(31):数据缺失的处理
option
在处理数据时,缺失数据和无效数据是一个常见的问题。在Python中,通常使用None来表示缺失数据。
在F#中,我们可以使用FSharpOption来表示缺失数据。FSharpOption是一个泛型类型,它有两个值:Some和None。Some表示一个值,而None表示一个缺失的值。FSharpOption的定义如下:
type FSharpOption<T> =
| None
| Some of T
从ADT的组合数来分析,这个和类型的组合数是
1
+
C
T
1+C_T
1+CT 。在通常的情况下,我们在F#中都把这个写作option<T>
,或者T option
。而构造一个option<T>
的值,我们可以使用Some
和None
这两个构造器,例如:
let x = Some 1
let y = None
一般的,我们可以使用match
表达式来处理option<T>
的值,例如:
let z =
match x with
| Some x -> x
| None -> 0
如果是定义输入参数是option<T>
类型的函数,还有一个语法糖:
let f (x: int option) =
match x with
| Some x -> x
| None -> 0
可以写成:
let f =
function
| Some x -> x
| None -> 0
后面这个情况非常适用于配合|>
操作符使用,例如:
let x = Some 1
x |> (function Some x -> x | None -> 0)
在对一个T option
的集合类型时,我们就可以很方便的用上面的语法糖来构造一些小的判定函数。
FSharp.Core.Option
而在实际的工作中,上面的这些都是不需要的,因为F#已经提供了FSharp.Core.Option
模块来处理option<T>
类型的值。下表按照ADT分析的结果,列出了FSharp.Core.Option
模块中的函数:
Function or value | ADT | Description |
---|---|---|
isSome option | ('a option -> bool) | 返回 true, 如果对象不是None. |
isNone option | ('a option -> bool) | 返回 true, 如果对象是None. |
count option | ('a option -> int) | count inp 展开成为 match inp with None -> 0 \| Some _ -> 1 . |
get option | ('a option -> 'a) | 得到option所包含的值. |
toObj value | ('a option -> 'a) | 转换成一个对象. |
toNullable option | ('a option -> System.Nullable<'a>) | 转换成Nullable值. |
toArray option | ('a option -> 'a array) | 转换成一个数组,长度是0或者1. |
toList option | ('a option -> 'a list) | 转换成一个列表,长度是0或者1. |
ofObj value | ('a -> 'a option) | 转换成一个option,空值为None. |
ofNullable value | (System.Nullable<'a> -> 'a option) | 转换成一个option,null变为None. |
flatten option | ('a option option -> 'a option) | flatten inp 展开成为 match inp with None -> None\| Some x -> x |
contains value option | ('a -> 'a option -> bool) | 如果option为Some value返回true否则返回false. |
forall predicate option | (('a -> bool) -> 'a option -> bool) | forall p inp 展开成为 match inp with None -> true\| Some x -> p x . |
exists predicate option | (('a -> bool) -> 'a option -> bool) | exists p inp 展开成为 match inp with None -> false\| Some x -> p x . |
defaultValue value option | ('a -> 'a option -> 'a) | 如果对象Some value得到对应value, 否则返回默认值. |
defaultWith defThunk option | ((unit -> 'a) -> 'a option -> 'a) | 如果对象Some value得到对应value, 否则求值defThunk并返回结果. |
iter action option | (('a -> unit) -> 'a option -> unit) | iter f inp 展开成为 match inp with None -> ()\| Some x -> f x . |
orElse ifNone option | ('a option -> 'a option -> 'a option) | 如果option为Some value直接返回, 否则返回ifNone. |
bind binder option | (('a -> 'b option) -> 'a option -> 'b option) | bind f inp 展开成为 match inp with None -> None \| Some x -> f x |
filter predicate option | (('a -> bool) -> 'a option -> 'a option) | filter f inp 展开成为 match inp with None -> None\| Some x -> if f x then Some x else None . |
map mapping option | (('a -> 'b) -> 'a option -> 'b option) | map f inp 展开成为 match inp with None -> None\| Some x -> Some (f x) . |
orElseWith ifNoneThunk option | ((unit -> 'a option) -> 'a option -> 'a option) | 如果option为Some value直接返回, 否则求值ifNoneThunk并返回结果. |
fold folder state option | (('a -> 'b -> 'a) -> 'a -> 'b option -> 'a) | fold f s inp 展开成为 match inp with None -> s\| Some x -> f s x . |
foldBack folder option state | (('a -> 'b -> 'b) -> 'a option -> 'b -> 'b) | fold f inp s 展开成为 match inp with None -> s\| Some x -> f x s . |
map2 mapping option1 option2 | (('a -> 'b -> 'c) -> 'a option -> 'b option -> 'c option) | map f option1 option2 展开成为 match option1, option2 with Some x, Some y -> Some (f x y)\| _ -> None . |
map3 mapping option1 option2 option3 | (('a -> 'b -> 'c -> 'd) -> 'a option -> 'b option -> 'c option -> 'd option) | map f option1 option2 option3 展开成为 match option1, option2, option3 with Some x, Some y, Some z -> Some (f x y z) \| _ -> None . |
其实这许多的功能,只需要略微看看源代码就知道,基本上就是替换为对应match
表达式的代码。例如isSome
的源代码如下:
let inline isSome option =
match option with
| None -> false
| Some _ -> true
这样的好处是,可以对一个option<T>
的集合类型使用map
和filter
等函数,例如:
let x = [Some 1; None; Some 2]
x |> List.filter Option.isSome
便于写出非常简洁的代码。
结论
- 在F#中,使用
option<T>
来表示缺失数据; - 使用
FSharp.Core.Option
模块来处理option<T>
类型的值; - 使用
|>
操作符配合function
语法糖来处理option<T>
类型的值。
Option源代码
namespace Microsoft.FSharp.Core
open Microsoft.FSharp.Core.Operators
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Option =
[<CompiledName("GetValue")>]
let get option =
match option with
| None -> invalidArg "option" (SR.GetString(SR.optionValueWasNone))
| Some x -> x
[<CompiledName("IsSome")>]
let inline isSome option =
match option with
| None -> false
| Some _ -> true
[<CompiledName("IsNone")>]
let inline isNone option =
match option with
| None -> true
| Some _ -> false
[<CompiledName("DefaultValue")>]
let inline defaultValue value option =
match option with
| None -> value
| Some v -> v
[<CompiledName("DefaultWith")>]
let inline defaultWith ([<InlineIfLambda>] defThunk) option =
match option with
| None -> defThunk ()
| Some v -> v
[<CompiledName("OrElse")>]
let inline orElse ifNone option =
match option with
| None -> ifNone
| Some _ -> option
[<CompiledName("OrElseWith")>]
let inline orElseWith ([<InlineIfLambda>] ifNoneThunk) option =
match option with
| None -> ifNoneThunk ()
| Some _ -> option
[<CompiledName("Count")>]
let inline count option =
match option with
| None -> 0
| Some _ -> 1
[<CompiledName("Fold")>]
let inline fold<'T, 'State> ([<InlineIfLambda>] folder) (state: 'State) (option: 'T option) =
match option with
| None -> state
| Some x -> folder state x
[<CompiledName("FoldBack")>]
let inline foldBack<'T, 'State> ([<InlineIfLambda>] folder) (option: option<'T>) (state: 'State) =
match option with
| None -> state
| Some x -> folder x state
[<CompiledName("Exists")>]
let inline exists ([<InlineIfLambda>] predicate) option =
match option with
| None -> false
| Some x -> predicate x
[<CompiledName("ForAll")>]
let inline forall ([<InlineIfLambda>] predicate) option =
match option with
| None -> true
| Some x -> predicate x
[<CompiledName("Contains")>]
let inline contains value option =
match option with
| None -> false
| Some v -> v = value
[<CompiledName("Iterate")>]
let inline iter ([<InlineIfLambda>] action) option =
match option with
| None -> ()
| Some x -> action x
[<CompiledName("Map")>]
let inline map ([<InlineIfLambda>] mapping) option =
match option with
| None -> None
| Some x -> Some(mapping x)
[<CompiledName("Map2")>]
let inline map2 ([<InlineIfLambda>] mapping) option1 option2 =
match option1, option2 with
| Some x, Some y -> Some(mapping x y)
| _ -> None
[<CompiledName("Map3")>]
let inline map3 ([<InlineIfLambda>] mapping) option1 option2 option3 =
match option1, option2, option3 with
| Some x, Some y, Some z -> Some(mapping x y z)
| _ -> None
[<CompiledName("Bind")>]
let inline bind ([<InlineIfLambda>] binder) option =
match option with
| None -> None
| Some x -> binder x
[<CompiledName("Flatten")>]
let inline flatten option =
match option with
| None -> None
| Some x -> x
[<CompiledName("Filter")>]
let inline filter ([<InlineIfLambda>] predicate) option =
match option with
| None -> None
| Some x -> if predicate x then Some x else None
[<CompiledName("ToArray")>]
let inline toArray option =
match option with
| None -> [||]
| Some x -> [| x |]
[<CompiledName("ToList")>]
let inline toList option =
match option with
| None -> []
| Some x -> [ x ]
[<CompiledName("ToNullable")>]
let inline toNullable option =
match option with
| None -> System.Nullable()
| Some v -> System.Nullable(v)
[<CompiledName("OfNullable")>]
let inline ofNullable (value: System.Nullable<'T>) =
if value.HasValue then
Some value.Value
else
None
[<CompiledName("OfObj")>]
let inline ofObj value =
match value with
| null -> None
| _ -> Some value
[<CompiledName("ToObj")>]
let inline toObj value =
match value with
| None -> null
| Some x -> x
module ValueOption =
[<CompiledName("GetValue")>]
let get voption =
match voption with
| ValueNone -> invalidArg "option" (SR.GetString(SR.optionValueWasNone))
| ValueSome x -> x
[<CompiledName("IsSome")>]
let inline isSome voption =
match voption with
| ValueNone -> false
| ValueSome _ -> true
[<CompiledName("IsNone")>]
let inline isNone voption =
match voption with
| ValueNone -> true
| ValueSome _ -> false
[<CompiledName("DefaultValue")>]
let inline defaultValue value voption =
match voption with
| ValueNone -> value
| ValueSome v -> v
// We're deliberately not using InlineIfLambda, because benchmarked code ends up slightly slower at the time of writing (.NET 8 Preview)
[<CompiledName("DefaultWith")>]
let inline defaultWith defThunk voption =
match voption with
| ValueNone -> defThunk ()
| ValueSome v -> v
[<CompiledName("OrElse")>]
let inline orElse ifNone voption =
match voption with
| ValueNone -> ifNone
| ValueSome _ -> voption
[<CompiledName("OrElseWith")>]
let inline orElseWith ([<InlineIfLambda>] ifNoneThunk) voption =
match voption with
| ValueNone -> ifNoneThunk ()
| ValueSome _ -> voption
[<CompiledName("Count")>]
let inline count voption =
match voption with
| ValueNone -> 0
| ValueSome _ -> 1
[<CompiledName("Fold")>]
let inline fold<'T, 'State> ([<InlineIfLambda>] folder) (state: 'State) (voption: voption<'T>) =
match voption with
| ValueNone -> state
| ValueSome x -> folder state x
[<CompiledName("FoldBack")>]
let inline foldBack<'T, 'State> ([<InlineIfLambda>] folder) (voption: voption<'T>) (state: 'State) =
match voption with
| ValueNone -> state
| ValueSome x -> folder x state
[<CompiledName("Exists")>]
let inline exists ([<InlineIfLambda>] predicate) voption =
match voption with
| ValueNone -> false
| ValueSome x -> predicate x
[<CompiledName("ForAll")>]
let inline forall ([<InlineIfLambda>] predicate) voption =
match voption with
| ValueNone -> true
| ValueSome x -> predicate x
[<CompiledName("Contains")>]
let inline contains value voption =
match voption with
| ValueNone -> false
| ValueSome v -> v = value
[<CompiledName("Iterate")>]
let inline iter ([<InlineIfLambda>] action) voption =
match voption with
| ValueNone -> ()
| ValueSome x -> action x
[<CompiledName("Map")>]
let inline map ([<InlineIfLambda>] mapping) voption =
match voption with
| ValueNone -> ValueNone
| ValueSome x -> ValueSome(mapping x)
[<CompiledName("Map2")>]
let inline map2 ([<InlineIfLambda>] mapping) voption1 voption2 =
match voption1, voption2 with
| ValueSome x, ValueSome y -> ValueSome(mapping x y)
| _ -> ValueNone
[<CompiledName("Map3")>]
let inline map3 ([<InlineIfLambda>] mapping) voption1 voption2 voption3 =
match voption1, voption2, voption3 with
| ValueSome x, ValueSome y, ValueSome z -> ValueSome(mapping x y z)
| _ -> ValueNone
[<CompiledName("Bind")>]
let inline bind ([<InlineIfLambda>] binder) voption =
match voption with
| ValueNone -> ValueNone
| ValueSome x -> binder x
[<CompiledName("Flatten")>]
let inline flatten voption =
match voption with
| ValueNone -> ValueNone
| ValueSome x -> x
[<CompiledName("Filter")>]
let inline filter ([<InlineIfLambda>] predicate) voption =
match voption with
| ValueNone -> ValueNone
| ValueSome x ->
if predicate x then
ValueSome x
else
ValueNone
[<CompiledName("ToArray")>]
let inline toArray voption =
match voption with
| ValueNone -> [||]
| ValueSome x -> [| x |]
[<CompiledName("ToList")>]
let inline toList voption =
match voption with
| ValueNone -> []
| ValueSome x -> [ x ]
[<CompiledName("ToNullable")>]
let inline toNullable voption =
match voption with
| ValueNone -> System.Nullable()
| ValueSome v -> System.Nullable(v)
[<CompiledName("OfNullable")>]
let inline ofNullable (value: System.Nullable<'T>) =
if value.HasValue then
ValueSome value.Value
else
ValueNone
[<CompiledName("OfObj")>]
let inline ofObj value =
match value with
| null -> ValueNone
| _ -> ValueSome value
[<CompiledName("ToObj")>]
let inline toObj value =
match value with
| ValueNone -> null
| ValueSome x -> x