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 string
s,
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
Error
s, thenOk
s can be discarded, and anError concatenatedStrings
should 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