Basics 19 - Double-Lifted Data
What is Double-Lifted Data
Consider this:
let opt: int option = Some 1
let res: Result<int option, exn> = Ok opt
opt
is anint option
, which means value 1 is packed inoption
containeropt
is then packed in aResult
Simply put, 1 is double-lifted
While any combination of double-lifting is theoretically possible, some of the cases you will practically and frequently encounter are:
Result<'T option, 'TError>
:Indicates the result of an operation that may fail
In
Ok
case, you may or may not have dataIn
Error
case, you have an exceptionExample: executing a DB query that gets one result/row/document. There are various outcomes:
DB operation failed,
Error
caseDB operation succeeded, found the record,
Ok
case ->Some
caseDB operation succeeded, no record found,
Ok
case ->None
case
Result<'T seq, 'TError> or Result<'T list, 'TError> or Result<'T array, 'TError>
:Indicates the result of an operation that may fail
In
Ok
case, you will get aseq
/list
/array
of itemsIn
Error
case, you have an exceptionExample: executing a DB query that gets multiple results/rows/documents. There are various outcomes:
DB operation failed,
Error
caseDB operation succeeded, found the records,
Ok
case ->'T seq/list/array
- In
Ok
case, there may be 0 or more items in'T seq/list/array
- In
Problem with Double-Lifted Data
Consider this:
let res: Result<int option, exn> = Ok (Some 1)
let add1 num = num + 1
We have
add1
function which can be used for mappingHow do we
use
add1 withres
?Result.map add1 res
won't work. In this caseResult.map
expects a function (mapper) that operates onint option
, notint
.There is no point in changing
add1
;add1
is our business logic, typically reusable
There are various ways to solve this issue.
Wrapper Function or Lamba Expression
The wrapper function add1Wrapper
(or the lambda expression) takes int option
as input:
let add1 num = num + 1
let add1Wrapper opt = Option.map add1 opt
Result.map add1Wrapper res
//// OR ////
Result.map (fun opt -> Option.map add1 opt) res
The problem with this approach is that the burden is now yours... for how many such functions can you create wrappers or lambda expressions.
Lifted Function
Remember map
function can be used as a function lifter. Function liftedAdd1
is the lifted version of add1
:
let add1 num = num + 1
let liftedAdd1 = Option.map add1
Result.map liftedAdd1 res
//// OR ////
let x = Result.map (Option.map add1) res
The problem with this approach, once again, is that the burden is yours... for how many such functions can you create lifted versions.
Let's look at another approach.
Functions that are aware of Double-Lifted Data
module Result =
let optMap mapper result = Result.map (Option.map mapper) result
let optBind binder result = Result.map (Option.bind binder) result
Here we have created two functions optMap
and optBind
, and put those in the Result
module. This is done for consistency, you can call Result.optMap
and Result.optBind
.
let add1 num = num + 1
let add1ifEven num =
if num % 2 = 0 then Some(num + 1) else None
let res: Result<int option, exn> = Ok(Some 1)
Result.optMap add1 res |> printfn "%A" // Ok (Some 2)
Result.optBind add1ifEven res |> printfn "%A" // Ok None
Concluding Remarks
Dealing with double-lifted data isn't the most straight-forward
For rare cases, you may use lambda expressions or lifted functions
Best to create a library of functions that are aware of double-lifted data, as we will in coming articles
If you have reached so far, congratulations.
Keep reading!