Specifying symbol type for `HasField x r a`

I know that the HasField typeclass is used to access fields in DAML using the dot notation. e.g. foo.a.b.c as I’ve seen error message allude to this when I mistype a field name.

I’d like to use this to write a function wich modifies data. Note you can’t do foo.a.b.c = x, and have to write instead foo with a = foo.a.b with ... which is really messy. I’d like to be able to write a function that takes the field name as parameter and then modifies it in some way.

For instance, say I have a data type:

data Outer = Outer with 
  a: Option[Inner]
  b: List[Inner]

and I want to modify the Inner data type. I’ve come up with something like this:

updateInner : (Functor f, HasField x Outer (f Inner)) => (Inner -> Inner) -> Outer -> Outer
updateInner fi o = setField (fmap fi (getField o)) o

To get this to compile I had to enable AllowAmbiguousTypes language feature. But even so, calls to this function complain about x being ambiguous at the call site. Note the type-class is declared as

class HasField x r a where

    getField
        : r -> a

    setField
        : a -> r -> r

You see, x is only in the class declaration, but it’s not passed in as a parameter to either of the functions.

Is there a way to work around this?

I’ve tried to specify the type of inner to be a like so updateInner @"a" (\inner -> ...) outer, but it complains that x is of the wrong kind (Symbol).

I’ve tried adding x as a parameter to the function, but the compiler complains similarly. Some googling has shown that there is a Proxy type in Haskell that allows you to work around this, but I don’t think it’s in the DAML stdlib.

1 Like

Hi Luciano,

that’s a very interesting problem. The solution mostly involves sprinkling a few more types across your code. Despite that, you will still need to enable the AllowAmbiguousTypes language extension because the type parameter f only appears within the type class constraints.

First of all, you need to tell the compiler that you want to access the x field in updateInner's setField and getField:

updateInner : forall x f. (Functor f, HasField x Outer (f Inner)) => (Inner -> Inner) -> Outer -> Outer
updateInner fi o = setField @x (fmap fi (getField @x o)) o

Notice how we added @x type applications to setField and getField. For x to be in scope at these places, we need to explicitly introduce it (and also f) using the forall x f. abstraction in the type signature of updateInner. Having the x as the first type parameter makes call sites more convenient since you only have to provide the field name, as in updateInner @"a", and don’t need to provide the functor, for field a it would be Optional here. Thus, if we assume type Inner = Int, we can use updateInner like

incrementA : Outer -> Outer
incrementA outer = updateInner @"a" (\i -> i + 1) outer

I hope that solves your problem,

Martin.

1 Like

Hey @Martin_Huschenbett , thank you for that lengthy explanation. That was in fact really helpful - I’ve previously seen forall many times, but often as not I could just leave it out and my code would compile fine. But now I understand what it actually does! It’s made a lot of things clear for me beyond this particular problem.

So, in retrospect, those error messages now make sense. The compiler expected the first type parameter to be f (because it appeared first in the function signature, it expected it first), and hence was complaining that type Symbol (of x) didn’t match * -> * (of f).

Thanks!

You might use lenses and prisms in such a case (they have been implemented in daml, although Im not implying this is supported or advocated and you would need to pull in all the machinery).
I’ve added a ‘c’ field to illustrate how the lenses can be rejigged to “get” at the list inside a list of tuples. You would use “set”-ters to create another outer with some (possibly) changed structure.

data Outer = Outer with
  a : Optional [Inner]
  b : [[Inner]] 
  c : [(Text, [Inner])]

   deriving Show

_a : Lens' Outer (Optional [Inner])
_a = fieldLens @ "a"

_b : Lens' Outer [[Inner]]
_b = fieldLens @ "b"

_c : Lens' Outer [(Text, [Inner])]
_c = fieldLens @ "c"

let outer = Outer with a=Some [1,2], b=[[1..10]], c=[("In a Tuple", [1..10])]

Again using type Inner= Int , in a scenario these would be used as:

  -- a
  debug $ over traversed (+1) $ outer ^. _a . _Some
  -- [2,3]

  -- b
  debug $ over (traversed . traversed) (* 10) $ outer ^. _b
  -- [[10,20,30,40,50,60,70,80,90,100]]

  -- c
  debug $ over (traversed . _2 . traversed ) (+10) $ outer ^. _c
  -- [("In a Tuple",[11,12,13,14,15,16,17,18,19,20])]

:grin: That’s in fact where I was going next … I was curious whether it would be possible to write a ‘generic’ lens using this typeclass. I guess fieldLens does just that.

I wonder if you can write directly something like outer ^. (fieldLens @"a" @Outer) . (fieldLens @"b" @B) without having to declare the lenses on a separate line?

Yes, @Luciano, this is possible:

  -- a
  debug $ over traversed (+1) $ outer ^. (fieldLens @"a" @Outer) . _Some
  -- [2,3]

  -- b
  debug $ over (traversed . traversed) (* 10) $ outer ^. (fieldLens @"b" @Outer)
  -- [[10,20,30,40,50,60,70,80,90,100]]

  -- c
  debug $ over (traversed . _2 . traversed ) (+10) $ outer ^. (fieldLens @"c" @Outer)
  -- [("Text",[11,12,13,14,15,16,17,18,19,20])]
1 Like

If you care about performance, please be very careful with your use of lenses. For instance, if you write code like

set (fieldLens @"field_1") v_1 . set (fieldLens @"field_2") v_2 . ... . set (fieldLens @"field_n") v_n $ foo

you will allocate n intermediate records. If you record has m fields, this seemingly innocent line has a runtime and memory complexity of O(n*m). It is incredibly hard to write an optimizer for a strict language like DAML which simplifies this into the straightforward

foo with field_1 = v1; field_2 = v_2; ...; field_n = v_n

(And it is by no means easy to do so in a lazy language either.)

Currently, the latter also has a complexity of O(n*m) but we’re already working on reducing this to it’s optimal complexity of O(n+m).

1 Like

:thinking: Do you really mean the second example i.e. with syntax? Or do you mean the lens example?

I really mean the second example wih the with syntax. But as I said that’s the current state of affairs and we’re alrady working on fixing it.

1 Like

The promised fix has landed in master and will be contained in the next snapshot release and later on in DAML SDK 1.5.0.

1 Like