Using multiple dispatch and the type system correctly

Hello everyone, I am currently trying to write a program using WebDriver.jl. My functions act on the ‘Element’ type of WebDriver (WebDriver.jl/06_Element.jl at master · Nosferican/WebDriver.jl · GitHub). I am mainly using two functions, one to identify the ‘Elements’ on the page I want to process and store their ‘id’ and ‘class’ in a dictionary, and another function that processes each element based on it’s class. I do, however, struggle with the latter. In a first test I simply used one function with multiple elseif blocks which did not even seem like a good idea on the first look (but was simple to deploy). It looks roughly like this:

if class_of_my_element == "exactly_this_class"
    do this
        elseif class_of_my_element == "exactly_this_other_class"
            do this
        elseif class_of_my_element == "exactly_this_other_third_class"
            do this

However, there is going to be a larger number of classes, and each of it will require a slightly different function to be called. Obviously I do not want to write 30 different functions with different names, but instead I would like to call the same function with different methods based on the class of each element. After reading up on the type system and multiple dispatch I still do not know how I could accomplish a clean solution in this case.

Can I create a subtype of ‘Element’ that has a field ‘class’ which requires a certain value? (This still means I am creating about 30 sub-types, but the work has to be done somewhere I believe). Or is it possible to use a dictionary to call different methods of a function based on a class => method mapping?

Really grateful for any input on how to approach this kind of problem.

This sounds like a use case for dispatching on a value: Types · The Julia Language

Have you considered this? Of course the implementation differences need to be … well,… implemented.

I would avoid using if to test for classes but instead write functions depending on types.

Something like this:

struct MyClass end
struct MyOtherClass end
...

# identify elements and map to class
function element_to_struct(el)
    if el == ... 
        return MyClass
    elseif ...
        return MyOtherClass
    ...
    end

end

# do something depending on which struct
function myfunc(el::MyClass)
     // do stuff
end

function myfunc(el::MyOtherClass)
     // do stuff
end

# Combine both
function main(el)
    cls = element_to_struct(el)
    return myfunc(cls)
end

Not sure if this approach is suitable for 30 different functions. Maybe there is a better way. But this would be my starting point.

I am more or less saying the same thing as above, but is this the type of behavior you want?

julia> abstract type Class1 end

julia> abstract type Class2 end

julia> struct Type1 <: Class1 end

julia> struct Type2 <: Class2 end

julia> do_stuff(x::Class1) = "doing stuff with something of Class1"
do_stuff (generic function with 1 method)

julia> do_stuff(x::Class2) = "doing stuff with something of Class2"
do_stuff (generic function with 2 methods)

julia> x = Type1()
Type1()

julia> do_stuff(x)
"doing stuff with something of Class1"

julia> y = Type2()
Type2()

julia> do_stuff(y)
"doing stuff with something of Class2"


1 Like

Thanks for all the suggestions. Dispatching on values seems helpful and it is my mistake that I have not realized it could be applied to my case when I read the docs. Thanks for pointing that out @oheil .

I can totally get behind the logic of @ZettEff s approach as this was very much along the lines of my initial thought, but I was lacking the concept of a helper function that maps the elements to certain classes first.

For the suggestions by @lmiq I am not sure if I understand it correctly, I am not yet sure why I would need Type1, Type2, etc. next to Class1, Class2, etc. but maybe I will realize that when I get deeper into the problem.

Thanks everyone for helping, I have a lot of ideas to work with now! :slight_smile:

Sorry, I am not sure either. I’ve shown that because the terminology “class” is usually associated with object-oriented programming, where types of “things” belong to different “classes”. The most similar construct in Julia (which is not object-oriented) is to define abstract types for the “classes” and, and types of variables (concrete types) which are subtypes of that abstract type. Then you can dispatch on the classes or on the concrete types, depending on the use case.

You shouldn’t convert elements to structs in a single function, because it is literally going to be a type unstable function.

On a general note, this kind of task is complicated because it is basically runtime dispatch and runtime dispatch is slow. It’s a pity, but big fat if is the best solution from the performance point of view. You can use macros to avoid repetitive code to some degree. Also, if per-class functions are producing the same type (for example Nothing) then you can still use static multiple dispatch to some degree.

# do something depending on which struct
function myfunc(el::MyClass)
     // do stuff
end

function myfunc(el::MyOtherClass)
     // do stuff
end

# apply functions to classes
function action(f, el)
    if el == ... 
        return f(MyClass(el))
    elseif ...
        return f(MyOtherClass(el))
    ...
    end
end

# Usage
action(myfunc, el)

Definitely you shouldn’t put mapping to Dict, since it will introduce slowdown. You can use NamedTuple though, but it has it’s limits too.

1 Like

@skoffer raises an important point, but it may be that the run time dispatch performance is not important for your application, and in this case you can choose the clearest and most extensible solution you want. In Julia we are somewhat obsessed with speed, but that is sometimes off the point.

2 Likes

Thanks for the argument on not converting elements to structs.

Could you explain what exactly is the problem with this approach in some more detail?

This is one of the threads discussing that: Performance drawback with subtyping - #6 by lmiq

and other threads are linked in that one. @skoffer usually post the best answers there (and I am struggling to understand what is going on the middle).

Maybe this topic can be of interest: How to work with type unstable functions? I think it’s rather similar to what you are doing and it has some explanations.

Thanks, very helpful.