Best practice to support multiple implementations

Hi

I am a newbie to the language, coming from an OOP background. I have a method as below:

function run(downloader)
       downloader.download()
end

the downloader can have several implementations, for instance, could be downloading from internet or getting data from csv etc.

I am looking for advice on how Julia supports this. keeping in view, type safety, multiple dispatch etc.
In other languages, such as Java, this would be supported by creating an interface

Java example

void run(IDownloader downloader)
       downloader.download()
end

interface IDownloader {
         void download();
}

class CsvDownloader implements IDownloader{
         void download(){}
}

Thanks!
Roh

In this case, it sounds like you could just pass a function:

function run(download)
    download()
end

For more general cases, the analogue of OOP’s object.method(args...) in Julia is method(object, args...). Just like how in OOP you can overload a method for an object, in the same way for Julia you can overload functions for particular argument types (based on any/all of the argument types — this is called “multiple dispatch”, whereas traditional OOP is “single dispatch” because the method called only depends on the type of object and not on the other arguments).

For example, if you write

sqr(a) = a * a

in Julia, then it will work on any type of a object that has implemented a method for the function (*)(a, a), i.e. * with two arguments of the type of a.

1 Like

thanks for your elaborate response. this is indeed a wonderful community.

“Julia you can overload functions for particular argument types (based on any/all of the argument types”

in my case, the arguments are the same for multiple implementations of the download function.

Also, not sure:

  1. Does this impact performance? not specifying the arguments with function passed in as an arg. Also, please consider the scenario where run() takes in multiple functions as args.
  2. Can this be solved using traits? I am reading about them as I write.

ta!

No, they’re not. The analogue of downloader.download(args...) in Julia would be download(downloader, args...), so the type of downloader (the self object in Python or this in C++) is different and you can dispatch on that.

Not for properly written (type-stable/type-inferrable) Julia code — the Julia complier compiles a type-specialized version of run based on the type of the argument (downloader) at the call site. This is called “devirtualization” in C++, and is commonplace in Julia because all concrete types are “final” (in C++ parlance).

Traits are essentially a way to get benefits analogous to multiple inheritance / mix-in interfaces. It doesn’t sound like you need that here, and in general you should learn the basics (how dispatch and generic programming works) before you learn fancier techniques.

3 Likes

Got it! thanks a lot for your comments and feedback. I will give this a shot.

@stevengj Looks like I do need to read a bit more on Julia docs. May I ask you another question please? Do you think this would be a good solution in my case:

function downloader()
... download some data
end

function run(downloader)
downloader()
end

I haven’t enforced any types downloader function when I passed it in as an arg. Does this matter? if it does, how can I enforce types on functions? I tried the below and it didn’t work.

function run(downloader::Function{String, Number})
    # download data
    downloader()
end

Julia currently does not support typing functions, because unlike c, a function is a set of different methods. It is like the visitor pattern in Java where you pass a set of methods to be dispatched.

For example, this is possible in Julia:

function run(downloader, source)
    downloader(source)
end
run(downloader, ip) #  ip of type IPv4
run(downloader, "http://julialang.org")

this isn’t quite true. Julia stores support young functions, it’s just that every function has a unique type and the type isn’t determinedby the argument tropes.

2 Likes

No, it’s not a problem.

See also Argument-Type Declarations in the Julia manual.

Thanks to everyone who pitched in. this is the solution I came up with:

using BenchmarkTools

abstract type Downloader end

function downloadFromUrl(url::String)
    # println("downloading from url $url")
    # some operation
    count = 0.
    for i in rand(100000)
        count+=i
    end
end

function downloadFromCsv(filepath::String)
    # println("downloading from url $url")
    # some operation
    count = 0.
    for i in rand(100000)
        count+=i
    end
end


Base.@kwdef struct UrlDownloader <: Downloader
    url::String = "some--url"
    token::String = "my-token"
    process::Function = () -> downloadFromUrl(url)
end

Base.@kwdef struct CsvDownloader <: Downloader
    filePath::String = "file-path"
    process::Function = () -> downloadFromCsv(filePath)
end

function run(downloader::Downloader)
    println("Running $(typeof(downloader))")
    downloader.process()
end

# call run with url downloader
run(UrlDownloader())
# call run with csv downloader
run(CsvDownloader())

println("Calling methods directly")
@btime downloadFromUrl("some url")
@btime downloadFromCsv("some file path")

println("Calling methods from struct")
@btime UrlDownloader().process()
@btime CsvDownloader().process()

Output:

Running UrlDownloader
Running CsvDownloader
Calling methods directly
  66.208 μs (2 allocations: 781.30 KiB)
  66.167 μs (2 allocations: 781.30 KiB)
Calling methods from struct
  66.208 μs (2 allocations: 781.30 KiB)
  66.167 μs (2 allocations: 781.30 KiB)
  1. I can call run with any implementation of the downloader.
  2. I have checked that calling the method directly vs calling a method referenced from inside a struct does not have any performance penalties (I didn’t expect any, this is just a learning exercise)
  3. It allows me to pass in a struct to run() function. the struct has the params and the method specified, which I feel fits nicely together. the alternative was that I don’t use structs and pass in the functions as args to run. the functions can get very complicated and have multiple args, so I feel this pattern fits better

I look forward to everyone’s comments and suggestions. Please do let me know if there is a better approach. ta!

Don’t try to emulate OOP syntax this way. The process field is abstractly typed, which basically prevents the compiler from doing type inference or inlining anything when you call this function. It’s also not idiomatic Julia style.

(Function-call performance may not matter in this particular example, or in any example where the body of the function takes so much time that everything else is negligible, but it’s a bad habit.)

As I said, the analogue of OOP’s downloader.process() UFC syntax in Julia is process(downloader). i.e. write:

function process(x::UrlDownloader)
    # do something
end

function process(x::CsvDownloader)
    # do something else
end

function run(downloader::Downloader)
    println("Running $(typeof(downloader))")
    process(downloader)
end

See also Allowing the object.method(args...) syntax as an alias for method(object, args ...)

7 Likes

@stevengj : thanks a lot for your patience and review comments. I will keep those in mind and have changed the code as below

abstract type Downloader end

Base.@kwdef struct UrlDownloader <: Downloader
    url::String = "some--url"
    token::String = "my-token"
end

Base.@kwdef struct CsvDownloader <: Downloader
    filePath::String = "file-path"
end

function process(x::UrlDownloader)
    println("downloading from url $(typeof(x))")
end

function process(x::CsvDownloader)
    println("downloading from csv $(typeof(x))")
end

function run(downloader::Downloader)
    process(downloader)
end

# call run with url downloader
run(UrlDownloader())
# call run with csv downloader
run(CsvDownloader())

ta!

3 Likes

though C++ has multiple dispatch too. I just always get discouraged to keep learning it sometime because it has so many subtleties :slight_smile:

no it doesn’t. operator overloading and templating are both different from multiple dispatch.

1 Like

Okay good to know. What are the main difference?(I do remember from C++ book the mention of templates and overloading, I was thinking I saw multiple dispatch too, but now I see I was wrong). Because in overloading you make two function with same name, and template is to allow things like “T integrate(T,T)” for any number, so what is different in dispatch?
Thanks for help

The difference is that both templates and overloading must know all the types of a program if you don’t want to call fallback implementations. JuliaCon 2019 | The Unreasonable Effectiveness of Multiple Dispatch | Stefan Karpinski - YouTube has a good description of the differences.

1 Like

thanks.