F# Training
F# Training
F# Training
  • Presentation
  • Fundamentals
    • Introduction
    • Syntax
      • Bases
      • Functions
      • Rules
      • Exceptions
    • First concepts
    • πŸ”Quiz
  • Functions
    • Signature
    • Concepts
    • Syntax
    • Standard functions
    • Operators
    • Addendum
    • πŸ”Quiz
    • πŸ“œSummary
  • Types
    • Overview
    • Tuples
    • Records
    • Unions
    • Enums
    • Anonymous records
    • Value types
    • πŸ“œRecap
    • Addendum
  • Monadic types
    • Intro
    • Option type
    • Result type
    • Smart constructor
    • πŸš€Computation expression (CE)
    • πŸš€CE theoretical basements
    • πŸ“œRecap
  • Pattern matching
    • Patterns
    • Match expression
    • Active patterns
    • πŸš€Fold function
    • πŸ“œRecap
    • πŸ•ΉοΈExercises
  • Collections
    • Overview
    • Types
    • Common functions
    • Dedicated functions
    • πŸ”Quiz
    • πŸ“œRecap
  • Asynchronous programming
    • Asynchronous workflow
    • Interop with .NET TPL
    • πŸ“œRecap
  • Module and Namespace
    • Overview
    • Namespace
    • Module
    • πŸ”Quiz
    • πŸ“œRecap
  • Object-oriented
    • Introduction
    • Members
    • Type extensions
    • Class, Struct
    • Interface
    • Object expression
    • Recommendations
Powered by GitBook
On this page
  • Records: key points
  • Declaration
  • Formatting styles
  • Styles comparison
  • Styles configuration
  • Members
  • Single-line style
  • Multi-line Cramped and Aligned styles
  • Multi-line Stroustrup style
  • Construction: record expression
  • Deconstruction
  • Inference
  • Pattern matching
  • Name conflict
  • Modification: copy and update
  • Copy and update: Cβ™― vs Fβ™― vs JS
  • Copy and update limits (< Fβ™― 8)
  • Copy and update: Fβ™― 8 improvements

Was this helpful?

Edit on GitHub
  1. Types

Records

PreviousTuplesNextUnions

Last updated 6 hours ago

Was this helpful?

Records: key points

Product type with named elements called fields.

Alternative to tuples when they are imprecise, for instance float * float:

  • Point? Coordinates? Vector?

  • Real and imaginary parts of a complex number?

Eleviate the doubt by naming both the type and its elements:

type Point = { X: float; Y: float }
type Coordinate = { Latitude: float; Longitude: float }
type ComplexNumber = { Real: float; Imaginary: float }

Declaration

Base syntax:

type RecordName =
    { Label1: type1
      Label2: type2
      ... }

☝️ Field labels in PascalCase, not camelCase β†’ see

Complete syntax:

[ attributes ]                                // [<Struct>]
type [accessibility-modifier] RecordName =    // private, internal
    { [ mutable ] Label1: type1
      [ mutable ] Label2: type2
      ... }
    [ member-list ]                           // Properties, methods...

Formatting styles

  • Single-line: properties separated by ;

  • Multi-line: properties separated by line breaks

    • 3 variations: Cramped, Aligned, Stroustrup

// Single line
type PostalAddress = { Address: string; City: string; Zip: string }

// Cramped: historical ┆  // Aligned: C#-like      ┆  // Stroustrup: C++-like
type PostalAddress =   ┆  type PostalAddress =     ┆  type PostalAddress = {
    { Address: string  ┆      {                    ┆      Address: string
      City: string     ┆          Address: string  ┆      City: string
      Zip: string }    ┆          City: string     ┆      Zip: string
                       ┆          Zip: string      ┆  }
                       ┆      }

Styles comparison

Criterion
Best styles πŸ†

Compactness

Single-line, Cramped

Refacto Easiness (re)indentation, fields (re)ordering

Aligned, Stroustrup

☝️ Recommendation: Strive for Consistency β†’ Apply consistently the same multi-line style across a repository β†’ In addition, use the single-line style when relevant: line with < 80 chars

Styles configuration

Fantomas configuration in the .editorconfig file:

max_line_length = 180
fsharp_multiline_bracket_style = cramped | aligned | stroustrup

fsharp_record_multiline_formatter = number_of_items
fsharp_max_record_number_of_items = 3
# or
fsharp_record_multiline_formatter = character_width
fsharp_max_record_width = 120

Members

πŸ‘‰ Members are declared after the fields

Single-line style

// `with` keyword required
type PostalAddress = { Address: string; City: string; Zip: string } with
    member x.ZipAndCity = $"{x.Zip} {x.City}"

// Or use line breaks (recommended when >= 2 members)
type PostalAddress =
    { Address: string; City: string; Zip: string }

    member x.ZipAndCity = $"{x.Zip} {x.City}"
    member x.CityAndZip = $"%s{x.City}, %s{x.Zip}"

Multi-line Cramped and Aligned styles

☝️ 2 line breaks

type PostalAddress =
    { Address: string
      City: string
      Zip: string }

    member x.ZipAndCity = $"{x.Zip} {x.City}"
    member x.CityAndZip = $"%s{x.City}, %s{x.Zip}"

Multi-line Stroustrup style

☝️ with keyword + 1 line break + indentation

type PostalAddress = {
    Address: string
    City: string
    Zip: string
} with
    member x.ZipAndCity = $"{x.Zip} {x.City}"
    member x.CityAndZip = $"%s{x.City}, %s{x.Zip}"

Construction: record expression

  • Same syntax as an anonymous Cβ™― object without the new keyword

  • All fields must be populated, but in any order (but can be confusing)

  • Same possible styles: single/multi-lines

type Point = { X: float; Y: float }
let point1 = { X = 1.0; Y = 2.0 }
let pointKo = { Y = 2.0 }           // πŸ’₯ Error FS0764
//            ~~~~~~~~~~~ FS0764: No assignment given for field 'X' of type 'Point'

⚠️ Trap: differences declaration / instanciation β†’ : for field type in record declaration β†’ = for field value in record expression

Deconstruction

  • Fields are accessible by "dotting" into the object

  • Alternative: deconstruction

    • Same syntax for deconstructing a Record as for creating it πŸ‘

    • Unused fields can be ignored πŸ’‘

let { X = x1 } = point1
let { X = x2; Y = y2 } = point1

⚠️ Additional members (properties) cannot be deconstructed!

type PostalAddress =
    {
        Address: string
        City: string
        Zip: string
    }
    member x.CityLine = $"{x.Zip} {x.City}"

let address = { Address = ""; City = "Paris"; Zip = "75001" }

let { CityLine = cityLine } = address   // πŸ’₯ Error FS0039
//    ~~~~~~~~ The record label 'CityLine' is not defined
let cityLine = address.CityLine         // πŸ‘Œ OK

Inference

  • A record type can be inferred from the fields used πŸ‘ but not with members ❗

  • As soon as the type is inferred, IntelliSense will work

type PostalAddress =
    { Address: string
      City: string
      Zip: string }

let department address =
    address.Zip.Substring(0, 2) |> int
    //     ^^^^ πŸ’‘ Infer that address is of type `PostalAddress`.

let departmentKo zip =
    zip.Substring(0, 2) |> int
//  ~~~~~~~~~~~~~ Error FS0072: Lookup on object of indeterminate type

Pattern matching

Let's use an example: inhabitantOf is a function giving the inhabitants name (in French) at a given address (in France)

type Address = { Street: string; City: string; Zip: string }

let department { Zip = zip } = int zip[0..1] // Address -> int

let private IleDeFrance = Set [ 75; 77; 78; 91; 92; 93; 94; 95 ]
let inIleDeFrance departmentNum = IleDeFrance.Contains(departmentNum) // int -> bool

let inhabitantOf address = // Address -> string
    match address with
    | { Street = "Pôle"; City = "Nord" } -> "Père Noël"
    | { City = "Paris" } -> "Parisien"
    | _ when department address = 78 -> "Yvelinois"
    | _ when department address |> inIleDeFrance -> "Francilien"
    | _ -> "FranΓ§ais"

Name conflict

In Fβ™―, typing is nominal, not structural as in TypeScript β†’ Use qualification to resolve ambiguity β†’ Even better: write β‰  types or put them in β‰  modules

type Person1 = { First: string; Last: string }
type Person2 = { First: string; Last: string }
let alice = { First = "Alice"; Last = "Jones"}  // val alice: Person2... (by proximity)

// ⚠️ Deconstruction
let { First = firstName } = alice   // Warning FS0667 (in F# 6)
//  ~~~~~~~~~~~~~~~~~~~~~  The labels of this record do not uniquely
//                         determine a corresponding record type

let { Person2.Last = lastName } = alice     // πŸ‘Œ OK with qualification
let { Person1.Last = lastName } = alice     // πŸ’₯ Error FS0001
//                                ~~~~~ Type 'Person1' expected, 'Person2' given

Modification: copy and update

Record is immutable, but easy to get a modified copy β†’ copy and update expression of a Record β†’ use multi-line formatting for long expressions

// Single-line
let address2 = { address with Street = "Rue Vivienne" }

// Multi-line
let address3 =
    { address with
        City = "Lyon"
        Zip  = "69001" }

Copy and update: Cβ™― vs Fβ™― vs JS

// Record Cβ™― 9.0
address with { Street = "Rue Vivienne" }
// Fβ™― copy and update
{ address with Street = "Rue Vivienne" }
// Object destructuring with spread operator
{ ...address, street: "Rue Vivienne" }

Copy and update limits (< Fβ™― 8)

Reduced readability with several nested levels

type Street = { Num: string; Label: string }
type Address = { Street: Street }
type Person = { Address: Address }

let person = { Address = { Street = { Num = "15"; Label = "rue Neuf" } } }

let person' =
    { person with
        Address =
          { person.Address with
              Street =
                { person.Address.Street with
                    Num = person.Address.Street.Num + " bis" } } }

Copy and update: Fβ™― 8 improvements

type Street = { Num: string; Label: string }
type Address = { Street: Street }
type Person = { Address: Address }

let person = { Address = { Street = { Num = "15"; Label = "rue Neuf" } } }

let person' =
    { person with
        Person.Address.Street.Num = person.Address.Street.Num + " bis" }

Qualification

Usually, we have to qualify the field to avoid a compilation error: see Person.Address.

πŸ”—

MS style guide
https://fsprojects.github.io/fantomas/docs/end-users/Configuration.html#fsharp_record_multiline_formatter