diff --git a/src/ConstructionBase.jl b/src/ConstructionBase.jl index 17ec276..cef054a 100644 --- a/src/ConstructionBase.jl +++ b/src/ConstructionBase.jl @@ -1,5 +1,148 @@ module ConstructionBase -greet() = print("Hello World!") +export setproperties +export constructorof + + +""" + constructorof(T::Type) + +Return an object `ctor` that can be used to construct objects of type `T` +from their field values. Typically `ctor` will be the type `T` with all parameters removed: +```jldoctest +julia> struct T{A,B};a::A;b::B;end + +julia> constructorof(T{Int,Int}) +T +``` +It is however not guaranteed, that `ctor` is a type at all: +```jldoctest +julia> struct S + a + b + checksum + S(a,b) = new(a,b,a+b) + end + +julia> ConstructionBase.constructorof(S) = (a,b,checksum) -> (@assert a+b == checksum; S(a,b)) + +julia> constructorof(S)(1,2) +S(1, 2, 3) +``` +Instead `ctor` can be any object that satisfies the following properties: +* It must be possible to reconstruct an object from its fields: +```julia +ctor = constructorof(typeof(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: +```julia +ctor = constructorof(T) +fieldvalues(ctor(args...)) == args +``` +For instance given a suitable parametric type it should be possible to change +the type of its fields: +```jldoctest +julia> using ConstructionBase: constructorof + +julia> struct T{A,B};a::A;b::B;end + +julia> t = T(1,2) +T{Int64,Int64}(1, 2) + +julia> constructorof(typeof(t))(1.0, 2) +T{Float64,Int64}(1.0, 2) + +julia> constructorof(typeof(t))(10, 2) +T{Int64,Int64}(10, 2) +``` +""" +@generated function constructorof(::Type{T}) where T + getfield(parentmodule(T), nameof(T)) +end + +function assert_hasfields(T, fnames) + for fname in fnames + if !(fname in fieldnames(T)) + msg = "$T has no field $fname" + throw(ArgumentError(msg)) + end + end +end + +""" + setproperties(obj, patch) + +Return a copy of `obj` with attributes updates accoring to `patch`. + +# Examples +```jldoctest +julia> using ConstructionBase + +julia> struct S;a;b;c; end + +julia> s = S(1,2,3) +S(1, 2, 3) + +julia> setproperties(s, (a=10,c=4)) +S(10, 2, 4) + +julia> setproperties((a=1,c=2,b=3), (a=10,c=4)) +(a = 10, c = 4, b = 3) +``` + +There is also a convenience method, which builds the `patch` argument from +keywords: + + setproperties(obj; kw...) + +# Examples +```jldoctest +julia> struct S;a;b;c; end + +julia> o = S(10, 2, 4) +S(10, 2, 4) + +julia> setproperties(o, a="A", c="cc") +S("A", 2, "cc") +``` + +# Overloading + +**WARNING** The signature `setproperties(obj::MyType; kw...)` should never be overloaded. +Instead `setproperties(obj::MyType, patch)` should be overloaded. +""" +function setproperties end + +function setproperties(obj; kw...) + setproperties(obj, (;kw...)) +end + +@generated function setproperties(obj, patch) + assert_hasfields(obj, fieldnames(patch)) + args = map(fieldnames(obj)) do fn + if fn in fieldnames(patch) + :(patch.$fn) + else + :(obj.$fn) + end + end + Expr(:block, + Expr(:meta, :inline), + Expr(:call,:(constructorof($obj)), args...) + ) +end + +@generated function setproperties(obj::NamedTuple, patch) + # this function is only generated to force the following check + # at compile time + assert_hasfields(obj, fieldnames(patch)) + Expr(:block, + Expr(:meta, :inline), + :(merge(obj, patch)) + ) +end + end # module diff --git a/test/runtests.jl b/test/runtests.jl index b3079b2..dda242b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,29 @@ using ConstructionBase using Test -@testset "ConstructionBase.jl" begin - # Write your own tests here. +struct Empty end +struct AB{A,B} + a::A + b::B +end + +@testset "constructorof" begin + @test constructorof(Empty)() === Empty() + @test constructorof(AB{Int, Int})(1, 2) === AB(1,2) + @test constructorof(AB{Int, Int})(1.0, 2) === AB(1.0,2) +end + +@testset "setproperties" begin + o = AB(1,2) + @test setproperties(o, (a=2, b=3)) === AB(2,3) + @test setproperties(o, (a=2, b=3.0)) === AB(2,3.0) + @test setproperties(o, a=2, b=3.0) === AB(2,3.0) + + @test_throws ArgumentError setproperties(o, (a=2, c=3.0)) + @test_throws ArgumentError setproperties(o, a=2, c=3.0) + @test setproperties(Empty(), NamedTuple()) === Empty() + @test setproperties(Empty()) === Empty() + + @test setproperties((a=1, b=2), (a=1.0,)) === (a=1.0, b=2) + @test setproperties((a=1, b=2), a=1.0) === (a=1.0, b=2) end