Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange behavior of F# records

Tags:

f#

There is a few cases when F# records behavior is strange to me:

No warning on ambiguity

type AnotherPerson = {Id: int; Name: string}
type Person = {Id: int; Name: string;}

// F# compiler will use second type without any complains or warnings
let p = {Id = 42; Name = "Foo";}

Warning on records deconstruction instead of records construction

Instead of getting a warning on records construction in previous case, F# compiler issued a warning on records "deconstruction":

// Using Person and AnotherPerson types and "p" from the previous example!
// We'll get a warning here: "The field labels and expected type of this 
// record expression or pattern do not uniquely determine a corresponding record type"
let {Id = id; Name = name} = p

Note, that there is no warnings with pattern matching (I suspect thats because patterns are built using "records construction expressions" and not with "records deconstruction expression"):

match p with
| {Id = _; Name = "Foo"} -> printfn "case 1"
| {Id = 42; Name = _} -> printfn "case 2"
| _ -> printfn "case 3"

Type inference error with missing field

F# compiler will choose second type and than will issue an error because Age field is missing!

type AnotherPerson = {Id: int; Name: string}
type Person = {Id: int; Name: string; Age: int}

// Error: "No assignment given for field 'Age' of type 'Person'"
let p = {Id = 42; Name = "Foo";}

Ugly syntax for "records deconstruction"

I asked several collegues of mine a question: "What this code is all about?"

type Person = {Id: int; Name: string;}
let p = {Id = 42; Name = "Foo";}

// What will happend here?
let {Id = id; Name = name} = p

That was total surprise for everyone that "id" and "name" are actually "lvalues" although they placed at the "right hand side" of the the expression. I understand that this is much more about personal preferences, but for most people seems strange that in one particular case output values are placed at the right side of the expression.

I don't think that all of this are bugs, I suspect that most of this stuff are actually features.
My question is: is there any rational behind such obscure behavior?

like image 917
Sergey Teplyakov Avatar asked Apr 15 '13 13:04

Sergey Teplyakov


2 Answers

Your examples can be divided into two categories: record expressions and record patterns. While record expressions require to declare all fields and return some expressions, record patterns have optional fields and are for the purpose of pattern matching. The MSDN page on Records have two clear sections on them, it may worth a read.

In this example,

type AnotherPerson = {Id: int; Name: string}
type Person = {Id: int; Name: string;}

// F# compiler will use second type without any complains or warnings
let p = {Id = 42; Name = "Foo";}

the behaviour is clear from the rule stated in the MSDN page above.

The labels of the most recently declared type take precedence over those of the previously declared type

In the case of pattern matching, you focus on creating some bindings you need. So you can write

type Person = {Id: int; Name: string;}
let {Id = id} = p

in order to get id binding for later use. Pattern matching on let bindings may look a bit weird, but it's very similar to the way you usually pattern match in function parameters:

type Person = {Id: int; Name: string;}
let extractName {Name = name} = name

I think warnings on your pattern matching examples are justified because the compiler can't guess your intention.

Nevertheless, different records with duplicate fields are not recommended. At least you should use qualified names to avoid confusion:

type AnotherPerson = {Id: int; Name: string}
type Person = {Id: int; Name: string; Age: int}

let p = {AnotherPerson.Id = 42; Name = "Foo"}
like image 115
pad Avatar answered Jan 03 '23 05:01

pad


I think most of your comments are related to the fact that record names become directly available in the namespace where the record is defined - that is, when you define a record Person with properties Name and Id, the names Name and Id are globally visible. This has both advantages and disadvantages:

  • The good thing is that it makes programming easier, because you can just write {Id=1; Name="bob"}
  • The bad thing is that the names can clash with other record names that are in scope and so if your names are not unique (your first example), you get into troubles.

You can tell the compiler that you want to always explicitly qualify the name using RequireQualifiedAccess attribute. This means that you won't be able to write just Id or Name, but you'll need to always include the type name:

[<RequireQualifiedAccess>]
type AnotherPerson = {Id: int; Name: string}
[<RequireQualifiedAccess>]
type Person = {Id: int; Name: string;}

// You have to use `Person.Id` or `AnotherPerson.Id` to determine the record
let p = {Person.Id = 42; Person.Name = "Foo" }

This gives you a stricter mode, but it makes programming less convenient. The default (a bit more ambiguous behavior) is already explained by @pad - the compiler will simply pick a name that is defined later in your source. It does this even in the case where it could infer the type by looking at other fields in the expression - simply because looking at other fields would not always work (e.g. when you use the with keyword), so it is better to stick to a simple consistent strategy.

As for the pattern matching, I was quite confused when I've seen the syntax for the first time too. I think it is not used very often, but it can be useful.

It is important to realize that F# does not use structural typing (meaning that you cannot use record with more fields as an argument to a function that takes record with fewer fields). This might be a useful feature, but it does not fit well with .NET type system. This basically means you cannot expect too fancy things - the argument has to be a record of a well known named record type.

When you write:

let {Id = id; Name = name} = p

The term lvalue refers to the fact that id and name appear in the pattern rather than in an expression. The syntax definition in F# tells you something like this:

expr := let <pat> = <expr>
      | { id = <expr>; ... }
      | <lots of other expressions>

pat  := id
      | { id = <pat>; ... }
      | <lots of other patterns>

So, the left hand side of = in let is a pattern, while the right hand side is an expression. The two share similar structure in F# - (x, y) can be used both to construct and to de-construct a tuple. And the same is the case for records...

like image 25
Tomas Petricek Avatar answered Jan 03 '23 05:01

Tomas Petricek