F# 具有十进制选项类型的算术

F# 具有十进制选项类型的算术,f#,F#,我正在尝试对自定义类型使用十进制选项进行一些数学运算: 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 , So

我正在尝试对自定义类型使用
十进制选项进行一些数学运算:

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; ...] 
我得到了错误

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'
我本以为会有一个
seq


我一定是遗漏了什么,但我似乎看不出我遗漏了什么。

选项中。map
x
实际上是一个小数,但是
选项的签名。map
't Option->'U Option
。因此,这里:

Option.map (fun x -> x - (line.Cost |> Option.map (fun x -> x)))
您具有以下功能:

Option.map (fun x -> /*decimal*/ x - /*decimal option*/(line.Cost |> Option.map (fun x -> x)))
因此,必须将
decimal选项
转换为decimal,以便与第一个
选项.map中的内容兼容。但是现在您必须处理
None
结果

下面是一个快速(且令人讨厌)的修复程序,它只是使用
if
语句来提取
值(将是一个小数),或者如果
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))

对于更复杂的解决方案,我建议使用此选项。

您将
十进制
十进制选项
混合使用

如果您试图使用
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)
这可能是一个有趣的练习,特别是如果你想理解,那么你将能够使用,你可以创建自己的或使用库中的,如或:

但是,如果链接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(funx->x)
不会转换任何东西。

您遇到的一个问题是以下代码:

(line.Cost |>Option.map(乐趣x->x))
lambda函数
(funx->x)
已经存在。这是
id
功能。它只会返回您没有更改的内容。您也可以这样编写代码:

(line.Cost |>Option.map id)
接下来呢。映射到
id
函数没有意义。打开选项中的任何内容,对其应用
id
函数。什么根本没有改变小数点。然后将小数再次包装到选项中。你也可以写:

line.Cost
并完全删除
选项.map
,因为它什么也不做

这里的代码是:

|>Option.map(funx->x-(line.Cost |>Option.map(funx->x)))
与以下内容相同:

|>Option.map(乐趣x->x-line.Cost)
这显然不起作用,因为这里您尝试用
line.Cost
a
option decimal
减去
x
a
decimal
。所以你得到了一个类型错误

我想你真正想做的是从
line.Price
中减去
line.Cost
,如果
line.Cost
存在,否则你想保持
line.Price
不变

一种方法是只为
line.Costs
提供一个默认值,该值可以使用并且对减法没有影响。例如,如果
line.Costs
None
,则可以使用值
0
进行减法

所以你也可以这样写:

|>Option.map(乐趣x->x-(defaultArg line.Cost 0m))
乘法的默认值为
1m
。所以你总的来说是以

项目
|>Seq.map(趣味线->
线路,价格
|>Option.map(乐趣x->折扣*x)
|>Option.map(乐趣x->x-(默认参数行.成本0米))
|>Option.map(乐趣x->x*(默认参数行数量1m)))
例如,上面的代码返回:

[无;部分-2.500米;部分-21.250米;部分0.500米;部分-10.500米]
如果您的目标是一次计算完成后,整个计算将变成
None
值为
None
。我只想添加
map2
作为辅助函数

模块选项=
设map2fxy=
将x,y与
|一些x,一些y->一些(f x y)
|无
然后你就可以写:

项目
|>List.map(趣味线->
线路,价格
|>Option.map(趣味价格->价格*折扣)
|>Option.map2(乐趣成本价格->价格-成本)行.cost
|>Option.map2(乐趣数量价格->价格*数量)行数量)
它将返回:

[无;无;部分-21.250米;无;无]

为完整起见,您还可以通过“提升”选项之外的值来利用选项类型的一元属性。这是链接方式和显示方式的一个稍微简单的变体。应用程序不仅在monad中包装值,还包装应用于它们的函数

我们首先使选项类型在
bind
return
方面适合于一元操作。谢天谢地,这些函数已经定义好了,参数顺序只做了一点调整

let (>>=) ma f = Option.bind f ma
let ``return`` = Some
除此之外,还有
提升
功能和两个操作员,以方便操作。如果需要,可以通过内联标记它们来对其进行泛化

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")
哪个会打印


约21.250米

为什么要继续做Option.map(funx->x)
?是的,我同意。应该缩短。修复了这个问题。在与同事交谈后,我在看到这个答案之前编写了一个
optionMap2
函数。@Steven如果有3个选项,会发生什么?你会写
map3
?好的,这只是一个细节,但是可读性呢?我认为用前进管道一步一步地做数学公式读起来不好。当你所有的数学公式都在一个地方时,看看我和其他人提供的备选方案。我推荐哪一种
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>