Why can't I simplify this iteration through a list of members of a discriminated union?

223 Views Asked by At

Frequently one wants to iterate (with either map, iter, or fold) through a collection of heterogeneous objects (different types). One way to deal with this is to create a discriminated union, which allows one to create a list with the objects suitably converted to DU cases. The following code does that in a simple example:

type MYDU = | X1 of int
            | X2 of float
            | X3 of string

let bar (y: MYDU) =
    match y with
    | X1 x -> printfn "%A" x  
    | X2 x -> printfn "%A" x
    | X3 x -> printfn "%A" x

[X1(1); X2(2.0); X3("3"); X1(4)]
|> List.map bar |> ignore

This code runs fine and prints

1
2.0
"3"
4

Great! But I wonder if one can avoid repeating the call to printfn. I tried the following and it does not compile:

let baz (y: MYDU) =
    match y with
    | X1 x | X2 x | X3 x -> printfn "%A" x // red squiggly line under X1 x

The compiler issues this message:

This expression was expected to have type 'int' but here has type 'float'

I suspect avoiding repetition is feasible but I must be making a basic mistake. Any suggestions?

3

There are 3 best solutions below

3
On BEST ANSWER

You're not making a mistake there, it's just not something F#'s type system would allow.

You can have multiple patterns on the left side of the match case arrow, but they are required to bind the same set of values (incl. the types). Here, x has a different type for each pattern, and that's enough for the compiler to complain.

There are ways to alleviate the pain (you could have a member on the DU that would return a boxed value, or you could have an active pattern that would do the boxing in the match case), but they're highly situational. Splitting the patterns into separate cases and repeating the right side for each one of them is always a better solution in a vacuum.

1
On

Something you could do instead is convert your arguments to a common type for printing, then print that value out instead. And you still get the advantages of pattern matching and discriminated unions :)

Here is an example of this approach

type MYDU = 
  | X1 of int
  | X2 of float
  | X3 of string

let bar y =
    let myStr = 
      match y with
      | X1 x -> string x  
      | X2 x -> string x
      | X3 x -> x
    printfn "%s" myStr

bar (X1 5)
0
On

It is possible to avoid this repetition to some degree by "boxing" the values to the obj type. This is an alias for the .NET's System.Object: "the ultimate base class of all classes in the .NET Framework". This means that any value of any type is also an obj.

However, when you box an object you lose static typing. You are subverting the F# type system and increasing the chance of an error. This is why it should generally be avoided unless you have a good reason for doing so.

The function printfn "%A" can take any type so its type signature is effectively obj -> unit. If all you want to do is run this function on the value, then it might be considered reasonable to use boxing. You could define this active pattern, which uses the box function:

let (|Box|) x = box x

And then use the pattern like this:

let printMyDu myDu =
    match myDu with
    | X1 (Box x)
    | X2 (Box x)
    | X3 (Box x) -> printfn "%A" x

Again, you should avoid doing this if possible as you are losing type safety in many cases. For example, if you ever box a value only to later check which type the value is, you are probably taking the wrong approach for F#. In this example we box the value x and then immediately use it and discard it, so we don't reduce overall type safety.