4

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.

4 Answers 4

7

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.

Sign up to request clarification or add additional context in comments.

Comments

5

One problem you have is the following code:

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

The lambda function (fun x -> x) already exists. This is the id function. It just returns whatever you have unchanged. You also could write the code you have like this:

(line.Cost |> Option.map id) 

And the next thing. Mapping over the id function makes no sense. You unwrap whatever is inside the option, apply the id function to it. What didn't change the decimal at all. Then you wrap the decimal again in an option. You also can just write:

line.Cost 

and completely remove the Option.map as it does nothing.

So the code you have here:

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

is identical to:

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

This obviously does not work because here you try to subtract x a decimal with line.Cost a option decimal. So you get a type-error.

I guess what you really want to do is to subtract line.Cost from line.Price if line.Cost is present, otherwise you want to preserve line.Price unchanged.

One way would be just to provide a default value for line.Costs that can be used and have no effect on the subtraction. For example you could use the value 0 for subtraction if line.Costs is None.

So you also could write something like this instead:

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

A default value for multiplication would be 1m. So you overall end with.

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

The above code for example returns:

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

If your goal is that a whole computation turns into None as soon a single value is None. I would just add map2 as a helper function.

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

then you just can write:

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) 

and it will return:

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

2 Comments

After talking with a coworker, I ended up writing an optionMap2 function before I saw this answer.
@Steven and what will happen when you have 3 options? Would you write map3? Ok, that's just a detail but what about readability? I don't think that using forward pipe to do math formulas step by step reads well. Have a look at the alternatives me and others provided when all your math formula is in one place. I would recommend whichever solution you pick, have all the formula in one place and not split across different lines.
1

Within the Option.map the x is actually a decimal, but the signature for Option.map is 'T option -> 'U option. So here:

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

you have the following:

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

So the decimal option has to be converted to decimal to be compatible with what's in the first Option.map. But now you have to deal with None result.

Below a quick (and nasty) fix that just is using an if statement to extract Value (which will be a decimal) or if None then return 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)) 

For a more sophisticated solution I recommend this answer.

2 Comments

Why keep doing Option.map (fun x -> x)?
Yeah, Agreed. Should be shortened. Fixed that.
1

For completeness sake, you may also leverage the monadic properties of the option type by "lifting" the values outside of the option. This is a somewhat simpler variant of the applicative approach linked by @PiotrWolkowski and those shown by @Gustavo. Applicatives not only wrap values in the monad, but also the functions applied to them.

We start by making the option type amenable to monadic operations in terms of bind and return. Thankfully, those functions are already defined, there's only a little tweak in the argument order.

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

On top of this there's the lift function and a couple of operators for convenience. If needed, they could be generalized by marking them inline.

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

Now your calculation becomes

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

which will print

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.