Home

ConstructionBase.jl

Interface

ConstructionBaseModule.

ConstructionBase

Stable Dev Build Status Codecov GitHub stars

ConstructionBase is a very lightwight package, that provides primitive functions for construction of objects:

setproperties(obj::MyType, patch::NamedTuple)
constructorof(MyType)

These functions can be overloaded and doing so provides interoperability with the following packages:

source
constructorof(T::Type) -> constructor

Return an object constructor that can be used to construct objects of type T from their field values. Typically constructor will be the type T with all parameters removed:

julia> using ConstructionBase

julia> struct T{A,B}
           a::A
           b::B
       end

julia> constructorof(T{Int,Int})
T

It is however not guaranteed, that constructor is a type at all:

julia> struct S
           a
           b
           checksum
           S(a,b) = new(a,b,a+b)
       end

julia> ConstructionBase.constructorof(::Type{<:S}) =
           (a, b, checksum=a+b) -> (@assert a+b == checksum; S(a,b))

julia> constructorof(S)(1,2)
S(1, 2, 3)

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 its fields:
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:
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:

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)

See also Tips section in the manual

source
setproperties(obj, patch::NamedTuple)

Return a copy of obj with attributes updates accoring to patch.

Examples

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

julia> using ConstructionBase

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")

Implementation

For a custom type MyType, a method setproperties(obj::MyType, patch::NamedTuple) may be defined.

  • Prefer to overload constructorof 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:

  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. This means that any subset p₁, p₂, ..., pₙ of propertynames(obj) is a valid set of properties, with respect to which the lens laws below must hold.
  3. setproperties should satisfy the lens laws:

For any valid set of properties p₁, p₂, ..., pₙ, following equalities must hold:

  • You get what you set.
let obj′ = setproperties(obj, ($p₁=v₁, $p₂=v₂, ..., $pₙ=vₙ))
    @assert obj′.$p₁ == v₁
    @assert obj′.$p₂ == v₂
    ...
    @assert obj′.$pₙ == vₙ
end
  • Setting what was already there changes nothing:
@assert setproperties(obj, ($p₁=obj.$p₁, $p₂=obj.$p₂, ..., $pₙ=obj.$pₙ)) == obj
  • The last set wins:
let obj′ = setproperties(obj, ($p₁=v₁, $p₂=v₂, ..., $pₙ=vₙ)),
    obj′′ = setproperties(obj′, ($p₁=w₁, $p₂=w₂, ..., $pₙ=wₙ))
    @assert obj′′.$p₁ == w₁
    @assert obj′′.$p₂ == w₂
    ...
    @assert obj′′.$pₙ == wₙ
end
source

Tips for designing types

When designing types from scratch, it is often possible to structure the types in such a way that overloading constructorof or setproperties is unnecessary in the first place. It let types in your package work nicely with the ecosystem built on top of ConstructionBase even without explicitly depending on it. For simple structs whose type parameters can be determined from field values, ConstructionBase works without any customization, provided that the "type-less" constructor exists. However, it is often useful or required to have type parameters that cannot be determined from field values. One way to solve this problem is to define singleton types that would determine the type parameters:

abstract type OutputBy end
struct Mutating <: OutputBy end
struct Returning <: OutputBy end

struct Add{O <: OutputBy, T}
    outputby::O
    value::T
end

(f::Add{Mutating})(y, x) = y .= x .+ f.value
(f::Add{Returning})(x) = x .+ f.value

add1! = Add(Mutating(), 1)

using ConstructionBase
add2 = constructorof(typeof(add1!))(Returning(), 2)
add2(1)

# output

3

setproperties works as well:

add3 = setproperties(add2; value=3)
add3(1)

# output

4

Note that no overloading of ConstructionBase functions was required. Importantly, this also provides an interface to change type parameters out-of-the-box:

add3! = setproperties(add3; outputby=Mutating())
add3!([0], 1)

# output

1-element Array{Int64,1}:
 4

Furthermore, it would work with packages depending on ConstructionBase such as Setfield.jl.

using Setfield: @set
add3′ = @set add3!.outputby = Returning()
add3′ === add3

# output

true
Note

If it is desirable to keep fields as an implementation detail, combining trait functions and Setfield.FunctionLens may be useful:

OutputBy(x) = typeof(x)
OutputBy(::Type{<:Add{O}}) where O = O()

using Setfield: Setfield, @lens
Setfield.set(add::Add, ::typeof(@lens OutputBy(_)), o::OutputBy) =
    @set add.outputby = o

obj = (add=add3!,)
obj′ = @set OutputBy(obj.add) = Returning()
obj′ === (add=add3,)

# output

true
Setfield.set(::Type{Add{O0, T}}, ::typeof(@lens OutputBy(_)), ::O1) where {O0, T, O1 <: OutputBy} =
    Add{O1, T}

T1 = typeof(add3!)
T2 = @set OutputBy(T1) = Returning()
T2 <: Add{Returning}

# output

true