Function name conflict: ADL / function merging?

89 consistent “meanings” for size (before loading libraries)? I don’t buy it.

I just checked all of these and they are all consistent:

  • All the methods of * implement some form of multiplication.
  • All the methods of size behave in this manner:
    • they return a tuple of dimension sizes when called with a single argument
    • they return the size of single dimension when called with two arguments
    • they return a tuple of the requested dimension sizes when called with three or more arguments

If I had found any methods stuck in there that were doing something else, I would have immediately opened an issue to fix it. Incredulity at the number of methods for such simple function probably has to do with not quite being used to how heavily Julia uses types and dispatch to implement behaviors that are consistent as abstractions yet highly polymorphic in implementation. It’s also possible that some of these could be condensed into a smaller number of more general method definitions, but we can fix that at any point since it doesn’t change the meaning or behavior of the functions.

Incredulity at the consistency across so many types and methods may come from familiarity with class-based languages where x.f and y.f may not be related at all, let alone mean the same thing. The fact that the f in f(x) and f(y) in Julia is the same object has kept us very vigilant about being consistent about abstract meaning, which in turn makes writing highly generic code more feasible in Julia than in any other language I’ve encountered. If x.size() might returns a tuple for one type and a scalar for another type and an array for another type, how are you going to write code that works across all kinds of x?

The litmus test here is whether one can write generic code that uses these functions without knowing or caring what specific types and methods implement them. This is the case in Julia precisely because we’ve gone to such great pains to make sure that all our generic functions are conceptually consistent. Consider string exponentiation—i.e. repetition in the string monoid where multiplication is concatenation. This has a specialized definition for performance but you can just as correctly call the generic Base.power_by_squaring function which is used to implement many other generic ^ methods:

julia> "abc"^10
"abcabcabcabcabcabcabcabcabcabc"

julia> Base.power_by_squaring("abc", 10)
"abcabcabcabcabcabcabcabcabcabc"

julia> @which "foo"^10
^(s::Union{AbstractChar, AbstractString}, r::Integer) in Base at strings/basic.jl:623

julia> @which Base.power_by_squaring("abc", 10)
power_by_squaring(x_, p::Integer) in Base at intfuncs.jl:186

The fact that these will produce equivalent results stems directly from the fact that they implement the same concept. Similarly, you can call size anywhere and know that it will behave consistently regardless of what type you call it on. For example:

julia> size(x^2 for x = 1:10)
(10,)

julia> @which size(x^2 for x = 1:10)
size(g::Base.Generator) in Base at generator.jl:117

How a generator knows its size is quite different from how an array knows its size, but the concept is the same and you can call the size function on an array or generator and not care which since they behave the same and mean the same thing. Julia is far more disciplined about this kind of consistency than any other language I’m familiar with and, as a result, it is far more possible to write generic code that actually works in Julia.

7 Likes