Generating record update functions to be used as parameters

Hi,

Assuming there is a requirement to condense the syntax for updating record fields, I am wondering how to go about this.

Let’s say there is

data Foo = Foo with
    a : Int
    b : Text
    -- and many, many more fields

and instead of saying

processFieldA : Input -> Foo -> Foo
processFieldA input foo = foo with a = value
    where
        value = function_of_input

I would like to be able to say

processFieldA : Input -> Foo -> Foo
processFieldA input = set field_a $
    function_of_input

Now, a is auto generated and has type a : Foo -> Int. What I need in addition is the record update function field_a : Int -> Foo -> Foo that I can pass on to the combinator set as a parameter.

Other than implementing those record update functions manually for each field, what are my options? I don’t assume Template Haskell and lenses are available, are they?

Kind regards,
Alex

1 Like

Hi Alex. We do use lens libraries internally, and these are proven to work; however, you do need to write some boilerplate as we don’t support template haskell, as you pointed out.

We’re somewhat divided in this approach as it poses some performance challenges. I think there is a blog post somewhere by @Martin_Huschenbett that discusses this in more detail. Here:

Maybe he can comment further on the (performance conscious) way of doing field updates! :grin:

3 Likes

The HasField typeclass from DA.Record is perfect for this!

HasField gives you getter and setter functions for each record field automatically. The stdlib documentation is a bit lacking currently, so let me try to explain how you can use it.

HasField x r a is a typeclass that takes three parameters. The first parameter x is the field name, the second parameter r is the record type, and the last parameter a is the type of the field in this record. For example, if I define a type:

data MyRecord = MyRecord with
    foo : Int
    bar : Text

Then I get, for free, the following HasField instances:

HasField "foo" MyRecord Int
HasField "bar" MyRecord Text

If I want to get a value using HasField, I can use the getField function:

getFoo : MyRecord -> Int
getFoo r = getField @"foo" r

getBar : MyRecord -> Text
getBar r = getField @"bar" r

Note that this uses the “type application” syntax (f @t) to specify the field name.

Likewise, if I want to set the value in the field, I can use the setField function:

setFoo : Int -> MyRecord -> MyRecord
setFoo a r = setField @"foo" a r

setBar : Text -> MyRecord -> MyRecord
setBar a r = setField @"bar" a r

Note that in all these examples I don’t have to pass in the arguments explicitly. I could have written it this way:

getFoo : MyRecord -> Int
getFoo = getField @"foo"

getBar : MyRecord -> Text
getBar = getField @"bar"

setFoo : Int -> MyRecord -> MyRecord
setFoo = setField @"foo"

setBar : Text -> MyRecord -> MyRecord
setBar = setField @"bar"

So to answer the original question, you can use setField @"a" to set the "a" field in your record. And this will work for any record type with an "a" field.

HasField is actually part of Daml’s built-in mechanism for record handling, and it’s used by the Daml compiler, so it’s something that is specifically optimized in the Daml compiler. As long as the record type is fixed (i.e. not polymorphic), it should be as efficient as writing a record get/set manually. So, compared to using a Lens library, this shouldn’t have any negative performance impact.

5 Likes

Thank you, @Sofia_Faro for this comprehensive and very helpful response!

2 Likes