Here’s why lawful lenses matter (even in Elixir):

Couldn’t fit optics in my book, but I can build them into Funx.

funx

3 Likes

I certainly feel like lenses could get a lot more attention on elixir, but does a blogpost in favor of them need to start with dunking on elixirs built in lenses? – especially given most of the critic stated could be mitigated by a single Enum.map?

garfield = %{
  name: "Garfield",
  weight: 20,
  owner: %{
    name: "Jon"
  }
}

invalid_owner_age_path = [:owner, :age] |> Enum.map(&Access.key!/1)

get_in(garfield, invalid_owner_age_path)
# ** (KeyError) key :age not found in: %{name: "Jon"}

put_in(garfield, invalid_owner_age_path, 40)
#** (KeyError) key :age not found in: %{name: "Jon"}

update_in(garfield, invalid_owner_age_path, fn curr -> curr + 1 end)
# ** (KeyError) key :age not found in: %{name: "Jon"}

There’s also reasons why elixir doesn’t allow you to access structs when using bare keys. Structs in elixir are not treated as “data containers”, but as user defined datatypes, which might want to be used as a scalar. If you want that user defined datatype to be accessible like other map data you can implement the Access behaviour, which can then take into account that structs internals. The reason for a behaviour over a protocol is iirc due to performance reasons, potentially even ones of the past, but I’m not sure. You can certainly see how Access wants to be a protocol in spirit. Sadly this means this is less likely to be implemented that e.g. an Enumerable especially for library code.

But it’s always possible to access struct keys without Access implemented by forcing map key access using Access.key or Access.key!, just like Map.get works on structs.

7 Likes

As someone unfamiliar with the surrounding theory, I found the framing in terms of Elixir’s stdlib to be pedagogically helpful. Of course the points you make about Access are also useful context and could be incorporated.

The difference between behavio(u)rs and protocols is that of extensibility, right? So by turning Access into a protocol you are essentially saying that “other people” can decide whether my struct can be accessed like a map. Not just the user themselves, but any third party that invokes defimpl in the user’s dependency tree.

Does that actually make semantic sense?

TBH this question may be too theoretical. What’s the canonical example of a struct that should implement Access?

1 Like

You are correct about protocols. In my experience they’re criminally underused in Elixir and most people have fixation on behaviours for polymorphism but protocols are where it’s at.

1 Like

Elixir’s approach is pragmatic and works well. But when the same path behaves differently depending on the operation (nil vs. create vs. crash), it’s easy to miss encoding an invariant. Lenses just package that consistency into a reusable abstraction.

Hmm… You can.. but should? That’s a tough one. I searched Ash and Phoenix for “defimpl Access” and didn’t see any.

This Lens does not convert a struct into a map, allowing it to continue to behave as a scalar while exposing a narrowly defined, explicit access path.

Access is not a protocol, you will not find defimpl Access. You should search for “def fetch”!

2 Likes

I finally had some time to go through the article. Knowing lenses from Haskell and being disappointed by the lack of interest in functional programming in the community, it’s a breath of fresh air. Nice introduction and examples. Good job!

Thanks!