View Source Lenses tutorial

This guide will show you how to create powerful lenses using Pathex.Lenses module. Here we will take a look at common tasks in nested data structure manipulation which can be solved using Pathex.Lenses.

For all values in collection

What if we need to update all values in the collection matching specific pattern?

This simple task can be solved using Elixir's Enum module but it is quite tough 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.star/0 and Pathex.Lenses.matching/1 this would look like

import 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]))

For any value in collection

What if we need to update first value matching specific pattern (in our example it will be {:option, _}) 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 a polymorphic solution that would fit in less than 20 lines of code.

But with Pathex.Lenses.some/0 and Pathex.Lenses.matching/1 this would be as simple as

use Pathex; import Pathex.Lenses
def update_first_option(collection, update_func) do
  Pathex.over(collection, some() ~> matching({:option, _}), update_func)
end

For any value in nested structure

Allright, we have a nested structure with various types inside and we need to find any value in any map for which the special condition occurs and change it

Think of an HTML-like structure without attributes like

{"html", [
  {"head", [...]},
  {"body", [...]}
  ]}

And we need to update just one label with string which ends with "Please click subscribe button"

In Elixir we'd need to write a recursive function, which would untrivially update tuples and lists

Using Pathex.Combinator.combine/1, Pathex.Lenses.some/0 and Pathex.Lenses.filtering/1 it's very simple

use Pathex; import Pathex.Lenses; import Pathex.Lenses.Recur

path_to_subscribe =
  combine(fn recursive -> some() ~> (recursive() ||| matching()) end)
  ~> matching({"label", _}) # To find a label
  ~> path(1)               # To get to value of a label
  ~> filtering(& String.ends_with?(&1, "Please click subscribe button")

Pathex.set(document, path_to_subscribe, "Do not subscribe, hehe")

Recurring

Sometimes you may want to create a recurring lens. We have a Pathex.Combinator.combine/1 which can help you. It is a fixed point combinator for paths, what may sound too smart, but this is a function which makes your lens be able to compose with itself.

Consider this representation of an HTML

{"html", [], [
  {"body", [], ...}
  {"html", [], ...}]}

And we want to update a node which has id=big-image. It would be nice to have a lens which would do something like

path(2) ~> some()
  ~> path(2) ~> some()
    ~> path(2) ~> some()`

Until it finds the tag we're looking for.

Meet the Pathex.Combinator.combine/1. Using this higher order function we can represent lens combining with itself. The lens above can be specified with this code.

combine(fn recursive ->
  path(2) ~> some() ~> recursive
end)

And we also need to find a specific id, so let's combine this lens with matching(_)

path_to_id =
  combine(fn recursive ->
    path(2) ~> some ~> recursive
  end)
  ~> matching({_, [{"id", "big-image"}], _})

However, it has a drawback -- this recursive lens will never succeed, because we didn't specify the exit. Let's fix it

path_to_big_image =
  combine(fn recursive ->
    path(2) ~> some ~> (recursive ||| matching(_))
  end)
  ~> matching({_, [{"id", "big-image"}], _})

{"div", [{"id", "big-image"}], [
  {"img", [{"src", "/image.png"}], ""}]} = Pathex.view!(html, path_to_big_image)