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
}
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" } }
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
}
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 }
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 } } )
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")
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 } )
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 } )
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 }
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.
Top comments (1)
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.