Julia: Implement traits Iterable and Callable

Created on 27 Jan 2020  路  7Comments  路  Source: JuliaLang/julia

Consider a Julia neophyte trying to sort Chars:

julia> maximum('A', 'B')
ERROR: MethodError: objects of type Char are not callable
Stacktrace:
 [1] mapreduce_first(::Char, ::Function, ::Char) at ./reduce.jl:293
 [2] mapfoldl_impl(::Char, ::Function, ::NamedTuple{(),Tuple{}}, ::Char) at ./reduce.jl:60
 [3] #mapfoldl#186(::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(mapfoldl), ::Char, ::Function, ::Char) at ./reduce.jl:72
 [4] mapfoldl(::Char, ::Function, ::Char) at ./reduce.jl:72
 [5] #mapreduce#194(::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(mapreduce), ::Char, ::Function, ::Char) at ./reduce.jl:200
 [6] mapreduce(::Char, ::Function, ::Char) at ./reduce.jl:200
 [7] maximum(::Char, ::Char) at ./reduce.jl:524
 [8] top-level scope at none:0

Of course they should have used max. But that stack trace is long and confusing, considering that already the very first call maximum(::Char, ::Char), the types are all wrong for maximum. Normally, Julia methods have a signature that takes only appropritate abstract types, like Number and so on, precisely so that the method only apply to inputs of the correct types.

The problem is that there are no types for Callable or Iterable, so the signature of maximum is totally untyped: maximum(f, a) = mapreduce(f, max, a).

To solve this, I suggest implementing Holy traits Iterable and Callable:

maximum(f, itr) = maximum(f, callable(f), itr, iterable(itr))
maximum(f, ::Callable, itr, ::Iterable)

One argument against doing this is that it could open the door to all kinds of traits in Base for this or that property. However, in my experience, it seems like the properties "iterable" and "callable" are particularly often used, just consider the amount of confusion about what is iterable or not we often see.

breaking speculative

Most helpful comment

For addressing this specific case --- giving a better error message --- something much simpler would suffice. If we make applicable checks efficient, these functions could just add checks that raise a clear error if the expected methods aren't defined.

But, understanding the implications of method errors is critical to understanding how the language works. While traits might be very useful and indeed could be part of the future of the language, it's questionable whether adding a complex language feature truly ameliorates the confusion here. If you get a confusing error when trying to call maximum, your best move is ?maximum. Some rewording might also help here, e.g. "a Char was passed to a context that expected a function".

All 7 comments

Base.Callable already exists.

Seems like this would be a breaking change, unless iterable(x) = Iterable() is the default, which would make it much less useful.

For addressing this specific case --- giving a better error message --- something much simpler would suffice. If we make applicable checks efficient, these functions could just add checks that raise a clear error if the expected methods aren't defined.

But, understanding the implications of method errors is critical to understanding how the language works. While traits might be very useful and indeed could be part of the future of the language, it's questionable whether adding a complex language feature truly ameliorates the confusion here. If you get a confusing error when trying to call maximum, your best move is ?maximum. Some rewording might also help here, e.g. "a Char was passed to a context that expected a function".

Base.Callable already exists.

It does not seem to work with function-like objects?

julia> struct A end

julia> A isa Base.Callable
true

julia> (::A)() = 1

julia> a = A()
A()

julia> a isa Base.Callable
false

Correct. Since any object is callable this way, that would make Callable the same as Any.

OK, based on the responses in this thread:

  • Since any object could be callable or iterable, making code dispatch on these traits would place a burden on the developer to define these traits for every type they create, or else risk Base functions computing the wrong result, which is not reasonable.
  • If it is possible at compile time to determine whether a type is iterable and compute the Iterable() trait from that, say using a compile-time version of the function hasmethod(iterate, (Foo,)), then you might as well just use that function to check that the input types are applicable instead of using the trait.

So it seems to be not possible to make these traits practically useful barring some complete redesign of how traits work in Julia, which is something outside the scope of this issue anyway. Closing the issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tkoolen picture tkoolen  路  3Comments

wilburtownsend picture wilburtownsend  路  3Comments

TotalVerb picture TotalVerb  路  3Comments

yurivish picture yurivish  路  3Comments

ararslan picture ararslan  路  3Comments