7

So have gotten to record in my F# journey and at first they seem rather dangerous. At first this seemed clever:

type Card = { Name  : string;
          Phone : string;
          Ok    : bool }

let cardA = { Name = "Alf" ; Phone = "(206) 555-0157" ; Ok = false }

The idea that the cardA is patten matched with Card. Not to mention the simplified pattern matching here:

let withTrueOk =
  list 
  |> Seq.filter
    (function 
      | { Ok = true} -> true
      | _ -> false
  )

Problem is:

type Card = { Name  : string;
          Phone : string;
          Ok    : bool }

type CardTwo = { Name  : string;
          Phone : string;
          Ok    : bool }

let cardA = { Name = "Alf" ; Phone = "(206) 555-0157" ; Ok = false }

cardA is now of CardTwo type which I am guessing has to do with F# running everything in order.

Now this might be an impossible situation since there may never be a chance of the same signature taking on two type, but it is a possibility.

Is recording something that has only limited use or am I just over thinking this one?

svick
  • 236,525
  • 50
  • 385
  • 514
Programmin Tool
  • 6,507
  • 11
  • 50
  • 68
  • 8
    You're overthinking it -- give your record elements distinct names or fully qualify which record type you intend to match/instantiate e.g. `let cardA = { Card.Name = "Alf" ; Phone = "(206) 555-0157" ; Ok = false }`. – ildjarn Jan 11 '12 at 17:22
  • 15
    IMHO mutability by default, inheritance and lack of structural equality make classes more dangerous than records, but you don't see many people complaining about that ;-) – Mauricio Scheffer Jan 11 '12 at 17:35
  • 8
    @MauricioScheffer Add to that nullability by default. – David Grenier Jan 11 '12 at 18:28

3 Answers3

20

They are not dangerous and they are not only for limited use.

I think it's very rare that you would have two types with the same members. But if you do encounter that situation, you can qualify the record type you want to use:

let cardA = { Card.Name = "Alf" ; Phone = "(206) 555-0157" ; Ok = false }

Records are very useful for creating (mostly) immutable data structures. And the fact that you can easily create a copy with just some fields changed is great too:

let cardB = { cardA with Ok = true }
svick
  • 236,525
  • 50
  • 385
  • 514
12

I agree, record fields as members of the enclosing module/namespace seems odd at first coming from more traditional OO languages. But F# provides a fair amount of flexibility here. I think you'll find only contrived circumstances cause problems, such as two records that

  1. are identical
  2. have a subset/superset relationship

The first case should never happen. The latter could be solved by record B having a field of record A.

You only need one field to be different for the two to be distinguishable. Other than that, the definitions can be the same.

type Card =
  { Name : string
    Phone: string
    Ok : bool }

type CardTwo =
  { Name : string
    Phone: string
    Age : int }

let card = { Name = "Alf" ; Phone = "(206) 555-0157" ; Ok = false }
let cardTwo = { Name = "Alf" ; Phone = "(206) 555-0157" ; Age = 21 }

Pattern matching is also quite flexible as you only need to match on enough fields to distinguish it from other types.

let readCard card = 
  match card with
  | { Ok = false } -> () //OK
  | { Age = 21 } -> ()   //ERROR: 'card' already inferred as Card, but pattern implies CardTwo

Incidentally, your scenario is easily fixed with a type annotation:

let cardA : Card = { Name = "Alf" ; Phone = "(206) 555-0157" ; Ok = false }
Daniel
  • 47,404
  • 11
  • 101
  • 179
  • Why would a subset/superset relationship be a problem? AFAIK, you have to always specify all properties. – svick Jan 11 '12 at 17:51
  • 2
    When pattern matching. You only have to match on enough fields to distinguish it from other record types. Check my code. – Daniel Jan 11 '12 at 18:15
6

In order that you appreciate what F# provides, I just want to mention that there is no fully qualified accessor for records in OCaml. Therefore, to distinguish between record types with the same fields, you have to put them into submodules and reference them using module prefixes.

So your situation in F# is much better. Any ambiguity between similar record types could be resolved quickly using record accessor:

type Card = { Name: string;
          Phone: string;
          Ok: bool }

type CardSmall = { Address: string;
          Ok: bool }

let withTrueOk list =
  list 
  |> Seq.filter (function 
                 | { Card.Ok = true} -> true (* ambiguity could happen here *)
                 | _ -> false)

Moreover, F# record is not limited at all. It provides a lot of nice features out-of-the-box including pattern matching, default immutability, structural equality, etc.

pad
  • 41,040
  • 7
  • 92
  • 166
  • 9
    If you're defining two record types with the same fields, you can prevent unqualified access (and the inherent ambiguity) by annotating your types with `[]`. – Daniel Jan 11 '12 at 18:20