Gradual typing in Dhall

104 views Asked by At

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?

1

There are 1 answers

2
Gabriella Gonzalez On

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.