mauro3/Parameters.jl

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!