Stream: beginners

Topic: Implementing lenses in Roc


view this post on Zulip Aurélien Geron (Sep 20 2026 at 10:40):

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?

view this post on Zulip Richard Feldman (Sep 20 2026 at 12:46):

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:

view this post on Zulip Aurélien Geron (Sep 20 2026 at 20:40):

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

view this post on Zulip Richard Feldman (Sep 20 2026 at 21:14):

so to be completely honest I think lenses are a bad idea, and shouldn't be used in any language. :sweat_smile:

view this post on Zulip Richard Feldman (Sep 20 2026 at 21:15):

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:

view this post on Zulip Aurélien Geron (Sep 20 2026 at 21:28):

Gotcha. :+1:

view this post on Zulip Aurélien Geron (Sep 20 2026 at 22:11):

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?

view this post on Zulip Anton (Sep 21 2026 at 12:19):

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.

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:01):

yeah those are language-level things though

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:02):

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)

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:03):

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

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:04):

(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:)

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:05):

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)

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:06):

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

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:06):

whereas in Haskell the equivalent would be Str -> Str and Str -> IO(Str)

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:07):

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)

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:07):

and then Haskell's runtime takes care of turning that big chain of IOs into actual effects etc.

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:08):

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

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:08):

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

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:16):

there are various ways to do this in Haskell, and it's a common technique, but what they all have in common is:

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:17):

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:

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:19):

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)

view this post on Zulip Richard Feldman (Sep 21 2026 at 15:20):

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:

view this post on Zulip Aurélien Geron (Sep 21 2026 at 19:13):

Thanks for the clear explanation. :blush:

view this post on Zulip Jared Ramirez (Sep 23 2026 at 21:55):

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

view this post on Zulip Jared Ramirez (Sep 23 2026 at 21:56):

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