Skip to content

Instantly share code, notes, and snippets.

@nikita-volkov
Last active July 4, 2022 13:33
Show Gist options
  • Save nikita-volkov/6977841 to your computer and use it in GitHub Desktop.
Save nikita-volkov/6977841 to your computer and use it in GitHub Desktop.
Anonymous records. A solution to the problems of record-system.

#Anonymous records. A solution to the problems of record-system.

Please, beware that the proposal that follows has been implemented as a library.

The current record system is notorious for three major flaws:

  1. It does not solve the namespacing problem. I.e., you cannot have two records sharing field names in a single module. E.g., the following won't compile:

    data A = A { field :: String }
    data B = B { field :: String }
  2. It's partial. The following code will result in a runtime error:

    data A = A1 { field1 :: String } | A2 { field2 :: String }
    
    main = print $ field1 $ A2 "abc"
  3. It does not allow you to use the same field name for different types across constructors:

    data A = A1 { field :: String } | A2 { field :: Int }

This proposal approaches all the problems above and also a fourth one, which is unrelated to the current record system: it allows one to avoid declaration of intermediate types (see details below).

Gentlemen, I want you to meet,

##Anonymous Records

When a record-syntax is used in Haskell it's almost always a single-constructor ADT. A question rises then: why use ADT when you don't need its main feature (i.e., the multiple constructors)? This main feature is actually the root of the second and the third problem of record-syntax from the list above. In such situations one doesn't actually need ADT, but something more like a tuple with ability to access its items by name. E.g.:

f :: (a :: Int, b :: String) -> String
f rec = rec.b ++ show rec.a

application:

f (a = 123, b = "abc")

So now you think "Okay, but how about naming it?". Well, not a problem at all - use the existing type-construct:

type TheRecord = (a :: Int, b :: String) 

Now, about the avoidance of intermediate types:

type Person = (name :: String, phone :: (country :: Int, area :: Int, number :: Int))

See? No need to declare separate types for inner values. But, of course, if you need, you still can:

type Phone = (country :: Int, area :: Int, number :: Int)
type Person = (name :: String, phone :: Phone)

We can nicely access the deeply nested fields, e.g.:

personCountryCode :: Person -> Int
personCountryCode person = person.phone.country

We can make the function above polymorphic, by letting it work on all records containing a field phone with a record containing a field country:

recordPhoneCountry :: (phone :: (country :: a)) -> a
recordPhoneCountry record = record.phone.country

Okay. What about the type ambiguity? E.g., in the following the Person is actually the same type as Company:

type Person = (name :: String, phone :: Phone)
type Company = (name :: String, phone :: Phone)

Easily solvable with a help of newtype:

newtype Person = Person (name :: String, phone :: Phone)
newtype Company = Company (name :: String, phone :: Phone)

What about ADTs? Again, easy:

data Product = Tea (brand :: Company)
             | Milk (brand :: Company, fatness :: Float)
             
productBrand :: Product -> Company
productBrand (Tea record) = record.brand
productBrand (Milk record) = record.brand

Now, the beautiful fact:

This solution does not conflict with any existing feature of Haskell! As the examples show, it easily fits into the language as an extension. It can peacefully coexist with the existing record system of ADTs. Hence a complete backwards compatibility with old codebase. There's also a potential for many other additional features.

##Research Turns out, there's been plenty of related proposals:

There even exist implementations in some Haskell dialects:

There is a devoted page on related research at GHC wiki:

##Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment