What's the best way to dispatch on an _iterable_ trait?

I find that it’s quite common for me to write a function that requires the input to be iterable. But I don’t think there is a good way to restrict it to that. I think I would love to see a trait system built into Julia e.g.

fn(x::Symbol) = fn((x,))

function fn(x_itr:::IterableTrait)
   ## do something to each success element of `x_itr`
end

What’s the best way to achieve what I want so far in your opinion.

The best way to my knowledge is GitHub - oxinabox/Tricks.jl: Cunning tricks though the julia compiler internals. The iterable thing is actually the motivating example for the package. There are heavy caveats, but the package does appear to work well and be fairly robust for this.

using Tricks: static_hasmethod
struct Iterable end
struct NonIterable end

isiterable(::Type{T}) where {T} = static_hasmethod(iterate, Tuple{T}) ? Iterable() : NonIterable()

fn(x::T) where {T} = fn(isiterable(T), x)
fn(::Iterable, x) = map(x -> (x, x), x)  
fn(::NonIterable, x) = (x,)

fn(:hi), fn(fn(:hi))

#+RESULTS:
: ((:hi,), ((:hi, :hi),))
@btime fn(:hi)

#+RESULTS:
:   1.329 ns (0 allocations: 0 bytes)
: (:hi,)
5 Likes

I am kinda warming to the syntax of

fn(x::Type1:::Trait1)
fn(x::Union{Type1, Type2}:::Both{Trait1, Trait2})

Just document that it should be iterable and accept any argument.

7 Likes

While it wasn’t clear in the prose, the pseudo-code shown seems to imply that the OP wanted to dispatch on whether something is iterable and act differently depending on whether or not it’s iterable, in which case simply duck typing won’t work.

1 Like

Why would you want to do that? Seems like that should be two different functions since they likely do completely different things.

3 Likes

:man_shrugging:

I don’t think it’s too hard to imagine cases where you care whether or not an input is has properties like being iterable or being callable and I don’t think telling people “don’t care about that” is particularly helpful (though one may often be right that there’s a better solution that doesn’t involve doing this).

4 Likes

I think the message is that trying to design an API which does something different depending on whether the input is iterable (callable etc) is the wrong way to go about it. Eg an imaginary

function do_stuff(thing)
    if is_iterable(thing)
        collect(thing)
    elseif is_callable(thing)
        thing()
    else
        fallback(thing)
    end
end

can be a very confusing design, regardless of whether it is done with traits or not.

Technically a trait for something supporting the iterate protocol would be very easy to add to the language, but I suspect that one of the reasons this has not happened is that usually there is a better way to organize code.

4 Likes

FYI, there was a similar discussion in

which introduced mergewith(f, dicts...) that superseded merge(f, dicts...). This is because merge(f, dicts...) cannot be robustly distinguished from merge(dicts...) since any type can implement the call operator. I like how Jeff summarized it:

With multiple dispatch, just as the meaning of a function is important to get consistent, it’s also important to get the meanings of the argument slots consistent when possible. It’s not ideal for a function to treat an argument in a fundamentally different way based on its type

5 Likes

However, this is often the case in base julia. E.g.

  strip(pred, str::AbstractString) -> SubString
  strip(str::AbstractString, chars) -> SubString

basically does opposite things depending on which argument is a String.

Yes, this is unfortunate, but it can only be changed with Julia 2.0.

An issue collecting these would make sense.

The use-case I had in mind was simply so that fn when applied to a non-iterable will not run and just generate an error. That’s all.

If the object doesn’t implement iterate, it’ll throw an error anyway.

If it is about giving a good error message, something like Base.Experimental.register_error_hint would be a better approach. Not sure if it’s usable here though.

6 Likes

Self plug here :slight_smile:

If you use BinaryTraits.jl, then you can statically verify the types assigned to the Iterable trait. So you would find any implementation problem during development rather than at runtime.

An example can be found here:
https://github.com/tk3369/BinaryTraits.jl/blob/master/examples/iteration_indexing.jl#L6-L29

This still requires that the user go and assign any type they might accept with the Iterable trait though right? I think the OP wants to just automatically detect if something is iterable and dispatch on that.

That’s right. OP was also thinking about error reporting and so I am just suggesting a different approach for consideration.

2 Likes