2

Let's say I have a function that expects an dict as input. Within that, this function can only handle values of that dict that are in a certain Union of allowed types. For this argument, the input can be Number, String, or Bool:

allowed_types = Union{String, Int, AbstractFloat, Bool}

The function can also permit Dicts whose values are these allowed types (Dict{String,allowed_types}) or Arrays whose items are these types (Array{allowed_types,Int}) as they can be "disassembled" down in to one of those allowed types. (This can keep continuing downwards - so an array of arrays, etc)

full_allowed_types = Union{allowed_types, Dict{String,allowed_types}, Array{allowed_types,Int}}

I can then define my function as

function my_func(input::Dict{String,full_allowed_types})
...
end

How, then, do I structure my function arguments so that I can pass, I.E, my_func(Dict("a"=>"astr","b"=>1))? Normally that Dict(...) call results in a Dict{String,Any}, which doesn't work to call with my function as Any is not an allowed type.

The error I get with my current implementation is:

my_func(Dict("a"=>"astr","b"=>1))
ERROR: MethodError: no method matching my_func(::Dict{String,Any})
Closest candidates are:
  my_func(::Dict{String,Union{Bool, Int64, Dict{String,Union{Bool, Int64, AbstractFloat, String}}, AbstractFloat, Array{Union{Bool, Int64, AbstractFloat, String},Int64}, String}}) at <snip>/my_func.jl:41
Stacktrace:
 [1] top-level scope at none:0

I'm picturing this issue from a user standpoint, where the user would likely just create a dict using the default constructor, without considering what my_func wants to be "allowed" (meaning I don't expect them to call Dict{String,my_pkg.full_allowed_types}(...)).

Is the best option to just allow Any as the input to my_func and then throw an error if any of the elements don't fit with my allowed types as I iterate through the input?

fergu
  • 329
  • 1
  • 5
  • 12
  • 1
    Careful with `Array{allowed_types,Int}`, this is not what you want. The second generic parameter of `Array` is not a type, but an actual integer. E.g. a Vector of Ints is of type `Vector{Int, 1}`. So you probably want to just use `Array{full_allowed_types}` which is the same as `Array{full_allowed_types,N} where N`. – Simon Schoelly Apr 27 '20 at 23:01
  • Without testing, I'm pretty sure you can express those type constraints, yes. But are you sure that you need to? Good practice in Julia is to be as permissive about types as possible and write generic code. Why must the types be restricted to strings, ints and floats? Maybe you have a good reason, but make really sure before you create a super-complicated type signature that can be buggy and needlessly restrictive. – DNF Apr 27 '20 at 23:07
  • @DNF The main reason for the restriction is that I am writing a TOML printer, and the TOML spec only permits certain types of variables. As I've been playing with this, though, I may stick more with allowing the user to input anything and trying to coerce those inputs in to something that TOML does allow, and throw an error if I can't. – fergu Apr 28 '20 at 20:10

2 Answers2

4

First, when creating your dictionary, you can specify the parameters like this:

d = Dict{String, full_allowed_types}("a" => "astr", "b" => 2)

Second, your way of creating my_func is actually correct, and will work with d. But when you create a a dictionary

d2 = Dict{String, Int64}("a" => 1, "b" => 2)

you might be surprised, that you cannot call my_func with d. The reason is, that you only allowed Dicts that are of type Dict{String, full_allowed_types} and not Dict{String, Int64} even though Int64 is a subtype of full_allowed_types. If you want to be able to pass also subtypes, you can declare my_func like

function my_func(input::Dict{String, <: full_allowed_types})
    ...
end

Note the additional <: here, this is just syntactic sugar for

function my_func(input::Dict{String, T}) where {T <: full_allowed_types}
    ...
end
Simon Schoelly
  • 697
  • 5
  • 11
  • Excellent, this is exactly what I was looking for! Out of curiosity, does similar logic exist to catch additional levels? I.E - I'm writing a TOML printer, so there could be many levels of the input to `my_func` - I.E the input dict could itself have a dict which contains an array... etc etc. Is there a way to just say "So long as the final element in this chain is in my `union` then we're good"? – fergu Apr 28 '20 at 00:11
  • I don't think that is possible, and even if it where, that would be quite dangerous. Whenever you call a function in Julia with some arguments, then Julia checks if there is already code with that combination of arguments. If not, new code is generated. (Unless the `@nospecialize macro is used). So as a TOML is basically a tree, for each different form of that tree, new code would be created. – Simon Schoelly Apr 28 '20 at 21:25
3
function f(a::Dict{String, A}) where A <: Union{Int,String}
   println("Got elem type $A")
end

Usage:

julia> f(Dict{String,Union{String,Int}}("a"=>"astr","b"=>1))
Got elem type Union{Int64, String}

Now if you want to make it convenient for a user you could add an additional function (However, the type conversion will come at a cost):

function f(a::Dict{String,A}) where A
    @warn "Provided unsupported type of elements $A will try narrow it to Union{Int,String}"
    f(Dict{String,Union{Int,String}}(d))
end

Example usage:


julia> f(Dict("a"=>"astr","b"=>1))
┌ Warning: Provided unsuported type of elements Any will try narrow it to Union{Int,String}
└ @ Main REPL[31]:2
Got elem type Union{Int64, String}
Przemyslaw Szufel
  • 40,002
  • 3
  • 32
  • 62