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.

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.

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])]

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).

: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.

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.