Lightweight Elixir lenses
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
config
img
lib
test
.gitignore
.iex.exs
.pre-commit-config.yaml
.tool-versions
CHANGELOG.org
LICENSE.md
README.org
circle.yml
mix.exs
mix.lock

README.org

Focus

https://circleci.com/gh/tpoulsen/focus.svg?style=svg https://img.shields.io/hexpm/v/focus.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.