Kaleido.jl

KaleidoModule

Kaleido: some useful lenses

Stable Dev GitHub Actions Codecov Coveralls Aqua QA GitHub commits since tagged version

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 StaticArray or any arbitrary multi-valued container. See getting.
  • Get/set fields with different parametrizations. See converting, setting, getting.
  • Computing other fields during set and get; i.e., adding constraints between fields. See constraining.
  • 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] ≈ 1

It 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.

source

Setting/getting multiple locations

Kaleido.@batchlensMacro
@batchlens begin
    lens_expression_1
    lens_expression_2
    ...
    lens_expression_n
end

From $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),)
source
Kaleido.batchFunction
batch(lens₁, lens₂, ..., lensₙ) :: Lens

From $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),)
source
Kaleido.MultiLensType
MultiLens([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)
source
Kaleido.PropertyBatchLensType
PropertyBatchLens(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)
source
Kaleido.KeyBatchLensType
KeyBatchLens(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)
source
Kaleido.IndexBatchLensType
IndexBatchLens(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)
source
Kaleido.FlatLensType
FlatLens(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))
source

Bijective transformations as lenses

Kaleido.convertingFunction
converting(; fromfield, tofield) :: Lens

Examples

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))
source
Kaleido.settingFunction
setting(xf::TransformVariables.AbstractTransform) :: Lens

Lens 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.0
source
Kaleido.gettingFunction
getting(xf::TransformVariables.AbstractTransform) :: Lens

Lens to get value transformed by xf (and set value via the inverse transformation).

source
Kaleido.settingasℝ₊Constant
settingasℝ₊ :: BijectionLens

This 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))
source
Kaleido.settingasℝ₋Constant
settingasℝ₋ :: BijectionLens

This 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))
source
Kaleido.settingas𝕀Constant
settingas𝕀 :: BijectionLens

This 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] ≈ 0
source

Misc lenses

Kaleido.gettingMethod
getting(f) :: Lens

Apply 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 value x by C(y) where C is a constructor of x; i.e., for any x that could be retrieved from the object through this lens,

    C(f(x)) == x
  • The conversion in the reverse direction also holds; i.e., for any y that 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.

source
Kaleido.constrainingFunction
constraining(f; onget=true, onset=true)

Create a lens to impose constraints by a callable f.

  • The callable f must be idempotent; i.e., f ∘ f = f.

  • If the original object already satisfies the constraint (i.e. f(obj) == obj), onget=false can be passed safely to skip calling f during get.

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)
source
Kaleido.FLensType
FLens(functor_based_lens) :: Lens

FLens 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:

  1. setter: a one-argument callable that accepts a value in the "field" and return an object that can be passed to the second argument of Kaleido.fmap.

  2. 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 the obj to the value x.
  • 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)))
true

Notice 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 s

Informally, this type synonym maps to the signature of FLens:

FLens(((::A -> ::F{A}), ::S) -> ::F{S} where F <: Functor) :: Lens
source

Setters

Kaleido.nullsetterConstant
nullsetter :: Setter

A 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)
1
source
Kaleido.ToFieldType
ToField(f) :: Setter

Apply 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)
source

Utilities

Kaleido.prettylensFunction
prettylens(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→)〉"
source