Lenses tutorial

This guide will show you how to create powerful lenses using Pathex.Lenses module

Why

Pathex.Lenses module provides functions and macro for creating lenses which can work with collections, do pattern-matching and solve common problems in an elegant and reusable way

Common tasks

Here we will take a look at common tasks in nested data structure manipulation

Star lens

What if we need to update all values matching matching specific pattern

This simple task can be solved using Elixir's Enum module but is kind of tought to be polymorphic and reusable for different patterns or types of collections

Let's say we have a list of users with roles and we want to add access to admin page for all admins:

# The list looks like this
users = [
  %{fname: "John", sname: "Doe", role: "CEO",   access: ["admin_page", "users_page"]},
  %{fname: "Mike", sname: "Lee", role: "admin", access: ["users_page"]},
  %{fname: "Fred", sname: "Can", role: "admin", access: ["users_page"]},
  %{fname: "Dave", sname: "Lee", role: "user",  access: []}
]

With Enum this would look like

new_users =
  Enum.map(users, fn
    %{role: "admin", access: access} = user ->
      %{user | access: Enum.uniq(["admin_page" | access])}

    other ->
      other
  end)

But using Pathex.Lenses this would look like

use Pathex
import Pathex.Lenses

# `l` in the end stands for `lens`
adminl = matching(%{role: "admin"})
accessl = path(:access)

# `star()` works like `*` in shell.
# so `star() ~> path(:file)` works almost like `*/file`

# Here `star() ~> adminl` translates to `select * where role == "admin"`
# and `star() ~> adminl ~> accessl` translates to `select access where role == "admin"`
new_users = Pathex.over!(users, star() ~> adminl ~> accessl, & Enum.uniq(["admin_page" | &1]))

Some lens

What if we need to update first value matching specific pattern (in our example it will be {:hello, _}) and we need to return {:ok, updated_collection} if the first value was updated and :error if not

This task can be also done using Enum, but what if we can write the solution

which would be as simple as saying Update first value in collection, which matches the pattern?

With Enum this would look really terrible. I couldn't come up with polymorphic solution which would fit less than 20 lines of code

But with Pathex.Lenses this would be as simple as

use Pathex; import Pathex.Lenses
def update_first_hello(collection, update_func) do
  hellol = matching({:hello, _})
  Pathex.over(collection, some() ~> hellol, update_func)
end

Matching lens

Conditional lens, which returns the value if the value itself matches the given pattern

use Pathex
import Pathex.Lenses

adminl = matching(%{role: :admin})

user1 = %User{role: :user, name: "Mr Dog"}
:error = Pathex.view(user1, adminl)

user2 = %User{role: :admin, name: "Mr Dog"}
{:ok, ^user2} = Pathex.view(user2, adminl)

Most useful lens in combination with start and some

use Pathex
import Pathex.Lenses

@spec change_roles([User.t()]) :: :ok
def change_roles(users) do
  adminsl = star() ~> matching(%{role: :admin})
  Pathex.at(users, adminsl, &send_email/1)

  :ok
end