@ChrisRackauckas Btw I am curious, why are @generated
functions should not statically be compilable?
I would have guessed that f(obj)
always works, if f(::typeof(obj))
was seen at static compiletime and
never works if f(::typeof(obj))
was not seen at compiletime. Why does it matter if f
is generated?
Thatâs why youâd want to have something like constructor_of
where you can register foreign âfakeâ constructor/factory function.
Keyword-only constructor is just a suboptimal solution to hook arbitrary pre-construction check/processing, which is (sometimes) needed to be done in the inner constructor. If it can be done with positional-only constructor then thatâs also a nice solution.
Last time I checked, it was broken also in nightly
Really? Did you build it yourself? The nightlies havenât built in a very long time so that would be the problem if you downloaded the binary.
No, I didnât build myself. It was the downloaded binary. But I thought it was showing something like âN days older than masterâ where N < 10.
Redhat using COPR?
FWIW I build a master from two days ago and it does not work:
julia> struct S{X,Y}
x::X
y::Y
S(;x::X=1,y::Y=2) where {X,Y} = new{X,Y}(x,y)
end
julia> Test.@inferred S(x=:a, y="")
ERROR: return type S{Symbol,String} does not match inferred return type Any
Stacktrace:
[1] error(::String) at ./error.jl:33
[2] top-level scope
I was using https://julialangnightlies-s3.julialang.org/bin/linux/x64/julia-latest-linux64.tar.gz linked from https://julialang.org/downloads/nightlies.html
Iâm not sure which one in the https://status.julialang.org/ was that.
It seems thatâs what Travis is using. For example, this shows âCommit a237986bf9 (2018-02-20 19:08 UTC)â and the @inferred
with just keyword-only constructor is broken:
This didnât handle it?
Apparently not. You think this is a bug?
Worth opening an issue to track it (if that hasnât been done already).
It was apparently quite easy to get âtheoreticalâ type-stability from the keyword-only approach, with a similar @generated
trick:
@generated function recon(thing; kwargs...)
fields = [Expr(:kw, k, :(thing.$k)) for k in fieldnames(thing)]
return quote
constructor = $constructor_of(thing)
constructor(; $(fields...), kwargs...)
end
end
https://github.com/tkf/Reconstructables.jl/commit/68075dafe54c749ecea1179c8d839e76b7d76753
Of course, itâs only âtheoreticalâ at this point since the constructor itself is not type-stable.
Possible benefits over lens/setproperty
approach are that (1) you can do a âbatchâ update in which the constructor is called only once and (2) it generates only one (or two [*]) function for each struct instead of length(fieldnames(...))
functions.
[*] I may need to turn constructor_of
into a generated function for type-stability.
I would have thought that with the kw-constructor you shouldnât need a generated function for âtheoreticalâ type stability.
Well, I donât know if it is actually needed since Iâve never tried it with Julia with type-stable keyword-only constructor. I just thought the getfield
& fieldnames
combo in the following code would be hard for Julia to make it type-stable. (or not?)
function recon(thing; kwargs...)
constructor = constructor_of(thing)
defaults = [(n => getfield(thing, n)) for n in fieldnames(typeof(thing))]
return constructor(; defaults..., kwargs...)
end
The type of array defaults
becomes Array{Pair{Symbol}}
(the âvalueâ part is undetermined) for sufficiently complex struct. If the type of thing
has type parameters that depend on the fields, the return type cannot be determined, right? Can you avoid it?
Also, in Julia 0.6 and 0.7, it seems that the type of defaults
itself cannot be inferred:
using Base.Test
struct S{X,Y,Z}
x::X
y::Y
z::Z
S(x::X, y::Y, z::Z) where {X, Y, Z} = new{X, Y, Z}(x, y, z)
end
get_fields(thing) =
[(n => getfield(thing, n)) for n in fieldnames(typeof(thing))]
s = S(1, 2.0, "3")
@show @inferred get_fields(s) # fails
The most specific inference I can get out of variants of get_fields
was when I used tuple and generator. But it still was not perfect.
There is a way to avoid @generated
functions, have full type stability and no need of a keyword constructor. It can be adapted to both the Lens
design and your design. I sketch for simplicity the case of only updating a single field.
macro register(T)
M = current_module()
fields = eval(M, :(fieldnames($T)))
quote
function Reconstructables.recon(thing::$T, field, val)
new_fields = map($fields) do fld
fld == field ? val : getfield(thing, field)
end
constructor_of($T)(new_fields...)
end
end
end
The price is that you need to add @register MyStruct
to each struct you want to use this feature with. This is even possible for structs living in other modules.
Isnât it generating the similar amount code as the @generated
function version? How is it better?
Also, I want to have custom inner constructors. In that case, you need to generate keyword-only or positional-only constructors anyway for it to have some systematic mutation mechanism. So, âyou donât need keyword-only constructorsâ is not a valid appealing point for use-cases I have in mind.
Good question. I donât know if this is better then using @generated
, as I donât know what the problem with @generated
is. If its only the amount of generated code, then I donât think its any better. I even think there is no way to get the same performance with less generated code. At the end of the day each variant with perfect performance has to generate fully specialized @code_llvm
. So I guess the total amount of generated code will always be the same for fully typestable variants.
In Parameters.jl I require that there is a positional inner constructor for all fields defined (either automatically via @with_kw
or by the user). That does all the enforcing of constraints and other things you may want to do. All other inner/outer constructors then go through that one. I think this scheme should be able to cover almost all cases.
Yeah I think this is good design. I usually do the same thing:
-
Have one and only one âlow levelâ inner constructor. All other constuctors must go through this one in the end. Its only job is to enforce invariants, it does nothing else. It is positional and its signature is the fields of the struct.
-
Have whatever high level outer constructors you like.
I think thatâs a source of bugs. If I understand correctly, that means users have to manually call the inner constructor from the outer constructor with positional arguments. If there are many structs with more than 5 fields (say) it would be terrifying to do any kind of refactoring touching the fields.
(OK. We need to do that already when calling new
. But please donât increase the burden.)
Obviously, it does not imply that @with_kw
is a bad design since it also generates a keyword-only constructor as well.
If you let users to write a inner-constructor, it has to be usable by humans. Positional-only constructor is not for humans. Humans are good at referring to things by names, not positions. I think thatâs the primary reason why we use structs instead of tuples.
Then what is required for packages like Reconstructables.jl and Setfield.jl are to generate constructor usable by programs. That means a keyword-only or positional-only constructor. They both have pros and cons.