Skip to content

Add some fancy optics #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Oct 2, 2020
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ jobs:
fail-fast: false
matrix:
version:
- 'nightly' # TODO stable
- 1 # Supporting < 1.5 is a bit awkward since \bbsemi is missing
- 'nightly'
os:
- ubuntu-latest
- macOS-latest
Expand Down
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ authors = ["Takafumi Arakaki <[email protected]>", "Jan Weidner <[email protected]
version = "0.1.0"

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

[extras]
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
Expand Down
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
build
site
Manifest.toml
3 changes: 3 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ using Accessors, Documenter, Literate
inputdir = joinpath(@__DIR__, "..", "examples")
outputdir = joinpath(@__DIR__, "src", "examples")
mkpath(outputdir)
mkpath(joinpath(outputdir, "examples"))
for filename in readdir(inputdir)
inpath = joinpath(inputdir, filename)
cp(inpath, joinpath(outputdir, "examples", filename), force=true)
Literate.markdown(inpath, outputdir; documenter=true)
end

Expand All @@ -16,6 +18,7 @@ makedocs(
"Lenses" => "lenses.md",
"Docstrings" => "index.md",
"Custom Macros" => "examples/custom_macros.md",
"Experimental features" => "examples/specter.md",
hide("internals.md"),
],
strict = true, # to exit with non-zero code on error
Expand Down
1 change: 1 addition & 0 deletions docs/src/examples/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.md
examples
6 changes: 3 additions & 3 deletions docs/src/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ This api may be useful in its own right and works as follows:
```jldoctest
julia> using Accessors

julia> l = @lens _.a.b
(@lens _.b) ∘ (@lens _.a)
julia> l = @optic _.a.b
(@optic _.b) ∘ (@optic _.a)

julia> struct AB;a;b;end

Expand All @@ -83,6 +83,6 @@ For instance
```
expands roughly to
```julia
l = @lens _.a.b
l = @optic _.a.b
set(obj, l, 42)
```
8 changes: 4 additions & 4 deletions docs/src/lenses.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ julia> struct T;a;b; end

julia> obj = T("AA", "BB");

julia> lens = @lens _.a
(@lens _.a)
julia> lens = @optic _.a
(@optic _.a)

julia> lens(obj)
"AA"
Expand All @@ -34,7 +34,7 @@ Implementing lenses is straight forward. They can be of any type and just need t

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

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

See also [`@lens`](@ref), [`set`](@ref), [`modify`](@ref).
See also [`@optic`](@ref), [`set`](@ref), [`modify`](@ref).
22 changes: 11 additions & 11 deletions examples/custom_macros.jl
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# # Extending `@set` and `@lens`
# This code demonstrates how to extend the `@set` and `@lens` mechanism with custom
# # Extending `@set` and `@optic`
# This code demonstrates how to extend the `@set` and `@optic` mechanism with custom
# lenses.
# As a demo, we want to implement `@mylens!` and `@myreset`, which work much like
# `@lens` and `@set`, but mutate objects instead of returning modified copies.
# `@optic` and `@set`, but mutate objects instead of returning modified copies.

using Accessors
using Accessors: IndexLens, PropertyLens, ComposedLens
using Accessors: IndexLens, PropertyLens, ComposedOptic

struct Lens!{L}
pure::L
end

(l::Lens!)(o) = l.pure(o)
function Accessors.set(o, l::Lens!{<: ComposedLens}, val)
o_inner = Accessors.inner(l.pure)(o)
set(o_inner, Lens!(Accessors.outer(l.pure)), val)
function Accessors.set(o, l::Lens!{<: ComposedOptic}, val)
o_inner = l.pure.inner(o)
set(o_inner, Lens!(l.pure.outer), val)
end
function Accessors.set(o, l::Lens!{PropertyLens{prop}}, val) where {prop}
setproperty!(o, prop, val)
Expand All @@ -36,25 +36,25 @@ mutable struct M
end

o = M(1,2)
l = Lens!(@lens _.b)
l = Lens!(@optic _.b)
set(o, l, 20)
@test o.b == 20

l = Lens!(@lens _.foo[1])
l = Lens!(@optic _.foo[1])
o = (foo=[1,2,3], bar=:bar)
set(o, l, 100)
@test o == (foo=[100,2,3], bar=:bar)

# Now we can implement the syntax macros

using Accessors: setmacro, lensmacro
using Accessors: setmacro, opticmacro

macro myreset(ex)
setmacro(Lens!, ex)
end

macro mylens!(ex)
lensmacro(Lens!, ex)
opticmacro(Lens!, ex)
end

o = M(1,2)
Expand Down
46 changes: 46 additions & 0 deletions examples/molecules.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# # Molecules
#
# inspired by https://hackage.haskell.org/package/lens-tutorial-1.0.3/docs/Control-Lens-Tutorial.html
#
using Accessors
using Test

molecule = (
name="water",
atoms=[
(name="H", position=(x=0,y=1)), # in reality the angle is about 104deg
(name="O", position=(x=0,y=0)),
(name="H", position=(x=1,y=0)),
]
)

oc = @optic _.atoms |> Elements() |> _.position.x
res_modify = modify(x->x+1, molecule, oc)

res_macro = @set molecule.atoms |> Elements() |> _.position.x += 1
@test res_macro == res_modify

res_expected = (
name="water",
atoms=[
(name="H", position=(x=1,y=1)),
(name="O", position=(x=1,y=0)),
(name="H", position=(x=2,y=0)),
]
)

@test res_expected == res_macro

res_set = set(molecule, oc, 4.0)
res_macro = @set molecule.atoms |> Elements() |> _.position.x = 4.0
@test res_macro == res_set

res_expected = (
name="water",
atoms=[
(name="H", position=(x=4.0,y=1)),
(name="O", position=(x=4.0,y=0)),
(name="H", position=(x=4.0,y=0)),
]
)
@test res_expected == res_set
121 changes: 121 additions & 0 deletions examples/specter.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# # Traversal
# These examples are taken from the README.md of the specter clojure library.
# Many of the features here are experimental, please consult the docstrings of the
# involved optics.

using Test
using Accessors

import Accessors: modify, OpticStyle
using Accessors: ModifyBased, SetBased, setindex

function mapvals(f, d)
Dict(k => f(v) for (k,v) in pairs(d))
end

mapvals(f, nt::NamedTuple) = map(f, nt)
function mapkeys(f, d)
Dict(f(k) => v for (k,v) in pairs(d))
end

function mapkeys(f, nt::NamedTuple)
kw = map(pairs(nt)) do (key, val)
f(key) => val
end
(;kw...)
end

struct Keys end
OpticStyle(::Type{Keys}) = ModifyBased()
modify(f, obj, ::Keys) = mapkeys(f, obj)

struct Vals end
OpticStyle(::Type{Vals}) = ModifyBased()
modify(f, obj, ::Vals) = mapvals(f, obj)

struct Filter{F}
keep_condition::F
end
OpticStyle(::Type{<:Filter}) = ModifyBased()
(o::Filter)(x) = filter(o.keep_condition, x)
function modify(f, obj, optic::Filter)
I = eltype(eachindex(obj))
inds = I[]
for i in eachindex(obj)
x = obj[i]
if optic.keep_condition(x)
push!(inds, i)
end
end
vals = f(obj[inds])
setindex(obj, vals, inds)
end

# ### Increment all even numbers
data = (a = [(aa=1, bb=2), (cc=3,)], b = [(dd=4,)])

out = @set data |> Vals() |> Elements() |> Vals() |> If(iseven) += 1

@test out == (a = [(aa = 1, bb = 3), (cc = 3,)], b = [(dd = 5,)])

# ### Append to nested vector
data = (a = 1:3,)

optic = @optic _.a
out = modify(v -> vcat(v, [4,5]), data, optic)

@test out == (a = [1,2,3,4,5],)

# ### Increment last odd number in a sequence

data = 1:4
out = @set data |> Filter(isodd) |> last += 1
@test out == [1,2,4,4]

### Map over a sequence

data = 1:3
out = @set data |> Elements() += 1
@test out == [2,3,4]

# ### Increment all values in a nested Dict

data = Dict(:a => Dict(:aa =>1), :b => Dict(:ba => -1, :bb => 2))
out = @set data |> Vals() |> Vals() += 1
@test out == Dict(:a => Dict(:aa => 2),:b => Dict(:bb => 3,:ba => 0))

# ### Increment all the even values for :a keys in a sequence of maps

data = [Dict(:a => 1), Dict(:a => 2), Dict(:a => 4), Dict(:a => 3)]
out = @set data |> Elements() |> _[:a] += 1
@test out == [Dict(:a => 2), Dict(:a => 3), Dict(:a => 5), Dict(:a => 4)]

# ### Retrieve every number divisible by 3 out of a sequence of sequences

function getall(obj, optic)
out = Any[]
modify(obj, optic) do val
push!(out, val)
end
out
end

data = [[1,2,3,4],[], [5,3,2,18],[2,4,6], [12]]
optic = @optic _ |> Elements() |> Elements() |> If(x -> mod(x, 3) == 0)
out = getall(data, optic)
@test out == [3, 3, 18, 6, 12]
@test_broken eltype(out) == Int

# ### Increment the last odd number in a sequence

data = [2, 1, 3, 6, 9, 4, 8]
out = @set data |> Filter(isodd) |> _[end] += 1
@test out == [2, 1, 3, 6, 10, 4, 8]
@test_broken eltype(out) == Int

# ### Remove nils from a nested sequence

data = (a = [1,2,missing, 3, missing],)
optic = @optic _.a |> Filter(!ismissing)
out = optic(data)
@test out == [1,2,3]
7 changes: 3 additions & 4 deletions src/Accessors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ using MacroTools: isstructdef, splitstructdef, postwalk
using Requires: @require

include("setindex.jl")
include("lens.jl")
include("optics.jl")
include("sugar.jl")
include("functionlenses.jl")
include("testing.jl")

function __init__()
@require StaticArrays="90137ffa-7385-5640-81b9-e52037218182" begin
setindex(a::StaticArrays.StaticArray, args...) =
Base.setindex(a, args...)
@require StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" begin
setindex(a::StaticArrays.StaticArray, args...) = Base.setindex(a, args...)
end
end

Expand Down
23 changes: 10 additions & 13 deletions src/functionlenses.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,21 @@ set(obj, ::typeof(inv), new_inv) = inv(new_inv)
set(obj::Array, ::typeof(eltype), T::Type) = collect(T, obj)
set(obj::Number, ::typeof(eltype), T::Type) = T(obj)
set(obj::Type{<:Number}, ::typeof(eltype), ::Type{T}) where {T} = T
set(obj::Type{<:Array{<:Any, N}}, ::typeof(eltype), ::Type{T}) where {N, T} = Array{T, N}
set(obj::Type{<:Dict}, ::typeof(eltype), ::Type{Pair{K, V}}) where {K, V} = Dict{K, V}
set(obj::Type{<:Array{<:Any,N}}, ::typeof(eltype), ::Type{T}) where {N,T} = Array{T,N}
set(obj::Type{<:Dict}, ::typeof(eltype), ::Type{Pair{K,V}}) where {K,V} = Dict{K,V}
set(obj::Dict, ::typeof(eltype), ::Type{T}) where {T} = set(typeof(obj), eltype, T)(obj)

set(obj::Dict, lens::Union{typeof(keytype), typeof(valtype)}, T::Type) =
set(obj::Dict, lens::Union{typeof(keytype),typeof(valtype)}, T::Type) =
set(typeof(obj), lens, T)(obj)
set(obj::Type{<:Dict{<:Any,V}}, lens::typeof(keytype), ::Type{K}) where {K, V} =
Dict{K, V}
set(obj::Type{<:Dict{K}}, lens::typeof(valtype), ::Type{V}) where {K, V} =
Dict{K, V}
set(obj::Type{<:Dict{<:Any,V}}, lens::typeof(keytype), ::Type{K}) where {K,V} = Dict{K,V}
set(obj::Type{<:Dict{K}}, lens::typeof(valtype), ::Type{V}) where {K,V} = Dict{K,V}

################################################################################
##### os
################################################################################
set(path, ::typeof(splitext), (stem, ext)) = string(stem, ext)
set(path, ::typeof(splitdir), (dir, last)) = joinpath(dir, last)
set(path, ::typeof(splitext), (stem, ext)) = string(stem, ext)
set(path, ::typeof(splitdir), (dir, last)) = joinpath(dir, last)
set(path, ::typeof(splitdrive), (drive, rest)) = joinpath(drive, rest)
set(path, ::typeof(splitpath), pieces) = joinpath(pieces...)
set(path, ::typeof(dirname), new_name) = @set splitdir(path)[1] = new_name
set(path, ::typeof(basename), new_name) = @set splitdir(path)[2] = new_name

set(path, ::typeof(splitpath), pieces) = joinpath(pieces...)
set(path, ::typeof(dirname), new_name) = @set splitdir(path)[1] = new_name
set(path, ::typeof(basename), new_name) = @set splitdir(path)[2] = new_name
Loading