From f72c06e37bbf8aa0bd8791ec97423a156a285a8d Mon Sep 17 00:00:00 2001 From: Carlo Lucibello Date: Wed, 13 Mar 2024 04:51:57 +0100 Subject: [PATCH 1/4] reorganize API docs (#78) * reorganize API docs * removed one header level --- docs/src/api.md | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index dfa38ae..69a1ba0 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,16 +1,37 @@ -```@docs -Functors.fmap -Functors.fmap_with_path -Functors.@functor -Functors.@leaf +```@meta +CurrentModule = Functors +``` + +# API + +```@index +Modules = [Functors] +Pages = ["api.md"] ``` +## Constructors and helpers + ```@docs +Functors.@functor +Functors.@leaf Functors.functor Functors.children Functors.isleaf +Functors.fcollect +Functors.fleaves +``` + +## Maps + +```@docs +Functors.fmap +Functors.fmap_with_path +Functors.fmapstructure +Functors.fmapstructure_with_path ``` +## Walks + ```@docs Functors.AbstractWalk Functors.execute @@ -23,12 +44,7 @@ Functors.AnonymousWalk Functors.IterateWalk ``` -```@docs -Functors.fmapstructure -Functors.fmapstructure_with_path -Functors.fcollect -Functors.fleaves -``` +## KeyPath ```@docs Functors.KeyPath From c6ce6e8c7f4c55cb06d8cd8818aaccb4d8a984c3 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Wed, 13 Mar 2024 09:38:08 -0400 Subject: [PATCH 2/4] functor `Base.Splat` (#80) --- src/base.jl | 4 ++++ test/base.jl | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/base.jl b/src/base.jl index 6bcd8b0..913aaa7 100644 --- a/src/base.jl +++ b/src/base.jl @@ -10,6 +10,10 @@ @functor Base.Fix2 @functor Base.Broadcast.BroadcastFunction +@static if VERSION >= v"1.9" + @functor Base.Splat +end + @static if VERSION >= v"1.7" @functor Base.Returns end diff --git a/test/base.jl b/test/base.jl index 5ac5bea..e06d415 100644 --- a/test/base.jl +++ b/test/base.jl @@ -56,6 +56,12 @@ VERSION >= v"1.7" && @testset "Returns" begin @test Functors.functor(ret)[2]((value = 1:3,)) === Returns(1:3) end +VERSION >= v"1.9" && @testset "Splat" begin + ret = Base.splat(Returns([0, pi, 2pi])) + @test Functors.functor(ret)[1].f.value == [0, pi, 2pi] + @test Functors.functor(ret)[2]((f = sin,)) === Base.splat(sin) +end + @testset "LinearAlgebra containers" begin @test fmapstructure(identity, [1,2,3]') == (parent = [1, 2, 3],) @test fmapstructure(identity, transpose([1,2,3])) == (parent = [1, 2, 3],) From bccabfc575a334927c13a7f4d8b739f9b51cda5e Mon Sep 17 00:00:00 2001 From: Carlo Lucibello Date: Tue, 2 Apr 2024 08:59:33 +0200 Subject: [PATCH 3/4] add `getkeypath` and `haskeypath` (#76) * add getkeypath, haskeypath * docs * getindex(x, kp) = getkeypath(x, kp) * remove keypath indexing * use getproperty --- docs/src/api.md | 2 + src/Functors.jl | 5 ++- src/keypath.jl | 109 +++++++++++++++++++++++++++++++++++++++++++++++- test/basics.jl | 9 ++-- test/keypath.jl | 53 +++++++++++++++++++++++ 5 files changed, 172 insertions(+), 6 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 69a1ba0..70dd2ff 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -48,4 +48,6 @@ Functors.IterateWalk ```@docs Functors.KeyPath +Functors.haskeypath +Functors.getkeypath ``` diff --git a/src/Functors.jl b/src/Functors.jl index 16c2f01..e7ed30d 100644 --- a/src/Functors.jl +++ b/src/Functors.jl @@ -1,7 +1,8 @@ module Functors -export @functor, @flexiblefunctor, fmap, fmapstructure, fcollect, execute, fleaves, - KeyPath, fmap_with_path, fmapstructure_with_path +export @functor, @flexiblefunctor, fmap, fmapstructure, fcollect, execute, fleaves, + fmap_with_path, fmapstructure_with_path, + KeyPath, getkeypath, haskeypath include("functor.jl") include("keypath.jl") diff --git a/src/keypath.jl b/src/keypath.jl index c352d1e..ac468fa 100644 --- a/src/keypath.jl +++ b/src/keypath.jl @@ -1,3 +1,5 @@ +using Base: tail + KeyT = Union{Symbol, AbstractString, Integer} """ @@ -7,14 +9,46 @@ A type for representing a path of keys to a value in a nested structure. Can be constructed with a sequence of keys, or by concatenating other `KeyPath`s. Keys can be of type `Symbol`, `String`, or `Int`. +For custom types, access through symbol keys is assumed to be done with `getproperty`. +For consistency, the method `Base.propertynames` is used to get the viable property names. + +For string and integer keys instead, the access is done with `getindex`. + +See also [`getkeypath`](@ref), [`haskeypath`](@ref). + # Examples ```jldoctest julia> kp = KeyPath(:b, 3) KeyPath(:b, 3) -julia> KeyPath(:a, kp, :c, 4) +julia> KeyPath(:a, kp, :c, 4) # construct mixing keys and keypaths KeyPath(:a, :b, 3, :c, 4) + +julia> struct T + a + b + end + +julia> function Base.getproperty(x::T, k::Symbol) + if k in fieldnames(T) + return getfield(x, k) + elseif k === :ab + return "ab" + else + error() + end + end; + +julia> Base.propertynames(::T) = (:a, :b, :ab); + +julia> x = T(3, Dict(:c => 4, :d => 5)); + +julia> getkeypath(x, KeyPath(:ab)) # equivalent to x.ab +"ab" + +julia> getkeypath(x, KeyPath(:b, :c)) # equivalent to (x.b)[:c] +4 ``` """ struct KeyPath{T<:Tuple} @@ -29,10 +63,14 @@ function KeyPath(keys::Union{KeyT, KeyPath}...) return KeyPath(((ks...)...,)) end +Base.isempty(kp::KeyPath) = false +Base.isempty(kp::KeyPath{Tuple{}}) = true Base.getindex(kp::KeyPath, i::Int) = kp.keys[i] Base.length(kp::KeyPath) = length(kp.keys) Base.iterate(kp::KeyPath, state=1) = iterate(kp.keys, state) Base.:(==)(kp1::KeyPath, kp2::KeyPath) = kp1.keys == kp2.keys +Base.tail(kp::KeyPath) = KeyPath(Base.tail(kp.keys)) +Base.last(kp::KeyPath) = last(kp.keys) function Base.show(io::IO, kp::KeyPath) compat = get(io, :compact, false) @@ -45,3 +83,72 @@ end keypathstr(kp::KeyPath) = join(kp.keys, ".") +_getkey(x, k::Integer) = x[k] +_getkey(x, k::Symbol) = getproperty(x, k) +_getkey(x::AbstractDict, k::Symbol) = x[k] +_getkey(x, k::AbstractString) = x[k] + +_haskey(x, k::Integer) = haskey(x, k) +_haskey(x::Tuple, k::Integer) = 1 <= k <= length(x) +_haskey(x::AbstractArray, k::Integer) = 1 <= k <= length(x) # TODO: extend to generic indexing +_haskey(x, k::Symbol) = k in propertynames(x) +_haskey(x::AbstractDict, k::Symbol) = haskey(x, k) +_haskey(x, k::AbstractString) = haskey(x, k) + +""" + getkeypath(x, kp::KeyPath) + +Return the value in `x` at the path `kp`. + +See also [`KeyPath`](@ref) and [`haskeypath`](@ref). + +# Examples +```jldoctest +julia> x = Dict(:a => 3, :b => Dict(:c => 4, "d" => [5, 6, 7])) +Dict{Symbol, Any} with 2 entries: + :a => 3 + :b => Dict{Any, Any}(:c=>4, "d"=>[5, 6, 7]) + +julia> getkeypath(x, KeyPath(:b, "d", 2)) +6 +``` +""" +function getkeypath(x, kp::KeyPath) + if isempty(kp) + return x + else + return getkeypath(_getkey(x, first(kp)), tail(kp)) + end +end + +""" + haskeypath(x, kp::KeyPath) + +Return `true` if `x` has a value at the path `kp`. + +See also [`KeyPath`](@ref) and [`getkeypath`](@ref). + +# Examples +```jldoctest +julia> x = Dict(:a => 3, :b => Dict(:c => 4, "d" => [5, 6, 7])) +Dict{Any,Any} with 2 entries: + :a => 3 + :b => Dict{Any,Any}(:c=>4,"d"=>[5, 6, 7]) + +julia> haskeypath(x, KeyPath(:a)) +true + +julia> haskeypath(x, KeyPath(:b, "d", 1)) +true + +julia> haskeypath(x, KeyPath(:b, "d", 4)) +false +""" +function haskeypath(x, kp::KeyPath) + if isempty(kp) + return true + else + k = first(kp) + return _haskey(x, k) && haskeypath(_getkey(x, k), tail(kp)) + end +end diff --git a/test/basics.jl b/test/basics.jl index c3ae540..feee703 100644 --- a/test/basics.jl +++ b/test/basics.jl @@ -400,10 +400,13 @@ end @functor A a = A(1) @test Functors.children(a) === (x = 1,) - Functors.@leaf A - children, re = Functors.functor(a) + + struct B; x; end + Functors.@leaf B + b = B(1) + children, re = Functors.functor(b) @test children == Functors.NoChildren() - @test re(children) === a + @test re(children) === b end @testset "IterateWalk" begin diff --git a/test/keypath.jl b/test/keypath.jl index d5d5e55..a206d53 100644 --- a/test/keypath.jl +++ b/test/keypath.jl @@ -15,4 +15,57 @@ kp0 = KeyPath() @test (kp0...,) === () + + struct Tkp + a + b + c + end + + function Base.getproperty(x::Tkp, k::Symbol) + if k in fieldnames(Tkp) + return getfield(x, k) + elseif k === :ab + return "ab" + else + error() + end + end + + Base.propertynames(::Tkp) = (:a, :b, :c, :ab) + + @testset "getkeypath" begin + x = Dict(:a => 3, :b => Dict(:c => 4, "d" => [5, 6, 7])) + @test getkeypath(x, KeyPath(:a)) == 3 + @test getkeypath(x, KeyPath(:b, :c)) == 4 + @test getkeypath(x, KeyPath(:b, "d", 2)) == 6 + + x = Tkp(3, Tkp(4, 5, (6, 7)), 8) + kp = KeyPath(:b, :c, 2) + @test getkeypath(x, kp) == 7 + + @testset "access through getproperty" begin + x = Tkp(3, Dict(:c => 4, :d => 5), 6); + + @test getkeypath(x, KeyPath(:ab)) == "ab" + @test getkeypath(x, KeyPath(:b, :c)) == 4 + end + end + + @testset "haskeypath" begin + x = Dict(:a => 3, :b => Dict(:c => 4, "d" => [5, 6, 7])) + @test haskeypath(x, KeyPath(:a)) + @test haskeypath(x, KeyPath(:b, :c)) + @test haskeypath(x, KeyPath(:b, "d", 2)) + @test !haskeypath(x, KeyPath(:b, "d", 4)) + @test !haskeypath(x, KeyPath(:b, "e")) + + @testset "access through getproperty" begin + x = Tkp(3, Dict(:c => 4, :d => 5), 6); + + @test haskeypath(x, KeyPath(:ab)) + @test haskeypath(x, KeyPath(:b, :c)) + @test !haskeypath(x, KeyPath(:b, :e)) + end + end end From b35e8547cbe575f69b485e1f1904c489c67d9722 Mon Sep 17 00:00:00 2001 From: Carlo Lucibello Date: Tue, 2 Apr 2024 14:10:16 +0200 Subject: [PATCH 4/4] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7239db0..8dcf4dc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Functors" uuid = "d9f16b24-f501-4c13-a1f2-28368ffc5196" authors = ["Mike J Innes "] -version = "0.4.8" +version = "0.4.9" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"