Enhancing types without breaking compatibility - best practice

Hi,

Our team is using dhall for some time now and we see some real benefits in using it. At the same time we are still struggling with it.

One of the things we might haven’t done right is something the team members were calling “backward compatibility”. It means that we want to enhance a type by adding a new “parameter/field” without breaking existing usages of such type.

So something that you could do in programming languages by providing sensible defaults, e.g. changing your api from

func(a: String)
to
func(a:String, extraOption: String = “bar”)
does not break existing code.

In dhall we define our types like this (simplified version of what we actually do):

let CsvOptions : Type  =  { header : Bool,  separator : Text }

Those definitions go into some repository and we reference them via http link

So our “users” utilize it like this (again simplified)

let Metadata = https:// ….
let options : Metadata.CsvOptions  =  { header = True, separator = ";" }

So now we want to add a quote attribute/field to CsvOptions and we could also provide a sensible default. Just adding the quote to the type definition would look like this

let CsvOptions : Type  =  { header : Bool,  separator : Text, quote : Text }

As a consequence thereof the user code would break, because he doesn’t have quote defined. That is undesireable.

How can I extend my CscOptions with the quote field (and some default value) in such a way that the user code doesn’t break?

We experimented with some ways of handling this, but I am not sure what the best way would be to handle that.

So finally the question: What is the best practice to cope with this issue without putting any burden to the user utilizing the type?

Thanks a lot

Christian

1 Like

I believe what you want is Dhall’s support for defining “schemas” (record types with default values) and then instantiating schemas using the record completion operator (::).

For example, your starting example would look like this:

let CsvOptions =
      { Type = { header : Bool, separator : Text }
      , default = {=}
      }

in  CsvOptions::{ header = True, separator = ";" }

… and then you could later add a quote field to that type without breaking existing record completions, like this:

let CsvOptions =
      { Type = { header : Bool, separator : Text, quote : Text }
      , default = { quote = "\"" }
      }

in  CsvOptions::{ header = True, separator = ";" }

To learn more, see:

1 Like

Hi,
sorry for my late feedback and thanks for your answer. We moved forward with the mentioned approach. The following example works, but is there a simpler way of using it? We define several “action types” called ColumnTransformation (CopyValue, etc). All of them have slightly different attributes.

let ColumnNames
    : Type
    = { sourceColumn : Text, targetColumn : Text }

let CopyValueParams
    = { Type = ColumnNames
        , default = {=}
      }

let String2DateParams
    = {
        Type = ColumnNames ⩓ { pattern : Text }
        , default = {=}
    }

let String2TimestampParams =
    {
        Type = ColumnNames ⩓ {  pattern : Text, timezone: Text  }
        , default = { timezone = "Europe/Berlin" }
    }


let ColumnTransformation : Type
    = < CopyValue : CopyValueParams.Type
      | String2Date : String2DateParams.Type
      | String2Timestamp : String2TimestampParams.Type
      >

let example0: ColumnTransformation = ColumnTransformation.String2Timestamp String2TimestampParams::{
        sourceColumn="foo"
        , targetColumn ="bar"
        , pattern = "yyyy-M-d"
        , timezone = "UTC"
    }

let example1: ColumnTransformation = ColumnTransformation.String2Timestamp String2TimestampParams::{
        sourceColumn="foo"
        , targetColumn ="bar"
        , pattern = "yyyy-M-d"
    }

let example2: ColumnTransformation = ColumnTransformation.CopyValue CopyValueParams::{
        sourceColumn="foo"
        , targetColumn ="bar"
    }

in [example0, example1, example2]

When we want to use such types we hace always to provide the param name along with the type name. Is there a way of simply ommitting the name of the parameter? So just writing

let example1: ColumnTransformation = ColumnTransformation.String2Timestamp {
        sourceColumn="foo"
        , targetColumn ="bar"
        , pattern = "yyyy-M-d"
    }

, thus omitting “String2TimestampParams” but still get the default set?

Thanks and br

Christian