Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

partial deconstruction in pattern-matching (F#)

Following a minimal example of an observation (that kind of astonished me):

type Vector = V of float*float

// complete unfolding of type is OK
let projX (V (a,_)) = a

// also works
let projX' x =
   match x with
   | V (a, _) -> a

// BUT:
// partial unfolding is not Ok 
let projX'' (V x) = fst x

// consequently also doesn't work 
let projX''' x =
   match x with
   | V y -> fst y

What is the reason that makes it impossible to match against a partially deconstructed type?

Some partial deconstructions seem to be ok:

// Works
let f (x,y) = fst y

EDIT: Ok, I now understand the "technical" reason of the behavior described (Thanks for your answers & comments). However, I think that language wise, this behavior feels a bit "unnatural" compared to rest of the language:

"Algebraically", to me, it seems strange to distinguish a type "t" from the type "(t)". Brackets (in this context) are used for giving precedence like e.g. in "(t * s) * r" vs "t * (s * r)". Also fsi answers accordingly, whether I send

type Vector = (int * int)

or

type Vector = int * int

to fsi, the answer is always

type Vector = int * int

Given those observations, one concludes that "int * int" and "(int * int)" denote exactly the same types and thus that all occurrences of one could in any piece of code be replaced with the other (ref. transparency)... which as we have seen is not true.

Further it seems significant that in order to explain the behavior at hand, we had to resort to talk about "how some code looks like after compilation" rather than about semantic properties of the language which imo indicates that there are some "tensions" between language semantics an what the compiler actually does.

like image 569
D.F.F Avatar asked Oct 31 '22 06:10

D.F.F


1 Answers

In F#

type Vector = V of float*float

is just a degenerated union (you can see that by hovering it in Visual Studio), so it's equivalent to:

type Vector = 
    | V of float*float

The part after of creates two anonymous fields (as described in F# reference) and a constructor accepting two parameters of type float.

If you define

type Vector2 = 
    | V2 of (float*float)

there's only one anonymous field which is a tuple of floats and a constructor with a single parameter. As it was pointed out in the comment, you can use Vector2 to do desired pattern matching.

After all of that, it may seem illogical that following code works:

let argsTuple = (1., 1.)
let v1 = V argsTuple

However, if you take into account that there's a hidden pattern matching, everything should be clear.

EDIT:

F# language spec (p 122) states clearly that parenthesis matter in union definitions:

Parentheses are significant in union definitions. Thus, the following two definitions differ:

type CType = C of int * int

type CType = C of (int * int)

The lack of parentheses in the first example indicates that the union case takes two arguments. The parentheses in the second example indicate that the union case takes one argument that is a first-class tuple value.

I think that such behavior is consistent with the fact that you can define more complex patterns at the definition of a union, e.g.:

    type Move = 
        | M of (int * int) * (int * int)

Being able to use union with multiple arguments also makes much sense, especially in interop situation, when using tuples is cumbersome.

The other thing that you used:

type Vector = int * int

is a type abbreviation which simply gives a name to a certain type. Placing parenthesis around int * int does not make a difference because those parenthesis will be treated as grouping parenthesis.

like image 169
Tomasz Maczyński Avatar answered Jan 04 '23 13:01

Tomasz Maczyński