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