Kaleido.jl
Kaleido
Kaleido.nullsetter
Kaleido.settingasℝ₊
Kaleido.settingasℝ₋
Kaleido.settingas𝕀
Kaleido.FLens
Kaleido.FlatLens
Kaleido.IndexBatchLens
Kaleido.KeyBatchLens
Kaleido.MultiLens
Kaleido.PropertyBatchLens
Kaleido.ToField
Kaleido.batch
Kaleido.constraining
Kaleido.converting
Kaleido.getting
Kaleido.getting
Kaleido.prettylens
Kaleido.setting
Kaleido.@batchlens
Kaleido
— ModuleKaleido: some useful lenses
Kaleido.jl is a collection of useful Lens
es 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. Seegetting
. - Get/set fields with different parametrizations. See
converting
,setting
,getting
. - Computing other fields during
set
andget
; 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] ≈ 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.
Setting/getting multiple locations
Kaleido.@batchlens
— Macro@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),)
Kaleido.batch
— Functionbatch(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),)
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) :: 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))
Kaleido.setting
— Functionsetting(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
Kaleido.getting
— Functiongetting(xf::TransformVariables.AbstractTransform) :: Lens
Lens to get value transformed by xf
(and set value via the inverse transformation).
Kaleido.settingasℝ₊
— Constantsettingasℝ₊ :: 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))
Kaleido.settingasℝ₋
— Constantsettingasℝ₋ :: 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))
Kaleido.settingas𝕀
— Constantsettingas𝕀 :: 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
Misc lenses
Kaleido.getting
— Methodgetting(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 valuex
byC(y)
whereC
is a constructor ofx
; i.e., for anyx
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
.
Kaleido.constraining
— Functionconstraining(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 callingf
duringget
.
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) :: 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:
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 theobj
to 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)))
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
Setters
Kaleido.nullsetter
— Constantnullsetter :: 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
Kaleido.ToField
— TypeToField(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)
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→)〉"