Sun, Jun 28, 2020

Understanding F# map/bind

This blog post aims to explain map/bind in F# in a code only example. It took me a while so I’m writing it up here so I can come back to it and re-read it for the 100th time most likely!

let add x y =
    x + y

let subtract x y =
    x - y

let multiply x y =
    x * y

let divide x y =
    try
        Ok(y / x) //This could blow up with DivideByZero exception
    with ex -> Error ex

let homeRolledMap fn result = 
    match result with
    | Ok s -> Ok(fn s)
    | Error f -> Error f

let homeRolledBind fn result =
    match result with
    | Ok s -> fn s
    | Error f -> Error f

[<EntryPoint>]
let main argv =
    //Understanding Result<int,exn>

    add 10 5 |> printfn "10+5 is %i"
    subtract 10 5 |> printfn "10-5 is %i"
    multiply 10 5 |> printfn "10*5 is %i"
    
    let divResult = divide 5 10
    //We have to pattern match the result because it won't be an int it will be Result<int,exn>
    match divResult with
    | Ok res -> printfn "10/5 is %i" res
    | Error err -> printfn "oops"

    //Let's chain some method calls together

    //add wants two integer inputs but because we called divide which returned Result<int,exn>
    //we need to call Result.map to match on the success value of a call to divide and then call add and then return a Result type
    //because once we have a Result type in the chain we have to continue with that
    let mapRes =
        multiply 10 10
        |> divide 2
        |> Result.map (add 50)
    match mapRes with
    | Ok res -> printfn "10*10/2+50 is %i" res
    | Error err -> printfn "oops"

    //An example of why that is beneficial is because if we call divide 0 this will also return a Result<int,exn> but this time it will have errored
    //map here will recognise that it doesnt have a success value but an exn/failure value and not call the success function and it will do this numerous times based on the
    //numerous functions we call
    let mapRes =
        multiply 10 10
        |> divide 0
        |> Result.map (add 50)
        |> Result.map (subtract 10)
        |> Result.map (add 5)
    match mapRes with
    | Ok res -> printfn "10*10/0+50-10+5 is %i" res
    | Error err -> printfn "oops %A" err

    //divide wants two integer inputs but because we called divide previously which returned Result<int,exn>
    //we need to call Result.bind to match on the success value of a call to divide and then call divide but then NOT to return a Result type as divide returns us a Result type already
    //if we called Result.map instead of bind we'd get back Result<Result<int,exn>,exn>
    let mapRes =
        multiply 10 10
        |> divide 2
        |> Result.bind (divide 5)
    match mapRes with
    | Ok res -> printfn "10*10/2/5 is %i" res
    | Error err -> printfn "oops %A" err


    //what would it look like if we didnt have map/bind types?

    //Once you hit divide you will have to pattern match the result and break up the flow of chained function calls
    let multiRes = multiply 10 10 |> divide 2
    match multiRes with
    | Ok res ->
        let newres = res |> add 50
        printfn "10*10/2+50 is %i" newres
    | Error err -> printfn "oops %A" err

    //But what if you want subsequent calls to divide, you'll have to split up the pattern matching which is ugly
    let multiRes = multiply 10 10 |> divide 2
    match multiRes with
    | Ok res ->
        let newres =
            res
            |> add 50
            |> divide 2
        match newres with
        | Ok subsequentres -> printfn "10*10/2+50/2 is %i" subsequentres
        | Error err -> printfn "oops %A" err
    | Error err -> printfn "oops %A" err


    //What if you wanted to call divide 5 times, nested hell!
    let firstRes = multiply 10 10 |> divide 2
    match firstRes with
    | Ok res ->
        let secondRes =
            res
            |> add 50
            |> divide 2
        match secondRes with
        | Ok res ->
            let thirdRes =
                res
                |> subtract 10
                |> divide 2
            match thirdRes with
            | Ok res ->
                let fourthRes =
                    res
                    |> add 80
                    |> divide 2
                match fourthRes with
                | Ok res ->
                    let fifthRes =
                        res
                        |> add 25
                        |> add 5
                        |> divide 2
                    match fifthRes with
                    | Ok res -> printfn "10*10/2+50/2+80/2+25+5/2 = %i" res
                    | Error err -> printfn "oops %A" err
                | Error err -> printfn "oops %A" err
            | Error err -> printfn "oops %A" err
        | Error err -> printfn "oops %A" err
    | Error err -> printfn "oops %A" err



    //To understand how map/bind works and allow us to write more terse code we could roll our own functions to cope with all the pattern matching
    //and see what a map/bind looks like
    let homeRolled =
        multiply 10 10
        |> divide 2
        |> homeRolledMap (add 50)
        |> homeRolledBind (divide 2)
        |> homeRolledMap (subtract 10)
        |> homeRolledBind (divide 2)
        |> homeRolledMap (add 80)
        |> homeRolledBind (divide 2)
        |> homeRolledMap (add 25)
        |> homeRolledMap (add 5)
        |> homeRolledBind (divide 2)

    //Luckily as we know there is map and bind on the Result module that we can use rather than rolling our own
    let res =
        multiply 10 10
        |> divide 2
        |> Result.map (add 50)
        |> Result.bind (divide 2)
        |> Result.map (subtract 10)
        |> Result.bind (divide 2)
        |> Result.map (add 80)
        |> Result.bind (divide 2)
        |> Result.map (add 25)
        |> Result.map (add 5)
        |> Result.bind (divide 2)

    match res with
    | Ok res -> printfn "10*10/2+50/2+80/2+25+5/2 = %i" res
    | Error err -> printfn "oops %A" err


    0 // return an integer exit code

comments powered by Disqus