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:
- 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.
- 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)
- I can call run with any implementation of the downloader.
- 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)
- 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
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