Skip to content

Commit 183f823

Browse files
authored
Merge pull request #6 from JuliaObjects/optics
Add some fancy optics
2 parents 950ec92 + c0fd15d commit 183f823

22 files changed

+793
-321
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
version:
13-
- 'nightly' # TODO stable
13+
- 1 # Supporting < 1.5 is a bit awkward since \bbsemi is missing
14+
- 'nightly'
1415
os:
1516
- ubuntu-latest
1617
- macOS-latest

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ authors = ["Takafumi Arakaki <[email protected]>", "Jan Weidner <[email protected]
44
version = "0.1.0"
55

66
[deps]
7+
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
78
CompositionsBase = "a33af91c-f02d-484b-be07-31d278c5ca2b"
89
ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
910
Future = "9fa8497b-333b-5362-9e8d-4d0656e87820"
@@ -16,7 +17,8 @@ CompositionsBase = "0.1"
1617
ConstructionBase = "0.1, 1.0"
1718
MacroTools = "0.4.4, 0.5"
1819
Requires = "0.5, 1.0"
19-
julia = "1.6"
20+
StaticNumbers = "0.3"
21+
julia = "1.5"
2022

2123
[extras]
2224
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"

docs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
build
22
site
3+
Manifest.toml

docs/make.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ using Accessors, Documenter, Literate
33
inputdir = joinpath(@__DIR__, "..", "examples")
44
outputdir = joinpath(@__DIR__, "src", "examples")
55
mkpath(outputdir)
6+
mkpath(joinpath(outputdir, "examples"))
67
for filename in readdir(inputdir)
78
inpath = joinpath(inputdir, filename)
9+
cp(inpath, joinpath(outputdir, "examples", filename), force=true)
810
Literate.markdown(inpath, outputdir; documenter=true)
911
end
1012

@@ -16,6 +18,7 @@ makedocs(
1618
"Lenses" => "lenses.md",
1719
"Docstrings" => "index.md",
1820
"Custom Macros" => "examples/custom_macros.md",
21+
"Experimental features" => "examples/specter.md",
1922
hide("internals.md"),
2023
],
2124
strict = true, # to exit with non-zero code on error

docs/src/examples/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
*.md
2+
examples

docs/src/intro.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ This api may be useful in its own right and works as follows:
5555
```jldoctest
5656
julia> using Accessors
5757
58-
julia> l = @lens _.a.b
59-
(@lens _.b) ∘ (@lens _.a)
58+
julia> l = @optic _.a.b
59+
(@optic _.b) ∘ (@optic _.a)
6060
6161
julia> struct AB;a;b;end
6262
@@ -83,6 +83,6 @@ For instance
8383
```
8484
expands roughly to
8585
```julia
86-
l = @lens _.a.b
86+
l = @optic _.a.b
8787
set(obj, l, 42)
8888
```

docs/src/lenses.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ julia> struct T;a;b; end
1010
1111
julia> obj = T("AA", "BB");
1212
13-
julia> lens = @lens _.a
14-
(@lens _.a)
13+
julia> lens = @optic _.a
14+
(@optic _.a)
1515
1616
julia> lens(obj)
1717
"AA"
@@ -34,7 +34,7 @@ Implementing lenses is straight forward. They can be of any type and just need t
3434

3535
These must be pure functions, that satisfy the three lens laws:
3636

37-
```jldoctest; output = false, setup = :(using Accessors; (≅ = (==)); obj = (a="A", b="B"); lens = @lens _.a; val = 2; val1 = 10; val2 = 20)
37+
```jldoctest; output = false, setup = :(using Accessors; (≅ = (==)); obj = (a="A", b="B"); lens = @optic _.a; val = 2; val1 = 10; val2 = 20)
3838
@assert lens(set(obj, lens, val)) ≅ val
3939
# You get what you set.
4040
@assert set(obj, lens, lens(obj)) ≅ obj
@@ -51,4 +51,4 @@ else instead. For instance `==` does not work in `Float64` context, because
5151
`get(set(obj, lens, NaN), lens) == NaN` can never hold. Instead `isequal` or
5252
`≅(x::Float64, y::Float64) = isequal(x,y) | x ≈ y` are possible alternatives.
5353

54-
See also [`@lens`](@ref), [`set`](@ref), [`modify`](@ref).
54+
See also [`@optic`](@ref), [`set`](@ref), [`modify`](@ref).

examples/custom_macros.jl

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
# # Extending `@set` and `@lens`
2-
# This code demonstrates how to extend the `@set` and `@lens` mechanism with custom
1+
# # Extending `@set` and `@optic`
2+
# This code demonstrates how to extend the `@set` and `@optic` mechanism with custom
33
# lenses.
44
# As a demo, we want to implement `@mylens!` and `@myreset`, which work much like
5-
# `@lens` and `@set`, but mutate objects instead of returning modified copies.
5+
# `@optic` and `@set`, but mutate objects instead of returning modified copies.
66

77
using Accessors
8-
using Accessors: IndexLens, PropertyLens, ComposedLens
8+
using Accessors: IndexLens, PropertyLens, ComposedOptic
99

1010
struct Lens!{L}
1111
pure::L
1212
end
1313

1414
(l::Lens!)(o) = l.pure(o)
15-
function Accessors.set(o, l::Lens!{<: ComposedLens}, val)
16-
o_inner = Accessors.inner(l.pure)(o)
17-
set(o_inner, Lens!(Accessors.outer(l.pure)), val)
15+
function Accessors.set(o, l::Lens!{<: ComposedOptic}, val)
16+
o_inner = l.pure.inner(o)
17+
set(o_inner, Lens!(l.pure.outer), val)
1818
end
1919
function Accessors.set(o, l::Lens!{PropertyLens{prop}}, val) where {prop}
2020
setproperty!(o, prop, val)
@@ -36,25 +36,25 @@ mutable struct M
3636
end
3737

3838
o = M(1,2)
39-
l = Lens!(@lens _.b)
39+
l = Lens!(@optic _.b)
4040
set(o, l, 20)
4141
@test o.b == 20
4242

43-
l = Lens!(@lens _.foo[1])
43+
l = Lens!(@optic _.foo[1])
4444
o = (foo=[1,2,3], bar=:bar)
4545
set(o, l, 100)
4646
@test o == (foo=[100,2,3], bar=:bar)
4747

4848
# Now we can implement the syntax macros
4949

50-
using Accessors: setmacro, lensmacro
50+
using Accessors: setmacro, opticmacro
5151

5252
macro myreset(ex)
5353
setmacro(Lens!, ex)
5454
end
5555

5656
macro mylens!(ex)
57-
lensmacro(Lens!, ex)
57+
opticmacro(Lens!, ex)
5858
end
5959

6060
o = M(1,2)

examples/molecules.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# # Molecules
2+
#
3+
# inspired by https://hackage.haskell.org/package/lens-tutorial-1.0.3/docs/Control-Lens-Tutorial.html
4+
#
5+
using Accessors
6+
using Test
7+
8+
molecule = (
9+
name="water",
10+
atoms=[
11+
(name="H", position=(x=0,y=1)), # in reality the angle is about 104deg
12+
(name="O", position=(x=0,y=0)),
13+
(name="H", position=(x=1,y=0)),
14+
]
15+
)
16+
17+
oc = @optic _.atoms |> Elements() |> _.position.x
18+
res_modify = modify(x->x+1, molecule, oc)
19+
20+
res_macro = @set molecule.atoms |> Elements() |> _.position.x += 1
21+
@test res_macro == res_modify
22+
23+
res_expected = (
24+
name="water",
25+
atoms=[
26+
(name="H", position=(x=1,y=1)),
27+
(name="O", position=(x=1,y=0)),
28+
(name="H", position=(x=2,y=0)),
29+
]
30+
)
31+
32+
@test res_expected == res_macro
33+
34+
res_set = set(molecule, oc, 4.0)
35+
res_macro = @set molecule.atoms |> Elements() |> _.position.x = 4.0
36+
@test res_macro == res_set
37+
38+
res_expected = (
39+
name="water",
40+
atoms=[
41+
(name="H", position=(x=4.0,y=1)),
42+
(name="O", position=(x=4.0,y=0)),
43+
(name="H", position=(x=4.0,y=0)),
44+
]
45+
)
46+
@test res_expected == res_set

examples/specter.jl

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# # Traversal
2+
# These examples are taken from the README.md of the specter clojure library.
3+
# Many of the features here are experimental, please consult the docstrings of the
4+
# involved optics.
5+
6+
using Test
7+
using Accessors
8+
9+
import Accessors: modify, OpticStyle
10+
using Accessors: ModifyBased, SetBased, setindex
11+
12+
function mapvals(f, d)
13+
Dict(k => f(v) for (k,v) in pairs(d))
14+
end
15+
16+
mapvals(f, nt::NamedTuple) = map(f, nt)
17+
function mapkeys(f, d)
18+
Dict(f(k) => v for (k,v) in pairs(d))
19+
end
20+
21+
function mapkeys(f, nt::NamedTuple)
22+
kw = map(pairs(nt)) do (key, val)
23+
f(key) => val
24+
end
25+
(;kw...)
26+
end
27+
28+
struct Keys end
29+
OpticStyle(::Type{Keys}) = ModifyBased()
30+
modify(f, obj, ::Keys) = mapkeys(f, obj)
31+
32+
struct Vals end
33+
OpticStyle(::Type{Vals}) = ModifyBased()
34+
modify(f, obj, ::Vals) = mapvals(f, obj)
35+
36+
struct Filter{F}
37+
keep_condition::F
38+
end
39+
OpticStyle(::Type{<:Filter}) = ModifyBased()
40+
(o::Filter)(x) = filter(o.keep_condition, x)
41+
function modify(f, obj, optic::Filter)
42+
I = eltype(eachindex(obj))
43+
inds = I[]
44+
for i in eachindex(obj)
45+
x = obj[i]
46+
if optic.keep_condition(x)
47+
push!(inds, i)
48+
end
49+
end
50+
vals = f(obj[inds])
51+
setindex(obj, vals, inds)
52+
end
53+
54+
# ### Increment all even numbers
55+
data = (a = [(aa=1, bb=2), (cc=3,)], b = [(dd=4,)])
56+
57+
out = @set data |> Vals() |> Elements() |> Vals() |> If(iseven) += 1
58+
59+
@test out == (a = [(aa = 1, bb = 3), (cc = 3,)], b = [(dd = 5,)])
60+
61+
# ### Append to nested vector
62+
data = (a = 1:3,)
63+
64+
optic = @optic _.a
65+
out = modify(v -> vcat(v, [4,5]), data, optic)
66+
67+
@test out == (a = [1,2,3,4,5],)
68+
69+
# ### Increment last odd number in a sequence
70+
71+
data = 1:4
72+
out = @set data |> Filter(isodd) |> last += 1
73+
@test out == [1,2,4,4]
74+
75+
### Map over a sequence
76+
77+
data = 1:3
78+
out = @set data |> Elements() += 1
79+
@test out == [2,3,4]
80+
81+
# ### Increment all values in a nested Dict
82+
83+
data = Dict(:a => Dict(:aa =>1), :b => Dict(:ba => -1, :bb => 2))
84+
out = @set data |> Vals() |> Vals() += 1
85+
@test out == Dict(:a => Dict(:aa => 2),:b => Dict(:bb => 3,:ba => 0))
86+
87+
# ### Increment all the even values for :a keys in a sequence of maps
88+
89+
data = [Dict(:a => 1), Dict(:a => 2), Dict(:a => 4), Dict(:a => 3)]
90+
out = @set data |> Elements() |> _[:a] += 1
91+
@test out == [Dict(:a => 2), Dict(:a => 3), Dict(:a => 5), Dict(:a => 4)]
92+
93+
# ### Retrieve every number divisible by 3 out of a sequence of sequences
94+
95+
function getall(obj, optic)
96+
out = Any[]
97+
modify(obj, optic) do val
98+
push!(out, val)
99+
end
100+
out
101+
end
102+
103+
data = [[1,2,3,4],[], [5,3,2,18],[2,4,6], [12]]
104+
optic = @optic _ |> Elements() |> Elements() |> If(x -> mod(x, 3) == 0)
105+
out = getall(data, optic)
106+
@test out == [3, 3, 18, 6, 12]
107+
@test_broken eltype(out) == Int
108+
109+
# ### Increment the last odd number in a sequence
110+
111+
data = [2, 1, 3, 6, 9, 4, 8]
112+
out = @set data |> Filter(isodd) |> _[end] += 1
113+
@test out == [2, 1, 3, 6, 10, 4, 8]
114+
@test_broken eltype(out) == Int
115+
116+
# ### Remove nils from a nested sequence
117+
118+
data = (a = [1,2,missing, 3, missing],)
119+
optic = @optic _.a |> Filter(!ismissing)
120+
out = optic(data)
121+
@test out == [1,2,3]

src/Accessors.jl

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ using MacroTools: isstructdef, splitstructdef, postwalk
44
using Requires: @require
55

66
include("setindex.jl")
7-
include("lens.jl")
7+
include("optics.jl")
88
include("sugar.jl")
99
include("functionlenses.jl")
1010
include("testing.jl")
1111

1212
function __init__()
13-
@require StaticArrays="90137ffa-7385-5640-81b9-e52037218182" begin
14-
setindex(a::StaticArrays.StaticArray, args...) =
15-
Base.setindex(a, args...)
13+
@require StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" begin
14+
setindex(a::StaticArrays.StaticArray, args...) = Base.setindex(a, args...)
1615
end
1716
end
1817

src/functionlenses.jl

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,21 @@ set(obj, ::typeof(inv), new_inv) = inv(new_inv)
99
set(obj::Array, ::typeof(eltype), T::Type) = collect(T, obj)
1010
set(obj::Number, ::typeof(eltype), T::Type) = T(obj)
1111
set(obj::Type{<:Number}, ::typeof(eltype), ::Type{T}) where {T} = T
12-
set(obj::Type{<:Array{<:Any, N}}, ::typeof(eltype), ::Type{T}) where {N, T} = Array{T, N}
13-
set(obj::Type{<:Dict}, ::typeof(eltype), ::Type{Pair{K, V}}) where {K, V} = Dict{K, V}
12+
set(obj::Type{<:Array{<:Any,N}}, ::typeof(eltype), ::Type{T}) where {N,T} = Array{T,N}
13+
set(obj::Type{<:Dict}, ::typeof(eltype), ::Type{Pair{K,V}}) where {K,V} = Dict{K,V}
1414
set(obj::Dict, ::typeof(eltype), ::Type{T}) where {T} = set(typeof(obj), eltype, T)(obj)
1515

16-
set(obj::Dict, lens::Union{typeof(keytype), typeof(valtype)}, T::Type) =
16+
set(obj::Dict, lens::Union{typeof(keytype),typeof(valtype)}, T::Type) =
1717
set(typeof(obj), lens, T)(obj)
18-
set(obj::Type{<:Dict{<:Any,V}}, lens::typeof(keytype), ::Type{K}) where {K, V} =
19-
Dict{K, V}
20-
set(obj::Type{<:Dict{K}}, lens::typeof(valtype), ::Type{V}) where {K, V} =
21-
Dict{K, V}
18+
set(obj::Type{<:Dict{<:Any,V}}, lens::typeof(keytype), ::Type{K}) where {K,V} = Dict{K,V}
19+
set(obj::Type{<:Dict{K}}, lens::typeof(valtype), ::Type{V}) where {K,V} = Dict{K,V}
2220

2321
################################################################################
2422
##### os
2523
################################################################################
26-
set(path, ::typeof(splitext), (stem, ext)) = string(stem, ext)
27-
set(path, ::typeof(splitdir), (dir, last)) = joinpath(dir, last)
24+
set(path, ::typeof(splitext), (stem, ext)) = string(stem, ext)
25+
set(path, ::typeof(splitdir), (dir, last)) = joinpath(dir, last)
2826
set(path, ::typeof(splitdrive), (drive, rest)) = joinpath(drive, rest)
29-
set(path, ::typeof(splitpath), pieces) = joinpath(pieces...)
30-
set(path, ::typeof(dirname), new_name) = @set splitdir(path)[1] = new_name
31-
set(path, ::typeof(basename), new_name) = @set splitdir(path)[2] = new_name
32-
27+
set(path, ::typeof(splitpath), pieces) = joinpath(pieces...)
28+
set(path, ::typeof(dirname), new_name) = @set splitdir(path)[1] = new_name
29+
set(path, ::typeof(basename), new_name) = @set splitdir(path)[2] = new_name

0 commit comments

Comments
 (0)