Array with parametric type fails as default value
BenjaminGalliot opened this issue · 6 comments
Hello,
Maybe this is not the good place to ask, but I had a problem that was solved by avoiding to use your nice module, so I start to ask here…
I use custom parametric types to use multiple dispatch to help me separate functions depending on the source of each of these objects, which are related in a hierarchical way (with quite mutually recursive field types, that I avoid by using abstract types).
I tried to make this MWE to show you my problem, without all recursive things (normally, a Chapter
has a book::AbstractBook{Source}
field, for example), look at BadBook.chapters
:
using Parameters
abstract type AbstractSource end
abstract type ArborescentEntity{Source<:AbstractSource} end
abstract type AbstractBook{Source<:AbstractSource} <: ArborescentEntity{Source} end
abstract type AbstractChapter{Source<:AbstractSource} <: ArborescentEntity{Source} end
@with_kw struct Source <: AbstractSource
type::String = "website"
end
@with_kw mutable struct BadBook{Source<:AbstractSource} <: AbstractBook{Source}
title::String = "title"
chapters::Vector{AbstractChapter{Source}} = []
source::Source
end
mutable struct GoodBook{Source<:AbstractSource} <: AbstractBook{Source}
title::String
chapters::Vector{AbstractChapter{Source}}
source::Source
GoodBook(source) = new{typeof(source)}("title", [], source)
end
source = Source()
BadBook(source=source)
# ERROR: MethodError: no method matching BadBook(::String, ::Array{Any,1}, ::Source)
# Closest candidates are:
# BadBook(::Any, ::Array{AbstractChapter{Source},1}, ::Source)
GoodBook(source)
# GoodBook{Source}("title", AbstractChapter{Source}[], Source
# type: String "website"
# )
By using your module, the only workaround I found was to make this chapters
field type a Union
with Missing
, to initialize with missing
and after to assign []
on it later in code (at this moment, it is converted automatically to the good parametric type, as in GoodBook
version).
Did I miss something to make it work in a simple way without the use of the vanilla constructor?
Sincerely.
Your GoodBook
example violates the rule:
Custom inner constructors can be defined as long as:
- one defining all positional arguments is given
so that will not work anymore with the kw-constructors.
For BadBook, your default value is not compatible with the type. This works:
julia> @with_kw mutable struct BadBook{Source<:AbstractSource} <: AbstractBook{Source}
title::String = "title"
chapters::Vector{AbstractChapter{Source}} = AbstractChapter{Source}[]
source::Source
end
I'll close this but feel free to ask more questions here.
Hello @mauro3,
I did not know the GoodBook
example violated the rule, it was a vanilla one, to show comparison with and without Parameters, so without @with_kw
.
For BadBook
, I made a mistake by creating my MWE, because there were an ambiguity with Source
, which was there more a dynamic name. Let’s see this one:
using Parameters
abstract type AbstractSource end
abstract type ArborescentEntity{Source<:AbstractSource} end
abstract type AbstractBook{Source<:AbstractSource} <: ArborescentEntity{Source} end
abstract type AbstractChapter{Source<:AbstractSource} <: ArborescentEntity{Source} end
@with_kw struct SourceA <: AbstractSource
type::String = "website"
end
@with_kw struct SourceB <: AbstractSource
type::String = "file"
end
@with_kw mutable struct BadBook{Source<:AbstractSource} <: AbstractBook{Source}
title::String = "title"
chapters::Vector{AbstractChapter{Source}} = AbstractChapter{Source}[]
source::Source
end
mutable struct GoodBook{Source<:AbstractSource} <: AbstractBook{Source}
title::String
chapters::Vector{AbstractChapter{Source}}
source::Source
GoodBook(source) = new{typeof(source)}("title", [], source)
end
source_a = SourceA()
source_b = SourceB()
bad_book = BadBook(source=source_a)
# ERROR: UndefVarError: Source not defined
good_book = GoodBook(source_a)
# GoodBook{SourceA}("title", AbstractChapter{SourceA}[], SourceA
# type: String "website"
# )
quite_good_book = BadBook{typeof(source_a)}(source=source_a)
# BadBook{SourceA}
# title: String "title"
# chapters: Array{AbstractChapter{SourceA}}((0,))
# source: SourceA
It seems the Source
in source::Source
takes the dynamic one of the struct declaration, whereas the Source
of chapters::Vector{AbstractChapter{Source}} = AbstractChapter{Source}[]
expects a type called Source
, which does not exist because it is a dynamic name. There is no error if I comment this chapters
field.
The only way to use it is with explicit type declaration in caller (quite_good_book
), which is worse than vanilla good_book
because of redundancy.
Because it works pretty well with GoodBook
, a vanilla constructor, I expected the BadBook
to work. Is there something I missed for constructor, about syntax or anything else?
Sincerely.
Thanks for the detailed example. This works:
@with_kw mutable struct BadBook{Source<:AbstractSource} <: AbstractBook{Source}
title::String = "title"
source::Source
chapters::Vector{AbstractChapter{Source}} = AbstractChapter{typeof(source)}[]
end
A few things to note: the stuff on the left of the =
will be evaluated at keyword evaluation. At that time Source
is not defined yet. Also, the swapped order is necessary as keywords are evaluated one after the other, thus typeof(source)
has to be after source
is defined.
Thank you for explanation, @mauro3, I tried without success with a source
variable too but in declaration, not with field directly! So this was the trick!
Last question, I think, and maybe it is less related to your module than Julia’s proper behaviour: with vanilla GoodBook
, I was able to assign directly []
and it was converted to Vector{AbstractChapter{Source}}
, which seems clever because it avoids redundancy we have with Vector{AbstractChapter{Source}} = AbstractChapter{typeof(source)}[]
. It is for me a similar redundancy we can see in GoodBook(source) = new{typeof(source)}("title", [], source)
, in which we could think the typeof(source)
is quite obvious after new
.
Is there anyway to compact it, or because of some cases we don’t see here, Julia prefers more explicit type declarations?
It reminds me some assignments in main code in which a basic mydict = Dict("a" => "b")
without explicit type declaration would generate a MethodError
later because I would do after a simple mydict["b"] = 1
, but at least I understand in this case that it is an optimization, contrary to our struct…
Sincerely.
Yes, Julia tries to convert values when possible. And it may well be that in the "original" Julia types this might be handeled a bit better than in Parameters. Not however, in your GoodBook example, you still do the explicit type parameter calculation in GoodBook(source) = new{typeof(source)}("title", [], source)
. As this does not work either:
julia> mutable struct GoodBook2{Source<:AbstractSource} <: AbstractBook{Source}
title::String
chapters::Vector{AbstractChapter{Source}}
source::Source
end
julia> GoodBook2("a", [], source_a)
ERROR: MethodError: no method matching GoodBook2(::String, ::Array{Any,1}, ::SourceA)
Closest candidates are:
GoodBook2(::String, ::Array{AbstractChapter{Source},1}, ::Source) where Source<:AbstractSource at REPL[26]:2
Stacktrace:
[1] top-level scope at REPL[27]:1
[2] eval(::Module, ::Any) at ./boot.jl:331
[3] eval_user_input(::Any, ::REPL.REPLBackend) at /home/mauro/julia/julia-1.4/usr/share/julia/stdlib/v1.4/REPL/src/REPL.jl:86
[4] run_backend(::REPL.REPLBackend) at /home/mauro/.julia/packages/Revise/XFtoQ/src/Revise.jl:1162
[5] top-level scope at none:0
julia> GoodBook2{SourceA}("a", [], source_a) # this works but you have to tell Julia the type-parameter
GoodBook2{SourceA}("a", AbstractChapter{SourceA}[], SourceA
type: String "website"
)
Correct, I still need somewhere to be explicit!
Thank you for all your answers!