Repository: jw3126/Setfield.jl Branch: master Commit: 7e87f1f040f3 Files: 33 Total size: 60.2 KB Directory structure: gitextract_z43yv95v/ ├── .codecov.yml ├── .github/ │ └── workflows/ │ ├── CI.yml │ ├── CompatHelper.yml │ ├── Invalidations.yml │ └── TagBot.yml ├── .gitignore ├── LICENSE.md ├── Project.toml ├── README.md ├── appveyor.yml ├── docs/ │ ├── .gitignore │ ├── Project.toml │ ├── make.jl │ └── src/ │ ├── examples/ │ │ └── .gitignore │ ├── index.md │ ├── internals.md │ └── intro.md ├── examples/ │ └── custom_macros.jl ├── src/ │ ├── Setfield.jl │ ├── functionlenses.jl │ ├── lens.jl │ ├── setindex.jl │ └── sugar.jl └── test/ ├── dynamiclens_begin.jl ├── perf.jl ├── runtests.jl ├── test_core.jl ├── test_examples.jl ├── test_functionlenses.jl ├── test_quicktypes.jl ├── test_setindex.jl ├── test_setmacro.jl └── test_staticarrays.jl ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ comment: false ================================================ FILE: .github/workflows/CI.yml ================================================ name: CI on: - push - pull_request jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: version: - '1.6' - '1' - 'nightly' os: - ubuntu-latest arch: - x64 include: - os: macOS-latest version: '1' arch: x64 - os: windows-latest version: '1' arch: x64 steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} show-versioninfo: true - uses: actions/cache@v1 env: cache-name: cache-artifacts with: path: ~/.julia/artifacts key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} restore-keys: | ${{ runner.os }}-test-${{ env.cache-name }}- ${{ runner.os }}-test- ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-runtest@latest docs: name: Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 with: version: 1 - run: | julia --project=docs -e ' using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate()' - run: | julia --project=docs -e ' using Documenter: doctest using Setfield doctest(Setfield)' - run: julia --project=docs docs/make.jl env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} ================================================ FILE: .github/workflows/CompatHelper.yml ================================================ name: CompatHelper on: schedule: - cron: '00 * * * *' jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: julia-version: [1] julia-arch: [x86] os: [ubuntu-latest] steps: - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.julia-version }} - name: Pkg.add("CompatHelper") run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - name: CompatHelper.main() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: julia -e 'using CompatHelper; CompatHelper.main()' ================================================ FILE: .github/workflows/Invalidations.yml ================================================ name: Invalidations on: pull_request: concurrency: # Skip intermediate builds: always. # Cancel intermediate builds: always. group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: evaluate: # Only run on PRs to the default branch. # In the PR trigger above branches can be specified only explicitly whereas this check should work for master, main, or any other default branch if: github.base_ref == github.event.repository.default_branch runs-on: ubuntu-latest steps: - uses: julia-actions/setup-julia@v1 with: version: '1' - uses: actions/checkout@v3 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-invalidations@v1 id: invs_pr - uses: actions/checkout@v3 with: ref: ${{ github.event.repository.default_branch }} - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-invalidations@v1 id: invs_default - name: Report invalidation counts run: | echo "Invalidations on default branch: ${{ steps.invs_default.outputs.total }} (${{ steps.invs_default.outputs.deps }} via deps)" >> $GITHUB_STEP_SUMMARY echo "This branch: ${{ steps.invs_pr.outputs.total }} (${{ steps.invs_pr.outputs.deps }} via deps)" >> $GITHUB_STEP_SUMMARY - name: Check if the PR does increase number of invalidations if: steps.invs_pr.outputs.total > steps.invs_default.outputs.total run: exit 1 ================================================ FILE: .github/workflows/TagBot.yml ================================================ name: TagBot on: issue_comment: types: - created workflow_dispatch: jobs: TagBot: if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} ssh: ${{ secrets.DOCUMENTER_KEY }} ================================================ FILE: .gitignore ================================================ *.jl.cov *.jl.*.cov *.jl.mem Manifest.toml ================================================ FILE: LICENSE.md ================================================ The Setfield.jl package is licensed under the MIT "Expat" License: > Copyright (c) 2017: Jan Weidner. > > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal > in the Software without restriction, including without limitation the rights > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > copies of the Software, and to permit persons to whom the Software is > furnished to do so, subject to the following conditions: > > The above copyright notice and this permission notice shall be included in all > copies or substantial portions of the Software. > > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE > SOFTWARE. > ================================================ FILE: Project.toml ================================================ name = "Setfield" uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" version = "1.1.2" [deps] ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" Future = "9fa8497b-333b-5362-9e8d-4d0656e87820" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" [compat] ConstructionBase = "0.1, 1.0" StaticArraysCore = "1" MacroTools = "0.4.4, 0.5" julia = "1.6" [extras] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" PerformanceTestTools = "dc46b164-d16f-48ec-a853-60448fc869fe" QuickTypes = "ae2dfa86-617c-530c-b392-ef20fdad97bb" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" StaticNumbers = "c5e4b96a-f99f-5557-8ed2-dc63ef9b5131" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Documenter", "PerformanceTestTools", "QuickTypes", "StaticArrays", "BenchmarkTools", "InteractiveUtils", "StaticNumbers"] ================================================ FILE: README.md ================================================ # Setfield [![DocStable](https://img.shields.io/badge/docs-stable-blue.svg)](https://jw3126.github.io/Setfield.jl/stable/intro) [![DocDev](https://img.shields.io/badge/docs-dev-blue.svg)](https://jw3126.github.io/Setfield.jl/dev/intro) ![CI](https://github.com/jw3126/Setfield.jl/workflows/CI/badge.svg) Update deeply nested immutable structs. # Lifecycle We plan to maintain `Setfield.jl` for a long time (written 2020-09-21, reinforced 2021-08-01, 2022-09-08, 2024-02-15). We will however not add new features. For a successor, see [Accessors.jl](https://github.com/JuliaObjects/Accessors.jl). # Usage Updating deeply nested immutable structs was never easier: ```julia using Setfield @set obj.a.b.c = d ``` For more information, see [the documentation](https://jw3126.github.io/Setfield.jl/latest/intro/) and/or watch this video: [![JuliaCon2020 Changing the immutable](https://img.youtube.com/vi/vkAOYeTpLg0/0.jpg)](https://youtu.be/vkAOYeTpLg0 "Changing the immutable") # Some creative usages of Setfield * [VegaLite.jl](https://github.com/queryverse/VegaLite.jl) overloads `getproperty` and lens API to manipulate JSON-based nested objects. * [Kaleido.jl](https://github.com/tkf/Kaleido.jl) is a library of additional lenses. * [PhaseSpaceIO.jl](https://github.com/jw3126/PhaseSpaceIO.jl) overloads `getproperty` and `setproperties` to get/set values from/in packed bits. ================================================ FILE: appveyor.yml ================================================ environment: matrix: - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe" - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe" - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" ## uncomment the following lines to allow failures on nightly julia ## (tests will run but not make your overall status red) #matrix: # allow_failures: # - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" # - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" branches: only: - master - /release-.*/ notifications: - provider: Email on_build_success: false on_build_failure: false on_build_status_changed: false install: - ps: "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12" # If there's a newer build queued for the same PR, cancel this one - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` throw "There are newer queued builds for this pull request, failing early." } # Download most recent Julia Windows binary - ps: (new-object net.webclient).DownloadFile( $env:JULIA_URL, "C:\projects\julia-binary.exe") # Run installer silently, output to C:\projects\julia - C:\projects\julia-binary.exe /S /D=C:\projects\julia build_script: # Need to convert from shallow to complete for Pkg.clone to work - IF EXIST .git\shallow (git fetch --unshallow) - C:\projects\julia\bin\julia -e "versioninfo(); Pkg.clone(pwd(), \"Setfield\"); Pkg.build(\"Setfield\")" test_script: - C:\projects\julia\bin\julia -e "Pkg.test(\"Setfield\")" ================================================ FILE: docs/.gitignore ================================================ build site ================================================ FILE: docs/Project.toml ================================================ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" ================================================ FILE: docs/make.jl ================================================ using Setfield, Documenter, Literate inputdir = joinpath(@__DIR__, "..", "examples") outputdir = joinpath(@__DIR__, "src", "examples") mkpath(outputdir) for filename in readdir(inputdir) inpath = joinpath(inputdir, filename) Literate.markdown(inpath, outputdir; documenter=true) end makedocs( modules = [Setfield], sitename = "Setfield.jl", pages = [ "Introduction" => "intro.md", "Docstrings" => "index.md", "Custom Macros" => "examples/custom_macros.md", hide("internals.md"), ], strict = true, # to exit with non-zero code on error ) deploydocs( repo = "github.com/jw3126/Setfield.jl.git", ) ================================================ FILE: docs/src/examples/.gitignore ================================================ *.md ================================================ FILE: docs/src/index.md ================================================ ## Docstrings ```@autodocs Modules = [Setfield] Private = false ``` ================================================ FILE: docs/src/internals.md ================================================ # Internals ```@autodocs Modules = [Setfield] Public = false ``` ================================================ FILE: docs/src/intro.md ================================================ ## Usage Say we have a deeply nested struct: ```jldoctest spaceship julia> using StaticArrays; julia> struct Person name::Symbol age::Int end; julia> struct SpaceShip captain::Person velocity::SVector{3, Float64} position::SVector{3, Float64} end; julia> s = SpaceShip(Person(:julia, 2009), [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]) SpaceShip(Person(:julia, 2009), [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]) ``` Lets update the captains name: ```jldoctest spaceship; filter = r" .*$" julia> s.captain.name = :JULIA ERROR: type Person is immutable ``` It's a bit cryptic but what it means that Julia tried very hard to set the field but gave it up since the struct is immutable. So we have to do: ```jldoctest spaceship julia> SpaceShip(Person(:JULIA, s.captain.age), s.velocity, s.position) SpaceShip(Person(:JULIA, 2009), [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]) ``` This is messy and things get worse, if the structs are bigger. `Setfields` to the rescue! ```jldoctest spaceship julia> using Setfield julia> s = @set s.captain.name = :JULIA SpaceShip(Person(:JULIA, 2009), [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]) julia> s = @set s.velocity[1] += 999999 SpaceShip(Person(:JULIA, 2009), [999999.0, 0.0, 0.0], [0.0, 0.0, 0.0]) julia> s = @set s.velocity[1] += 1000001 SpaceShip(Person(:JULIA, 2009), [2.0e6, 0.0, 0.0], [0.0, 0.0, 0.0]) julia> @set s.position[2] = 20 SpaceShip(Person(:JULIA, 2009), [2.0e6, 0.0, 0.0], [0.0, 20.0, 0.0]) ``` ## Under the hood Under the hood this package implements a simple [lens](https://hackage.haskell.org/package/lens) api. This api may be useful in its own right and works as follows: ```jldoctest julia> using Setfield julia> l = @lens _.a.b (@lens _.a.b) julia> struct AB;a;b;end julia> obj = AB(AB(1,2),3) AB(AB(1, 2), 3) julia> set(obj, l, 42) AB(AB(1, 42), 3) julia> obj AB(AB(1, 2), 3) julia> get(obj, l) 2 julia> modify(x->10x, obj, l) AB(AB(1, 20), 3) ``` Now the `@set` macro simply provides sugar for creating a `lens` and applying it. For instance ```julia @set obj.a.b = 42 ``` expands roughly to ```julia l = @lens _.a.b set(obj, l, 42) ``` ================================================ FILE: examples/custom_macros.jl ================================================ # # Extending `@set` and `@lens` # This code demonstrates how to extend the `@set` and `@lens` mechanism with custom # lenses. # As a demo, we want to implement `@mylens!` and `@myset!`, which work much like # `@lens` and `@set`, but mutate objects instead of returning modified copies. using Setfield using Setfield: IndexLens, PropertyLens, ComposedLens struct Lens!{L <:Lens} <: Lens pure::L end Setfield.get(o, l::Lens!) = Setfield.get(o, l.pure) function Setfield.set(o, l::Lens!{<: ComposedLens}, val) o_inner = get(o, l.pure.outer) set(o_inner, Lens!(l.pure.inner), val) end function Setfield.set(o, l::Lens!{PropertyLens{prop}}, val) where {prop} setproperty!(o, prop, val) o end function Setfield.set(o, l::Lens!{<:IndexLens}, val) where {prop} o[l.pure.indices...] = val o end # Now this implements the kind of `lens` the new macros should use. # Of course there are more variants like `Lens!(<:DynamicIndexLens)`, for which we might # want to overload `set`, but lets ignore that. Instead we want to check, that everything works so far: using Test mutable struct M a b end o = M(1,2) l = Lens!(@lens _.b) set(o, l, 20) @test o.b == 20 l = Lens!(@lens _.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 Setfield: setmacro, lensmacro macro myset!(ex) setmacro(Lens!, ex) end macro mylens!(ex) lensmacro(Lens!, ex) end o = M(1,2) @myset! o.a = :hi @myset! o.b += 98 @test o.a == :hi @test o.b == 100 deep = [[[[1]]]] @myset! deep[1][1][1][1] = 2 @test deep[1][1][1][1] === 2 l = @mylens! _.foo[1] o = (foo=[1,2,3], bar=:bar) set(o, l, 100) @test o == (foo=[100,2,3], bar=:bar) # Everything works, we can do arbitrary nesting and also use `+=` syntax etc. ================================================ FILE: src/Setfield.jl ================================================ __precompile__(true) module Setfield using MacroTools using MacroTools: isstructdef, splitstructdef, postwalk using StaticArraysCore if VERSION < v"1.1-" using Future: copy! end include("setindex.jl") include("lens.jl") include("sugar.jl") include("functionlenses.jl") # To correctly dispatch to `show(::IO, ::CustomLens)` when it is defined by a # user, we avoid defining the generic `show(::IO, ::Lens)`. This way, we can # safely call `show` inside `ComposedLens` without worrying about the # `StackOverflowError` that can be easily triggered in the previous approach. # See also: # * https://github.com/jw3126/Setfield.jl/pull/86 # * https://github.com/jw3126/Setfield.jl/pull/88 for n in names(Setfield, all=true) T = getproperty(Setfield, n) if T isa Type && T <: Lens && (T === ComposedLens || has_atlens_support(T)) @eval Base.show(io::IO, l::$T) = _show(io, nothing, l) end end end ================================================ FILE: src/functionlenses.jl ================================================ set(obj, ::typeof(@lens last(_)), val) = @set obj[lastindex(obj)] = val set(obj, ::typeof(@lens first(_)), val) = @set obj[firstindex(obj)] = val ################################################################################ ##### eltype ################################################################################ function set(obj, ::typeof(@lens eltype(_)), ::Type{T}) where {T} return set_eltype(obj, T) end set_eltype(obj::Array, T::Type) = collect(T, obj) set_eltype(obj::Number, T::Type) = T(obj) set_eltype(::Type{<:Number}, ::Type{T}) where {T} = T set_eltype(::Type{<:Array{<:Any, N}}, ::Type{T}) where {N, T} = Array{T, N} set_eltype(::Type{<:Dict}, ::Type{Pair{K, V}}) where {K, V} = Dict{K, V} set_eltype(obj::Dict, ::Type{T}) where {T} = set_eltype(typeof(obj), T)(obj) set(obj::Dict, l::Union{typeof(@lens keytype(_)), typeof(@lens valtype(_))}, T::Type) = set(typeof(obj), l, T)(obj) set(::Type{<:Dict{<:Any,V}}, ::typeof(@lens keytype(_)), ::Type{K}) where {K, V} = Dict{K, V} set(::Type{<:Dict{K}}, ::typeof(@lens valtype(_)), ::Type{V}) where {K, V} = Dict{K, V} ================================================ FILE: src/lens.jl ================================================ export Lens, set, get, modify export @lens export set, get, modify using ConstructionBase export setproperties export constructorof import Base: get, hash, == using Base: getproperty # used for hashing function make_salt(s64::UInt64)::UInt if UInt === UInt64 return s64 else return UInt32(s64 >> 32) ^ UInt32(s64 & 0x00000000ffffffff) end end """ Lens A `Lens` allows to access or replace deeply nested parts of complicated objects. # Example ```jldoctest julia> using Setfield julia> struct T;a;b; end julia> obj = T("AA", "BB") T("AA", "BB") julia> lens = @lens _.a (@lens _.a) julia> get(obj, lens) "AA" julia> set(obj, lens, 2) T(2, "BB") julia> obj T("AA", "BB") julia> modify(lowercase, obj, lens) T("aa", "BB") ``` # Interface Concrete subtypes of `Lens` have to implement * `set(obj, lens, val)` * `get(obj, lens)` These must be pure functions, that satisfy the three lens laws: ```jldoctest; output = false, setup = :(using Setfield; (≅ = (==)); obj = (a="A", b="B"); lens = @lens _.a; val = 2; val1 = 10; val2 = 20) @assert get(set(obj, lens, val), lens) ≅ val # You get what you set. @assert set(obj, lens, get(obj, lens)) ≅ obj # Setting what was already there changes nothing. @assert set(set(obj, lens, val1), lens, val2) ≅ set(obj, lens, val2) # The last set wins. # output ``` Here `≅` is an appropriate notion of equality or an approximation of it. In most contexts this is simply `==`. But in some contexts it might be `===`, `≈`, `isequal` or something 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), [`get`](@ref), [`modify`](@ref). """ abstract type Lens end """ modify(f, obj, l::Lens) Replace a deeply nested part `x` of `obj` by `f(x)`. See also [`Lens`](@ref). """ function modify end """ get(obj, l::Lens) Access a deeply nested part of `obj`. See also [`Lens`](@ref). """ function get end """ set(obj, l::Lens, val) Replace a deeply nested part of `obj` by `val`. See also [`Lens`](@ref). """ function set end @inline function modify(f, obj, l::Lens) old_val = get(obj, l) new_val = f(old_val) set(obj, l, new_val) end struct IdentityLens <: Lens end get(obj, ::IdentityLens) = obj set(obj, ::IdentityLens, val) = val struct PropertyLens{fieldname} <: Lens end function get(obj, l::PropertyLens{field}) where {field} getproperty(obj, field) end @inline function set(obj, l::PropertyLens{field}, val) where {field} patch = (;field => val) setproperties(obj, patch) end struct ComposedLens{LO, LI} <: Lens outer::LO inner::LI end function ==(l1::ComposedLens, l2::ComposedLens) return l1.outer == l2.outer && l1.inner == l2.inner end const SALT_COMPOSEDLENS = make_salt(0xcf7322dcc2129a31) hash(l::ComposedLens, h::UInt) = hash(l.outer, hash(l.inner, SALT_INDEXLENS + h)) """ compose([lens₁, [lens₂, [lens₃, ...]]]) Compose `lens₁`, `lens₂` etc. There is one subtle point here: While the two composition orders `(lens₁ ∘ lens₂) ∘ lens₃` and `lens₁ ∘ (lens₂ ∘ lens₃)` have equivalent semantics, their performance may not be the same. The compiler tends to optimize right associative composition (second case) better then left associative composition. The compose function tries to use a composition order, that the compiler likes. The composition order is therefore not part of the stable API. """ function compose end compose() = IdentityLens() compose(l::Lens) = l compose(::IdentityLens, ::IdentityLens) = IdentityLens() compose(::IdentityLens, l::Lens) = l compose(l::Lens, ::IdentityLens) = l compose(outer::Lens, inner::Lens) = ComposedLens(outer, inner) function compose(l1::Lens, ls::Lens...) compose(l1, compose(ls...)) end """ lens₁ ∘ lens₂ Compose lenses `lens₁`, `lens₂`, ..., `lensₙ` to access nested objects. # Example ```jldoctest julia> using Setfield julia> obj = (a = (b = (c = 1,),),); julia> la = @lens _.a lb = @lens _.b lc = @lens _.c lens = la ∘ lb ∘ lc (@lens _.a.b.c) julia> get(obj, lens) 1 ``` """ Base.:∘(l1::Lens, l2::Lens) = compose(l1, l2) function get(obj, l::ComposedLens) inner_obj = get(obj, l.outer) get(inner_obj, l.inner) end function set(obj,l::ComposedLens, val) inner_obj = get(obj, l.outer) inner_val = set(inner_obj, l.inner, val) set(obj, l.outer, inner_val) end struct IndexLens{I <: Tuple} <: Lens indices::I end ==(l1::IndexLens, l2::IndexLens) = l1.indices == l2.indices const SALT_INDEXLENS = make_salt(0x8b4fd6f97c6aeed6) hash(l::IndexLens, h::UInt) = hash(l.indices, SALT_INDEXLENS + h) Base.@propagate_inbounds function get(obj, l::IndexLens) getindex(obj, l.indices...) end Base.@propagate_inbounds function set(obj, l::IndexLens, val) setindex(obj, val, l.indices...) end struct DynamicIndexLens{F} <: Lens f::F end Base.@propagate_inbounds get(obj, I::DynamicIndexLens) = obj[I.f(obj)...] Base.@propagate_inbounds set(obj, I::DynamicIndexLens, val) = setindex(obj, val, I.f(obj)...) """ FunctionLens(f) @lens f(_) Lens with [`get`](@ref) method definition that simply calls `f`. [`set`](@ref) method for each function `f` must be implemented manually. Use `methods(set, (Any, Setfield.FunctionLens, Any))` to get a list of supported functions. Note that `FunctionLens` flips the order of composition; i.e., `(@lens f(_)) ∘ (@lens g(_)) == @lens g(f(_))`. # Example ```jldoctest julia> using Setfield julia> obj = ((1, 2), (3, 4)); julia> lens = (@lens first(_)) ∘ (@lens last(_)) (@lens last(first(_))) julia> get(obj, lens) 2 julia> set(obj, lens, '2') ((1, '2'), (3, 4)) ``` # Implementation To use `myfunction` as a lens, define a `set` method with the following signature: ```julia Setfield.set(obj, ::typeof(@lens myfunction(_)), val) = ... ``` `typeof` is used above instead of `FunctionLens` because how actual type of `@lens myfunction(_)` is implemented is not the part of stable API. """ struct FunctionLens{f} <: Lens end FunctionLens(f) = FunctionLens{f}() get(obj, ::FunctionLens{f}) where f = f(obj) ================================================ FILE: src/setindex.jl ================================================ Base.@propagate_inbounds function setindex(args...) Base.setindex(args...) end Base.@propagate_inbounds function setindex(xs::AbstractArray, v, I...) # we need to distinguish between scalar and sliced assignment I_normalized = Base.to_indices(xs, I) T = promote_type(eltype(xs), I_normalized isa Tuple{Vararg{Integer}} ? typeof(v) : eltype(v)) ys = similar(xs, T) if eltype(xs) !== Union{} copy!(ys, xs) end ys[I_normalized...] = v return ys end Base.@propagate_inbounds function setindex(d0::AbstractDict, v, k) K = promote_type(keytype(d0), typeof(k)) V = promote_type(valtype(d0), typeof(v)) d = empty(d0, K, V) copy!(d, d0) d[k] = v return d end setindex(a::StaticArraysCore.StaticArray, args...) = Base.setindex(a, args...) ================================================ FILE: src/sugar.jl ================================================ export @set, @lens, @set! using MacroTools """ @set assignment Return a modified copy of deeply nested objects. # Example ```jldoctest julia> using Setfield julia> struct T;a;b end julia> t = T(1,2) T(1, 2) julia> @set t.a = 5 T(5, 2) julia> t T(1, 2) julia> t = @set t.a = T(2,2) T(T(2, 2), 2) julia> @set t.a.b = 3 T(T(2, 3), 2) ``` """ macro set(ex) setmacro(identity, ex, overwrite=false) end """ @set! assignment Shortcut for `obj = @set obj...`. # Example ```jldoctest julia> using Setfield julia> t = (a=1,) (a = 1,) julia> @set! t.a=2 (a = 2,) julia> t (a = 2,) ``` """ macro set!(ex) setmacro(identity, ex, overwrite=true) end is_interpolation(x) = x isa Expr && x.head == :$ foldtree(op, init, x) = op(init, x) foldtree(op, init, ex::Expr) = op(foldl((acc, x) -> foldtree(op, acc, x), ex.args; init=init), ex) const HAS_BEGIN_INDEXING = VERSION ≥ v"1.5.0-DEV.666" function need_dynamic_lens(ex) return foldtree(false, ex) do yes, x (yes || x === :end || (HAS_BEGIN_INDEXING && x === :begin) || x == Expr(:end) || (HAS_BEGIN_INDEXING && x == Expr(:begin)) || x === :_) end end function lower_index(collection::Symbol, index, dim) if isexpr(index, :call) return Expr(:call, lower_index.(collection, index.args, dim)...) elseif (index === :end || index == Expr(:end)) if dim === nothing return :($(Base.lastindex)($collection)) else return :($(Base.lastindex)($collection, $dim)) end elseif HAS_BEGIN_INDEXING && (index === :begin || index == Expr(:begin)) if dim === nothing return :($(Base.firstindex)($collection)) else return :($(Base.firstindex)($collection, $dim)) end end return index end replace_underscore(ex, to) = postwalk(x -> x === :_ ? to : x, ex) function parse_obj_lenses_composite(lensexprs::Vector) if isempty(lensexprs) return esc(:_), () else obj, outermostlens = parse_obj_lens(lensexprs[1]) innerlenses = map(lensexprs[2:end]) do innerex o, lens = parse_obj_lens(innerex) @assert o == esc(:_) lens end return obj, (outermostlens, innerlenses...) end end function parse_obj_lenses(ex) if @capture(ex, ∘(lensexprs__)) return parse_obj_lenses_composite(lensexprs) elseif is_interpolation(ex) @assert length(ex.args) == 1 return esc(:_), (esc(ex.args[1]),) elseif @capture(ex, front_[indices__]) obj, frontlens = parse_obj_lenses(front) if any(need_dynamic_lens, indices) @gensym collection indices = replace_underscore.(indices, collection) dims = length(indices) == 1 ? nothing : 1:length(indices) lindices = esc.(lower_index.(collection, indices, dims)) lens = :($DynamicIndexLens($(esc(collection)) -> ($(lindices...),))) else index = esc(Expr(:tuple, indices...)) lens = :($IndexLens($index)) end elseif @capture(ex, front_.property_) obj, frontlens = parse_obj_lenses(front) if property isa Union{Symbol,String} lens = :($PropertyLens{$(QuoteNode(property))}()) elseif is_interpolation(property) lens = :($PropertyLens{$(esc(property.args[1]))}()) else throw(ArgumentError( string("Error while parsing :($ex). Second argument to `getproperty` can only be", "a `Symbol` or `String` literal, received `$property` instead.") )) end elseif @capture(ex, f_(front_)) obj, frontlens = parse_obj_lenses(front) lens = :($FunctionLens($(esc(f)))) else obj = esc(ex) return obj, () end obj, tuple(frontlens..., lens) end function parse_obj_lens(ex) obj, lenses = parse_obj_lenses(ex) lens = Expr(:call, compose, lenses...) obj, lens end function get_update_op(sym::Symbol) s = String(sym) if !endswith(s, '=') || isdefined(Base, sym) # 'x +=' etc. is actually 'x = x +', and so '+=' isn't defined in Base. # '>=' however is a function, and not an assignment operator. msg = "Operation $sym doesn't look like an assignment" throw(ArgumentError(msg)) end Symbol(s[1:end-1]) end struct _UpdateOp{OP,V} op::OP val::V end (u::_UpdateOp)(x) = u.op(x, u.val) """ setmacro(lenstransform, ex::Expr; overwrite::Bool=false) This function can be used to create a customized variant of [`@set`](@ref). It works by applying `lenstransform` to the lens that is used in the customized `@set` macro at runtime. ```julia function mytransform(lens::Lens)::Lens ... end macro myset(ex) setmacro(mytransform, ex) end ``` See also [`lensmacro`](@ref). """ function setmacro(lenstransform, ex::Expr; overwrite::Bool=false) @assert ex.head isa Symbol @assert length(ex.args) == 2 ref, val = ex.args obj, lens = parse_obj_lens(ref) lenssym = gensym(:lens) dst = overwrite ? obj : gensym("_") val = esc(val) ret = if ex.head == :(=) quote $lenssym = ($lenstransform)($lens) $dst = $set($obj, $lenssym, $val) end else op = get_update_op(ex.head) f = :($_UpdateOp($op,$val)) quote $lenssym = ($lenstransform)($lens) $dst = $modify($f, $obj, $lenssym) end end ret end """ @lens Construct a lens from a field access. # Example ```jldoctest julia> using Setfield julia> struct T;a;b;end julia> t = T("A1", T(T("A3", "B3"), "B2")) T("A1", T(T("A3", "B3"), "B2")) julia> l = @lens _.b.a.b (@lens _.b.a.b) julia> get(t, l) "B3" julia> set(t, l, 100) T("A1", T(T("A3", 100), "B2")) julia> t = ("one", "two") ("one", "two") julia> set(t, (@lens _[1]), "1") ("1", "two") julia> # Indices are always evaluated in external scope; for properties, you can use interpolation: n, i = :a, 10 @lens(_.\$n[i, i+1]) (@lens _.a[10, 11]) ``` """ macro lens(ex) lensmacro(identity, ex) end """ lensmacro(lenstransform, ex::Expr) This function can be used to create a customized variant of [`@lens`](@ref). It works by applying `lenstransform` to the created lens at runtime. ```julia function mytransform(lens::Lens)::Lens ... end macro mylens(ex) lensmacro(mytransform, ex) end ``` See also [`setmacro`](@ref). """ function lensmacro(lenstransform, ex) obj, lens = parse_obj_lens(ex) if obj != esc(:_) msg = """Cannot parse lens $ex. Lens expressions must start with _, got $obj instead.""" throw(ArgumentError(msg)) end :($(lenstransform)($lens)) end has_atlens_support(l::Lens) = has_atlens_support(typeof(l)) has_atlens_support(::Type{<:Lens}) = false has_atlens_support(::Type{<:Union{PropertyLens, IndexLens, FunctionLens, IdentityLens}}) = true has_atlens_support(::Type{ComposedLens{LO, LI}}) where {LO, LI} = has_atlens_support(LO) && has_atlens_support(LI) print_application(io::IO, l::PropertyLens{field}) where {field} = print(io, ".", field) print_application(io::IO, l::IndexLens) = print(io, "[", join(repr.(l.indices), ", "), "]") print_application(io::IO, l::IdentityLens) = print(io, "") function print_application(io::IO, l::ComposedLens) print_application(io, l.outer) print_application(io, l.inner) end function print_application(printer, io, ::FunctionLens{f}) where f print(io, f, '(') printer(io) print(io, ')') end function print_application(printer, io, l) @assert has_atlens_support(l) printer(io) print_application(io, l) end function print_application(printer, io, l::ComposedLens) print_application(io, l.inner) do io print_application(printer, io, l.outer) end end # Since `show` of `ComposedLens` needs to call `show` of other lenses, # we explicitly define text/plain `show` for `ComposedLens` to propagate # the "context" (2-arg or 3-arg `show`) with which `show` has to be called. # See: https://github.com/jw3126/Setfield.jl/pull/86 Base.show(io::IO, ::MIME"text/plain", l::ComposedLens) = _show(io, MIME("text/plain"), l) function _show(io::IO, mime, l::Lens) if has_atlens_support(l) print_in_atlens(io, l) elseif mime === nothing show(io, l) else show(io, mime, l) end end function _show(io::IO, mime, l::ComposedLens) if has_atlens_support(l) print_in_atlens(io, l) else _show(io, mime, l.outer) print(io, " ∘ ") _show(io, mime, l.inner) end end function print_in_atlens(io, l) print(io, "(@lens ") print_application(io, l) do io print(io, '_') end print(io, ')') end ================================================ FILE: test/dynamiclens_begin.jl ================================================ l = @lens _[begin] @test l isa Setfield.DynamicIndexLens obj = (1,2,3) @test get(obj, l) == 1 @test set(obj, l, true) == (true,2,3) l = @lens _[2*begin] @test l isa Setfield.DynamicIndexLens obj = (1,2,3) @test get(obj, l) == 2 @test set(obj, l, true) == (1,true,3) one = 1 plustwo(x) = x + 2 l = @lens _.a[plustwo(begin) - one].b obj = (a=(1, (a=10, b=20), 3), b=4) @test get(obj, l) == 20 @test set(obj, l, true) == (a=(1, (a=10, b=true), 3), b=4) ================================================ FILE: test/perf.jl ================================================ module Perf using BenchmarkTools using BenchmarkTools: Benchmark, TrialEstimate using Setfield using Test using InteractiveUtils using StaticArrays struct AB{A,B} a::A b::B end function lens_set_a((obj, val)) @set obj.a = val end function hand_set_a((obj, val)) AB(val, obj.b) end function lens_set_ab((obj, val)) @set obj.a.b = val end function hand_set_ab((obj, val)) a = AB(obj.a.a, val) AB(a, obj.b) end function lens_set_a_and_b((obj, val)) o1 = @set obj.a = val o2 = @set o1.b = val end function hand_set_a_and_b((obj, val)) AB(val, val) end function lens_set_i((obj, val, i)) @inbounds (@set obj[i] = val) end function hand_set_i((obj, val, i)) @inbounds Base.setindex(obj, val, i) end function benchmark_lens_vs_hand(b_lens::Benchmark, b_hand::Benchmark) te_hand = minimum(run(b_lens)) te_lens = minimum(run(b_hand)) @show te_lens @show te_hand @test te_lens.memory == te_hand.memory @test te_lens.allocs == te_hand.allocs @test te_lens.time <= 2*te_hand.time end function uniquecounts(iter) ret = Dict{eltype(iter), Int}() for x in iter ret[x] = get!(ret, x, 0) + 1 end ret end function test_ir_lens_vs_hand(info_lens::Core.CodeInfo, info_hand::Core.CodeInfo) heads(info) = [ex.head for ex in info.code if ex isa Expr] # test no needless kinds of operations heads_lens = heads(info_lens) heads_hand = heads(info_hand) @test Set(heads_lens) == Set(heads_hand) # test no intermediate objects or lenses @test count(==(:new), heads_lens) == count(==(:new), heads_hand) # this test might be too strict @test uniquecounts(heads_lens) == uniquecounts(heads_hand) end let obj = AB(AB(1,2), :b) val = (1,2) @testset "$(setup.lens)" for setup in [ (lens=lens_set_a, hand=hand_set_a, args=(obj, val)), (lens=lens_set_a, hand=hand_set_a, args=(obj, val)), (lens=lens_set_ab, hand=hand_set_ab, args=(obj, val)), (lens=lens_set_a_and_b, hand=hand_set_a_and_b, args=(obj, val)), (lens=lens_set_i, hand=hand_set_i, args=(@SVector[1,2], 10, 1)) ] f_lens = setup.lens f_hand = setup.hand args = setup.args @assert f_hand(args) == f_lens(args) @testset "IR" begin info_lens, _ = @code_typed f_lens(args) info_hand, _ = @code_typed f_hand(args) test_ir_lens_vs_hand(info_lens, info_hand) end @testset "benchmark" begin b_lens = @benchmarkable $f_lens($args) b_hand = @benchmarkable $f_hand($args) benchmark_lens_vs_hand(b_lens, b_hand) end end end function compose_left_assoc(obj, val) l = @lens ((_.a∘_.b)∘_.c)∘_.d set(obj, l, val) end function compose_right_assoc(obj, val) l = @lens _.a∘(_.b∘(_.c∘_.d)) set(obj, l, val) end function compose_default_assoc(obj, val) l = @lens _.a.b.c.d set(obj, l, val) end @testset "Lens composition compiler prefered associativity" begin obj = (a=(b=(c=(d=1,d2=2),c2=2),b2=3), a2=2) val = 2.2 @test compose_left_assoc(obj, val) == compose_default_assoc(obj, val) @test compose_right_assoc(obj, val) == compose_default_assoc(obj, val) b_default = minimum(@benchmark compose_default_assoc($obj, $val)) println("Default associative composition: $b_default") b_left = minimum(@benchmark compose_left_assoc($obj, $val) ) println("Left associative composition: $b_left") b_right = minimum(@benchmark compose_right_assoc($obj, $val) ) println("Right associative composition: $b_right") @test b_default.allocs == 0 @test b_right.allocs == 0 @test_broken b_left.allocs == 0 @test b_left.time > 2b_default.time @test b_right.time ≈ b_default.time rtol=0.8 end end ================================================ FILE: test/runtests.jl ================================================ module TestSetfield import PerformanceTestTools import Setfield using Documenter: doctest include("test_setindex.jl") include("test_examples.jl") include("test_setmacro.jl") include("test_core.jl") include("test_functionlenses.jl") include("test_staticarrays.jl") include("test_quicktypes.jl") PerformanceTestTools.@include("perf.jl") doctest(Setfield) end # module ================================================ FILE: test/test_core.jl ================================================ module TestCore using Test using Setfield using Setfield: compose, get_update_op using ConstructionBase: ConstructionBase using StaticNumbers: static struct T a b end struct TT{A,B} a::A b::B end @testset "get_update_op" begin @test get_update_op(:(&=)) === :(&) @test get_update_op(:(^=)) === :(^) @test get_update_op(:(-=)) === :(-) @test get_update_op(:(%=)) === :(%) @test_throws ArgumentError get_update_op(:(++)) @test_throws ArgumentError get_update_op(:(<=)) end @testset "@set!" begin a = 1 @set a = 2 @test a === 1 @set! a = 2 @test a === 2 t = T(1, T(2,3)) @set t.b.a = 20 @test t === T(1, T(2,3)) @set! t.b.a = 20 @test t === T(1,T(20,3)) a = 1 @set! a += 10 @test a === 11 nt = (a=1,) @set! nt.a = 5 @test nt === (a=5,) end @testset "@set" begin t = T(1, T(2, T(T(4,4),3))) s = @set t.b.b.a.a = 5 @test t === T(1, T(2, T(T(4,4),3))) @test s === T(1, T(2, T(T(5, 4), 3))) @test_throws ArgumentError @set t.b.b.a.a.a = 3 t = T(1,2) @test T(1, T(1,2)) === @set t.b = T(1,2) @test_throws ArgumentError @set t.c = 3 t = T(T(2,2), 1) s = @set t.a.a = 3 @test s === T(T(3, 2), 1) t = T(1, T(2, T(T(4,4),3))) s = @set t.b.b = 4 @test s === T(1, T(2, 4)) t = T(1,2) s = @set t.a += 1 @test s === T(2,2) t = T(1,2) s = @set t.b -= 2 @test s === T(1,0) t = T(10, 20) s = @set t.a *= 10 @test s === T(100, 20) t = T(2,1) s = @set t.a /= 2 @test s === T(1.0,1) t = T(1, 2) s = @set t.a <<= 2 @test s === T(4, 2) t = T(8, 2) s = @set t.a >>= 2 @test s === T(2, 2) t = T(1, 2) s = @set t.a &= 0 @test s === T(0, 2) t = T(1, 2) s = @set t.a |= 2 @test s === T(3, 2) t = T((1,2),(3,4)) @set t.a[1] = 10 s1 = @set t.a[1] = 10 @test s1 === T((10,2),(3,4)) i = 1 si = @set t.a[i] = 10 @test s1 === si se = @set t.a[end] = 20 @test se === T((1,20),(3,4)) se1 = @set t.a[end-1] = 10 @test s1 === se1 s1 = @set t.a[static(1)] = 10 @test s1 === T((10,2),(3,4)) i = 1 si = @set t.a[static(i)] = 10 @test s1 === si t = @set T(1,2).a = 2 @test t === T(2,2) t = (1, 2, 3, 4) @test (@set t[length(t)] = 40) === (1, 2, 3, 40) @test (@set t[length(t) ÷ 2] = 20) === (1, 20, 3, 4) end struct UserDefinedLens <: Lens end struct LensWithTextPlain <: Lens end Base.show(io::IO, ::MIME"text/plain", ::LensWithTextPlain) = print(io, "I define text/plain.") @testset "show it like you build it " begin i = 3 @testset for item in [ @lens _.a @lens _[1] @lens _[:a] @lens _["a"] @lens _[static(1)] @lens _[static(1), static(1 + 1)] @lens _.a.b[:c]["d"][2][static(3)] @lens _ @lens first(_) @lens last(first(_)) @lens last(first(_.a))[1] UserDefinedLens() (@lens _.a) ∘ UserDefinedLens() UserDefinedLens() ∘ (@lens _.b) (@lens _.a) ∘ UserDefinedLens() ∘ (@lens _.b) (@lens _.a) ∘ LensWithTextPlain() ∘ (@lens _.b) ] buf = IOBuffer() show(buf, item) item2 = eval(Meta.parse(String(take!(buf)))) @test item === item2 end end function test_getset_laws(lens, obj, val1, val2) # set ∘ get val = get(obj, lens) @test set(obj, lens, val) == obj # get ∘ set obj1 = set(obj, lens, val1) @test get(obj1, lens) == val1 # set idempotent obj12 = set(obj1, lens, val2) obj2 = set(obj, lens, val2) @test obj12 == obj2 end function test_modify_law(f, lens, obj) obj_modify = modify(f, obj, lens) old_val = get(obj, lens) val = f(old_val) obj_setfget = set(obj, lens, val) @test obj_modify == obj_setfget end @testset "lens laws" begin obj = T(2, T(T(3,(4,4)), 2)) i = 2 for lens ∈ [ @lens _.a @lens _.b @lens _.b.a @lens _.b.a.b[2] @lens _.b.a.b[i] @lens _.b.a.b[static(2)] @lens _.b.a.b[static(i)] @lens _.b.a.b[end] @lens _.b.a.b[identity(end) - 1] @lens _ ] val1, val2 = randn(2) f(x) = (x,x) test_getset_laws(lens, obj, val1, val2) test_modify_law(f, lens, obj) end end @testset "equality & hashing" begin # singletons (identity and property lens) are egal for (l1, l2) ∈ [ @lens(_) => @lens(_), @lens(_.a) => @lens(_.a), ] @test l1 === l2 @test l1 == l2 @test hash(l1) == hash(l2) end # composite and index lenses are structurally equal for (l1, l2) ∈ [ @lens(_[1]) => @lens(_[1]), @lens(_.a[2]) => @lens(_.a[2]), @lens(_.a.b[3]) => @lens(_.a.b[3]), @lens(_[1:10]) => @lens(_[1:10]), @lens(_.a[2:20]) => @lens(_.a[2:20]), @lens(_.a.b[3:30]) => @lens(_.a.b[3:30]), ] @test l1 == l2 @test hash(l1) == hash(l2) end # inequality for (l1, l2) ∈ [ @lens(_[1]) => @lens(_[2]), @lens(_.a[1]) => @lens(_.a[2]), @lens(_.a[1]) => @lens(_.b[1]), @lens(_[1:10]) => @lens(_[2:20]), @lens(_.a[1:10]) => @lens(_.a[2:20]), @lens(_.a[1:10]) => @lens(_.b[1:10]), ] @test l1 != l2 end # equality with non-equal range types (#165) for (l1, l2) ∈ [ @lens(_[1:10]) => @lens(_[Base.OneTo(10)]), @lens(_.a[1:10]) => @lens(_.a[Base.OneTo(10)]), @lens(_.a.b[1:10]) => @lens(_.a.b[Base.OneTo(10)]), @lens(_.a[Base.StepRange(1, 1, 5)].b[1:10]) => @lens(_.a[1:5].b[Base.OneTo(10)]), @lens(_.a.b[1:3]) => @lens(_.a.b[[1, 2, 3]]), ] @test l1 == l2 @test hash(l1) == hash(l2) end # Hash property: equality implies equal hashes, or in other terms: # lenses either have equal hashes or are unequal # Because collisions can occur theoretically (though unlikely), this is a property test, # not a unit test. random_lenses = (@lens(_.a[rand(Int)]) for _ in 1:1000) @test all((hash(l2) == hash(l1)) || (l1 != l2) for (l1, l2) in zip(random_lenses, random_lenses)) # Lenses should hash differently from the underlying tuples, to avoid confusion. # To account for potential collisions, we check that the property holds with high # probability. @test count(hash(@lens(_[i])) != hash((i,)) for i = 1:1000) > 900 # Same for tuples of tuples (√(1000) ≈ 32). @test count(hash(@lens(_[i][j])) != hash(((i,), (j,))) for i = 1:32, j = 1:32) > 900 end @testset "type stability" begin o1 = 2 o22 = 2 o212 = (4,4) o211 = 3 o21 = TT(o211, o212) o2 = TT(o21, o22) obj = TT(o1, o2) @assert obj === TT(2, TT(TT(3,(4,4)), 2)) i = 1 for (lens, val) ∈ [ ((@lens _.a ), o1 ), ((@lens _.b ), o2 ), ((@lens _.b.a ), o21), ((@lens _.b.a.b[2] ), 4 ), ((@lens _.b.a.b[i+1] ), 4 ), ((@lens _.b.a.b[static(2)] ), 4 ), ((@lens _.b.a.b[static((i+1))]), 4 ), ((@lens _.b.a.b[static(2)] ), 4.0), ((@lens _.b.a.b[static((i+1))]), 4.0), ((@lens _.b.a.b[end]), 4.0), ((@lens _.b.a.b[end÷2+1]), 4.0), ((@lens _ ), obj), ((@lens _ ), :xy), ] @inferred get(obj, lens) @inferred set(obj, lens, val) @inferred modify(identity, obj, lens) end end @testset "IndexLens" begin l = @lens _[] @test l isa Setfield.IndexLens x = randn() obj = Ref(x) @test get(obj, l) == x l = @lens _[][] @test l.outer isa Setfield.IndexLens @test l.inner isa Setfield.IndexLens inner = Ref(x) obj = Base.RefValue{typeof(inner)}(inner) @test get(obj, l) == x obj = (1,2,3) l = @lens _[1] @test l isa Setfield.IndexLens @test get(obj, l) == 1 @test set(obj, l, 6) == (6,2,3) l = @lens _[1:3] @test l isa Setfield.IndexLens @test get([4,5,6,7], l) == [4,5,6] end @testset "DynamicIndexLens" begin l = @lens _[end] @test l isa Setfield.DynamicIndexLens obj = (1,2,3) @test get(obj, l) == 3 @test set(obj, l, true) == (1,2,true) l = @lens _[end÷2] @test l isa Setfield.DynamicIndexLens obj = (1,2,3) @test get(obj, l) == 1 @test set(obj, l, true) == (true,2,3) two = 2 plusone(x) = x + 1 l = @lens _.a[plusone(end) - two].b obj = (a=(1, (a=10, b=20), 3), b=4) @test get(obj, l) == 20 @test set(obj, l, true) == (a=(1, (a=10, b=true), 3), b=4) if Setfield.HAS_BEGIN_INDEXING # Need to keep this in a separate file since `begin` won't parse # on older Julia versions. include("dynamiclens_begin.jl") end end @testset "StaticNumbers" begin obj = (1, 2.0, '3') l = @lens _[static(1)] @test (@inferred get(obj, l)) === 1 @test (@inferred set(obj, l, 6.0)) === (6.0, 2.0, '3') l = @lens _[static(1 + 1)] @test (@inferred get(obj, l)) === 2.0 @test (@inferred set(obj, l, 6)) === (1, 6, '3') n = 1 l = @lens _[static(3n)] @test (@inferred get(obj, l)) === '3' @test (@inferred set(obj, l, 6)) === (1, 2.0, 6) l = @lens _[static(1):static(3)] @test get([4,5,6,7], l) == [4,5,6] @testset "complex example (sweeper)" begin sweeper_with_const = ( model = (1, 2.0, 3im), axis = (@lens _[static(2)]), ) sweeper_with_noconst = @set sweeper_with_const.axis = @lens _[2] function f(s) a = sum(set(s.model, s.axis, 0)) for i in 1:10 a += sum(set(s.model, s.axis, i)) end return a end @test (@inferred f(sweeper_with_const)) == 66 + 33im @test_broken (@inferred f(sweeper_with_noconst)) == 66 + 33im end end mutable struct M a b end @testset "IdentityLens" begin id = @lens _ @test compose(id, id) === id obj1 = M(1,1) obj2 = M(2,2) @test obj2 === set(obj1, id, obj2) la = @lens _.a @test compose(id, la) === la @test compose(la, id) === la end struct ABC{A,B,C} a::A b::B c::C end @testset "type change during @set (default constructorof)" begin obj = TT(2,3) obj2 = @set obj.b = :three @test obj2 === TT(2, :three) end # https://github.com/tkf/Reconstructables.jl#how-to-use-type-parameters struct B{T, X, Y} x::X y::Y B{T}(x::X, y::Y = 2) where {T, X, Y} = new{T, X, Y}(x, y) end ConstructionBase.constructorof(::Type{<: B{T}}) where T = B{T} @testset "type change during @set (custom constructorof)" begin obj = B{1}(2,3) obj2 = @set obj.y = :three @test obj2 === B{1}(2, :three) end @testset "text/plain show" begin @testset for lens in [ LensWithTextPlain() (@lens _.a) ∘ LensWithTextPlain() LensWithTextPlain() ∘ (@lens _.b) (@lens _.a) ∘ LensWithTextPlain() ∘ (@lens _.b) ] @test occursin("I define text/plain.", sprint(show, "text/plain", lens)) end @testset for lens in [ UserDefinedLens() (@lens _.a) ∘ UserDefinedLens() UserDefinedLens() ∘ (@lens _.b) (@lens _.a) ∘ UserDefinedLens() ∘ (@lens _.b) ] @test sprint(show, lens) == sprint(show, "text/plain", lens) end end @testset "Named Tuples" begin t = (x=1, y=2) @test (@set t.x =2) === (x=2, y=2) @test (@set t.x += 2) === (x=3, y=2) @test (@set t.x =:hello) === (x=:hello, y=2) l = @lens _.x @test get(t, l) === 1 # do we want this to throw an error? @test_throws ArgumentError (@set t.z = 3) end struct CustomProperties _a _b end function ConstructionBase.setproperties(o::CustomProperties, patch::NamedTuple) CustomProperties(get(patch, :a, getfield(o, :_a)), get(patch, :b, getfield(o, :_b))) end ConstructionBase.constructorof(::Type{CustomProperties}) = error() @testset "setproperties overloading" begin o = CustomProperties("A", "B") o2 = @set o.a = :A @test o2 == CustomProperties(:A, "B") o3 = @set o.b = :B @test o3 == CustomProperties("A", :B) end @testset "issue #83" begin @test_throws ArgumentError Setfield.lensmacro(identity, :(_.[:a])) end @testset "@lens and ∘" begin @test @lens(∘()) === @lens(_) @test @lens(∘(_.a)) === @lens(_.a) @test @lens(∘(_.a, _.b)) === @lens(_.a) ∘ @lens(_.b) @test @lens(∘(_.a, _.b, _.c)) === Setfield.compose(@lens(_.a), @lens(_.b), @lens(_.c)) @test @lens(∘(_[1])) === @lens(_[1]) @test @lens(∘(_[1], _[2])) === @lens(_[1]) ∘ @lens(_[2]) @test @lens(∘(_[1], _[2], _[3])) === Setfield.compose(@lens(_[1]), @lens(_[2]), @lens(_[3])) @test @lens(_ ∘ (_[1] ∘ _.a) ∘ first(_)) == @lens(_) ∘ (@lens(_[1]) ∘ @lens(_.a)) ∘ @lens(first(_)) end @testset "@lens ∘ and \$" begin lbc = @lens _.b.c @test @lens($lbc)== lbc @test @lens(_.a ∘ $lbc) == @lens(_.a) ∘ lbc @test @lens(_.a ∘ $lbc ∘ _[1] ∘ $lbc) == @lens(_.a) ∘ lbc ∘ @lens(_[1]) ∘ lbc # property interpolation name = :a fancy(name, suffix) = Symbol("fancy_", name, suffix) @test @lens(_.$name) == @lens(_.a) @test @lens(_.x[1, :].$name) == @lens(_.x[1, :].a) @test @lens(_.x[1, :].$(fancy(name, "✨"))) == @lens(_.x[1, :].fancy_a✨) end end ================================================ FILE: test/test_examples.jl ================================================ module TestExamples using Test dir = joinpath("..", "examples") @testset "example $filename" for filename in readdir(dir) path = joinpath(dir, filename) include(path) end end#module ================================================ FILE: test/test_functionlenses.jl ================================================ module TestFunctionLenses using Test using Setfield @testset "first" begin obj = (1, 2.0, '3') l = @lens first(_) @test get(obj, l) === 1 @test set(obj, l, "1") === ("1", 2.0, '3') @test (@set first(obj) = "1") === ("1", 2.0, '3') obj2 = (a=((b=1,), 2), c=3) @test (@set first(obj2.a).b = '1') === (a=((b='1',), 2), c=3) end @testset "last" begin obj = (1, 2.0, '3') l = @lens last(_) @test get(obj, l) === '3' @test set(obj, l, '4') === (1, 2.0, '4') @test (@set last(obj) = '4') === (1, 2.0, '4') obj2 = (a=(1, (b=2,)), c=3) @test (@set last(obj2.a).b = '2') === (a=(1, (b='2',)), c=3) end @testset "eltype on Number" begin @test @set(eltype(Int) = Float32) === Float32 @test @set(eltype(1.0) = UInt8) === UInt8(1) @inferred set(Int, @lens(eltype(_)), Float32) @inferred set(1.2, @lens(eltype(_)), Float32) end @testset "eltype(::Type{<:Array})" begin obj = Vector{Int} @inferred set(obj, @lens(eltype(_)), Float32) obj2 = @set eltype(obj) = Float64 @test obj2 === Vector{Float64} end @testset "eltype(::Array)" begin obj = [1, 2, 3] @inferred set(obj, @lens(eltype(_)), Float32) obj2 = @set eltype(obj) = Float64 @test eltype(obj2) == Float64 @test obj == obj2 end @testset "(key|val|el)type(::Type{<:Dict})" begin obj = Dict{Symbol, Int} @test (@set keytype(obj) = String) === Dict{String, Int} @test (@set valtype(obj) = String) === Dict{Symbol, String} @test (@set eltype(obj) = Pair{String, Any}) === Dict{String, Any} obj2 = Dict{Symbol, Dict{Int, Float64}} @test (@set keytype(valtype(obj2)) = String) === Dict{Symbol, Dict{String, Float64}} @test (@set valtype(valtype(obj2)) = String) === Dict{Symbol, Dict{Int, String}} end @testset "(key|val|el)type(::Dict)" begin obj = Dict(1 => 2) @test typeof(@set keytype(obj) = Float64) === Dict{Float64, Int} @test typeof(@set valtype(obj) = Float64) === Dict{Int, Float64} @test typeof(@set eltype(obj) = Pair{UInt, Float64}) === Dict{UInt, Float64} end end # module ================================================ FILE: test/test_quicktypes.jl ================================================ module TestQuicktypes using Test import Base: == import MacroTools using QuickTypes using Setfield import ConstructionBase # this is a limitation in `MacroTools.splitarg`. If it is fixed # this test can be removed and our custom splitarg removed. try MacroTools.splitarg(:(x=$nothing)) println("MacroTools.splitarg can now handle literal nothing in AST. Revisit splitarg_no_default workaround") catch e @assert e isa AssertionError end # Examples are taken from https://github.com/cstjean/QuickTypes.jl # Strings are replaced with symbols so that `===` and `==` works # nicely. @qstruct Wall(width, height) @testset "Wall" begin x0 = Wall(400, 600) x1 = @set x0.width = 300 @test x1 === Wall(300, 600) end abstract type Vehicle end @qstruct Car{T<:Number, U}(size::T, nwheels::Int=4; manufacturer::U=nothing, brand::String="off-brand") <: Vehicle @testset "Car" begin c = Car(10; manufacturer=("Danone", "Hershey")) @test c isa Car @test Car <: Vehicle c2 = @set c.size = 10 @test c2.size === 10 c3 = @set c.manufacturer = 100 @test c3 === Car(10;manufacturer =100) end @qstruct Empty() @qstruct Cat(name, age::Int, nlegs=4; species=:Siamese) @testset "Cat" begin x0 = Cat(:Tama, 1) x1 = @set x0.nlegs = 8 @test x1 === Cat(:Tama, 1, 8) x2 = @set x0.species = :Singapura @test x2 === Cat(:Tama, 1, species=:Singapura) end @qstruct Pack{T, N}(animals::NTuple{N, T}) @testset "Pack" begin x = Pack((Cat(:Tama, 1), Cat(:Pochi, 2))) x = @set x.animals[2].nlegs = 5 @test x.animals == (Cat(:Tama, 1), Cat(:Pochi, 2, 5)) end abstract type Tree end @qstruct Maple(qty_syrup::Float64) <: Tree @testset "Maple" begin x0 = Maple(1) x1 = @set x0.qty_syrup = 2 @test x1 === Maple(2) end @qmutable Window(height::Float64, width::Float64) ==(x::Window, y::Window) = x.height == y.height && x.width == y.width @testset "Window" begin x0 = Window(1, 2) x1 = @set x0.width = 3.0 @test isequal(x1, Window(1, 3)) @test x1 == Window(1, 3) x2 = @set x0.width = 3 @test x1 == x2 @test !(x1 === x2) end @qstruct Human(; name=:Alice, height::Float64=170) do @assert height > 0 # arbitrary code, executed in the constructor end @testset "Human" begin x0 = Human() x1 = @set x0.name = :Bob @test x1 === Human(name=:Bob) @test_throws AssertionError @set x0.height = -10 end @qstruct Group{x}(members::x; _concise_show=true) @testset "Group" begin x = Group((0, 1)) x = @set x.members[2] = 111 @test x.members == (0, 111) end @qstruct_fp Plane1(nwheels, weight::Number; brand=:zoomba) @testset "Plane1" begin x0 = Plane1(3, 100) x1 = @set x0.nwheels = 5 @test x1 == Plane1(5, 100) @test (@set x0.brand = 31).brand === 31 end # Another way to "support" QuickTypes with type parameters is to use # QuickTypes.construct. @qstruct_fp Plane2(nwheels, weight::Number; brand=:zoomba) ConstructionBase.constructorof(::Type{<: Plane2}) = (args...) -> QuickTypes.construct(Plane2, args...) @testset "Plane2" begin x0 = Plane2(3, 100) x1 = @set x0.brand = 31 @test typeof(x1) != typeof(x0) @test x1 == Plane2(3, 100, brand=31) end end # module ================================================ FILE: test/test_setindex.jl ================================================ module TestSetindex using Setfield using Test """ ==ₜ(x, y) Check that _type_ and value of `x` and `y` are equal. """ ==ₜ(_, _) = false ==ₜ(x::T, y::T) where T = x == y @testset "==ₜ" begin @test 1 ==ₜ 1 @test !(1.0 ==ₜ 1) end @testset "setindex" begin arr = [1,2,3] @test_throws MethodError Base.setindex(arr, 10, 1) @test Setfield.setindex(arr, 10, 1) == [10, 2, 3] @test arr == [1,2,3] @test @set(arr[1] = 10) == [10, 2, 3] @test arr == [1,2,3] @test Setfield.setindex(arr, 10.0, 1) ==ₜ Float64[10.0, 2.0, 3.0] @test Setfield.setindex(ones(2, 2), zeros(2), 1, :) ==ₜ Float64[0.0 0.0; 1.0 1.0] @test Setfield.setindex(ones(BigInt, 2, 2), zeros(Float32, 2), 1, :) ==ₜ BigFloat[0.0 0.0; 1.0 1.0] @test Setfield.setindex(fill(ones(1), 2, 2), [im, im], :, 1) ==ₜ hcat([im, im], [[1.0], [1.0]]) d = Dict(:a => 1, :b => 2) @test_throws MethodError Base.setindex(d, 10, :a) @test Setfield.setindex(d, 10, :a) == Dict(:a=>10, :b=>2) @test d == Dict(:a => 1, :b => 2) @test @set(d[:a] = 10) == Dict(:a=>10, :b=>2) @test d == Dict(:a => 1, :b => 2) @test Setfield.setindex(d, 30, "c") ==ₜ Dict(:a=>1, :b=>2, "c"=>30) @test Setfield.setindex(d, 10.0, :a) ==ₜ Dict(:a=>10.0, :b=>2.0) end end ================================================ FILE: test/test_setmacro.jl ================================================ module TestSetMacro module Clone using Setfield: setmacro, lensmacro macro lens(ex) lensmacro(identity, ex) end macro set(ex) setmacro(identity, ex) end end#module Clone using Setfield: Setfield using Test using .Clone: Clone using StaticArrays: @SMatrix using StaticNumbers @testset "setmacro, lensmacro isolation" begin # test that no symbols like `IndexLens` are needed: @test Clone.@lens(_ ) isa Setfield.Lens @test Clone.@lens(_.a ) isa Setfield.Lens @test Clone.@lens(_[1] ) isa Setfield.Lens @test Clone.@lens(first(_) ) isa Setfield.Lens @test Clone.@lens(_[end] ) isa Setfield.Lens @test Clone.@lens(_[static(1)] ) isa Setfield.Lens @test Clone.@lens(_.a[1][end, end-2].b[static(1), static(1)]) isa Setfield.Lens @test Setfield.@lens(_.a) === Clone.@lens(_.a) @test Setfield.@lens(_.a.b) === Clone.@lens(_.a.b) @test Setfield.@lens(_.a.b[1,2]) === Clone.@lens(_.a.b[1,2]) o = (a=1, b=2) @test Clone.@set(o.a = 2) === Setfield.@set(o.a = 2) @test Clone.@set(o.a += 2) === Setfield.@set(o.a += 2) m = @SMatrix [0 0; 0 0] m2 = Clone.@set m[end-1, end] = 1 @test m2 === @SMatrix [0 1; 0 0] m3 = Clone.@set(first(m) = 1) @test m3 === @SMatrix[1 0; 0 0] end function test_all_inferrable(f, argtypes) typed = first(code_typed(f, argtypes)) code = typed.first @test all(T -> !(T isa UnionAll || T === Any), code.slottypes) end # Example of macro that caused inference issues before. macro test_macro(expr) quote function f($(esc(:x))) $(Setfield.setmacro(identity, expr, overwrite=true)) $(Setfield.setmacro(identity, expr, overwrite=true)) $(Setfield.setmacro(identity, expr, overwrite=true)) $(Setfield.setmacro(identity, expr, overwrite=true)) $(Setfield.setmacro(identity, expr, overwrite=true)) return $(esc(:x)) end end end if VERSION >= v"1.3" @testset "setmacro multiple usage" begin let f = @test_macro(x[end] = 1) test_all_inferrable(f, (Vector{Float64}, )) end end end end#module ================================================ FILE: test/test_staticarrays.jl ================================================ module TestStaticArrays using Test using Setfield using StaticArrays using StaticNumbers @testset "StaticArrays" begin obj = StaticArrays.@SMatrix [1 2; 3 4] @testset for l in [ (@lens _[2,1]), ] @test get(obj, l) == 3 @test set(obj, l, 5) == StaticArrays.@SMatrix [1 2; 5 4] @test setindex(obj, 5, 2, 1) == StaticArrays.@SMatrix [1 2; 5 4] end v = @SVector [1,2,3] @test (@set v[1] = 10) === @SVector [10,2,3] @test_broken (@set v[1] = π) === @SVector [π,2,3] @testset "Multi-dynamic indexing" begin two = 2 plusone(x) = x + 1 l1 = @lens _.a[2, 1].b l2 = @lens _.a[plusone(end) - two, end÷2].b m_orig = @SMatrix [ (a=1, b=10) (a=2, b=20) (a=3, b=30) (a=4, b=40) (a=5, b=50) (a=6, b=60) ] m_mod = @SMatrix [ (a=1, b=10) (a=2, b=20) (a=3, b=3000) (a=4, b=40) (a=5, b=50) (a=6, b=60) ] obj = (a=m_orig, b=4) @test get(obj, l1) === get(obj, l2) === 30 @test set(obj, l1, 3000) === set(obj, l2, 3000) === (a=m_mod, b=4) end end end