带小数选项类型的算术

时间:2016-09-26 17:19:48

标签: f#

我试图在自定义类型上使用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))))

我收到错误

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<decimal option>

我必须遗漏一些东西,但我似乎无法发现我失踪的任何东西。

4 个答案:

答案 0 :(得分:6)

您正在将decimaldecimal option混合。

如果您尝试使用Option.map解决所有问题,则可能需要尝试使用Option.bind,因此您的代码将被线性嵌套&#39;:

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,你可以自己创建或使用像{{{{}}这样的库。 3}}或F#x

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 :(得分: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)

这显然不起作用,因为在这里你尝试用xdecimal减去line.Cost option decimal。所以你得到了一个类型错误。

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

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

所以你也可以写这样的东西:

|> 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]

答案 2 :(得分:1)

Option.map xOption.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 option

因此Option.map必须转换为十进制才能与第一个None中的内容兼容。但现在你必须处理if结果。

下面是一个快速(而且令人讨厌的)修复,只是使用Value语句来提取None(这将是一个小数)或者如果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)) 然后返回0。

Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test

it('POST /api/v1/account/me status 500', function(done) {
  var agent = chai.request.agent(server);
    agent.post('/api/v1/account/login')
        .send({_email: 'test@test.com', _password: 'testtest'})
        .then(function(res){
        agent.get('/api/v1/account/logout')
            .then(function(res2){
                agent.get('/api/v1/account/me')
                    .then(function(res3){
                        res2.should.have.status(500);
                        done();
                    });
        });
    });
});

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

答案 3 :(得分:1)

为了完整起见,您还可以通过&#34;提升&#34;来利用选项类型的monadic属性。选项之外的值。这是由applicative approach链接的@PiotrWolkowski@Gustavo显示的更简单的变体。 Applicative不仅包装monad中的值,还包括应用于它们的函数。

我们首先根据bindreturn使选项类型适用于monadic操作。值得庆幸的是,这些函数已经定义,参数顺序只有一点调整。

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>