Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
How to write a function that only accepts a list of Error string Results in F# on the level of types?
For example, given a mergeErrors function where input is always a list of Error strings,
let es = [ Error 1; Error 2; Error 3 ]
let mergeErrors<'a> (errors: Result<'a,int> list) : Result<'a,int list> =
errors
|> List.map (function | Error e -> e)
|> Error
mergeErrors (es: Result<int,int> list)
//=> Error [1; 2; 3]
this will always blow up during runtime. Was wondering if there is a way to force failure to compile time?
Context
In my script, there is a Result<'a, string> seq seq type where each constituent Result<'a, string> seq needs to be flattened:
- if there are
Errors, thenOks can be discarded, and anError concatenatedStringsshould be returned - if all is
Ok, then they are merged depending of the shape of'a
My naive solution is to
let filterError (row: Result<'a, string> seq) =
row
|> Seq.filter (function
| Error _ -> true
| _ -> false)
let mergeErrors inputFromFilterError =
// ...
This is almost surely the wrong approach (and found the F# For Fun and Profit's "Map and Bind and Apply, Oh my!" series while researching for an answer), but I'm still curious.
1 answer
I'm pretty sure the answer is "no", especially in some reasonable way. If Result was defined in some object-oriented way, i.e. as an interface with Ok and Error being implementations of that interface, or if this was O'Caml and Result had been defined as a polymorphic variant, then you could do this. That said, the openness of these approaches would be inappropriate for Result.
Defined as a discriminated union, you can't accomplish this. However, the "right" way to handle this, which is roughly equivalent to the above approaches, but with explicit rather than implicit conversions, is not to put the strings into the Result type in the first place. Or, in your case, avoid boolean blindness, a term popularized (and coined?) in this blog post, but search engines will return many other more recent discussions.
Instead of discovering which case of the Result you have with the pattern match in filterError and then immediately discarding that type information by returning a bool, you could instead have a function fromError : Result<'a, 'e> -> Option<'e>, this maintains the type information. Your filterError function would then become Seq.collect (fromError >> Option.toList).
If we adapt to the new type of filterError, then applying the same aversion to discarding type information to mergeErrors would lead to it simply being the identity function.
In general, uses of bool should be considered skeptically, and you should see if a bool-producing function can be replaced with a function that gives evidence for the result it returns. Functions like Seq.isEmpty, Option.isSome, Option.isNone, Result.isError, Result.isOk, etc. are almost never what you want. They and booleans in general tend to lead to implicit contracts between parts of your code. That this contract is implicit is part of the "blindness" of boolean blindness. Replacing bool returning functions with Option or Choice returning functions is often a solution.

0 comment threads