What's an idiomatic way to implement lenses in Roc?
Astra suggested Lens(whole, part) : { get : whole -> part, set : whole, part -> whole }, to which I added a compose function and an over function:
Lens(whole, part) := { get : whole -> part, set : whole, part -> whole }.{
compose : Lens(outer, inner), Lens(inner, part) -> Lens(outer, part)
compose = |outer, inner| {
{
get: |whole| whole |> outer.get |> inner.get,
set: |whole, value| {
updated = whole |> outer.get |> (inner.set)(value)
whole |> (outer.set)(updated)
},
}
}
over : Lens(whole, part), whole, (part -> part) -> whole
over = |lens, whole, transform| {
updated = whole |> lens.get |> transform
whole |> (lens.set)(updated)
}
}
I can then use it like this:
Person := { address : Address, ... }.{
Address : { street : Str, house_number : U64, place : Str, country : Str }
...
}
address : Lens(Person, Person.Address)
address = {
get: |person| person.address,
set: |person, value| { ..person, address: value },
}
street : Lens(Person.Address, Str)
street = {
get: |street_address| street_address.street,
set: |street_address, value| { ..street_address, street: value },
}
set_current_street : Person, Str -> Person
set_current_street = |person, new_street| {
person |> (address.compose(street).set)(new_street)
}
It works fine, but perhaps I'm overlooking something much simpler?
I wouldn't think there would be demand for lenses in Roc because of how our record system works - is there a specific use case that isn't working well? :thinking:
It's just an Exercism exercise that the Haskell track has and I thought it would be nice to add to the Roc track as well (I submitted PR #227 to add it). However, I agree that the benefit of lenses isn't obvious in this exercise. I still thought it was interesting, however.
Perhaps the exercise would be more valuable if I tweaked it to work on a deeper structure? Or on a more heterogeneous data structure (i.e., containing more than just records, such as Dict)?
so to be completely honest I think lenses are a bad idea, and shouldn't be used in any language. :sweat_smile:
so my preference would be to not put it in the ecercism track at all, so people don't think it's something they should be doing in Roc :laughing:
Gotcha. :+1:
I closed the PR. I'm curious to know if there are other common functional programming patterns that you do not recommend (in general, or specifically in Roc)?
Also, could you please elaborate on why you don't recommend lenses in _any_ language? Is it because it doesn't really simplify the code at all?
if there are other common functional programming patterns that you do not recommend (in general, or specifically in Roc)?
I think I recall Richard saying that laziness did not prove out to be a good idea in practice. Currying is another one.
yeah those are language-level things though
it's totally fine to manually curry a function when you have a specific reason to - so, I could actually see an Exercism exercise on that - e.g. showing how to turn foo = |a, b, c| { ... } and foo(1, 2, 3) into foo = |a| |b| |c| { ... } and then foo(1)(2)(3)
and similarly it's also fine to make things lazy, e.g. might also be good to demonstrate my_expensive_calculation = || { ... } and then only calling my_expensive_calculation() in some conditional branches but not others
(note that for that one, you'd need to make sure that my_expensive_calculation closes over some value that isn't available at compile time...otherwise it will just get evaluated at compile time and it won't be a very useful example :smile:)
something that I'd say is more reasonable in other languages than Roc would be what I call "effect interpreters" although you can find them by other names (e.g. when people talk about "free monads" this is usually what they're talking about)
so the idea there is that, for example, in Roc you can have a Str -> Str function that's pure, and a Str => Str function that can also do effects
whereas in Haskell the equivalent would be Str -> Str and Str -> IO(Str)
and then you chain the IOs together (kinda like Promises in other languages, except unlike Promises they don't run the effect right when you instantiate them)
and then Haskell's runtime takes care of turning that big chain of IOs into actual effects etc.
so if you're already doing things in that IO style, a thing you can do in Haskell (and similar languages) is create your own IO alternative that explicitly enumerates the different operations, e.g. FileRead, FileWrite, etc. -so instead of Str -> IO(Str) it's like Str -> MyEffects(Str) or whatever
and then in main, you basically have an interpreter that traverses the whole MyEffects data structure and translates it into IO for the Haskell runtime
there are various ways to do this in Haskell, and it's a common technique, but what they all have in common is:
there are other, more hand-wavy benefits like "it separates the what from the how" but personally I think that's only beneficial once you have translated it into "doing the how part in some concretely different way, to get some concrete benefit" :smile:
anyway, Roc's effects system is intentionally designed so that you can avoid the downsides of effect systems (slower programs, plus the ergonomics difference between just changing -> to => while calls stay the same, as opposed to having to change to a wrapper with chaining) while getting what I consider to be all the important benefits (testing without actually running the effects, hooks for logging etc., and reflecting the effect in the types, should you decide those are things that actually make sense for your program - which I don't think is a given for every program, certainly)
so I wouldn't encourage using effect systems in Roc, because Roc has been intentionally designed to get their main benefits without their drawbacks! :smiley:
Thanks for the clear explanation. :blush:
you can see this same kind of thing manifest in other places too, eg in Elm, ports are kinda analogous the host’s effectful functions / a haskell effect systems senders/constructors
in headless elm you could also swap out one JS harness for a different one to use a different ports interpreter for eg tests
Last updated: Sep 24 2026 at 15:59 UTC