Today I encountered an implementation challenge with a large DU (sum-type) in F#, where I basically wanted to be able to map a set of functions over the values of the DU without having to repeat the matched case on the right hand side. The motivation being, that it would be quite easy to accidently return a wrong case, which the type checker obviously wouldn't be able to catch. To illustrate the problem, here is one candidate solution 'pattern' I came up with for F# using reflection
open FSharp.Reflection
type MyData =
| Foo of int
| Bar of bool
| Baz of string
| Hello of int
override this.ToString () =
let (case, _ ) = FSharpValue.GetUnionFields(this, typeof<MyData>)
case.Name
static member map (f : int -> int) (g : bool -> bool) (h : string -> string) x : MyData =
let f' = box << f
let g' = box << g
let h' = box << h
let cases = FSharpType.GetUnionCases typeof<MyData>
let matchCase = cases |> Array.find (fun case -> case.Name = x.ToString())
let res =
match x with
| Foo v -> f' v
| Bar v -> g' v
| Baz v -> h' v
| Hello v -> f' v
FSharpValue.MakeUnion(matchCase, [| res |]) :?> MyData
let mapMyData : MyData -> MyData = MyData.map ((+) 1) not (fun s -> s.ToUpper())
let foo : MyData = Foo 42
let newFoo : MyData = mapMyData foo
let bar : MyData = Bar true
let newBar : MyData = mapMyData bar
And now I am curious about what other solutions exists for this problem, in F# as well as in other languages that has sum-types. I can easily see this being a general problem in for example Haskell, Purescript, Idris, Rust.
The problem can be distilled down to:
- Mapping over the values of a sum-type without repeating the case names on the right-hand side.
- Leverage the type checker as much as possible for safety.