Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# Convert 'a discriminated union to string

I'm trying to convert a discriminated union to string but I don't understand why this code is not working.

type 'a sampleType =
 | A of 'a
 | B of 'a

let sampleTypeToString x =
 match x with
 | A (value) -> string value
 | B (value) -> string value

This is the fsharp interactive output

sampleTypeToString A(2);;
 Stopped due to error
 System.Exception: Operation could not be completed due to earlier error
 Successive arguments should be separated by spaces or tupled, and arguments involving function or method applications should be parenthesized at 3,19
 This expression was expected to have type
     'obj'    
 but here has type
     'int'     at 3,21
like image 801
DocLM Avatar asked Dec 24 '22 19:12

DocLM


1 Answers

There are two errors here: function application syntax and lost genericity.

Function application syntax

This is the error "function arguments should be separated by spaces or tupled..."

In the expression sampleTypeToString A(2), you actually have three terms, not two:

  1. sampleTypeToString
  2. A
  3. (2)

Don't let the lack of a space between A and (2) fool you. These are not considered, somehow, as "one expression". A and (2) are separate terms.

Therefore, the whole expression sampleTypeToString A(2) is being interpreted as function sampleTypeToString applied to two arguments - A and (2). This, of course, doesn't work, because sampleTypeToString takes only one argument, and term A doesn't fit, because it's of the wrong type.

The simplest way to fix it is to just put parentheses around what should be evaluated first:

   sampleTypeToString (A(2))

And of course, since function (or constructor) application in F# doesn't actually require parentheses by itself, you can drop the first set:

   sampleTypeToString (A 2)

Alternatively, you can use a pipe:

   sampleTypeToString <| A(2)

This works, because the pipe operator has lower precedence than function application (which is the highest of all), so that A(2) gets evaluated first, and only then piped into sampleTypeToString.

Lost genericity

This has to do with the error "expected obj, but here has type int"

This one is a bit trickier. See how you're using the string function within sampleTypeToString? That function is technically generic, but not in the regular way. It uses statically resolved type constraints. Without going into too much detail, this basically means that the concrete type of the argument has to be known at compile time.

But your function sampleTypeToString takes a parameter of generic type sampleType<'a>, and thus when it calls string, it passes the argument of type 'a. But string can't work like that: it needs to know the concrete type, can't be generic 'a. So the compiler tries its best to substitute a concrete type. Because it knows literally nothing of what 'a could be, it goes with the most general assumption obj.

As a result, your function sampleTypeToString actually ends up taking parameter of type sampleType<obj>, not sampleType<'a> as you might expect.

The solution? Declare you function as inline. This will tell the compiler not to actually compile it as a .NET method, but rather expand its definition wherever it's called (somewhat similar to a DEFINE in C or a template in C++). This way, the type 'a will always be known at compile time, and the picky function string would be satisfied.

let inline sampleTypeToString x =
 match x with
 | A (value) -> string value
 | B (value) -> string value

sampleTypeToString (A 2)
sampleTypeToString (B "abc")
sampleTypeToString (A true)

Alternatively, you could box the argument of string, turning it into obj:

let sampleTypeToString x =
 match x with
 | A (value) -> string (box value)
 | B (value) -> string (box value)

But then you'd slightly change the semantics of string, for it has special processing for certain types, converting to string in culture-invariant way. If you box the argument, it will basically always fall back to obj.ToString(). Plus, you'd suffer an extra heap allocation for the boxed value.

Even more alternatively, you could do away with string and just call .ToString() yourself:

let sampleTypeToString x =
 match x with
 | A (value) -> value.ToString()
 | B (value) -> value.ToString()

Keep in mind that this is not exactly the same as calling string.

like image 59
Fyodor Soikin Avatar answered Jan 04 '23 18:01

Fyodor Soikin