Kaleido.jl
KaleidoKaleido.nullsetterKaleido.settingasℝ₊Kaleido.settingasℝ₋Kaleido.settingas𝕀Kaleido.FLensKaleido.FlatLensKaleido.IndexBatchLensKaleido.KeyBatchLensKaleido.MultiLensKaleido.PropertyBatchLensKaleido.ToFieldKaleido.batchKaleido.constrainingKaleido.convertingKaleido.gettingKaleido.gettingKaleido.prettylensKaleido.settingKaleido.@batchlens
Kaleido — ModuleKaleido: some useful lenses
Kaleido.jl is a collection of useful Lenses and helper functions/macros built on top of Setfield.jl.
Features
Summary
- Batched/multi-valued update.  See 
@batchlens,MultiLens. - Get/set multiple and nested fields as a 
StaticArrayor any arbitrary multi-valued container. Seegetting. - Get/set fields with different parametrizations. See 
converting,setting,getting. - Computing other fields during 
setandget; i.e., adding constraints between fields. Seeconstraining. - Get/set dynamically computed locations.  See 
FLens. 
Batched/multi-valued update
Macro @batchlens can be used to update various nested locations in a complex immutable object:
julia> using Setfield, Kaleido
julia> lens_batch = @batchlens begin
           _.a.b.c
           _.a.b.d[1]
           _.a.b.d[3] ∘ settingas𝕀
           _.a.e
       end;
julia> obj = (a = (b = (c = 1, d = (2, 3, 0.5)), e = 5),);
julia> get(obj, lens_batch)
(1, 2, 0.0, 5)
julia> set(obj, lens_batch, (10, 20, Inf, 50))
(a = (b = (c = 10, d = (20, 3, 1.0)), e = 50),)(See below for what settingas𝕀 does.)
Get/set multiple and nested fields as a StaticArray
It is often useful to get the values of the fields as a vector (e.g., when optimizing a composite object with Optim.jl).  This can be done with getting(f) where f is a constructor.
julia> using StaticArrays
julia> lens_vec = lens_batch ∘ getting(SVector);
julia> @assert get(obj, lens_vec) === SVector(1, 2, 0.0, 5)
julia> set(obj, lens_vec, SVector(10, 20, Inf, 50))
(a = (b = (c = 10.0, d = (20.0, 3, 1.0)), e = 50.0),)Get/set fields with different parametrizations
Kaleido.jl comes with lenses settingasℝ₊, settingasℝ₋, and settingas𝕀 to manipulating fields that have to be restricted to be positive, negative, and in [0, 1] interval, respectively.  Similarly there are lenses gettingasℝ₊, gettingasℝ₋, and gettingas𝕀 to get values in those domains.  The naming is borrowed from TransformVariables.jl.
julia> lens = (@lens _.x) ∘ settingasℝ₊;
julia> get((x=1.0,), lens)  # log(1.0)
0.0
julia> set((x=1.0,), lens, -Inf)
(x = 0.0,)Kaleido.jl also works with AbstractTransform defined in TransformVariables.jl:
julia> using TransformVariables
julia> lens = (@lens _.y[2]) ∘ setting(as𝕀);
julia> obj = (x=0, y=(1, 0.5, 3));
julia> get(obj, lens)
0.0
julia> @assert set(obj, lens, Inf).y[2] ≈ 1It also is quite easy to define ad-hoc converting accessors using converting:
julia> lens = (@lens _.y[2]) ∘
           converting(fromfield=x -> parse(Int, x), tofield=string);
julia> obj = (x=0, y=(1, "5", 3));
julia> get(obj, lens)
5
julia> set(obj, lens, 1)
(x = 0, y = (1, "1", 3))Computing other fields during set and get
It is easy to add constraints between fields using constraining. For example, you can impose that field .c must be a sum of .a and .b by:
julia> obj = (a = 1, b = 2, c = 3);
julia> constraint = constraining() do obj
           @set obj.c = obj.a + obj.b
       end;
julia> lens = constraint ∘ MultiLens((
           (@lens _.a),
           (@lens _.b),
       ));
julia> get(obj, lens)
(1, 2)
julia> set(obj, lens, (100, 20))
(a = 100, b = 20, c = 120)Notice that .c is updated as well in the last line.
Get/set dynamically computed locations
You can use FLens to get and set, e.g., the last entry of a linked list.
Setting/getting multiple locations
Kaleido.@batchlens — Macro@batchlens begin
    lens_expression_1
    lens_expression_2
    ...
    lens_expression_n
endFrom $n$ "lens expression", create a lens that gets/sets $n$-tuple. Each "lens expression" is an expression that is supported by Setfield.@lens or such expression post-composed with other lenses using ∘.
See also batch which does all the heavy lifting of the transformation of the lenses.
Examples
julia> using Kaleido, Setfield
julia> lens = @batchlens begin
           _.a.b.c
           _.a.b.d ∘ converting(fromfield = x -> parse(Int, x), tofield = string)
           _.a.e
       end;
julia> obj = (a = (b = (c = 1, d = "2"), e = 3),);
julia> get(obj, lens)
(1, 2, 3)
julia> set(obj, lens, (10, 20, 30))
(a = (b = (c = 10, d = "20"), e = 30),)Kaleido.batch — Functionbatch(lens₁, lens₂, ..., lensₙ) :: LensFrom $n$ lenses, create a single lens that gets/sets $n$-tuple in such a way that the number of call to the constructor is minimized. This is done by calling IndexBatchLens whenever possible.
Examples
julia> using Kaleido, Setfield
julia> lens = @batchlens begin
           _.a.b.c
           _.a.b.d
           _.a.e
       end;
julia> @assert lens ==
           IndexBatchLens(:a) ∘ MultiLens((
               (@lens _[1]) ∘ IndexBatchLens(:b, :e) ∘ MultiLens((
                   (@lens _[1]) ∘ IndexBatchLens(:c, :d),
                   (@lens _[2]) ∘ Kaleido.SingletonLens(),
               )) ∘ FlatLens(2, 1),
           )) ∘ FlatLens(3)
julia> obj = (a=(b=(c=1, d=2), e=3),);
julia> get(obj, lens)
(1, 2, 3)
julia> set(obj, lens, (10, 20, 30))
(a = (b = (c = 10, d = 20), e = 30),)Kaleido.MultiLens — TypeMultiLens([castout,] lenses::Tuple)
MultiLens([castout,] lenses::NamedTuple)Examples
julia> using Setfield, Kaleido
julia> ml = MultiLens((
           (@lens _.x),
           (@lens _.y.z),
       ))
〈◻.x, ◻.y.z〉
julia> get((x=1, y=(z=2,)), ml)
(1, 2)
julia> set((x=1, y=(z=2,)), ml, ("x", "y.z"))
(x = "x", y = (z = "y.z",))
julia> ml = MultiLens((
           a = (@lens _.x),
           b = (@lens _.y.z),
       ))
〈◻.x, ◻.y.z〉
julia> get((x=1, y=(z=2,)), ml)
(a = 1, b = 2)
julia> set((x=1, y=(z=2,)), ml, (a=:x, b="y.z"))
(x = :x, y = (z = "y.z",))
julia> set((x=1, y=(z=2,)), ml, (b="y.z", a=:x))
(x = :x, y = (z = "y.z",))
julia> using StaticArrays
julia> ml = MultiLens(
           SVector,
           (
               (@lens _.x),
               (@lens _.y.z),
           )
       )
〈◻.x, ◻.y.z〉
julia> @assert get((x=1, y=(z=2,)), ml) === SVector(1, 2)Kaleido.PropertyBatchLens — TypePropertyBatchLens(names)Examples
julia> using Setfield, Kaleido
julia> lens = PropertyBatchLens(:a, :b, :c);
julia> get((a=1, b=2, c=3, d=4), lens)
(a = 1, b = 2, c = 3)
julia> set((a=1, b=2, c=3, d=4), lens, (a=10, b=20, c=30))
(a = 10, b = 20, c = 30, d = 4)Kaleido.KeyBatchLens — TypeKeyBatchLens(names)Examples
julia> using Setfield, Kaleido
julia> lens = KeyBatchLens(:a, :b, :c);
julia> get((a=1, b=2, c=3, d=4), lens)
(a = 1, b = 2, c = 3)
julia> set((a=1, b=2, c=3, d=4), lens, Dict(:a=>10, :b=>20, :c=>30))
(a = 10, b = 20, c = 30, d = 4)Kaleido.IndexBatchLens — TypeIndexBatchLens(names)Examples
julia> using Setfield, Kaleido
julia> lens = IndexBatchLens(:a, :b, :c);
julia> get((a=1, b=2, c=3, d=4), lens)
(1, 2, 3)
julia> set((a=1, b=2, c=3, d=4), lens, (10, 20, 30))
(a = 10, b = 20, c = 30, d = 4)Kaleido.FlatLens — TypeFlatLens(N₁, N₂, ..., Nₙ)Examples
julia> using Setfield, Kaleido
julia> l = MultiLens((
           (@lens _.x) ∘ IndexBatchLens(:a, :b, :c),
           (@lens _.y) ∘ IndexBatchLens(:d, :e),
       )) ∘ FlatLens(3, 2);
julia> get((x=(a=1, b=2, c=3), y=(d=4, e=5)), l)
(1, 2, 3, 4, 5)
julia> set((x=(a=1, b=2, c=3), y=(d=4, e=5)), l, (10, 20, 30, 40, 50))
(x = (a = 10, b = 20, c = 30), y = (d = 40, e = 50))Bijective transformations as lenses
Kaleido.converting — Functionconverting(; fromfield, tofield) :: LensExamples
julia> using Setfield, Kaleido
julia> halve(x) = x / 2;
julia> double(x) = 2x;
julia> l = (@lens _.y[2]) ∘ converting(fromfield = halve, tofield = double)
(@lens _.y[2]) ∘ (←double|halve→)
julia> obj = (x=0, y=(1, 2, 3));
julia> @assert get(obj, l) == 1.0 == 2/2
julia> set(obj, l, 0.5)
(x = 0, y = (1, 1.0, 3))Kaleido.setting — Functionsetting(xf::TransformVariables.AbstractTransform) :: LensLens to set value transformed by xf (and get value via the inverse transformation).
Examples
julia> using Setfield, Kaleido, TransformVariables
julia> l = (@lens _.y[2]) ∘ setting(as𝕀)
(@lens _.y[2]) ∘ (←|as𝕀→)
julia> obj = (x=0, y=(1, 0.5, 3));
julia> get(obj, l)
0.0
julia> @assert set(obj, l, Inf).y[2] ≈ 1
julia> @assert set(obj, l, -Inf).y[2] ≈ 0.0Kaleido.getting — Functiongetting(xf::TransformVariables.AbstractTransform) :: LensLens to get value transformed by xf (and set value via the inverse transformation).
Kaleido.settingasℝ₊ — Constantsettingasℝ₊ :: BijectionLensThis is a stripped-down version of setting(asℝ₊) that works without TransformVariables.jl.
Examples
julia> using Setfield, Kaleido
julia> l = (@lens _.y[2]) ∘ settingasℝ₊
(@lens _.y[2]) ∘ (←exp|log→)
julia> obj = (x=0, y=(0, 1, 2));
julia> @assert get(obj, l) == 0.0 == log(obj.y[2])
julia> @assert set(obj, l, -1) == (x=0, y=(0, exp(-1), 2))Kaleido.settingasℝ₋ — Constantsettingasℝ₋ :: BijectionLensThis is a stripped-down version of setting(asℝ₋) that works without TransformVariables.jl.
Examples
julia> using Setfield, Kaleido
julia> l = (@lens _.y[2]) ∘ settingasℝ₋
(@lens _.y[2]) ∘ (←negexp|logneg→)
julia> obj = (x=0, y=(0, -1, 2));
julia> @assert get(obj, l) == 0.0 == log(-obj.y[2])
julia> @assert set(obj, l, 1) == (x=0, y=(0, -exp(1), 2))Kaleido.settingas𝕀 — Constantsettingas𝕀 :: BijectionLensThis is a stripped-down version of setting(as𝕀) that works without TransformVariables.jl.
Examples
julia> using Setfield, Kaleido
julia> l = (@lens _.y[2]) ∘ settingas𝕀
(@lens _.y[2]) ∘ (←logistic|logit→)
julia> obj = (x=0, y=(0, 0.5, 2));
julia> get(obj, l)
0.0
julia> @assert set(obj, l, Inf).y[2] ≈ 1
julia> @assert set(obj, l, -Inf).y[2] ≈ 0Misc lenses
Kaleido.getting — Methodgetting(f) :: LensApply a callable f (typically a type constructor) before getting the value; i.e.,
get(obj, lens ∘ getting(f)) == f(get(obj, lens))This is useful for, e.g., getting a tuple as a StaticVector and converting it back to a tuple when setting.
Note that getting requires some properties for f and the values stored in the "field."  See the details below.
Examples
julia> using Kaleido, Setfield, StaticArrays
julia> obj = (x = ((0, 1, 2), "A"), y = "B");
julia> lens = (@lens _.x[1]) ∘ getting(SVector)
(@lens _.x[1]) ∘ (←|SVector{S, T} where {S, T}→)
julia> get(obj, lens) === SVector(obj.x[1])
true
julia> set(obj, lens, SVector(3, 4, 5))
(x = ((3, 4, 5), "A"), y = "B")julia> using Kaleido, Setfield, StaticArrays
julia> obj = (x = ((a = 0, b = 1, c = 2), "A"), y = "B");
julia> lens = (@lens _.x[1]) ∘ getting(Base.splat(SVector))
(@lens _.x[1]) ∘ (←|#60→)
julia> get(obj, lens) === SVector(obj.x[1]...)
true
julia> set(obj, lens, SVector(3, 4, 5))
(x = ((a = 3, b = 4, c = 5), "A"), y = "B")Details
The lens created by getting(f) relies on that:
The output value
y = f(x)can be converted back to the original valuexbyC(y)whereCis a constructor ofx; i.e., for anyxthat could be retrieved from the object through this lens,C(f(x)) == xThe conversion in the reverse direction also holds; i.e., for any
ythat could be stored into the object through this lens,f(C(y)) == y
The constructor C can be controlled by defining Setfield.constructor_of for custom types of x.
Kaleido.constraining — Functionconstraining(f; onget=true, onset=true)Create a lens to impose constraints by a callable f.
The callable
fmust be idempotent; i.e.,f ∘ f = f.If the original object already satisfies the constraint (i.e.
f(obj) == obj),onget=falsecan be passed safely to skip callingfduringget.
Examples
julia> using Kaleido, Setfield
julia> obj = (a = 1, b = 1);
julia> constraint = constraining() do obj
           @set obj.b = obj.a
       end
constraining(#1)
julia> lens = constraint ∘ @lens _.a
constraining(#1) ∘ (@lens _.a)
julia> get(obj, lens)
1
julia> set(obj, lens, 2)
(a = 2, b = 2)constraining is useful when combined with @batchlens or MultiLens:
julia> using Kaleido, Setfield
julia> obj = (a = 1, b = 2, c = 3);
julia> constraint = constraining() do obj
           @set obj.c = obj.a + obj.b
       end;
julia> lens = constraint ∘ MultiLens((
           (@lens _.a),
           (@lens _.b),
       ));
julia> get(obj, lens)
(1, 2)
julia> set(obj, lens, (100, 20))
(a = 100, b = 20, c = 120)Kaleido.FLens — TypeFLens(functor_based_lens) :: LensFLens provides an alternative ("isomorphic") way to create a Lens. It is useful for accessing dynamically determined "field" such as the last item in the linked list.
(Note: it's probably better to look at Examples first.)
FLens converts functor_based_lens (a two-argument callable) to the Lens defined in Setfield.  The callable functor_based_lens accepts the following two arguments:
setter: a one-argument callable that accepts a value in the "field" and return an object that can be passed to the second argument ofKaleido.fmap.obj: an object whose "field" is accessed.
Informally the signature of the functions appeared above may be written as
FLens(functor_based_lens) :: Lens
functor_based_lens(setter, obj)
setter(field::A) :: F{A} where {F <: Functor}
fmap(f, ::F{A}) :: F{B} where {F <: Functor}
f(field::A) :: B(note: there is no Functor in actual code)
Examples
Here is an implementation of @lens _[1] using FLens
julia> using Setfield
julia> using Kaleido: FLens, fmap
julia> fst = FLens((f, obj) -> fmap(x -> (x, obj[2:end]...), f(obj[1])));
julia> get((1, 2, 3), fst)
1
julia> set((1, 2, 3), fst, 100)
(100, 2, 3)A typical FLens usage has the form
FLens((f, obj) -> fmap(x -> SET(obj, x), f(GET(obj))))where
SET(obj, x)sets the "field" of theobjto the valuex.GET(obj)gets the value of the "field."
What GET and SET does may look like similar to Setfield.get and Setfield.set.  In fact, any lens can be converted into FLens:
julia> using Setfield
julia> using Kaleido: FLens, fmap
julia> asflens(lens::Lens) =
           FLens((f, obj) -> fmap(x -> set(obj, lens, x), f(get(obj, lens))));
julia> dot_a = asflens(@lens _.a);
julia> get((a=1, b=2), dot_a)
1
julia> set((a=1, b=2), dot_a, 100)
(a = 100, b = 2)If FLens is "isomorphic" to usual Lens, why not directly define Setfield.get and Setfield.set?  (They are easier to understand.)
This is because FLens is useful if the "field" of interest can only be dynamically determined.  For example, a lens to the last item of linked lists can be defined as follows:
julia> using Setfield
julia> using Kaleido: FLens, fmap
julia> struct Cons{T, S}
           car::T
           cdr::S
       end
julia> last_impl(f, list, g) =
           if list.cdr === nothing
               h = x -> g(Cons(x, nothing))
               fmap(h, f(list.car))
           else
               h = x -> g(Cons(list.car, x))
               last_impl(f, list.cdr, h)
           end;
julia> lst = FLens((f, list) -> last_impl(f, list, identity));
julia> list = Cons(1, Cons(2, Cons(3, nothing)));
julia> get(list, lst)
3
julia> set(list, lst, :last) === Cons(1, Cons(2, Cons(:last, nothing)))
trueNotice that last_impl dynamically builds the closure h that is passed as the first argument of fmap.  Although it is possible to implement the same lens by directly defining Setfield.get and Setfield.set, those two functions would have duplicated code for recursing into the last item.
Another (marginal?) benefit is that FLens can be more efficient when using modify.  This is because FLens can do modify in one recursion into the "field" while two recursions are necessary with get and set.  It can be relevant especially with complex object and lens where get and set used in modify cannot be inlined (e.g., due to type instability).
FLens can also be used for imposing some constraints in the fields. However, it may be better to use constraining for this purpose.
julia> using Setfield
julia> using Kaleido: FLens, fmap
julia> fstsnd = FLens((f, obj) -> fmap(
           x -> (x, x, obj[3:end]...),
           begin
               @assert obj[1] == obj[2]
               f(obj[1])
           end,
       ));
julia> get((1, 1, 2), fstsnd)
1
julia> set((1, 1, 2), fstsnd, 100)
(100, 100, 2)Side notes
FLens mimics the formalism used in the lens in Haskell. For an introduction to lens, the talk Lenses: compositional data access and manipulation by Simon Peyton Jones is highly recommended.  In this talk, a simplified form of lens uses in Haskell is explained in details:
type Lens' s a = forall f. Functor f
                        => (a -> f a) -> s -> f sInformally, this type synonym maps to the signature of FLens:
FLens(((::A -> ::F{A}), ::S) -> ::F{S} where F <: Functor) :: LensSetters
Kaleido.nullsetter — Constantnullsetter :: SetterA setter that does nothing; i.e., set(x, nullsetter, y) === x for any x and y.
Examples
julia> using Setfield, Kaleido
julia> set(1, nullsetter, 2)
1Kaleido.ToField — TypeToField(f) :: SetterApply f when setting.  Use x -> get(x, f) if f is a Lens.
Examples
julia> using Setfield, Kaleido
julia> setter = (@lens _.x) ∘ ToField(@lens _.a)
(@lens _.x) ∘ (←(@lens _.a)|❌→)
julia> set((x = 1, y = 2), setter, (a = 10, b = 20))
(x = 10, y = 2)Utilities
Kaleido.prettylens — Functionprettylens(lens::Lens; sprint_kwargs...) :: String
prettylens(io::IO, lens::Lens)Print or return more compact and easier-to-read string representation of lens than show.
Examples
julia> using Setfield, Kaleido
julia> prettylens(
           (@lens _.a) ∘ MultiLens((
               (@lens last(_)),
               (@lens _[:c].d) ∘ settingasℝ₊,
           ));
           context = :compact => true,
       )
"◻.a∘〈last(◻),◻[:c].d∘(←exp|log→)〉"