From 7d90f503db65c778a4341781dabac50fa5826de1 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sat, 26 Mar 2022 20:43:58 +0100 Subject: [PATCH 01/20] implement and document fieldvalues --- src/ConstructionBase.jl | 13 ++++++++++++ src/constructorof.md | 12 +++++------ src/fieldvalues.md | 45 +++++++++++++++++++++++++++++++++++++++++ src/setproperties.md | 2 +- test/runtests.jl | 17 ++++++++++++++++ 5 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 src/fieldvalues.md diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 0302fc0..7ef7bd8 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -3,11 +3,14 @@ module ConstructionBase export getproperties export setproperties export constructorof +export fieldvalues + # Use markdown files as docstring: for (name, path) in [ :ConstructionBase => joinpath(dirname(@__DIR__), "README.md"), :constructorof => joinpath(@__DIR__, "constructorof.md"), + :fieldvalues => joinpath(@__DIR__, "fieldvalues.md"), :getproperties => joinpath(@__DIR__, "getproperties.md"), :setproperties => joinpath(@__DIR__, "setproperties.md"), ] @@ -52,6 +55,16 @@ getproperties(o::Tuple) = o :(NamedTuple{$fnames}($fvals)) end +################################################################################ +#### fieldvalues +################################################################################ +fieldvalues(x::Tuple) = x +fieldvalues(x::NamedTuple) = Tuple(x) +@generated function fieldvalues(x::T) where {T} + fields = (:(getfield(x, $i)) for i in 1:fieldcount(T)) + Expr(:tuple, fields...) +end + ################################################################################ ##### setproperties ################################################################################ diff --git a/src/constructorof.md b/src/constructorof.md index 4c88f81..d955c3b 100644 --- a/src/constructorof.md +++ b/src/constructorof.md @@ -32,17 +32,17 @@ julia> constructorof(S)(1,2,4) ERROR: AssertionError: a + b == checksum ``` Instead `constructor` can be any object that satisfies the following properties: -* It must be possible to reconstruct an object from the `NamedTuple` returned by -`getproperties`: +* It must be possible to reconstruct an object from the `Tuple` returned by +[`fieldvalues`](@ref): ```julia ctor = constructorof(typeof(obj)) -@assert obj == ctor(getproperties(obj)...) -@assert typeof(obj) == typeof(ctor(getproperties(obj)...)) +@assert obj == ctor(fieldvalues(obj)...) +@assert typeof(obj) == typeof(ctor(fieldvalues(obj)...)) ``` -* The other direction should hold for as many values of `args` as possible: +* The other direction should hold for as many values of `args::Tuple` as possible: ```julia ctor = constructorof(T) -getproperties(ctor(args...)) == args +fieldvalues(ctor(args...)) == args ``` For instance given a suitable parametric type it should be possible to change the type of its fields: diff --git a/src/fieldvalues.md b/src/fieldvalues.md new file mode 100644 index 0000000..d155b7a --- /dev/null +++ b/src/fieldvalues.md @@ -0,0 +1,45 @@ + fieldvalues(obj) -> Tuple + +Return a tuple containing field values of `obj`. + +# Examples +```jldoctest +julia> using ConstructionBase + +julia> struct S + a::A + b::B + end + +julia> fieldvalues(S(1,2)) +(1,2) + +julia> fieldvalues((a=10,b=20)) +(10,20) + +julia> fieldvalues((4,5,6)) +(4,5,6) +``` + +# Specification + +Semantically `fieldvalues` boils down to `getfield` and `fieldcount`: +```julia +fieldvalues(obj) == Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) +``` +The following relation to [`constructorof`](@ref) should be satisfied: +```julia +@assert obj == constructorof(obj)(fieldvalues(obj)...) +``` + +# Implementation + +The semantics of `fieldvalues` should generally not be changed. It should equivalent to +```julia +Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) +``` +even if that included private fields of `obj`. +See also [`getproperties`](@ref), [`constructorof`](@ref) + + +See also [Tips section in the manual](@ref type-tips) diff --git a/src/setproperties.md b/src/setproperties.md index e40ac4d..11faa4b 100644 --- a/src/setproperties.md +++ b/src/setproperties.md @@ -1,6 +1,6 @@ setproperties(obj, patch::NamedTuple) -Return a copy of `obj` with attributes updates accoring to `patch`. +Return a copy of `obj` with attributes updates according to `patch`. # Examples ```jldoctest diff --git a/test/runtests.jl b/test/runtests.jl index b3b0198..8279cc3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,6 +19,18 @@ end @test constructorof(Tuple{Nothing, Missing})(1.0, 2) === (1.0, 2) end +@testset "fieldvalues" begin + @test fieldvalues(()) === () + @test fieldvalues([]) === () + @test fieldvalues(Empty()) === () + @test fieldvalues(NamedTuple()) === () + @test fieldvalues((10,20,30)) === (10,20,30) + @test fieldvalues((a=10,b=20f0,c=true)) === (10,20f0,true) + @test fieldvalues(AB(1, 10)) === (1, 10) + +end + + @testset "getproperties" begin o = AB(1, 2) @test getproperties(o) === (a=1, b=2) @@ -310,4 +322,9 @@ end @inferred getproperties(funny_numbers(S1)) @inferred getproperties(funny_numbers(S20)) @inferred getproperties(funny_numbers(S40)) + + @inferred fieldvalues(funny_numbers(S0)) + @inferred fieldvalues(funny_numbers(S1)) + @inferred fieldvalues(funny_numbers(S20)) + @inferred fieldvalues(funny_numbers(S40)) end From 22fc7c72261b45619009ecbcaa2ca4c60db1ca4b Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sat, 26 Mar 2022 20:49:33 +0100 Subject: [PATCH 02/20] fix typos and add test --- src/fieldvalues.md | 5 +---- test/runtests.jl | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/fieldvalues.md b/src/fieldvalues.md index d155b7a..38391c5 100644 --- a/src/fieldvalues.md +++ b/src/fieldvalues.md @@ -38,8 +38,5 @@ The semantics of `fieldvalues` should generally not be changed. It should equiva ```julia Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) ``` -even if that included private fields of `obj`. +even if that includes private fields of `obj`. See also [`getproperties`](@ref), [`constructorof`](@ref) - - -See also [Tips section in the manual](@ref type-tips) diff --git a/test/runtests.jl b/test/runtests.jl index 8279cc3..5280885 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -27,7 +27,8 @@ end @test fieldvalues((10,20,30)) === (10,20,30) @test fieldvalues((a=10,b=20f0,c=true)) === (10,20f0,true) @test fieldvalues(AB(1, 10)) === (1, 10) - + adder(a) = x -> x + a + @test fieldvalues(adder(1)) === (1,) end From 91ba22cff4608898cf5c3e6a0bb1c59c7b822371 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sun, 27 Mar 2022 07:42:50 +0200 Subject: [PATCH 03/20] fix doctests --- docs/Manifest.toml | 101 +++++++++++++++++++++++++++++++++++---------- src/fieldvalues.md | 8 ++-- 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/docs/Manifest.toml b/docs/Manifest.toml index fe71747..4910790 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,27 +1,51 @@ # This file is machine-generated - editing it directly is not advised +[[ANSIColoredPrinters]] +git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" +uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" +version = "0.0.1" + +[[Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + [[Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +[[CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" + +[[ConstructionBase]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "f74e9d5388b8620b4cee35d4c5a618dd4dc547f4" +uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +version = "1.3.0" + [[Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -[[Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" - [[DocStringExtensions]] -deps = ["LibGit2", "Markdown", "Pkg", "Test"] -git-tree-sha1 = "0513f1a8991e9d83255e0140aace0d0fc4486600" +deps = ["LibGit2"] +git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.8.0" +version = "0.8.6" [[Documenter]] -deps = ["Base64", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] -git-tree-sha1 = "1b6ae3796f60311e39cd1770566140d2c056e87f" +deps = ["ANSIColoredPrinters", "Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] +git-tree-sha1 = "7d9a46421aef53cbd6b8ecc40c3dcbacbceaf40e" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "0.23.3" +version = "0.27.15" + +[[Future]] +deps = ["Random"] +uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" + +[[IOCapture]] +deps = ["Logging", "Random"] +git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a" +uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" +version = "0.2.2" [[InteractiveUtils]] deps = ["Markdown"] @@ -29,16 +53,30 @@ uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" [[JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" +git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.0" +version = "0.21.3" [[LibGit2]] +deps = ["Base64", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + [[Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +[[MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.9" + [[Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" @@ -46,39 +84,54 @@ uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" [[Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" +[[NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" + +[[OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" + [[Parsers]] -deps = ["Dates", "Test"] -git-tree-sha1 = "ef0af6c8601db18c282d092ccbd2f01f3f0cd70b" +deps = ["Dates"] +git-tree-sha1 = "85b5da0fa43588c75bb1ff986493443f821c70b7" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "0.3.7" - -[[Pkg]] -deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "2.2.3" [[Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" [[REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets"] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [[Random]] -deps = ["Serialization"] +deps = ["SHA", "Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +[[Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.3.0" + [[SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" [[Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +[[Setfield]] +deps = ["ConstructionBase", "Future", "MacroTools", "Requires"] +git-tree-sha1 = "38d88503f695eb0301479bc9b0d4320b378bafe5" +uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" +version = "0.8.2" + [[Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" [[Test]] -deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [[UUIDs]] @@ -87,3 +140,7 @@ uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [[Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[libblastrampoline_jll]] +deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" diff --git a/src/fieldvalues.md b/src/fieldvalues.md index 38391c5..61534bf 100644 --- a/src/fieldvalues.md +++ b/src/fieldvalues.md @@ -6,19 +6,19 @@ Return a tuple containing field values of `obj`. ```jldoctest julia> using ConstructionBase -julia> struct S +julia> struct S{A,B} a::A b::B end julia> fieldvalues(S(1,2)) -(1,2) +(1, 2) julia> fieldvalues((a=10,b=20)) -(10,20) +(10, 20) julia> fieldvalues((4,5,6)) -(4,5,6) +(4, 5, 6) ``` # Specification From a0082e441f839433c9412d49067de2f03a323036 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Sun, 27 Mar 2022 07:49:44 +0200 Subject: [PATCH 04/20] add fieldvalues to the manual --- docs/src/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/index.md b/docs/src/index.md index 0411784..e03c0db 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -8,6 +8,7 @@ ```@docs ConstructionBase ConstructionBase.constructorof +ConstructionBase.fieldvalues ConstructionBase.getproperties ConstructionBase.setproperties ``` From 1b12fc8a3c05cde16ba9ac0cd0338d388f196f2e Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Wed, 30 Mar 2022 11:39:01 +0200 Subject: [PATCH 05/20] rename fieldvalues -> getfields --- docs/src/index.md | 2 +- src/ConstructionBase.jl | 12 ++++++------ src/constructorof.md | 8 ++++---- src/fieldvalues.md | 16 ++++++++-------- test/runtests.jl | 26 +++++++++++++------------- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index e03c0db..5c15141 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -8,7 +8,7 @@ ```@docs ConstructionBase ConstructionBase.constructorof -ConstructionBase.fieldvalues +ConstructionBase.getfields ConstructionBase.getproperties ConstructionBase.setproperties ``` diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 7ef7bd8..bcae5a7 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -3,14 +3,14 @@ module ConstructionBase export getproperties export setproperties export constructorof -export fieldvalues +export getfields # Use markdown files as docstring: for (name, path) in [ :ConstructionBase => joinpath(dirname(@__DIR__), "README.md"), :constructorof => joinpath(@__DIR__, "constructorof.md"), - :fieldvalues => joinpath(@__DIR__, "fieldvalues.md"), + :getfields => joinpath(@__DIR__, "getfields.md"), :getproperties => joinpath(@__DIR__, "getproperties.md"), :setproperties => joinpath(@__DIR__, "setproperties.md"), ] @@ -56,11 +56,11 @@ getproperties(o::Tuple) = o end ################################################################################ -#### fieldvalues +#### getfields ################################################################################ -fieldvalues(x::Tuple) = x -fieldvalues(x::NamedTuple) = Tuple(x) -@generated function fieldvalues(x::T) where {T} +getfields(x::Tuple) = x +getfields(x::NamedTuple) = Tuple(x) +@generated function getfields(x::T) where {T} fields = (:(getfield(x, $i)) for i in 1:fieldcount(T)) Expr(:tuple, fields...) end diff --git a/src/constructorof.md b/src/constructorof.md index d955c3b..e548966 100644 --- a/src/constructorof.md +++ b/src/constructorof.md @@ -33,16 +33,16 @@ ERROR: AssertionError: a + b == checksum ``` Instead `constructor` can be any object that satisfies the following properties: * It must be possible to reconstruct an object from the `Tuple` returned by -[`fieldvalues`](@ref): +[`getfields`](@ref): ```julia ctor = constructorof(typeof(obj)) -@assert obj == ctor(fieldvalues(obj)...) -@assert typeof(obj) == typeof(ctor(fieldvalues(obj)...)) +@assert obj == ctor(getfields(obj)...) +@assert typeof(obj) == typeof(ctor(getfields(obj)...)) ``` * The other direction should hold for as many values of `args::Tuple` as possible: ```julia ctor = constructorof(T) -fieldvalues(ctor(args...)) == args +getfields(ctor(args...)) == args ``` For instance given a suitable parametric type it should be possible to change the type of its fields: diff --git a/src/fieldvalues.md b/src/fieldvalues.md index 61534bf..5823904 100644 --- a/src/fieldvalues.md +++ b/src/fieldvalues.md @@ -1,4 +1,4 @@ - fieldvalues(obj) -> Tuple + getfields(obj) -> Tuple Return a tuple containing field values of `obj`. @@ -11,30 +11,30 @@ julia> struct S{A,B} b::B end -julia> fieldvalues(S(1,2)) +julia> getfields(S(1,2)) (1, 2) -julia> fieldvalues((a=10,b=20)) +julia> getfields((a=10,b=20)) (10, 20) -julia> fieldvalues((4,5,6)) +julia> getfields((4,5,6)) (4, 5, 6) ``` # Specification -Semantically `fieldvalues` boils down to `getfield` and `fieldcount`: +Semantically `getfields` boils down to `getfield` and `fieldcount`: ```julia -fieldvalues(obj) == Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) +getfields(obj) == Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) ``` The following relation to [`constructorof`](@ref) should be satisfied: ```julia -@assert obj == constructorof(obj)(fieldvalues(obj)...) +@assert obj == constructorof(obj)(getfields(obj)...) ``` # Implementation -The semantics of `fieldvalues` should generally not be changed. It should equivalent to +The semantics of `getfields` should generally not be changed. It should equivalent to ```julia Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) ``` diff --git a/test/runtests.jl b/test/runtests.jl index 5280885..2704fe7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,16 +19,16 @@ end @test constructorof(Tuple{Nothing, Missing})(1.0, 2) === (1.0, 2) end -@testset "fieldvalues" begin - @test fieldvalues(()) === () - @test fieldvalues([]) === () - @test fieldvalues(Empty()) === () - @test fieldvalues(NamedTuple()) === () - @test fieldvalues((10,20,30)) === (10,20,30) - @test fieldvalues((a=10,b=20f0,c=true)) === (10,20f0,true) - @test fieldvalues(AB(1, 10)) === (1, 10) +@testset "getfields" begin + @test getfields(()) === () + @test getfields([]) === () + @test getfields(Empty()) === () + @test getfields(NamedTuple()) === () + @test getfields((10,20,30)) === (10,20,30) + @test getfields((a=10,b=20f0,c=true)) === (10,20f0,true) + @test getfields(AB(1, 10)) === (1, 10) adder(a) = x -> x + a - @test fieldvalues(adder(1)) === (1,) + @test getfields(adder(1)) === (1,) end @@ -324,8 +324,8 @@ end @inferred getproperties(funny_numbers(S20)) @inferred getproperties(funny_numbers(S40)) - @inferred fieldvalues(funny_numbers(S0)) - @inferred fieldvalues(funny_numbers(S1)) - @inferred fieldvalues(funny_numbers(S20)) - @inferred fieldvalues(funny_numbers(S40)) + @inferred getfields(funny_numbers(S0)) + @inferred getfields(funny_numbers(S1)) + @inferred getfields(funny_numbers(S20)) + @inferred getfields(funny_numbers(S40)) end From 1c88ff8813c6cafd19fcdb706be36db24e6d5860 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Wed, 30 Mar 2022 12:00:16 +0200 Subject: [PATCH 06/20] fix --- src/{fieldvalues.md => getfields.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{fieldvalues.md => getfields.md} (100%) diff --git a/src/fieldvalues.md b/src/getfields.md similarity index 100% rename from src/fieldvalues.md rename to src/getfields.md From 4e13aebc2afea483b5403303ae280b940d5cc4d6 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Wed, 30 Mar 2022 12:26:36 +0200 Subject: [PATCH 07/20] fix docs/Manifest.toml --- docs/Manifest.toml | 73 ++++++++++++++++++++++++---------------------- docs/Project.toml | 1 + 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 4910790..ae8bbc4 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,146 +1,149 @@ # This file is machine-generated - editing it directly is not advised -[[ANSIColoredPrinters]] +julia_version = "1.7.0" +manifest_format = "2.0" + +[[deps.ANSIColoredPrinters]] git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" version = "0.0.1" -[[Artifacts]] +[[deps.Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" -[[Base64]] +[[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -[[CompilerSupportLibraries_jll]] +[[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -[[ConstructionBase]] +[[deps.ConstructionBase]] deps = ["LinearAlgebra"] -git-tree-sha1 = "f74e9d5388b8620b4cee35d4c5a618dd4dc547f4" +path = ".." uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" version = "1.3.0" -[[Dates]] +[[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -[[DocStringExtensions]] +[[deps.DocStringExtensions]] deps = ["LibGit2"] git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b" uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" version = "0.8.6" -[[Documenter]] +[[deps.Documenter]] deps = ["ANSIColoredPrinters", "Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] git-tree-sha1 = "7d9a46421aef53cbd6b8ecc40c3dcbacbceaf40e" uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" version = "0.27.15" -[[Future]] +[[deps.Future]] deps = ["Random"] uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" -[[IOCapture]] +[[deps.IOCapture]] deps = ["Logging", "Random"] git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a" uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" version = "0.2.2" -[[InteractiveUtils]] +[[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -[[JSON]] +[[deps.JSON]] deps = ["Dates", "Mmap", "Parsers", "Unicode"] git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" version = "0.21.3" -[[LibGit2]] +[[deps.LibGit2]] deps = ["Base64", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" -[[Libdl]] +[[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -[[LinearAlgebra]] +[[deps.LinearAlgebra]] deps = ["Libdl", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -[[Logging]] +[[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" -[[MacroTools]] +[[deps.MacroTools]] deps = ["Markdown", "Random"] git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" version = "0.5.9" -[[Markdown]] +[[deps.Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -[[Mmap]] +[[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" -[[NetworkOptions]] +[[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -[[OpenBLAS_jll]] +[[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -[[Parsers]] +[[deps.Parsers]] deps = ["Dates"] git-tree-sha1 = "85b5da0fa43588c75bb1ff986493443f821c70b7" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" version = "2.2.3" -[[Printf]] +[[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -[[REPL]] +[[deps.REPL]] deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" -[[Random]] +[[deps.Random]] deps = ["SHA", "Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -[[Requires]] +[[deps.Requires]] deps = ["UUIDs"] git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7" uuid = "ae029012-a4dd-5104-9daa-d747884805df" version = "1.3.0" -[[SHA]] +[[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -[[Serialization]] +[[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -[[Setfield]] +[[deps.Setfield]] deps = ["ConstructionBase", "Future", "MacroTools", "Requires"] git-tree-sha1 = "38d88503f695eb0301479bc9b0d4320b378bafe5" uuid = "efcf1570-3423-57d1-acb7-fd33fddbac46" version = "0.8.2" -[[Sockets]] +[[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -[[Test]] +[[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -[[UUIDs]] +[[deps.UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -[[Unicode]] +[[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -[[libblastrampoline_jll]] +[[deps.libblastrampoline_jll]] deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" diff --git a/docs/Project.toml b/docs/Project.toml index 13764a2..33447c3 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,4 @@ [deps] +ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" From 191b1d34c14b72b479da48059b46a01bc8aa553c Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Wed, 30 Mar 2022 18:35:21 +0200 Subject: [PATCH 08/20] special case getfields for Base types with potential undef fields --- src/getfields.md | 4 ---- src/nonstandard.jl | 1 + test/runtests.jl | 30 +++++++++++++++--------------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/getfields.md b/src/getfields.md index 5823904..22ac70f 100644 --- a/src/getfields.md +++ b/src/getfields.md @@ -27,10 +27,6 @@ Semantically `getfields` boils down to `getfield` and `fieldcount`: ```julia getfields(obj) == Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) ``` -The following relation to [`constructorof`](@ref) should be satisfied: -```julia -@assert obj == constructorof(obj)(getfields(obj)...) -``` # Implementation diff --git a/src/nonstandard.jl b/src/nonstandard.jl index 8ca9104..1da660d 100644 --- a/src/nonstandard.jl +++ b/src/nonstandard.jl @@ -35,6 +35,7 @@ end function tridiagonal_constructor(dl::V, d::V, du::V, du2::V) where {V<:AbstractVector{T}} where T Tridiagonal{T,V}(dl, d, du, du2) end +getfields(o::Tridiagonal) = Tuple(getproperties(o)) # `du2` may be undefined, so we need a custom `getproperties` that checks `isdefined` function getproperties(o::Tridiagonal) diff --git a/test/runtests.jl b/test/runtests.jl index 2704fe7..4d535cc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -133,29 +133,29 @@ end @testset "SubArray" begin subarray = view(A1, 1:2, 3:4) - @test constructorof(typeof(subarray))(getproperties(subarray)...) === subarray + @test constructorof(typeof(subarray))(getfields(subarray)...) === subarray @test all(constructorof(typeof(subarray))(A2, (Base.OneTo(2), 3:4), 0, 0) .== Float32[1 1; 1 1]) - @inferred constructorof(typeof(subarray))(getproperties(subarray)...) + @inferred constructorof(typeof(subarray))(getfields(subarray)...) @inferred constructorof(typeof(subarray))(A2, (Base.OneTo(2), 3:4), 0, 0) end @testset "ReinterpretArray" begin ra1 = reinterpret(Float16, A1) @test constructorof(typeof(ra1))(A1) === ra1 - @test constructorof(typeof(ra1))(getproperties(ra1)...) === ra1 + @test constructorof(typeof(ra1))(getfields(ra1)...) === ra1 ra2 = constructorof(typeof(ra1))(A2) @test size(ra2) == (10, 6) @test eltype(ra2) == Float16 - @inferred constructorof(typeof(ra1))(getproperties(ra1)...) + @inferred constructorof(typeof(ra1))(getfields(ra1)...) @inferred constructorof(typeof(ra1))(A2) end @testset "PermutedDimsArray" begin pda1 = PermutedDimsArray(A1, (2, 1)) @test constructorof(typeof(pda1))(A1) === pda1 - @test constructorof(typeof(pda1))(getproperties(pda1)...) === pda1 + @test constructorof(typeof(pda1))(getfields(pda1)...) === pda1 @test eltype(constructorof(typeof(pda1))(A2)) == Float32 - @inferred constructorof(typeof(pda1))(getproperties(pda1)...) + @inferred constructorof(typeof(pda1))(getfields(pda1)...) @inferred constructorof(typeof(pda1))(A2) end @@ -166,24 +166,24 @@ end tda = Tridiagonal(dl, d, du) @test isdefined(tda, :du2) == false @test constructorof(typeof(tda))(dl, d, du) === tda - @test constructorof(typeof(tda))(getproperties(tda)...) === tda + @test constructorof(typeof(tda))(getfields(tda)...) === tda # lu factorization defines du2 tda_lu = lu!(tda).factors @test isdefined(tda_lu, :du2) == true - @test constructorof(typeof(tda_lu))(getproperties(tda_lu)...) === tda_lu - @test constructorof(typeof(tda_lu))(getproperties(tda)...) !== tda_lu - @test constructorof(typeof(tda_lu))(getproperties(tda)...) === tda - @inferred constructorof(typeof(tda))(getproperties(tda)...) - @inferred constructorof(typeof(tda))(getproperties(tda_lu)...) + @test constructorof(typeof(tda_lu))(getfields(tda_lu)...) === tda_lu + @test constructorof(typeof(tda_lu))(getfields(tda)...) !== tda_lu + @test constructorof(typeof(tda_lu))(getfields(tda)...) === tda + @inferred constructorof(typeof(tda))(getfields(tda)...) + @inferred constructorof(typeof(tda))(getfields(tda_lu)...) end @testset "LinRange" begin lr1 = LinRange(1, 7, 10) lr2 = LinRange(1.0f0, 7.0f0, 10) @test constructorof(typeof(lr1))(1, 7, 10, nothing) === lr1 - @test constructorof(typeof(lr1))(getproperties(lr2)...) === lr2 - @inferred constructorof(typeof(lr1))(getproperties(lr1)...) - @inferred constructorof(typeof(lr1))(getproperties(lr2)...) + @test constructorof(typeof(lr1))(getfields(lr2)...) === lr2 + @inferred constructorof(typeof(lr1))(getfields(lr1)...) + @inferred constructorof(typeof(lr1))(getfields(lr2)...) end end From c76960390361a903564084bc192083ae05136891 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Thu, 31 Mar 2022 13:30:15 +0200 Subject: [PATCH 09/20] make getfields return a NamedTuple --- src/ConstructionBase.jl | 23 +++++++++-------------- src/getfields.md | 28 ++++++++++++++++++++-------- src/nonstandard.jl | 5 ++--- test/runtests.jl | 23 +++++++++++++++++------ 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index bcae5a7..848ee63 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -44,27 +44,22 @@ struct NamedTupleConstructor{names} end end end -getproperties(o::NamedTuple) = o -getproperties(o::Tuple) = o -@generated function getproperties(obj) - fnames = fieldnames(obj) - fvals = map(fnames) do fname - Expr(:call, :getproperty, :obj, QuoteNode(fname)) - end - fvals = Expr(:tuple, fvals...) - :(NamedTuple{$fnames}($fvals)) -end ################################################################################ #### getfields ################################################################################ getfields(x::Tuple) = x -getfields(x::NamedTuple) = Tuple(x) -@generated function getfields(x::T) where {T} - fields = (:(getfield(x, $i)) for i in 1:fieldcount(T)) - Expr(:tuple, fields...) +getfields(x::NamedTuple) = x +@generated function getfields(obj) + fnames = fieldnames(obj) + fvals = map(fnames) do fname + Expr(:call, :getfield, :obj, QuoteNode(fname)) + end + fvals = Expr(:tuple, fvals...) + :(NamedTuple{$fnames}($fvals)) end +getproperties(o) = getfields(o) ################################################################################ ##### setproperties ################################################################################ diff --git a/src/getfields.md b/src/getfields.md index 22ac70f..9da7448 100644 --- a/src/getfields.md +++ b/src/getfields.md @@ -1,6 +1,7 @@ - getfields(obj) -> Tuple + getfields(obj) -> NamedTuple + getfields(obj::Tuple) -> Tuple -Return a tuple containing field values of `obj`. +Return a `NamedTuple` containing fields of `obj`. # Examples ```jldoctest @@ -12,10 +13,10 @@ julia> struct S{A,B} end julia> getfields(S(1,2)) -(1, 2) +(a = 1, b = 2) julia> getfields((a=10,b=20)) -(10, 20) +(a = 10, b = 20) julia> getfields((4,5,6)) (4, 5, 6) @@ -23,16 +24,27 @@ julia> getfields((4,5,6)) # Specification -Semantically `getfields` boils down to `getfield` and `fieldcount`: +Semantically `getfields` boils down to `getfield` and `fieldnames`: ```julia -getfields(obj) == Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) +function getfields(obj) + pairs = (fnames => getfield(obj, fname) for fname in fieldnames(typeof(obj))) + (;pairs...) +end ``` +However the actual implementation can be more optimized. For builtin types, there can also be deviations from this semantics: +* `getfields(::Tuple)::Tuple` since `Tuples` don't have symbolic fieldnames +* There are some types in `Base` that have `undef` fields. Since accessing these results +in an error, `getfields` instead just omits these. # Implementation -The semantics of `getfields` should generally not be changed. It should equivalent to +The semantics of `getfields` should not be changed for user defined types. It should equivalent to ```julia -Tuple(getfield(obj,i) for i in 1:fieldcount(obj)) +function getfields(obj) + pairs = (fnames => getfield(obj, fname) for fname in fieldnames(typeof(obj))) + (;pairs...) +end ``` even if that includes private fields of `obj`. +If a change of semantics is desired, consider overloading [`getproperties`](@ref) instead. See also [`getproperties`](@ref), [`constructorof`](@ref) diff --git a/src/nonstandard.jl b/src/nonstandard.jl index 1da660d..640306e 100644 --- a/src/nonstandard.jl +++ b/src/nonstandard.jl @@ -35,10 +35,9 @@ end function tridiagonal_constructor(dl::V, d::V, du::V, du2::V) where {V<:AbstractVector{T}} where T Tridiagonal{T,V}(dl, d, du, du2) end -getfields(o::Tridiagonal) = Tuple(getproperties(o)) -# `du2` may be undefined, so we need a custom `getproperties` that checks `isdefined` -function getproperties(o::Tridiagonal) +# `du2` may be undefined, so we need a custom `getfields` that checks `isdefined` +function getfields(o::Tridiagonal) if isdefined(o, :du2) (dl=o.dl, d=o.d, du=o.du, du2=o.du2) else diff --git a/test/runtests.jl b/test/runtests.jl index 4d535cc..8d93b0c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,16 +21,27 @@ end @testset "getfields" begin @test getfields(()) === () - @test getfields([]) === () - @test getfields(Empty()) === () - @test getfields(NamedTuple()) === () + @test getfields([]) === NamedTuple() + @test getfields(Empty()) === NamedTuple() + @test getfields(NamedTuple()) === NamedTuple() @test getfields((10,20,30)) === (10,20,30) - @test getfields((a=10,b=20f0,c=true)) === (10,20f0,true) - @test getfields(AB(1, 10)) === (1, 10) + @test getfields((a=10,b=20f0,c=true)) === (a=10,b=20f0,c=true) + @test getfields(AB(1, 10)) === (a=1, b=10) adder(a) = x -> x + a - @test getfields(adder(1)) === (1,) + @test getfields(adder(1)) === (a=1,) end +struct DontTouchProperties + a + b +end +Base.propertynames(::DontTouchProperties) = error() +Base.getproperty(::DontTouchProperties, ::Symbol) = error() +ConstructionBase.getproperties(::DontTouchProperties) = error() +@testset "getfields does not depend on properties" begin + @test getfields(DontTouchProperties(1,2)) === (a=1, b=2) + @test constructorof(DontTouchProperties) === DontTouchProperties +end @testset "getproperties" begin o = AB(1, 2) From 8cc0f98c1152ccd562a274da0ebdee305a6abe3b Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Tue, 28 Jun 2022 20:51:20 +0200 Subject: [PATCH 10/20] less @generated --- src/ConstructionBase.jl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 75bb161..ee9d79b 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -46,13 +46,6 @@ end ################################################################################ getfields(x::Tuple) = x getfields(x::NamedTuple) = x -@generated function getfields(obj) - fnames = fieldnames(obj) - fvals = map(fnames) do fname - Expr(:call, :getfield, :obj, QuoteNode(fname)) - end - :(NamedTuple{$fnames}(($(fvals...),))) -end getproperties(o::NamedTuple) = o getproperties(o::Tuple) = o if VERSION >= v"1.7" @@ -60,7 +53,18 @@ if VERSION >= v"1.7" fnames = propertynames(obj) NamedTuple{fnames}(getproperty.(Ref(obj), fnames)) end + function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) + end else + @generated function getfields(obj) + fnames = fieldnames(obj) + fvals = map(fnames) do fname + Expr(:call, :getfield, :obj, QuoteNode(fname)) + end + :(NamedTuple{$fnames}(($(fvals...),))) + end function getproperties(obj) check_properties_are_fields(obj) getfields(obj) From 1b3ed8f7967571b5f72b3485189e266a2dc3618e Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Tue, 28 Jun 2022 20:57:03 +0200 Subject: [PATCH 11/20] make CI fail fast --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f99801a..7c14f88 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,7 +10,7 @@ jobs: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: true matrix: version: - '1.0' From 2b1222f6581111b7dd5f104c750c4bc1b8e0932b Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Thu, 30 Jun 2022 16:06:31 +0200 Subject: [PATCH 12/20] clarify semantic vs raw in the docs --- docs/src/index.md | 15 +++++++++++++-- src/constructorof.md | 7 +++---- src/getfields.md | 23 +++++++++++++---------- src/getproperties.md | 37 ++++++++++++++++++++++--------------- src/setproperties.md | 33 +++++++++++++++++---------------- 5 files changed, 68 insertions(+), 47 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 5c15141..8eb1022 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,10 +1,21 @@ # ConstructionBase.jl -```@index -``` +[`ConstructionBase`](@ref) allows flexible construction and destructuring of objects. +There are two levels of under which this can be done: +### [The raw level](@id the-raw-level) +This is where `Base.fieldnames`, `Base.getfield`, `Base.setfield!` live. +This level is what an object is ultimately composed of including all private details. +At the raw level [`ConstructionBase`](@ref) adds [`constructorof`](@ref) and [`getfields`](@ref). +### [The semantic level](@id the-semantic-level) +This is where `Base.propertynames`, `Base.getproperty` and `Base.setproperty!` live. This level is typically the public interface of a type, it may hide private details and do magic tricks. +At the semantic level [`ConstructionBase`](@ref) adds [`setproperties`](@ref) and [`getproperties`](@ref). + ## Interface +```@index +``` + ```@docs ConstructionBase ConstructionBase.constructorof diff --git a/src/constructorof.md b/src/constructorof.md index e548966..42fef1e 100644 --- a/src/constructorof.md +++ b/src/constructorof.md @@ -32,14 +32,13 @@ julia> constructorof(S)(1,2,4) ERROR: AssertionError: a + b == checksum ``` Instead `constructor` can be any object that satisfies the following properties: -* It must be possible to reconstruct an object from the `Tuple` returned by -[`getfields`](@ref): +* It must be possible to reconstruct an object from the elements of [`getfields`](@ref): ```julia ctor = constructorof(typeof(obj)) @assert obj == ctor(getfields(obj)...) @assert typeof(obj) == typeof(ctor(getfields(obj)...)) ``` -* The other direction should hold for as many values of `args::Tuple` as possible: +* The other direction should hold for as many values of `args` as possible: ```julia ctor = constructorof(T) getfields(ctor(args...)) == args @@ -61,7 +60,7 @@ T{Float64, Int64}(1.0, 2) julia> constructorof(typeof(t))(10, 2) T{Int64, Int64}(10, 2) ``` - +`constructorof` belongs to [the raw level](@ref the-raw-level). `constructorof` is generated for all anonymous `Function`s lacking constructors, identified as having `gensym` `#` in their names. A custom struct `<: Function` with a `gensym` name may need to define `constructorof` manually. diff --git a/src/getfields.md b/src/getfields.md index 9da7448..d4f0d89 100644 --- a/src/getfields.md +++ b/src/getfields.md @@ -1,7 +1,8 @@ getfields(obj) -> NamedTuple getfields(obj::Tuple) -> Tuple -Return a `NamedTuple` containing fields of `obj`. +Return a `NamedTuple` containing the fields of `obj`. On `Tuples` `getfields` is +the identity function instead, since `Tuple` fields have no symbolic names. # Examples ```jldoctest @@ -24,25 +25,27 @@ julia> getfields((4,5,6)) # Specification +`getfields` belongs to the [the raw level](@ref the-raw-level). Semantically `getfields` boils down to `getfield` and `fieldnames`: ```julia -function getfields(obj) - pairs = (fnames => getfield(obj, fname) for fname in fieldnames(typeof(obj))) - (;pairs...) +function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) end ``` However the actual implementation can be more optimized. For builtin types, there can also be deviations from this semantics: * `getfields(::Tuple)::Tuple` since `Tuples` don't have symbolic fieldnames -* There are some types in `Base` that have `undef` fields. Since accessing these results -in an error, `getfields` instead just omits these. +* There are some types in `Base` that have `undef` fields. Since accessing these results in an error, `getfields` instead just omits these. # Implementation -The semantics of `getfields` should not be changed for user defined types. It should equivalent to +The semantics of `getfields` should not be changed for user defined types. It should +return the raw fields as a `NamedTuple` in the struct order. In other words it should be +equivalent to ```julia -function getfields(obj) - pairs = (fnames => getfield(obj, fname) for fname in fieldnames(typeof(obj))) - (;pairs...) +function getfields(obj::T) where {T} + fnames = fieldnames(T) + NamedTuple{fnames}(getfield.(Ref(obj), fnames)) end ``` even if that includes private fields of `obj`. diff --git a/src/getproperties.md b/src/getproperties.md index b70d009..61a6501 100644 --- a/src/getproperties.md +++ b/src/getproperties.md @@ -1,6 +1,8 @@ - getproperties(obj) + getproperties(obj)::NamedTuple + getproperties(obj::Tuple)::Tuple -Return the fields of `obj` as a `NamedTuple`. +Return the properties of `obj` as a `NamedTuple`. Since `Tuple` don't have symbolic properties, +`getproperties` is the identity function on tuples. # Examples ```jldoctest @@ -17,26 +19,31 @@ S(1, 2, 3) julia> getproperties(s) (a = 1, b = 2, c = 3) -``` - -# Implementation -`getproperties` is defined by default for all objects. However for a custom type `MyType`, -`getproperties(obj::MyType)` may be defined when objects may have undefined fields, -when it has calculated fields that should not be accessed or set manually, or -other conditions that do not meet the specification with the default implementation. +julia> getproperties((10,20)) +(10, 20) +``` ## Specification +`getproperties` belongs to [the semantic level](@ref the-semantic-level). `getproperties` guarantees a couple of invariants. When overloading it, the user is responsible for ensuring them: -1. Relation to `propertynames` and `fieldnames`: `getproperties` relates to `propertynames` and `getproperty`, not to `fieldnames` and `getfield`. - This means that any series `p₁, p₂, ..., pₙ` of `propertynames(obj)` that is not undefined should be returned by `getproperties`. -2. `getproperties` is defined in relation to `constructorof` so that: - ```julia - obj == constructorof(obj)(getproperties(obj)...) - ``` +1. `getproperties` should be consistent with `Base.propertynames`, `Base.getproperty`, `Base.setproperty!`. + Semantically it should be equivalent to: + ```julia + function getproperties(obj) + fnames = propertynames(obj) + NamedTuple{fnames}(getproperty.(Ref(obj), fnames)) + end + ``` 2. `getproperties` is defined in relation to `setproperties` so that: ```julia obj == setproperties(obj, getproperties(obj)) ``` + The only exception from this semantics is that undefined properties may be avoided + in the return value of `getproperties`. + +# Implementation + +`getproperties` is defined by default for all objects. It should be very rare that a custom type `MyType`, has to implement `getproperties(obj::MyType)`. Reasons to do so are undefined fields or performance considerations. diff --git a/src/setproperties.md b/src/setproperties.md index 11faa4b..7e44f13 100644 --- a/src/setproperties.md +++ b/src/setproperties.md @@ -1,6 +1,6 @@ setproperties(obj, patch::NamedTuple) -Return a copy of `obj` with attributes updates according to `patch`. +Return a copy of `obj` with properties updates according to `patch`. # Examples ```jldoctest @@ -44,23 +44,9 @@ julia> setproperties(o, a="A", c="cc") S("A", 2, "cc") ``` -# Implementation - -For a custom type `MyType`, a method `setproperties(obj::MyType, patch::NamedTuple)` -may be defined. - -* Prefer to overload [`constructorof`](@ref) whenever makes sense (e.g., no `getproperty` - method is defined). Default `setproperties` is defined in terms of `constructorof`. - -* If `getproperty` is customized, it may be a good idea to define `setproperties`. - -!!! warning - The signature `setproperties(obj::MyType; kw...)` should never be overloaded. - Instead `setproperties(obj::MyType, patch::NamedTuple)` should be overloaded. - ## Specification -`setproperties` guarantees a couple of invariants. When overloading it, the user is responsible for ensuring them: +`setproperties` belongs to [the semantic level](@ref the-semantic-level). If satisfies the following invariants: 1. Purity: `setproperties` is supposed to have no side effects. In particular `setproperties(obj, patch::NamedTuple)` may not mutate `obj`. 2. Relation to `propertynames` and `fieldnames`: `setproperties` relates to `propertynames` and `getproperty`, not to `fieldnames` and `getfield`. @@ -100,3 +86,18 @@ let obj′ = setproperties(obj, ($p₁=v₁, $p₂=v₂, ..., $pₙ=vₙ)), @assert obj′′.$pₙ == wₙ end ``` + +# Implementation + +For a custom type `MyType`, a method `setproperties(obj::MyType, patch::NamedTuple)` +may be defined. When doing so it is important to ensure compliance with the specification. + +* Prefer to overload [`constructorof`](@ref) whenever makes sense (e.g., no `getproperty` + method is defined). Default `setproperties` is defined in terms of `constructorof` and `getproperties`. + +* If `getproperty` is customized, it may be a good idea to define `setproperties`. + +!!! warning + The signature `setproperties(obj::MyType; kw...)` should never be overloaded. + Instead `setproperties(obj::MyType, patch::NamedTuple)` should be overloaded. + From 96807ee21a1396c8e77238ae02efca71be21f83b Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 1 Jul 2022 12:28:50 +0200 Subject: [PATCH 13/20] v1.4.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 65cdbad..a7866e5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ConstructionBase" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" authors = ["Takafumi Arakaki", "Rafael Schouten", "Jan Weidner"] -version = "1.3.1" +version = "1.3.2" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" From 0d1f5c14cc274f0408cc235f529a9b684dae48a1 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 1 Jul 2022 15:48:46 +0200 Subject: [PATCH 14/20] check for propertynames overload in setproperties --- Project.toml | 2 +- src/ConstructionBase.jl | 53 ++++++++++++++++++++++------------------- test/runtests.jl | 37 ++++++++++++++++++---------- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/Project.toml b/Project.toml index a7866e5..2b835d7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ConstructionBase" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" authors = ["Takafumi Arakaki", "Rafael Schouten", "Jan Weidner"] -version = "1.3.2" +version = "1.4.0" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index ee9d79b..80a1af3 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -48,6 +48,29 @@ getfields(x::Tuple) = x getfields(x::NamedTuple) = x getproperties(o::NamedTuple) = o getproperties(o::Tuple) = o + +@generated function check_properties_are_fields(::Type{T}) where {T} + if is_propertynames_overloaded(T) + return quote + msg = """ + The function `Base.propertynames` was overloaded for type `$T`. + Please make sure the following methods are also overloaded for this type: + ```julia + ConstructionBase.setproperties + ConstructionBase.getproperties # optional in VERSION >= julia v1.7 + ``` + """ + error(msg) + end + else + :() + end +end + +function is_propertynames_overloaded(T::Type)::Bool + which(propertynames, Tuple{T}).sig !== Tuple{typeof(propertynames), Any} +end + if VERSION >= v"1.7" function getproperties(obj) fnames = propertynames(obj) @@ -69,22 +92,6 @@ else check_properties_are_fields(obj) getfields(obj) end - @generated function check_properties_are_fields(obj) - if which(propertynames, Tuple{obj}).sig != Tuple{typeof(propertynames), Any} - # custom propertynames defined for this type - return quote - msg = """ - Different fieldnames and propertynames are only supported on Julia v1.7+. - For older julia versions, consider overloading - `ConstructionBase.getproperties(obj::$(typeof(obj))`. - See also https://github.com/JuliaObjects/ConstructionBase.jl/pull/60. - """ - error(msg) - end - else - :() - end - end end ################################################################################ @@ -112,20 +119,17 @@ setproperties_namedtuple(obj, patch::Tuple{}) = obj end function setproperties_namedtuple(obj, patch) res = merge(obj, patch) - validate_setproperties_result(res, obj, obj, patch) + check_patch_properties_exist(res, obj, obj, patch) res end -function validate_setproperties_result( +function check_patch_properties_exist( nt_new::NamedTuple{fields}, nt_old::NamedTuple{fields}, obj, patch) where {fields} nothing end -@noinline function validate_setproperties_result(nt_new, nt_old, obj, patch) +@noinline function check_patch_properties_exist(nt_new, nt_old, obj, patch) O = typeof(obj) msg = """ Failed to assign properties $(propertynames(patch)) to object with properties $(propertynames(obj)). - You may want to overload - ConstructionBase.setproperties(obj::$O, patch::NamedTuple) - ConstructionBase.getproperties(obj::$O) """ throw(ArgumentError(msg)) end @@ -178,10 +182,11 @@ setproperties_object(obj, patch::Tuple{}) = obj end setproperties_object(obj, patch::NamedTuple{()}) = obj function setproperties_object(obj, patch) + check_properties_are_fields(typeof(obj)) nt = getproperties(obj) nt_new = merge(nt, patch) - validate_setproperties_result(nt_new, nt, obj, patch) - constructorof(typeof(obj))(Tuple(nt_new)...) + check_patch_properties_exist(nt_new, nt, obj, patch) + constructorof(typeof(obj))(nt_new...) end include("nonstandard.jl") diff --git a/test/runtests.jl b/test/runtests.jl index c43575b..0cfb291 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -79,14 +79,10 @@ end res = @test_throws ArgumentError setproperties(AB(1,2), (a=2, this_field_does_not_exist=3.0)) msg = sprint(showerror, res.value) @test occursin("this_field_does_not_exist", msg) - @test occursin("overload", msg) - @test occursin("ConstructionBase.setproperties", msg) res = @test_throws ArgumentError setproperties(AB(1,2), a=2, this_field_does_not_exist=3.0) msg = sprint(showerror, res.value) @test occursin("this_field_does_not_exist", msg) - @test occursin("overload", msg) - @test occursin("ConstructionBase.setproperties", msg) @test setproperties(42, NamedTuple()) === 42 @test setproperties(42) === 42 @@ -268,22 +264,37 @@ struct FieldProps{NT <: NamedTuple{(:a, :b)}} components::NT end -Base.propertynames(obj::FieldProps) = (:a, :b) +Base.propertynames(::FieldProps) = (:a, :b) Base.getproperty(obj::FieldProps, name::Symbol) = getproperty(getfield(obj, :components), name) -ConstructionBase.constructorof(::Type{<:FieldProps}) = (a, b) -> FieldProps((a=a, b=b)) @testset "use properties, not fields" begin x = FieldProps((a=1, b=:b)) + @test constructorof(typeof(x)) === FieldProps + @test getfields(x) === (components=(a=1, b=:b),) + res = @test_throws ErrorException setproperties(x, c=0) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + @test_throws ErrorException setproperties(x, components=(a=1,b=:b)) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + @test_throws ErrorException setproperties(x, a="aaa") + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("setproperties", msg) + @test occursin("FieldProps", msg) + # == FieldProps((a="aaa", b=:b) if VERSION >= v"1.7" @test getproperties(x) == (a=1, b=:b) - @test setproperties(x, a="aaa") == FieldProps((a="aaa", b=:b)) - VERSION >= v"1.8-dev" ? - (@test_throws "Failed to assign properties (:c,) to object with properties (:a, :b)" setproperties(x, c=0)) : - (@test_throws ArgumentError setproperties(x, c=0)) else - @test_throws ErrorException getproperties(x) - @test_throws ErrorException setproperties(x, a="aaa") - @test_throws ErrorException setproperties(x, c=0) + res = @test_throws ErrorException getproperties(x) + msg = sprint(showerror, res.value) + @test occursin("overload", msg) + @test occursin("getproperties", msg) + @test occursin("FieldProps", msg) end end From 27de07fc164d38aeabe4cf6d37d8f1d8299247d3 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 1 Jul 2022 16:33:03 +0200 Subject: [PATCH 15/20] fix --- src/ConstructionBase.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 80a1af3..54d804f 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -49,9 +49,10 @@ getfields(x::NamedTuple) = x getproperties(o::NamedTuple) = o getproperties(o::Tuple) = o -@generated function check_properties_are_fields(::Type{T}) where {T} - if is_propertynames_overloaded(T) +@generated function check_properties_are_fields(obj) + if is_propertynames_overloaded(obj) return quote + T = typeof(obj) msg = """ The function `Base.propertynames` was overloaded for type `$T`. Please make sure the following methods are also overloaded for this type: @@ -182,7 +183,7 @@ setproperties_object(obj, patch::Tuple{}) = obj end setproperties_object(obj, patch::NamedTuple{()}) = obj function setproperties_object(obj, patch) - check_properties_are_fields(typeof(obj)) + check_properties_are_fields(obj) nt = getproperties(obj) nt_new = merge(nt, patch) check_patch_properties_exist(nt_new, nt, obj, patch) From bf80a504c59787753d98b3a96dfbcfa87018a1c8 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 1 Jul 2022 16:40:04 +0200 Subject: [PATCH 16/20] probe ci --- .github/workflows/CI.yml | 2 +- src/ConstructionBase.jl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7c14f88..f99801a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,7 +10,7 @@ jobs: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} runs-on: ${{ matrix.os }} strategy: - fail-fast: true + fail-fast: false matrix: version: - '1.0' diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 54d804f..317a668 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -64,7 +64,7 @@ getproperties(o::Tuple) = o error(msg) end else - :() + :(nothing) end end @@ -183,10 +183,10 @@ setproperties_object(obj, patch::Tuple{}) = obj end setproperties_object(obj, patch::NamedTuple{()}) = obj function setproperties_object(obj, patch) - check_properties_are_fields(obj) + check_properties_are_fields(obj)::Nothing nt = getproperties(obj) nt_new = merge(nt, patch) - check_patch_properties_exist(nt_new, nt, obj, patch) + check_patch_properties_exist(nt_new, nt, obj, patch)::Nothing constructorof(typeof(obj))(nt_new...) end From 3fe9b781f5e50687da70dbe3f9024699067aa9b5 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 1 Jul 2022 16:56:12 +0200 Subject: [PATCH 17/20] probe ci --- .github/workflows/CI.yml | 2 +- src/ConstructionBase.jl | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f99801a..7c14f88 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,7 +10,7 @@ jobs: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: true matrix: version: - '1.0' diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 317a668..246ec66 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -182,13 +182,21 @@ setproperties_object(obj, patch::Tuple{}) = obj throw(ArgumentError(msg)) end setproperties_object(obj, patch::NamedTuple{()}) = obj + function setproperties_object(obj, patch) - check_properties_are_fields(obj)::Nothing + check_properties_are_fields_except_old_julia(obj)::Nothing nt = getproperties(obj) nt_new = merge(nt, patch) check_patch_properties_exist(nt_new, nt, obj, patch)::Nothing constructorof(typeof(obj))(nt_new...) end +if VERSION < v"1.3" + # on old julia versions check_properties_are_fields + # trips inference of setproperties + check_properties_are_fields_except_old_julia(_) = nothing +else + check_properties_are_fields_except_old_julia(obj) = check_properties_are_fields(obj) +end include("nonstandard.jl") include("functions.jl") From e712a316d53cb3a08d0ba47161dab56891231872 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 1 Jul 2022 17:03:16 +0200 Subject: [PATCH 18/20] probe ci --- src/ConstructionBase.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 246ec66..8a94c42 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -184,7 +184,7 @@ end setproperties_object(obj, patch::NamedTuple{()}) = obj function setproperties_object(obj, patch) - check_properties_are_fields_except_old_julia(obj)::Nothing + #check_properties_are_fields_except_old_julia(obj)::Nothing nt = getproperties(obj) nt_new = merge(nt, patch) check_patch_properties_exist(nt_new, nt, obj, patch)::Nothing From 7825e81ca124af7d3098dcb45635c878c63dad61 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 1 Jul 2022 17:15:28 +0200 Subject: [PATCH 19/20] probe ci --- src/ConstructionBase.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 8a94c42..3c483d9 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -184,11 +184,12 @@ end setproperties_object(obj, patch::NamedTuple{()}) = obj function setproperties_object(obj, patch) - #check_properties_are_fields_except_old_julia(obj)::Nothing + check_properties_are_fields_except_old_julia(obj)::Nothing nt = getproperties(obj) nt_new = merge(nt, patch) check_patch_properties_exist(nt_new, nt, obj, patch)::Nothing - constructorof(typeof(obj))(nt_new...) + args = Tuple(nt_new) # old julia inference prefers if we wrap in Tuple + constructorof(typeof(obj))(args...) end if VERSION < v"1.3" # on old julia versions check_properties_are_fields From cd06f8c1863d1f90faa1ba2770c386ca1058e061 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 1 Jul 2022 17:27:53 +0200 Subject: [PATCH 20/20] remove codecov --- .github/workflows/CI.yml | 5 ----- README.md | 1 - src/ConstructionBase.jl | 11 ++--------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7c14f88..d2be379 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,8 +33,3 @@ jobs: arch: ${{ matrix.arch }} - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-runtest@latest - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 - with: - file: lcov.info - fail_ci_if_error: true diff --git a/README.md b/README.md index 3ad6f61..f624da6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaObjects.github.io/ConstructionBase.jl/stable) [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaObjects.github.io/ConstructionBase.jl/dev) [![Build Status](https://github.com/JuliaObjects/ConstructionBase.jl/workflows/CI/badge.svg)](https://github.com/JuliaObjects/ConstructionBase.jl/actions?query=workflow%3ACI) -[![Codecov](https://codecov.io/gh/JuliaObjects/ConstructionBase.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaObjects/ConstructionBase.jl) [![GitHub stars](https://img.shields.io/github/stars/JuliaObjects/ConstructionBase.jl?style=social)](https://github.com/JuliaObjects/ConstructionBase.jl) ConstructionBase is a very lightwight package, that provides primitive functions for construction of objects: diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 3c483d9..86a7be4 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -184,20 +184,13 @@ end setproperties_object(obj, patch::NamedTuple{()}) = obj function setproperties_object(obj, patch) - check_properties_are_fields_except_old_julia(obj)::Nothing + check_properties_are_fields(obj) nt = getproperties(obj) nt_new = merge(nt, patch) - check_patch_properties_exist(nt_new, nt, obj, patch)::Nothing + check_patch_properties_exist(nt_new, nt, obj, patch) args = Tuple(nt_new) # old julia inference prefers if we wrap in Tuple constructorof(typeof(obj))(args...) end -if VERSION < v"1.3" - # on old julia versions check_properties_are_fields - # trips inference of setproperties - check_properties_are_fields_except_old_julia(_) = nothing -else - check_properties_are_fields_except_old_julia(obj) = check_properties_are_fields(obj) -end include("nonstandard.jl") include("functions.jl")