Okay I was not suggesting to apply this manually to every type under the sun. That is indeed error prone and ugly and what not. Also I was not suggesting this constructor for human use in types with lots of fields.
I think most types should have 0-3 fields and no custom constructor at all. If I apply this pattern I usually use a macro.
You are right, we need a standard constructor for the machine, for packages like Reconstructables.jl and Setfield.jl to work.
I think the positional constructor has one killer advantage. Its the default. This means it automatically works for most types. Especially useful for types defined in packages you don’t own.
I agree that the positional constructor has a huge advantage in that it is the (only) default at the moment. But keyworded constructor may have language support in the future as well: https://github.com/JuliaLang/julia/issues/10146
Anyway, now that constructor lookup can be customized via Setfield.constructor_of, I guess I can do whatever I want to do on top of Setfield.jl which is great!
I started liking lens-based approach! Now that lens can change type parameters, we have “differentiable lens”!
using ForwardDiff
using Parameters: @with_kw, @unpack
using Setfield: @lens, Lens, set, get
import Setfield
@with_kw struct Coordinate{X, Y, Z}
x::X = 1.0
y::Y = 1.0
z::Z = 1.0
end
# Since set called via ForwardDiff would specify a dual number, we
# have to ignore type parameters:
Setfield.constructor_of(::Type{<: Coordinate}) = Coordinate
function f(c::Coordinate)
@unpack x, y, z = c
return x^2 + y^2 + z^2
end
derivative(f, at, wrt::Lens) =
ForwardDiff.derivative(
(x) -> f(set(wrt, at, x)),
get(wrt, at))
@assert derivative(f, Coordinate(x=1.0), @lens _.x) ≈ 2
@assert derivative(f, Coordinate(x=2.0), @lens _.x) ≈ 4
This is actually super useful for building something like bifurcation analysis tool where you need a generic way to let user specify a few parameters in a differentiable manner.