0

I'm starting to play around with F# and challenged myself to write a FizzBuzz (ikr, dream big).

After watching a couple of Scott Wlaschin talks, I'm trying to refactor my mess by using all those cool maps and binds and etc, but I found myself being stuck on merging 2 results together.

What i have here is this function (Result<int, string> * Result<int, string>) -> Result<int list, string>:

    let rangeToList (from, ``to``) =
        match from, ``to`` with
        | Ok first, Ok last -> Ok [ first..last ]
        | Error error, Ok _ -> Error error
        | Ok _, Error error -> Error error
        | Error error1, Error error2 -> Error(error1 + "; " + error2)

Results here are the results of parsing int from string (console input).

I have a feeling, this notation can be simplified somehow, but I can't seem to figure out how. Any tips?

Lammot
  • 3
  • 2

2 Answers2

1

I think you are fairly close to the simplest way of doing this. There are only a few tweaks I can think of.

First, I would really advice against using ``to`` as a variable name. It can be done, but it just looks ugly!

Second, you can reduce the number of cases to just three - if you first match the OK, Ok and Error, Error cases, you know that you now have one Error and one Ok, so you can join those using the | (or) pattern and always return the Error result:

let rangeToList (from, last) =
    match from, last with
    | Ok first, Ok last -> Ok [ first..last ]
    | Error error1, Error error2 -> Error(error1 + "; " + error2)
    | Error error, _ | _, Error error -> Error error

Third, if you are passing the two arguments to the function as a tuple, you could further condense this using the function keyword which defines a function that immediately matches on the argument (but this would not work if the arguments were space separated):

let rangeToList = function
    | Ok first, Ok last -> Ok [ first..last ]
    | Error error1, Error error2 -> Error(error1 + "; " + error2)
    | Error error, _ | _, Error error -> Error error

I think this solution works great. If you wanted something more clever using higher-order functions (but I really do not think this is needed here), you could define a zip function that takes two results and combines them - producing a pair of values and collecting all errors into a list:

module Result =
  let zip r1 r2 = 
    match r1, r2 with
    | Ok v1, Ok v2 -> Ok (v1, v2)
    | Error e1, Error e2 -> Error [e1; e2]
    | Error e, _ | _, Error e -> Error [e]

Using this as a helper, you could now rewrite your original function as:

let rangeToList2 (from, last) =
     Result.zip from last 
     |> Result.mapError (String.concat "; ")
     |> Result.map (fun (first, last) -> [ first..last ])

I think this is nice, but perhaps unnecessarily clever. It is not much shorter than your first version and it is certainly not much easier to understand.

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • Hi, Tomas! Thanks for a very detailed answer! Out of curiousity... Looking at the zip function, is there a way to zip arbitrary number of parameters into a single tuple? From what I'm guessing, calling it twice for 3 values will produce ((v1, v2), v3). – Lammot Jun 24 '22 at 20:42
  • Yeah, calling it twice would result in a nested tuple - this is a bit of a hassle, but there is no way around it. (A list would not work, because the `zip` function can work with results of different types - and list can only be used if all the values have the same type.) – Tomas Petricek Jun 24 '22 at 23:29
  • Hm. But shouldn't Errors, by the same logic, be also merged into a tuple, since they can potentially be of diffrerent types? And since that impossible (because we have cases with different quantities of Errors), shouldn't the merging function just state that it can only work with identically typed results and output lists? Like concat or something? – Lammot Jun 25 '22 at 08:54
  • Although, I'm not sure on how to work with a list of Ok instead of tuple. Sort it and try to fill it by creating ranges between 2 nearest elements and concating them at the end? – Lammot Jun 25 '22 at 09:01
1

Your code is fine as is, but I think the pattern you're sensing is called an "applicative". Applicatives are like a lite version of monads, so instead of Bind, the important function is called MergeSources in F#. You can use this to create a simple computation builder, like this:

type ResultBuilder() =

    member _.BindReturn(result, f) =
        result |> Result.map f

    member _.MergeSources(result1, result2) =
        match result1, result2 with
            | Ok ok1, Ok ok2 -> Ok (ok1, ok2)
            | Error error, Ok _
            | Ok _, Error error -> Error error
            | Error error1, Error error2 -> Error (error1 + "; " + error2)

let result = ResultBuilder()

And you can then use this to implement an elegant version of rangeToList with a computation expression, like this:

let rangeToList (from, ``to``) =
    result {
        let! first = from
        and! last = ``to``
        return [ first..last ]
    }

The nice thing about this approach is that it separates the general Result-wrangling logic in MergeSources from the domain-specific logic in rangeToList. This allows you to reuse the same result builder freely in other domains as well.

More details here and here.

Brian Berns
  • 15,499
  • 2
  • 30
  • 40
  • Hi, Brian! Thanks, for the answer. Unfortunately, I'm not yet familiar with computational expressions, so it's hard for me to wrap around what's going on here and how 2 seemingly random methods correlate with seemingly unrelated `{ ... }` block with `let!` and `and!` that somehow manage to extract `Ok` (i think) and yet handle the possible error combinations. Got any pointers? – Lammot Jun 24 '22 at 21:06
  • 1
    If you like Scott Wlaschin, I recommend [his series on the topic](https://fsharpforfunandprofit.com/series/computation-expressions/). – Brian Berns Jun 24 '22 at 21:08
  • (By the way, I only mentioned applicatives since you expressed interest in "cool maps and binds". It's definitely not a topic for most beginners, so you can wait until it makes more sense to tackle.) – Brian Berns Jun 24 '22 at 21:17
  • Regarding applicatives - yeah, I have to confess - a lot of stuff is going quite hard :D But if anything - it's fun so far. A good break from my usual OOP + not without benefit. Thanks again for showing this alternative, I'll be sure to check it out as soon as I can. :) – Lammot Jun 24 '22 at 21:35
  • Welcome to the F# world! Fair warning: Once you grok functional programming, you'll never want to go back to OOP again. :) – Brian Berns Jun 24 '22 at 22:03