You're right that the most ergonomic solution would be language support for row polymorphism. See, for example, another proof-of-concept project of mine which does support row polymorphism:
However, Dhall does have a language feature which can still help here, which is support for projecting a record's fields by the expected type. In other words, given a record r
containing a superset of the fields in Config
, you can project out just the fields of interest using r.(Config)
.
Normally I'd illustrate how this would work in the context of the example you referenced from dhall-lang.org, but the issue for that specific example is that there's no way it can silently ignore the presence of the extra foo
field, even if Dhall were to support row polymorphism. This is because the makeUser
function produces a Config
instead of consuming a Config
, so if you commit to producing that extra field then you have to actually add the expected field.
However, in the spirit of your request I'll use a related example which I believe is closer to what you had in mind. The only difference is that we'll change our function to one that consumes a Config
:
let Config : Type = { home : Text, privateKey : Text, publicKey : Text }
let render =
λ(config : Config) →
''
Home: ${config.home}
Private Key: ${config.privateKey}
Public Key: ${config.publicKey}
''
let example =
{ home = "/home/gabriella"
, privateKey = "/home/gabriella/.ssh/id_ed25519"
, publicKey = "/home/gabriella/.ssh/id_ed25519.pub"
}
in render example
We'd like to be able to add another foo
field to example but without breaking the render
function. In other words, we want the render
function to silently ignore any fields other than the ones in the Config
type.
Normally, in a language with row polymorphism you'd address this by changing the type of render
to something like this:
let render =
λ(config : { home : Text, privateKey : Text, privateKey : Text | r }) →
''
Home: ${config.home}
Private Key: ${config.privateKey}
Public Key: ${config.publicKey}
''
let example =
{ home = "/home/gabriella"
, privateKey = "/home/gabriella/.ssh/id_ed25519"
, publicKey = "/home/gabriella/.ssh/id_ed25519.pub"
, foo = "bar"
}
in render example
… where the r
in the type of render
denotes "other fields that we want to ignore". Dhall doesn't support that, as you noted, but if we use Dhall's support for projecting fields by type, we can do this:
let Config : Type = { home : Text, privateKey : Text, publicKey : Text }
let render =
λ(config : Config) →
''
Home: ${config.home}
Private Key: ${config.privateKey}
Public Key: ${config.publicKey}
''
let example =
{ home = "/home/gabriella"
, privateKey = "/home/gabriella/.ssh/id_ed25519"
, publicKey = "/home/gabriella/.ssh/id_ed25519.pub"
, foo = "bar"
}
in render example.(Config)
It is not as ergonomic as row-polymorphism, but at least it addresses the use case of making the code insensitive to changes in the upstream input that add new fields.