Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there an analogous to tryFind for the new Result type in F# 4.1?

The F# language contains the Discriminated Union type option<'T>. Several modules contain useful functions XYZ.tryFind whose return value is an object of type option<'T>. (Examples: List.tryFind, Map.tryFind, Array.tryFind). F# 4.1 added the type Result<'S,'T> which is analogous to option<'T> but provides more information. Are there functions analogous to tryFind for the Result<'S,'T> type?

The code below is an attempt at creating such a function.

let resultFind (ef: 'K -> 'T) (tryfind: 'K -> 'M -> 'T option) (m: 'M) (k: 'K) =
    let y = tryfind k m
    match y with
    | Some i -> Result.Ok i
    | None -> Result.Error (ef k)

let fields = [("Email", "[email protected]"); ("Name", "John Doe")]
let myMap = fields |> Map.ofList
let ef k = sprintf "%s %A" "Map.tryFind called on myMap with bad argument " k
let rF = resultFind ef Map.tryFind myMap // analogous to tryFind
rF "Name"
rF "Whatever"


val resultFind :
  ef:('K -> 'T) ->
    tryfind:('K -> 'M -> 'T option) -> m:'M -> k:'K -> Result<'T,'T>
val fields : (string * string) list =
  [("Email", "[email protected]"); ("Name", "John Doe")]
val myMap : Map<string,string> =
  map [("Email", "[email protected]"); ("Name", "John Doe")]
val ef : k:'a -> string
val rF : (string -> Result<string,string>)

[<Struct>]
val it : Result<string,string> = Ok "John Doe"

[<Struct>]
val it : Result<string,string> =
  Error "Map.tryFind called on myMap with bad argument  "Whatever"" 

Also, why does the [<Struct>] declaration appear above the Result objects?

like image 647
Soldalma Avatar asked May 06 '17 18:05

Soldalma


1 Answers

The standard library doesn't have these functions, and it shouldn't. With a function like tryFind, there is really only one thing that could go wrong: the value can't be found. Therefore, there is really no need for comprehensive representation of the error case. Just a simple "yes/no" signal is enough.

But it is a legitimate use case when, in a known context, you need to "tag" the failure with specific error information, so that you can pass it out to your higher-level consumer.

However, for this use case, it would be wasteful and repetitive to invent a wrapper for every function: these wrappers would be completely identical except the function that they're calling. Your attempt goes in the right direction by making your function a higher-order one, but it does not go far enough: even though you're accepting the function as parameter, you've "baked in" the shape of that function. When you find yourself in a need to work with a function of, say, two arguments, you'll have to copy&paste the wrapper. This ultimately comes from the fact that your function takes care of two aspects - calling the function and converting the result. You can't reuse one without the other.

Let's try to stretch the approach further: break up the problem into smaller pieces, then combine them together to get the complete solution.

First, let's just invent ourselves a way to "convert" an Option value into a Result one. Obviously, we'd need to provide the value of the error:

module Result =
    let ofOption (err: 'E) (v: 'T option) =
        match v with
        | Some x -> Ok x
        | None -> Error err

Now we can use this to convert any Option into Result:

let r = someMap |> Map.tryFind k |> 
        Result.ofOption (sprintf "Key %A couldn't be found" k)

So far so good. But the next thing to notice is that the error value is not always needed, so it would be wasteful to compute it every time. Let's make this computation deferred:

module Result =
    let ofOptionWith (err: unit -> 'E) (v: 'T option) =
        match v with
        | Some x -> Ok x
        | None -> Error (err())

    let ofOption (err: 'E) = ofOptionWith (fun() -> err)

Now we can still use ofOption when the error value is cheap to compute, but we can also defer the computation by using ofOptionWith:

let r = someMap |> Map.tryFind k 
        |> Result.ofOptionWith (fun() -> sprintf "Key %A couldn't be found" k)

Next up, we can use this conversion function to create wrappers around functions that return Option to make them return Result:

module Result =
    ...
    let mapOptionWith (err: 'a -> 'E) (f: 'a -> 'T option) a =
       f a |> ofOptionWith (fun() -> err a)

Now we can define your rF function in terms of Result.mapOptionWith:

let rF = Result.mapOptionWith
           (sprintf "Map.tryFind called on myMap with bad argument %s")
           (fun k -> Map.tryFind k myMap)
like image 74
Fyodor Soikin Avatar answered Sep 18 '22 15:09

Fyodor Soikin