有缺失值的数据
var code = "940ed96f-02b8-49fd-afbd-67d8f9eca85a"
假设我们有一个类似于CSV的文件,每行的数据用’,'隔开。文件中有些数据是缺失的,也有些数据没有保存,采用字符串说明。
12,45,23,,23,99,33,24,,"help, Oh, help",34
7,8,3,53,,9,13,22,"help, Oh, help",,24
我们省略文件读取的过程,仅仅把第一行数据拿来作为例子。
// fake data seperated with ',', contains string and empty slot that are invalid
let data = "12,45,23,,23,99,33,24,,\"help, Oh, help\",34"
printfn "%A" data
"12,45,23,,23,99,33,24,,"help, Oh, help",34"
这个数据的解析也挺麻烦的,因为碰到有引号的字符串,我们要把他当做一个单独的单元,考虑到引号内可能会有’,'。
我们单独实现一个String
的方法,来完成这个功能。这个函数里有两个相互调用的递归函数,定义的语法如下。
let rec func1 ... =
......
and func2 ... =
......
这样定义才能保证在func1
中递归调用func2
。
module String =
let split separator (s:string) =
let values = ResizeArray<string>()
let rec gather start i =
let add () = s.Substring(start,i-start) |> values.Add
if i = s.Length then add()
elif s[i] = '"' then inQuotes start (i+1)
elif s[i] = separator then add(); gather (i+1) (i+1)
else gather start (i+1)
and inQuotes start i =
if s[i] = '"' then gather start (i+1)
else inQuotes start (i+1)
gather 0 0
values.ToArray()
识别字符串中的数字可以使用System.Int32.TryParse
,这个函数返回的是一个元组,元组的第一个元素是个布尔变量,标识是否成功识别,如果成功识别,则元组的第二个元素是数字本身。
当然,这个函数识别之后得到是一个int option
。
let tryParse (s:string) =
match System.Int32.TryParse(s) with
| true, i -> Some i
| _ -> None
现在所有的工具都齐全了,我们就能对CSV文件的数据行进行处理。
首先,我们把这个字符串数组转变成一个int option
数组。
let nums =
data
|> String.split ','
|> Array.map tryParse
printfn "%A" nums
[|Some 12; Some 45; Some 23; None; Some 23; Some 99; Some 33; Some 24; None;
None; Some 34|]
数据的使用
接下来我们就简单展示一下如何使用这个array<int option>
数组。在实际的数据中,这是一个比较常见的状态。
快来快来数一数
首先,我们需要对数据的状态进行报告,例如有多少个数?
printfn "There are %A items in the array." (Array.length nums)
There are 11 items in the array.
那么接下来就是,有多少个数是有效的呢?
// count valid numbers and test valids and invalids
let count numbers =
numbers
|> Array.map Option.count
|> Array.sum
printfn "There are %A valid number." (count nums)
There are 8 valid number.
如果我们想知道哪些数字是有效的,哪些数字是无效的呢?
let testInvalid numbers =
numbers
|> Array.map Option.isNone
let testValid numbers =
numbers
|> Array.map Option.isSome
printfn "Are items invalid? \n%A" (testInvalid nums)
printfn "Are items valid? \n%A" (testValid nums)
Are items invalid?
[|false; false; false; true; false; false; false; false; true; true; false|]
Are items valid?
[|true; true; true; false; true; true; true; true; false; false; true|]
如果我们只关心有效的数字,并且想把他们打印出来呢?
// print only valid numbers
let print numbers =
numbers
|> Array.iter (Option.iter (printf "%i "))
print nums
12 45 23 23 99 33 24 34
注意这里,我们的Option.iter
配合对应集合的iter
方法使用。
缺失数字填充
还有一种情况,我们经常会遇到,就是要把缺失的数据补全。
// fill invalid number with default value
let fill num numbers = numbers |> Array.map (Option.orElse (Some num))
let fill0 = fill 0
printfn "%A" (fill0 nums)
[|Some 12; Some 45; Some 23; Some 0; Some 23; Some 99; Some 33; Some 24; Some 0;
Some 0; Some 34|]
上面这个函数运行也是很完美的,所有的缺失数据都被填上了Some 0
。当然,当我们把缺失数据都填上了的时候,我们有很大的可能性就不需要那个什么option
。
下面这个函数,填上数据之后,就把Some
给脱掉。
// fill invalid number with default value and return numbers instead of option array
let fillNum defaultValue numbers = numbers |> Array.map (Option.defaultValue defaultValue)
let fillNum0 = fillNum 0
printfn "%A" (fillNum0 nums)
[|12; 45; 23; 0; 23; 99; 33; 24; 0; 0; 34|]
对数据进行算术运算
如果我们需要对数据进行算术运算,当然,只包含哪些有意义的数字。
这个就可以通过Option.map
函数配合集合的map
函数来完成,或者用Option.bind
配合集合的map
来完成。
两个的效果是一样的,但是我们会更加倾向于用Option.map
,可以少写一个Some
。
// valid number arithmatic: same result
let doubleIt numbers = numbers |> Array.map (Option.map (fun x-> 2*x))
let doubleItBind numbers = numbers |> Array.map (Option.bind (fun x->Some (2*x)))
printfn "Option.bind: %A" (doubleItBind nums)
printfn "Option.map: %A" (doubleIt nums)
Option.bind: [|Some 24; Some 90; Some 46; None; Some 46; Some 198; Some 66; Some 48; None;
None; Some 68|]
Option.map: [|Some 24; Some 90; Some 46; None; Some 46; Some 198; Some 66; Some 48; None;
None; Some 68|]
累计运算
如果要对包含这个集合进行累计运算,也有相应的Option.fold
和Option.foldBack
来完成。
下面的例子很简单的把中缀的(*)
和(+)
应用起来,当然任何int -> int -> int
的函数都可以用来替换这两个函数。
配合Array.fold
也是比较清晰的。
let product numbers =
numbers
|> Array.fold (Option.fold (*)) 1
let sum numbers =
numbers
|> Array.fold (Option.fold (+)) 0
printfn "product: %A" (product nums)
printfn "sum: %A" (sum nums)
product: 1323784128
sum: 293
直接对数据进行判断
最后还有一个语义,还比较有意思,一个是forall
,对于集合而言,就代表所有元素均满足判断条件;而对于option
,则是直接跳过缺失的值(取为true
)。
另外一个是exists
,对于集合而言,就需要一个满足条件的元素即可;对于option
而言,同样是跳过缺失的值(取为false
)。
// Test to find all items satisfy some prediction, so None returns true
let allBiggerThan num numbers = numbers |> Array.forall (Option.forall (fun x -> x > num))
// Test to find any items satisfy some prediction, so None returns false
let anyBiggerThan num numbers = numbers |> Array.exists (Option.exists (fun x -> x > num))
printfn "%A" (anyBiggerThan 90 nums, allBiggerThan 10 nums)
(true, true)
结论
option
的各个函数中,基本上都是替换Some
的语义;option
的map
,iter
,fold
,forall
,exists
与集合的对应函数同名,其语义值得好好体会一下。