Skip to content

Commit aa10199

Browse files
authored
Merge pull request #1 from JuliaObjects/init
Decide API for setproperties and constructorof
2 parents a0b60f7 + ecaf7e0 commit aa10199

File tree

2 files changed

+169
-3
lines changed

2 files changed

+169
-3
lines changed

src/ConstructionBase.jl

+144-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,148 @@
11
module ConstructionBase
22

3-
greet() = print("Hello World!")
3+
export setproperties
4+
export constructorof
5+
6+
7+
"""
8+
constructorof(T::Type)
9+
10+
Return an object `ctor` that can be used to construct objects of type `T`
11+
from their field values. Typically `ctor` will be the type `T` with all parameters removed:
12+
```jldoctest
13+
julia> struct T{A,B};a::A;b::B;end
14+
15+
julia> constructorof(T{Int,Int})
16+
T
17+
```
18+
It is however not guaranteed, that `ctor` is a type at all:
19+
```jldoctest
20+
julia> struct S
21+
a
22+
b
23+
checksum
24+
S(a,b) = new(a,b,a+b)
25+
end
26+
27+
julia> ConstructionBase.constructorof(S) = (a,b,checksum) -> (@assert a+b == checksum; S(a,b))
28+
29+
julia> constructorof(S)(1,2)
30+
S(1, 2, 3)
31+
```
32+
Instead `ctor` can be any object that satisfies the following properties:
33+
* It must be possible to reconstruct an object from its fields:
34+
```julia
35+
ctor = constructorof(typeof(obj))
36+
@assert obj == ctor(fieldvalues(obj)...)
37+
@assert typeof(obj) == typeof(ctor(fieldvalues(obj)...))
38+
```
39+
* The other direction should hold for as many values of `args` as possible:
40+
```julia
41+
ctor = constructorof(T)
42+
fieldvalues(ctor(args...)) == args
43+
```
44+
For instance given a suitable parametric type it should be possible to change
45+
the type of its fields:
46+
```jldoctest
47+
julia> using ConstructionBase: constructorof
48+
49+
julia> struct T{A,B};a::A;b::B;end
50+
51+
julia> t = T(1,2)
52+
T{Int64,Int64}(1, 2)
53+
54+
julia> constructorof(typeof(t))(1.0, 2)
55+
T{Float64,Int64}(1.0, 2)
56+
57+
julia> constructorof(typeof(t))(10, 2)
58+
T{Int64,Int64}(10, 2)
59+
```
60+
"""
61+
@generated function constructorof(::Type{T}) where T
62+
getfield(parentmodule(T), nameof(T))
63+
end
64+
65+
function assert_hasfields(T, fnames)
66+
for fname in fnames
67+
if !(fname in fieldnames(T))
68+
msg = "$T has no field $fname"
69+
throw(ArgumentError(msg))
70+
end
71+
end
72+
end
73+
74+
"""
75+
setproperties(obj, patch)
76+
77+
Return a copy of `obj` with attributes updates accoring to `patch`.
78+
79+
# Examples
80+
```jldoctest
81+
julia> using ConstructionBase
82+
83+
julia> struct S;a;b;c; end
84+
85+
julia> s = S(1,2,3)
86+
S(1, 2, 3)
87+
88+
julia> setproperties(s, (a=10,c=4))
89+
S(10, 2, 4)
90+
91+
julia> setproperties((a=1,c=2,b=3), (a=10,c=4))
92+
(a = 10, c = 4, b = 3)
93+
```
94+
95+
There is also a convenience method, which builds the `patch` argument from
96+
keywords:
97+
98+
setproperties(obj; kw...)
99+
100+
# Examples
101+
```jldoctest
102+
julia> struct S;a;b;c; end
103+
104+
julia> o = S(10, 2, 4)
105+
S(10, 2, 4)
106+
107+
julia> setproperties(o, a="A", c="cc")
108+
S("A", 2, "cc")
109+
```
110+
111+
# Overloading
112+
113+
**WARNING** The signature `setproperties(obj::MyType; kw...)` should never be overloaded.
114+
Instead `setproperties(obj::MyType, patch)` should be overloaded.
115+
"""
116+
function setproperties end
117+
118+
function setproperties(obj; kw...)
119+
setproperties(obj, (;kw...))
120+
end
121+
122+
@generated function setproperties(obj, patch)
123+
assert_hasfields(obj, fieldnames(patch))
124+
args = map(fieldnames(obj)) do fn
125+
if fn in fieldnames(patch)
126+
:(patch.$fn)
127+
else
128+
:(obj.$fn)
129+
end
130+
end
131+
Expr(:block,
132+
Expr(:meta, :inline),
133+
Expr(:call,:(constructorof($obj)), args...)
134+
)
135+
end
136+
137+
@generated function setproperties(obj::NamedTuple, patch)
138+
# this function is only generated to force the following check
139+
# at compile time
140+
assert_hasfields(obj, fieldnames(patch))
141+
Expr(:block,
142+
Expr(:meta, :inline),
143+
:(merge(obj, patch))
144+
)
145+
end
146+
4147

5148
end # module

test/runtests.jl

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
using ConstructionBase
22
using Test
33

4-
@testset "ConstructionBase.jl" begin
5-
# Write your own tests here.
4+
struct Empty end
5+
struct AB{A,B}
6+
a::A
7+
b::B
8+
end
9+
10+
@testset "constructorof" begin
11+
@test constructorof(Empty)() === Empty()
12+
@test constructorof(AB{Int, Int})(1, 2) === AB(1,2)
13+
@test constructorof(AB{Int, Int})(1.0, 2) === AB(1.0,2)
14+
end
15+
16+
@testset "setproperties" begin
17+
o = AB(1,2)
18+
@test setproperties(o, (a=2, b=3)) === AB(2,3)
19+
@test setproperties(o, (a=2, b=3.0)) === AB(2,3.0)
20+
@test setproperties(o, a=2, b=3.0) === AB(2,3.0)
21+
22+
@test_throws ArgumentError setproperties(o, (a=2, c=3.0))
23+
@test_throws ArgumentError setproperties(o, a=2, c=3.0)
24+
@test setproperties(Empty(), NamedTuple()) === Empty()
25+
@test setproperties(Empty()) === Empty()
26+
27+
@test setproperties((a=1, b=2), (a=1.0,)) === (a=1.0, b=2)
28+
@test setproperties((a=1, b=2), a=1.0) === (a=1.0, b=2)
629
end

0 commit comments

Comments
 (0)