Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Arithmetic with decimal option types

Tags:

f#

I'm trying to do some mathematical operations with decimal options on a custom type:

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

I can do some very simple arithmetic with

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

which gives me

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

If I try to actually calculate the thing I need

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

I get the error

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'

where I would have expected a seq<decimal option>.

I must be missing something but I can't seem to spot whatever it is I'm missing.

like image 677
Steven Avatar asked Dec 08 '22 21:12

Steven


1 Answers

You are mixing decimal with decimal option.

If you're trying to solve everything with Option.map you may want to try with Option.bind instead, so your code will be 'linearly nested':

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)

which can be an interesting exercise, especially if you want to understand monads, then you will be able to use a computation expression, you can create your own or use one from a library like F#x or F#+:

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)
    }
)

but if you link F#+ you'll have Applicative Math Operators available:

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

That's nice stuff to learn but otherwise I would suggest to use F# built-in features instead, like pattern-match, it would be easier:

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)

Then since you can also pattern-match over records it can be further reduced to:

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

Note that Option.map (fun x -> x) doesn't transform anything.

like image 197
Gus Avatar answered Dec 14 '22 14:12

Gus