/focus

Lightweight Elixir lenses

Primary LanguageElixirBSD 2-Clause "Simplified" LicenseBSD-2-Clause

Focus

https://img.shields.io/hexpm/v/focus.svg https://github.com/smpoulsen/focus/actions/workflows/test.yml/badge.svg

img/focus_lens_prism.png

Lightweight, pure Elixir functional optics[fn:1].

A lens is a value that composes a getter and a setter function to produce a bidirectional view into a data structure. This definition is intentionally broad—lenses are a very general concept, and they can be applied to almost any kind of value that encapsulates data. – Racket ‘lens’ documentation

Usage

To construct a lens:

# A lens for the key :name
Lens.make_lens(:name)

# A lens for the key "name"
Lens.make_lens("name")

# A lens for the second item in a tuple:
Lens.make_lens(1)

Each lens provides both a getter and a setter for the accessor it was created for.

Lenses can be used to access and/or modify structured data:

# Extract a value from a simple map:

person = %{name: "Homer"}
nameLens = Lens.make_lens(:name)

Focus.view(nameLens, person)
# "Homer"

Focus.set(nameLens, person, "Bart")
# %{name: "Bart"}

Focus.over(nameLens, person, &String.upcase/1)
# %{name: "HOMER"}

Their real utility comes in operating on nested data. Lenses can be created by composing other lenses in order to traverse a data structure:

person = %{
  name: "Homer",
  address: %{
    locale: %{
      number: 742,
      street: "Evergreen Terrace",
      city: "Springfield",
    },
    state: "???"
  }
}

# To access the street, we can compose the lenses that lead there from the top level.
# Lenses can be composed with Focus.compose/2, or the infix (~>) operator.

address = Lens.make_lens(:address)
locale =  Lens.make_lens(:locale)
street =  Lens.make_lens(:street)

address
~> locale
~> street
|> Focus.view(person)
# "Evergreen Terrace"

address
~> locale
~> street
|> Focus.set(person, "Fake Street")
# person = %{
#   name: "Homer",
#   address: %{
#     locale: %{
#       number: 742,
#       street: "Fake Street",
#       city: "Springfield",
#     },
#     state: "???"
#   }
# }

Macros

Optic creation

deflenses
A wrapper around defstruct that additionally defines lenses for the struct’s keys inside the module.
defmodule User do
  import Lens
  deflenses name: nil, age: nil

  # deflenses defines %User{}, User.name_lens/0, and User.age_lens/0
end
    

Functions

Optic creation

  • Lens.make_lens/1
  • Lens.make_lenses/1
  • Lens.idx/1

Pre-made optics

  • Prism.ok/0
  • Prism.error/0

Optic application

  • Focus.view/2
  • Focus.over/3
  • Focus.set/3
  • Focus.view_list/2
  • Focus.has/2
  • Focus.hasnt/2
  • Focus.fix_view/2
  • Focus.fix_over/3
  • Focus.fix_set/3

Optic composition

  • Focus.compose/2, (~>)
  • Focus.alongside/2

Installation

  1. Add focus to your list of dependencies in mix.exs:
    def deps do
      [{:focus, "~> 0.3.5"}]
    end
        

References

Footnotes

[fn:1] This library currently combines Lenses and Prisms with Traversals in its implementation. Until v1.0.0, the API is subject to large and frequent change.