The Dhall website has a nice example:
{- You can optionally add types
`x : T` means that `x` has type `T`
-}
let Config : Type =
{- What happens if you add another field here? -}
{ home : Text
, privateKey : Text
, publicKey : Text
}
let makeUser : Text -> Config = \(user : Text) ->
let home : Text = "/home/${user}"
let privateKey : Text = "${home}/.ssh/id_ed25519"
let publicKey : Text = "${privateKey}.pub"
let config : Config = { home, privateKey, publicKey }
in config
let configs : List Config =
[ makeUser "bill"
, makeUser "jane"
]
in configs
Like the comment asks me to do, I add another field: foo : Text
to the type Config
and now the example fails to typecheck.
This leaves me without a way to gradually add types to existing projects. I'm missing an escape hatch to allow me to gradually add knowledge as I become more familiar with the dynamic configuration of a project that I don't own.
I bet this is a common situation: "I'm maintaining a project I don't own, configured in YAML or JSON without a schema, or with a loosely defined schema."
I believe what I want is called "row-polymorphism" which seems to have already been discussed (and rejected?) in the Dhall issues, but what alternative do I have to solve the same issue? From my perspective, it seems like I cannot use Dhall in the case where I don't have total information?
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 inConfig
, you can project out just the fields of interest usingr.(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 themakeUser
function produces aConfig
instead of consuming aConfig
, 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
:We'd like to be able to add another
foo
field to example but without breaking therender
function. In other words, we want therender
function to silently ignore any fields other than the ones in theConfig
type.Normally, in a language with row polymorphism you'd address this by changing the type of
render
to something like this:… where the
r
in the type ofrender
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: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.