Strange F # Record Behavior

There are several cases where the behavior of F # records is strange to me:

No ambiguity warning

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 about deconstructing records instead of building records

Instead of receiving a warning about building records in the previous case, the F # compiler issued a warning about writing "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 

Please note that there are no warnings with pattern matching (I suspect that this is because patterns are built using "record building expressions" rather than a "record decomposition expression"):

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

Type input error with missing field

The F # compiler will select the second type and will throw an error because the 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 decomposing records

I asked several colleagues my questions: "What is this code?"

 type Person = {Id: int; Name: string;} let p = {Id = 42; Name = "Foo";} // What will happend here? let {Id = id; Name = name} = p 

It was unexpected for everyone that the "id" and "name" are actually "lvalues", although they are placed on the "right side" of the expression. I understand that this is much more about personal preferences, but for most people it seems strange that in one particular case the output values ​​are located on the right side of the expression.

I don’t think that all these are errors, I suspect that most of these things are actually functions.
My question is: is there any reasonable behavior behind such obscure behavior?

+7
source share
2 answers

Your examples can be divided into two categories: expression records and record templates . While record expressions require all fields to be declared and some expressions returned, record templates have optional fields and are designed to match patterns. There are two distinct sections on the MSDN page on the records , it might be 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 behavior is clear from the rule outlined in the MSDN page above .

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

When matching patterns, you focus on creating the necessary bindings. So you can write

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

to get id binding for later use. Matching patterns on binding bindings may look a little strange, but very similar to the way you usually map patterns in function parameters:

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

I think the warnings on the pattern matching examples are justified, because the compiler cannot guess your intention.

However, different entries with duplicate fields are not recommended. At the very 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"} 
+6
source

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

  • It’s good that it simplifies programming, because you can simply write {Id=1; Name="bob"} {Id=1; Name="bob"}
  • The bad news is that names can interfere with other record names that are in scope, and therefore, if your names are not unique (your first example), you get into trouble.

You can tell the compiler that you should always explicitly specify a name using the RequireQualifiedAccess attribute. This means that you will not be able to write only Id or Name , but you should always specify a 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 more strict mode, but makes programming less convenient. By default (a bit more ambiguous behavior) @pad is already explained - the compiler will just pick the name defined later in your source. He does this even if he can infer the type by looking at other fields in the expression - simply because searching for other fields will not always work (for example, when you use the with keyword), so it’s better to stick to a simple consistent strategy.

Regarding pattern matching, I was very confused when I first saw the syntax. I think it is not used very often, but it can be useful.

It is important to understand that F # does not use structural typing (this means that you cannot use a record with a large number of fields as an argument to a function that takes a record with fewer fields). This may be a useful feature, but it is not suitable for a system like .NET. This basically means that you cannot expect too bizarre things - the argument should be a record of a known type of record.

When you write:

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

The term lvalue refers to the fact that Id and Name appear in the template, and not in the expression. The syntax definition in F # tells you the following:

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

So, the left side of = in let is a template, and the right side is an expression. Two similar structures in F # - (x, y) can be used both for constructing and for deconstructing a tuple. And the same goes for records ...

+5
source

All Articles