Daml support for named arguments

daml would be quite beautiful or should I say even more beautiful if it were to support named arguments :slight_smile: , if its not in the works already pls accept it as a feature request

2 Likes

Daml is based on Haskell, which doesn’t directly support named arguments. However, there’s a few ways of achieving the same result by taking advantage of the type system.

Let me show you how to do this by working through an example.

Here’s a normal function that takes a first name, a last name, an age, and then returns a text:

makeDescription : Text -> Text -> Int -> Text
makeDescription firstName lastName age =
  implode [firstName, " ", lastName, " is ", show age, " years old."] 

For example, running:

makeDescription "John" "Doe" 30

returns:

"John Doe is 30 years old."

Now how do we give this function named arguments in Daml?

Method #1: Use a Record Type

The first approach is to bundle all the arguments together into a record type. Then, when calling the function, you just have to pass in the record type. Because of the built-in support for named record fields, this is like giving the function a set of named arguments.

For example, we would define a record type:

data Person = Person with
    firstName : Text
    lastName : Text
    age : Int

And then use the record in the function:

makeDescription : Person -> Text
makeDescription p = 
  implode [p.firstName, " ", p.lastName, " is ", show p.age, " years old."]

The function call would look like this (using with syntax):

makeDescription Person with
  firstName = "John"
  lastName = "Doe"
  age = 30

Or like this (using curly braces):

makeDescription Person { firstName = "John", lastName = "Doe", age = 30 }

This approach bundles all the arguments together in a single type, which means you can do other useful things with them, e.g. you could define a johnDoe value and pass it in at once:

johnDoe : Person
johnDoe = Person { firstName = "John", lastName = "Doe", age = 30 }

makeDescription johnDoe

It also makes it easy to specify arguments that only differ in one or two fields, for example:

makeDescrption johnDoe { firstName = "Jane" }

returns

"Jane Doe is 30 years old."

As you can see, this is a very flexible approach, and it supports both “named arguments” (by using record field names) and “default/optional arguments” (by supplying a default record value and changing only the fields you need).

Method #2: Use Wrapper Types

Another approach that comes up is to wrap each argument up in a separate wrapper type. This way, the wrapper type acts like a “name” for the argument.

For example, we would define a wrapper type for each argument:

data FirstName = FirstName Text
data LastName = LastName Text
data Age = Age Int

Then we use these to define the function:

makeDescription : FirstName -> LastName -> Age -> Text
makeDescription (FirstName firstName) (LastName lastName) (Age age) =
  implode [firstName, " ", lastName, " is ", show age, " years old."] 

Then the call would be:

makeDescription (FirstName "John") (LastName "Doe") (Age 30)

This approach treats each argument separately, so if you have other related functions you may find it quite useful to reuse wrappers. It also makes the function type signature much more descriptive, which is a nice bonus. Also, if you provide the arguments in the wrong order by accident, the compiler will detect the error.

In my day-to-day programming in Daml and Haskell, I see and use both of these approaches. Personally I prefer the second approach, because I think it’s easier to reuse the wrapper types and because they provide a little bit more type safety (you can’t accidentally skip an argument), but this approach doesn’t really support optional arguments. So each method has its upsides and downsides.

6 Likes

How do these two methods impact the performance of Daml code?

2 Likes

thanks , the first approach is quite similar to the passing of a Map approach Groovy used before their more explicit support for named arguments i.e. via the @NamedVariant and kin annotation.

Nice to know

2 Likes

How do these two methods impact the performance of Daml code?

Both approaches are going to be slower than passing the argument directly. I don’t have numbers, but this is what I understand based on the runtime representation.

In the first approach, you’re paying the cost to construct the record, and the cost to access the record fields.

In the second approach, you’re paying the cost to wrap each argument and the cost to unwrap each argument. There is some cost mitigation if you can reuse the same wrapper types and only unwrap them when you have to (e.g. pass around FirstName instead of Text, unwrap only when you need to).

If you don’t have a lot of opportunities to reuse the wrapper types, or you have a lot of arguments, the first approach will probably be more efficient than the second approach.

2 Likes

I’d add that you can also use record puns when de-structuring, so you could write:

makeDescription : Person -> Text
makeDescription Person{firstName, lastName, age} = 
  implode [firstName, " ", lastName, " is ", show age, " years old."]

And even do variations of this like

makeDescription Person{firstName=name, lastName=last, ..} = 
3 Likes

I don’t much like it personally, but for the sake of completeness, you can also pun all the way:

makeDescription : Person -> Text
makeDescription p{..} = 
  implode [firstName, " ", lastName, " is ", show age, " years old."]

where the destructuring form p{..} brings all the fields of p into the local scope directly as local variables.

3 Likes