DEV Community

loading...

Thought Experiment: Namespaced record fields

robinheghan profile image Robin Heggelund Hansen ・3 min read

I don’t like to nest records in Elm. It’s not that big of a deal, but it always seem to be lead to more noise than initially thought. Part of this is that Elm doesn’t have syntax that’s convenient for nested updates. Take a look at this record definition:

type alias Person =
  { name : Int
  , age : Int
  , pet : Pet
  }

type alias Pet =
  { name : String
  , age : Int
}
Enter fullscreen mode Exit fullscreen mode

If I had a person record and wanted to rename the pet, the code would look like something like this:

-- Can't do this
{ person.pet | name = "Fido" }

-- Or this
{ person | pet = { person.pet | name = "Fido" } }

-- It has to be this
let
  pet = person.pet
in
{ person | pet = { pet | name = "Fido" } }
Enter fullscreen mode Exit fullscreen mode

As I said previously, this isn't a big deal but it does leave me with an itch. One way to allieviate this is by avoiding nested records all together:

type alias PersonWithPet =
  { personName : String
  , personAge : Int
  , petName : String
  , petAge : Int
  }
Enter fullscreen mode Exit fullscreen mode

We can make this scale by using extensible record syntax:

type alias Pet a =
  { a |
    petName : String
  , petAge : Int
  }

{- Works with PersonWithPet -}
renamePet : String -> Pet a -> Pet a
renamePet name pet =
  { pet | petName = name }
Enter fullscreen mode Exit fullscreen mode

So, this actually solves the problem but does require me to prefix all fields in the record. What if there was support in the compiler for making this nicer?

Let's switch gears a little bit and talk about my previous language-of-choice, Clojure. Clojure is a Lisp and is dynamically typed. Instead of Records one simply uses maps (in Elm we call it Dict) to group together data. Clojure has its own type to serve as keys in a map, called keywords. They look like this:

:name ;; keyword

;; Person with a Pet
(def person
  { :name "Robin"
    :age 29
    :pet { :name "Fido"
           :age 4 } } )
Enter fullscreen mode Exit fullscreen mode

In Clojure, nested updates is pretty simple. If I wanted to rename the pet using the person definition above, I would do this:

(assoc-in person [:pet :name] "Baldur")
Enter fullscreen mode Exit fullscreen mode

However, sometimes it makes perfect sense to avoid nesting and Clojure has wonderful support for that:

(def person-with-pet
  { :person/name "Robin"
    :person/age 29
    :pet/name "Fido"
    :pet/age 4 } )
Enter fullscreen mode Exit fullscreen mode

But this still requires us prefix everything. This is where namespaced keywords comes into play:

(ns person) ;; namespace is set to person

;; This equals our previous definition
(def person-with-pet
  { ::name "Robin" ;; notice the double colon
    ::age 29
    :pet/name "Fido"
    :pet/age 4 } )

;; This is also the same thing
(def person-with-pet
  #:person{ :name "Robin"
            :age 29
            :pet/name "Fido"
            :pet/age 4 } )
Enter fullscreen mode Exit fullscreen mode

The double colon in the example above will fill in the current namespace as the prefix of the keyword. What could this potentially look like in Elm?

module Pet exposing (Pet)

type alias Pet a =
  { a |
    :name : String ;; expands to pet/name
    :age : Int ;; expands to pet/age
  }

module Person

import Pet as P

type alias PersonWithPet =
  { :name : String -- person/name
  , :age : Int -- person/age
  , :P/name : String -- pet/name
  , :P/age : Int -- pet/age
  }

{- Works in PersonWithPet -}
renamePet : String -> P.Pet a -> P.Pet a
renamePet name pet =
  { pet | :P/name = name }
Enter fullscreen mode Exit fullscreen mode

Is this an improvement? Maybe. It might be better to instead find a good syntax for nested updates, but I wouldn't mind just having a simple syntax to work with flat records.

Discussion (1)

pic
Editor guide
Collapse
rwoodnz profile image
Richard Wood

This is a big deal as a small bit of time wasted by everyone working around it all the time means a lot of wasted time overall.

How about just cut through all the complexity and allow:
{ person | pet.name = "Fido" } }

It is the person record we are replicating, so that has to be on the left. Pet.name tells us exactly where in the record we are making a change so that is all we need on the right.