hi all :wave: With arrow optics is it possible to ...
# arrow
n
hi all 👋 With arrow optics is it possible to modify multiple values in the same instruction? maybe something like :
Copy code
fun t(dto: UpgradeTrigger) {
    UpgradeTrigger.run {
        title.modify(dto) { it.toUpperCase() }
        subtitle.modify(dto) { it.toUpperCase() }
    }
}
without having to create multiple copies
Copy code
fun t(dto: UpgradeTrigger) {
    UpgradeTrigger.run {
        val copy = title.modify(dto) { it.toUpperCase() }
        subtitle.modify(copy) { it.toUpperCase() }
    }
}
s
I am using my own function to chain the optics operations (Not sure it’s the best way, but works for me so far)
Copy code
fun <T> opticsCompose(entity: T, vararg transformations: (T) -> T): T {
    return transformations.fold(entity, { e, transformation -> transformation(e) })
}
Then you can use it as:
Copy code
opticsCompose(template,
            { t -> DocumentTemplate.title.set(t, input.title) },
            { t -> DocumentTemplate.description.set(t, input.description) },
            { t -> DocumentTemplate.template.set(t, input.template) }
        )
n
Nice 🙂
thanks
s
Yes, in that case you'd want to chain. You could also write a manual
Lens<UpdateTrigger, Pair<String, String>>
Which would allow you to do
UpdatedTrigger.titleAndSubtitle.modify(dto) { (title, subtitle) ->  Pair(title.toUpperCase(), subtitle.toUpperCase()) }
But we currently don't have such a combinator. It shouldn't be too hard to write, and I think this might be a useful addition in Optics in any1 is interested in contributing I'd be happy to help!
Also I'm giving a talk next week on Optics in case you're interested! https://www.47deg.com/academy/2020-07-14-looking-into-immutable-data-with-optics-talk/
👍 4
n
Cool ! I ll give the lens a try 🙂 in my case instead of 4 lines that ll be 2
j
Something like haskells "&" operator is much needed ...
Copy code
dto
  & title %~ toUpper
  & subtitle %~ toUpper
& is function application with the arguments reversed (instead of f(a) you have a & f. apply is probably closest to this)
%~ is modify
just reads much nicer. @simon.vergauwen is it possible to return a partially applied function application in the modify function so that this becomes possible? If modify would return
(s) -> t
instead of taking
s
and returning
t
one could write
dto.apply(title.modify { it.toUpperCase() }).apply(subtitle.modify { it.toUpperCase() })
Anyway, combining optics works, but avoiding the intermediate copy is not possible afaik. You will always need to invoke the lenses one by one even if you write a combinator
Copy code
combine :: Lens a b -> Lens a c -> Lens a (b, c)
combine la lb = lens (\c -> (c ^. la, c ^. lb)) (\c (a, b) -> c & la .~ a & lb .~ b)
Sorry for the haskell code, its just what I am most familiar with when it comes to optics :)
Only way I see to avoid that is to manually compose them. The reason this does not work generally is because a Lens makes no assumption about how the setter side works: Consider two lenses providing virtual fields
firstName
and
lastName
both backed by the same
fullName
field on the object (the lens splits the actual property and provides the first or last name). These two lenses cannot be combined easily as they both operate on the same field and the update semantics are not clear at all. For such a combinator we always need to know how the lens works. Combining only ever works if either done in sequence (creating intermediate copies) or by hand rolling combined lenses.
Edit apply is wrong here, but think of a similar function
fun <T, T1> T.applyF(f: (T) -> T1): T1
s
Yes, @Jannis that is definitely possible. That's probably
lift
, right?
j
As in the mtl one? 😕 Not entirely sure, actually I don't even think such an operator exists just yet. (Taking about an analog to
&
)
s
Nono, Optics have a lift operator. For
Plens<S, T, A, B>
it takes
(A) -> B
and returns
(S) -> T
So it lifts functions from the focus type to the source type.
Easily to use in
Stream(..).map(lens.lift(::mapper))
j
well yeah that is exactly it. Now all you need is reverse function application aka
&
and you can write:
Copy code
dto.applyF(title.lift { it.toUpperCase() }).applyF(subtitle.lift { it.toUpperCase() })
Not sure how the structure in arrow-optics works, but is
lift
available to all optics or just lenses because it should work for traversals as well right?
Btw that is a very confusing yet way too fitting name to change it ^^
s
Yes, it should be available for all optics.
Yes, everything in the library can be improved I guess. It was my first Arrow project ever, and I jumped into IO when I added the DSL and it became stable.
I.e. Monocle in Scala is looking to use inheritance, which would make Optics more lightweight on the JVM etc
j
Yeah ik. I also only really get this view because I've become quite familiar with haskell lenses and they just compose trivially, but replicating that outside of haskell seems hard which is probably an understatement. At least for haskell:lens' way of encoding that is. The haskell:optics library is probably much closer to what arrow-optics does as it uses actualy objects rather than functions...
s
Yes, I love haskell:lens but I don't think we can encode it in a similar manner atm. Perhaps later with Arrow-Meta we could build something that is more similar to Haskell:lens. However the idea is that the DSL is meant for usage. That would give you a more similar style to haskell:lens
j
Yeah stacksafety alone would kill such an encoding, the constraints on the functions are also terrible for inference and as long as the haskell:optics style is not too slow it will work fine for most things