Lunettes.jl is a small package for getting and setting fields in large nested data structures in a safe, mutation-free, way.
In a Julia REPL, run
import Pkg
Pkg.add("Lunettes")Simple as!
The package exports one type: a Lens{A,B}, and two functions: getr and setr.
Lenses are used to manipulate and query immutable data structures with a convenient notation.
Suppose we have defined the following struct:
struct MyStruct
    first_field
    second_field
endLet's instantiate two lenses:
first_field_lens = Lens{MyStruct,:first_field}()
second_field_lens = Lens{MyStruct,:second_field}()
We can then extend the function getr on these types in the following way
function getr(::Lens{MyStruct,:first_field}, a)
    a.first_field
end
function getr(::Lens{MyStruct,:second_field}, a)
    a.second_field
endand similarly for setr
function setr(::Lens{MyStruct,:first_field}, a, c)
    MyStruct(c, a.second_field)
end
function setr(::Lens{MyStruct,:second_field}, a, c)
    MyStruct(a.first_field, c)
endWe can now use our lenses in the following way:
my_struct = MyStruct("Hello", 9.9)
getr(first_field_lens, my_struct) == "Hello" #true
setr(second_field_lens, my_struct, 1.0) == MyStruct("Hello", 1.0) #trueWe have gained not a lot for quite a lot of typing! We will see the power of lenses when we compose them together for manipulating deeply nested data structures.
Lunettes also defines a macro: @lens. This macro automatically does the work for us of extending getr and setr in the obvious way.
Consider the complicated nested data structure below.
@lens struct Curtain
    color::String
    state::String
end@lens struct Window
    frame_color::String
    left_curtain::Curtain
    right_curtain::Curtain
endSuppose we would like to manipulate this data structure, and make queries of its values.
Let us define a new Lens as the composition of two simpler lenses.
left_curtain_state = Lens{Window,:left_curtain}() ○ Lens{Curtain,:state}()
left_curtain_color = Lens{Window,:left_curtain}() ○ Lens{Curtain,:color}()We will be able to use left_curtain_state as a way to access (with getr) and change (with setr) different windows.
Lets initialise a Window:
my_window = Window("White", Curtain("Purple", "Open"), Curtain("Orange","Shut"))Suppose we would like to reach into our Window and learn the state of the left curtain. We could write
my_window.left_window.stateor we could write
getr(left_curtain_state, my_window)Better yet, suppose we would like to update the window and have the left curtain shut. We could write:
my_new_window = Window("White", Curtain("Purple", "Shut"), Curtain("Orange","Shut"))or instead
setr(left_curtain_state, my_window, "Shut")In fact, both getr and setr are automatically curried, so we could even write
my_third_window = my_window |>
setr(left_curtain_state, "Shut") |>
setr(left_curtain_color, "Goldish Brown")producing a third window based off my_window that has its left curtain shut and a new color!
The @lens macro is doing nothing more that defining a method for the getr and setr functions for each member of the struct.
For instance, after defining the Curtain struct, the function getr(::Lens, a) is now defined for the Lens of types Lens{Curtain,:state}() and Lens{Curtain,:color}(). In fact their defintions are very simple:
function getr(l::Lens{Curtain,:state}, a)
    a.state
end
function setr(l::Lens{Curtain,:state}, a, c)
    Curtain(a.color, c)
endBy defining these getters and setters for these basic lenses, and then by composing lenses, we automatically obtain the correct definition of getr (and setr) for Lens{Window,:left_curtain}() ○ Lens{Curtain,:state}().