2016-09-26 64 views
3

我想在一個自定義類型做一些數學運算與decimal options算術十進制選項類型

type LineItem = {Cost: decimal option; Price: decimal option; Qty: decimal option} 

let discount = 0.25M 

let createItem (c, p, q) = 
    {Cost = c; Price = p; Qty = q} 

let items = 
    [ 
     (Some 1M , None , Some 1M) 
     (Some 3M , Some 2.0M , None) 
     (Some 5M , Some 3.0M , Some 5M) 
     (None , Some 1.0M , Some 2M) 
     (Some 11M , Some 2.0M , None) 
    ] 
    |> List.map createItem 

我可以做一些非常簡單的算術與

items 
|> Seq.map (fun line -> line.Price 
         |> Option.map (fun x -> discount * x)) 

這給了我

val it : seq<decimal option> = 
    seq [null; Some 0.500M; Some 0.750M; Some 0.250M; ...] 

如果我試着實際計算出我需要的東西

items 
|> Seq.map (fun line -> line.Price 
         |> Option.map (fun x -> discount * x) 
         |> Option.map (fun x -> x - (line.Cost 
                 |> Option.map (fun x -> x))) 
         |> Option.map (fun x -> x * (line.Qty 
                 |> Option.map (fun x -> x)))) 

我得到的,我本來期望一個seq<decimal option>錯誤

error FS0001: Type constraint mismatch. The type 
    'a option  
is not compatible with type 
    decimal  
The type ''a option' is not compatible with the type 'decimal' 

我必須錯過一些東西,但我似乎無法發現我失蹤的任何東西。

回答

4

的一個問題是下面的代碼:

(line.Cost |> Option.map (fun x -> x)) 

lambda函數(fun x -> x)已經存在。這是id函數。它只是返回你沒有改變的東西。你也可以寫下你這樣的代碼:

(line.Cost |> Option.map id) 

而接下來的事情。通過id函數映射是沒有意義的。您可以解開選項中的任何內容,將id函數應用於該選項。什麼都沒有改變小數。然後,您再次將小數包在一個選項中。您也可以只寫:

line.Cost 

並完全刪除Option.map,因爲它什麼都不做。

因此,代碼你在這裏:

|> Option.map (fun x -> x - (line.Cost |> Option.map (fun x -> x))) 

是相同的:

|> Option.map (fun x -> x - line.Cost) 

這顯然是行不通的,因爲在這裏你嘗試減去x一個decimalline.Cost一個option decimal。所以你會得到一個類型錯誤。

我猜你真正想要做的是從line.Price如果line.Cost存在減line.Cost,否則要保留line.Price不變。

一種方法是僅爲line.Costs提供一個默認值,可以使用該值並且對減法沒有影響。例如,如果line.CostsNone,則可以使用值0進行扣除。

所以,你也可以寫這樣的事情,而不是:

|> Option.map (fun x -> x - (defaultArg line.Cost 0m)) 

乘法的默認值是1m。所以你總的結束。

items 
|> Seq.map (fun line -> 
    line.Price 
    |> Option.map (fun x -> discount * x) 
    |> Option.map (fun x -> x - (defaultArg line.Cost 0m)) 
    |> Option.map (fun x -> x * (defaultArg line.Qty 1m))) 

例如上面的代碼返回:

[None; Some -2.500M; Some -21.250M; Some 0.500M; Some -10.500M] 

如果你的目標是一個整體計算,一旦一個 值None變成None。我只想添加map2作爲輔助功能。

module Option = 
    let map2 f x y = 
     match x,y with 
     | Some x, Some y -> Some (f x y) 
     | _ -> None 

,那麼你只可以這樣寫:

items 
|> List.map (fun line -> 
    line.Price 
    |> Option.map (fun price -> price * discount) 
    |> Option.map2 (fun cost price -> price - cost) line.Cost 
    |> Option.map2 (fun qty price -> price * qty) line.Qty) 

,它將返回:

[None; None; Some -21.250M; None; None] 
+0

與同事交談後,我看到這個答案之前寫了一個'optionMap2'函數。 – Steven

+0

@Steven當你有3個選項時會發生什麼?你會寫「map3」嗎?好吧,這只是一個細節,但可讀性呢?我不認爲用前進管道一步一步做數學公式會很好。當你所有的數學公式都在一個地方時,請看看我和其他人提供的替代方案。我會推薦你​​選擇的解決方案,將所有公式放在一個地方,而不是分成不同的行。 – Gustavo

1

Option.map之內x實際上是小數,但Option.map的簽名是'T option -> 'U option。所以在這裏:

您具備以下條件:

Option.map (fun x -> /*decimal*/ x - /*decimal option*/(line.Cost |> Option.map (fun x -> x))) 

所以decimal option必須轉換爲十進制與什麼在第一Option.map兼容。但是現在你必須處理None的結果。

下面一個快速(和骯髒的)解決,僅僅是使用if聲明提取Value(這將是一個小數),或者如果None則返回0

items 
|> Seq.map (fun line -> line.Price 
        |> Option.map (fun x -> discount * x) 
        |> Option.map (fun x -> x - if line.Cost.IsSome then line.Cost.Value else 0m) 
        |> Option.map (fun x -> x * if line.Qty.IsSome then line.Qty.Value else 0m)) 

對於更復雜的解決方案,我建議這answer

+0

爲什麼繼續做''Option.map(有趣X - > X)''? – Gustavo

+0

是的,同意。應該縮短。修正了。 – PiotrWolkowski

6

您在混合decimaldecimal option

如果你想解決Option.map一切您可能需要使用Option.bind嘗試相反,所以你的代碼將被「線性嵌套」:

items 
|> Seq.map ( 
    fun line -> 
     Option.bind(fun price -> 
      Option.bind(fun cost -> 
      Option.bind(fun qty -> 
       Some ((discount * price - cost) * qty)) line.Qty) line.Cost) line.Price) 

它可以是一個有趣的練習,特別是如果你想了解monads,那麼你將能夠使用computation expression,你可以創建自己的,或者使用一個從庫像F#xF#+

open FSharpPlus.Builders 

items |> Seq.map (fun line -> 
    monad { 
     let! price = line.Price 
     let! cost = line.Cost 
     let! qty = line.Qty 
     return ((discount * price - cost) * qty) 
    } 
) 

但是,如果您鏈​​接F#+你就必須應用型數學運算符可供選擇:

open FSharpPlus.Operators.ApplicativeMath 
items |> Seq.map (fun line -> ((discount *| line.Price) |-| line.Cost) |*| line.Qty) 

這是不錯的東西學習,但否則我會建議使用F#的內置功能,而不是像模式匹配,它會簡單:

items 
|> Seq.map (fun line -> match line.Price, line.Qty, line.Cost with 
         | Some price, Some qty, Some cost -> 
          Some ((discount * price - cost) * qty) 
         | _ -> None) 

然後,因爲你也可以模式匹配過的記錄,可以進一步簡化爲:

items 
|> Seq.map (function 
      | {Cost = Some cost; Price = Some price; Qty = Some qty} -> 
       Some ((discount * price - cost) * qty) 
      | _ -> None) 

注意Option.map (fun x -> x)不改變任何東西。你有

1

爲了完整起見,你也可以通過「提升利用的選項類型的一元性「選項之外的價值。這是由@PiotrWolkowski連接的applicative approach和由@Gustavo顯示的那些的稍微簡單的變體。應用程序不僅包含monad中的值,還包含應用於它們的函數。

我們從bindreturn這兩個選項類型開始,使選項類型適用於一元操作。值得慶幸的是,這些函數已經定義好了,只是在參數順序中稍微調整了一下。

let (>>=) ma f = Option.bind f ma 
let ``return`` = Some 

除此之外,還有lift函數和一些操作符以方便使用。如果需要,可以通過將它們標記爲內聯來對它們進行概括。

let liftOpt op x y = 
    x >>= fun a -> 
    y >>= fun b -> 
    ``return`` (op a b) 
let (.*) = liftOpt (*) 
let (.-) = liftOpt (-) 

現在你的計算變得

items 
|> Seq.map (fun line -> 
    (line.Price .* Some discount .- line.Cost) .* line.Qty) 
|> Seq.iter (printfn "%A") 

,它將打印

<null> 
<null> 
Some -21.250M 
<null> 
<null>