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?
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"}
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:
{Id=1; Name="bob"}
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With