juliaanonymous-functiontyping

Typing of anonymous function in nested dicts


I have a nested dict like this

function_dict_1 = Dict(
    :f => Dict(
        :func1 => x -> x^2
    )
)

I want to call a strongly typed function, which receives this dict as an argument.

My first thought was to type it like this:

dict_arg::Dict{Symbol, Dict{Symbol, <:Function}}

However, I got an unmatched function call error.

I am confused, since it seems to work with other datatypes:

string_dict = Dict(
    :s => Dict(
        :string1 => "a"
    )
)
typeof(string_dict) <: Dict{Symbol, Dict{Symbol, String}}
>> true

as well as with not-nested dicts:

function_dict_2 = Dict(
    :func1 => x -> x^2
)
typeof(function_dict_2) <: Dict{Symbol, <:Function}
>> true 

I tried the following types:

which all did not work.

What is the correct way to type for this variable?


Solution

  • function_dict_1::Dict{Symbol, <:Dict{Symbol, <:Function}}
    

    You could also solve this with an explicit generic parameter:

    function_dict_1::Dict{Symbol, Dict{Symbol, F}} where F <: Function
    

    You're probably wondering why these both work when Dict{Symbol, Dict{Symbol, <:Function}} doesn't; after all, isn't Dict{Symbol, Dict{Symbol, <:Function}} the same as Dict{Symbol, Dict{Symbol, F}} where F <: Function? No, because the implicit generic parameter is always scoped to the innermost {} containing the <:, so Dict{Symbol, Dict{Symbol, <:Function}} is actually the same as Dict{Symbol, Dict{Symbol, F where F <: Function}}.

    Now, except for tuples, T1 <: T2 does not imply that SomeType{T1} <: SomeType{T2}. So even though typeof(function_dict_1).parameters[2] is Dict{Symbol, var"#1#2"} (where var"#1#2" is the name of the type of the anonymous function), and Dict{Symbol, var"#1#2"} <: Dict{Symbol, <:Function} (but !(Dict{Symbol, var"#1#2"} <: Dict{Symbol, Function})!), we do not have that Dict{Symbol, Dict{Symbol, var"#1#2"}} <: Dict{Symbol, Dict{Symbol, <:Function}}.

    So why do the two answers I gave above work while Dict{Symbol, Dict{Symbol, <:Function}} doesn't? Because the implicit generic parameter is scoped to the innermost {} containing <:Function, the outer dict type is a concrete type, not a UnionAll.

    julia> dump(Dict{Symbol, Dict{Symbol, <:Function}})
    Dict{Symbol, Dict{Symbol, <:Function}} <: AbstractDict{Symbol, Dict{Symbol, <:Function}}
      slots::Memory{UInt8}
      keys::Memory{Symbol}
      vals::Memory{Dict{Symbol, <:Function}}
      ndel::Int64
      count::Int64
      age::UInt64
      idxfloor::Int64
      maxprobe::Int64
    

    Since function_dict_1 is not exactly this type, it cannot be a subtype of it.

    Now, compare this to what we get for other types. For subtyping to work (except for tuples), we need either a UnionAll or an abstract type as the outermost type.

    julia> dump(Dict{Symbol, <:Function})
    UnionAll
      var: TypeVar
        name: Symbol #s66
        lb: Union{}
        ub: Function <: Any
      body: Dict{Symbol, var"#s66"<:Function} <: AbstractDict{Symbol, var"#s66"<:Function}
        slots::Memory{UInt8}
        keys::Memory{Symbol}
        vals::Memory{var"#s66"}
        ndel::Int64
        count::Int64
        age::UInt64
        idxfloor::Int64
        maxprobe::Int64
    
    julia> dump(Dict{Symbol, Dict{Symbol, F where F <: Function}})
    Dict{Symbol, Dict{Symbol, Function}} <: AbstractDict{Symbol, Dict{Symbol, Function}}
      slots::Memory{UInt8}
      keys::Memory{Symbol}
      vals::Memory{Dict{Symbol, Function}}
      ndel::Int64
      count::Int64
      age::UInt64
      idxfloor::Int64
      maxprobe::Int64
    
    julia> dump(Dict{Symbol, Dict{Symbol, F}}  where F <: Function)
    UnionAll
      var: TypeVar
        name: Symbol F
        lb: Union{}
        ub: Function <: Any
      body: Dict{Symbol, Dict{Symbol, F<:Function}} <: AbstractDict{Symbol, Dict{Symbol, F<:Function}}
        slots::Memory{UInt8}
        keys::Memory{Symbol}
        vals::GenericMemory{:not_atomic, Dict{Symbol, F<:Function}, Core.AddrSpace{Core}(0x00)}
        ndel::Int64
        count::Int64
        age::UInt64
        idxfloor::Int64
        maxprobe::Int64
    
    julia> dump(Dict{Symbol, <:Dict{Symbol, <:Function}})
    UnionAll
      var: TypeVar
        name: Symbol #s67
        lb: Union{}
        ub: UnionAll
          var: TypeVar
            name: Symbol #s68
            lb: Union{}
            ub: Function <: Any
          body: Dict{Symbol, var"#s68"<:Function} <: AbstractDict{Symbol, var"#s68"<:Function}
            slots::Memory{UInt8}
            keys::Memory{Symbol}
            vals::Memory{var"#s68"}
            ndel::Int64
            count::Int64
            age::UInt64
            idxfloor::Int64
            maxprobe::Int64
      body: Dict{Symbol, var"#s67"<:(Dict{Symbol, <:Function})} <: AbstractDict{Symbol, var"#s67"<:(Dict{Symbol, <:Function})}
        slots::Memory{UInt8}
        keys::Memory{Symbol}
        vals::Memory{var"#s67"}
        ndel::Int64
        count::Int64
        age::UInt64
        idxfloor::Int64
        maxprobe::Int64
    
    julia> 
    

    In short, the first <: in Dict{Symbol, <:Dict{Symbol, <:Function}} is necessary to let the second <: make it out of its own {}, so that the whole type can be a UnionAll.