Makie (and Julia) do not automatically typecast obvious numerical representations

I have experienced that Julia is not very generous with automatic typecasting (or type conversion).
e.g.

f(x::Int) = x
f(1.0)
ERROR: MethodError: no method matching f(::Float64)
Closest candidates are:
  f(::Int64) at REPL[4]:1
Stacktrace:
 [1] top-level scope
   @ REPL[5]:1

Although I understand that this is a design choice, sometimes it can be a pain.

For example using the Makie package I discovered that the following yields an error

using GLMakie
a = Vector{Any}([1,2,3,4,5])
barplot(a)
ERROR: `Makie.convert_arguments` for the plot type Combined{Makie.barplot, Tuple{Vector{Any}}} and its conversion trait PointBased() was unsuccessful.

The signature that could not be converted was:
::Vector{Any}

Makie needs to convert all plot input arguments to types that can be consumed by the backends (typically Arrays with Float32 elements).
You can define a method for `Makie.convert_arguments` (a type recipe) for these types or their supertypes to make this set of arguments convertible (See http://makie.juliaplots.org/stable/documentation/recipes/index.html).

Alternatively, you can define `Makie.convert_single_argument` for single arguments which have types that are unknown to Makie but which can be converted to known types and fed back to the conversion pipeline.
...

I understand that the first one is a design choice of the language, with consistency being one of the advantages.

But in the second case, the package usability could probably be increased if such conversions could be automatically handled. I would guess for example that implementing a Any => Float64 conversion would deal with the majority of the error cases in the Makie package.

2 Likes

This is pretty fundamental to how dispatch works in Julia and isn’t something that can really be changed. If you write f(x::Int), you are telling the language that you only want this method to be called for Int arguments — the argument types act as a kind of filter.

Definitely library authors should try to accept the widest applicable type signatures in their APIs, but you will typically still have to pay more attention to types than in, e.g. Python.

If there is a specific set of functions in Makie that you think should accept Vector{Any}, you can file an issue there, but this isn’t something that’s going to change at the language level.

8 Likes

One alternative is for you to turn the question around: “Why is my code lugging around a Vector{Any}, and how can I fix it?”

Vector{Any} is commonly a sign that something went wrong somewhere (unless you are quite experienced), and that your program will be slow and bug prone. The most common source of these vectors are, as far as I’ve seen, that a vector was initialized as a = [].

6 Likes

I completely agree that we should have a flexible and extensible mechanism for doing conversion of arguments to the expected type when calling a function. Let’s think through a possible design for this feature.

We need some way of expressing what types should be converted to what other types and in what circumstances. After all, we definitely can’t convert floating point values to integers everywhere—if we did that, we couldn’t have different implementations for integers and floats, which is something one often needs. And sometimes you would want to convert a float to a complex number or something else rather than an integer. So we do need something with some flexibility for this—in particular, we would need to include the function that the rule applies to since that determines whether a float should be converted to an integer, say, or a complex number, or a rational or whatever. Then we’d need some kind of pattern specifying the kinds of arguments that the conversion rule applies to. And finally we’d need to specify how to do the conversion itself. Sometimes it might just be calling convert but sometimes it might be something a bit more complex. It might even involve an interaction between multiple arguments in a non-trivial way, so to be flexible enough we’d want to include how to re-invoke the function. But I think those three criteria should be sufficient for this feature.

Ok, now that we have the design requirements, let’s give it a syntax. We can put the function first, then the pattern that matches the arguments, and then the rule for how to convert the arguments and pass them back to the function. So maybe something like this:

f(x::Real) = f(Int(x))

Nice! That’s a pretty clear, concise way to express an automatic argument conversion for f. It captures all the essential factors quite minimally and it wouldn’t only work when you call f with a floating point argument, but also with any numeric value that is a subtype of the Real abstraction. I think this feature might be pretty useful in general.

3 Likes

Ideally, Vector{Any} is just a performance issue, not a correctness one. That’s why YASGuide at least suggests one shouldn’t dispatch on things like AbstractVector{<:Number}, since that doesn’t actually represent “any AbstractVector whose elements are all <:Number“.

The specific for running into this problem was using hcat to handle more conveniently my data structure, using Matrix indexing.
Assume I have the following generator

ge = (ns for ns in [("one",1), ("two", 2), ("three", 3)])
ma = vcat([[n;;s] for (n,s) in ge]...)
3×2 Matrix{Any}:
 "one"    1
 "two"    2
 "three"  3

in order to do something like this:

barplot(ma[:,1], axis=(xticks= (1:3, ma[:,2]),))

but it doesn’t work because all elements are reduced to the type of the Matrix, i.e. Any.
Then I also tried using a Matrix{Union{Int, String}}:

ma2 = Matrix{Union{String, Int}}(ma)
3×2 Matrix{Union{Int64, String}}:
 1  "one"
 2  "two"
 3  "three"

but this didn’t do the trick.

In the end I use a normal Vector of Vectors and broadcast the getindex function:

ns = [[n, s] for (n,s) in ge]
3-element Vector{Vector{Any}}:
 ["one", 1]
 ["two", 2]
 ["three", 3]

which although it’s of type Any, the inner vectors still hold their types:

getindex.(ns,2)
3-element Vector{Int64}:
 1
 2
 3

so plotting with Makie is possible:

barplot(getindex.(ns,2), axis=(xticks= (1:3, getindex.(ns,1)),))

I was thinking of Julia as of just not being super fast if someone didn’t care about the types,
but I see that there can be several implications.

Thanks everybody for the answers !

It can be generally more convenient to use a table instead of a matrix here:

tbl = [(label=ns[1], val=ns[2]) for ns in [("one",1), ("two", 2), ("three", 3)]]
barplot(getproperty.(tbl, :val), ... whatever ...)

# or
using Tables
tbl = [(label=ns[1], val=ns[2]) for ns in [("one",1), ("two", 2), ("three", 3)]] |> columntable
barplot(tbl.val, ... whatever ...)
1 Like