Wednesday, March 4, 2009

F# Quirks

A feature or a bug?

An innocent-looking pair of parentheses ends up influencing the runtime representation of discriminated union types.

I would prefer not to have this. The two representations seem conceptually redundant, it would be better to just pick the faster one.

Given this foo.fs:


open Microsoft.FSharp.Reflection

type A1 = A1 | B1 of (A1 * A1)

type A2 = A2 | B2 of A2 * A2

printfn "%A" ((FSharpType.GetUnionCases(typeof<A1>)
              |> (fun x -> x.GetFields())));;

printfn "%A" ((FSharpType.GetUnionCases(typeof<A2>)
              |> (fun x -> x.GetFields())));;

Run fsi --quiet --exec foo.fs to produce this:

[|[||]; [|Microsoft.FSharp.Core.Tuple`2[FSI_0001.Foo+A1,FSI_0001.Foo+A1] B11|]|]
[|[||]; [|FSI_0001.Foo+A2 B21; FSI_0001.Foo+A2 B22|]|]

I found this code while testing a library that produced different results depending on the presence or absence of the parentheses.

Update: A feature! Thanks to Zedd. I should know my specs and my OCaml:

Objective Caml version 3.10.2
# type c = C of int * int;;
type c = C of int * int
# match C(1, 2) with | C x -> x;;
The constructor C expects 2 argument(s), but is here applied to 1 argument(s)
# type c = C of (int * int);;
type c = C of (int * int)
# match C(1, 2) with | C x -> x;;
- : int * int = (1, 2)


  1. From the F# Language Specification 1.9.6 Draft:

    Parentheses are significant in discriminated union definitions:

    type c = C of int * int


    type c = C of (int * int)

    differ. The parentheses are used to indicate that the constructor takes on argument that is a first-class pair value. Without the parentheses you must write

    match c with
    | C (x,y) -> ...

    Listed under section 9.3: Union Types.

    You can quickly confirm the differing behavior with fsi.exe
    A1 can be matched with A1, B1(x,y) or B1(t). x and y will be matched A1 respectively. t will be matched as a tuple of A1 * A1.
    A2 can only by matched by A2 or B2(x,y)

    Hope that helps.

  2. Thanks a lot! It does.

    Just checked with OCaml, same behavior.

    SO, it IS a feature.