Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass a function as an argument for another function in julia?

Tags:

julia

Can we pass a function as an argument of another function in julia? How does it work? Does this mean the input function is run before the calling function, or does the input function only get called if the calling function calls it specifically?

like image 477
Fazeleh Avatar asked Oct 20 '17 04:10

Fazeleh


2 Answers

One of the greatest features of Julia is that you can dig out the answer to these kinda questions by yourself. Instead of making experiments and then observing the behaviors on the surface, directly asking Julia what it did under the hood is a more convenient and concise way to get the answer. Let's borrow these examples from the other two answers and ask Julia what happened via @code_lowered:

julia> f() = println("hello world")
julia> g(any_func::Function) = any_func()

#Me: hi, Julia, what happened here?
julia> @code_lowered g(f)

#Julia: hi there, this is the lowered code, anything else you want to know?
CodeInfo(:(begin 
        nothing
        return (any_func)() 
    end))
#Me: it looks like function `g` just returned `(any_func)()` and did nothing else, 
#    is it equvienlent to write `f()`?
julia> @code_typed g(f)

#Julia: yes, the codes looks the same after type inference stage: 
CodeInfo(:(begin 
        $(Expr(:inbounds, false))
        # meta: location REPL[1] f 1
        # meta: location coreio.jl println 5
        SSAValue(0) = (Core.typeassert)(Base.STDOUT, Base.IO)::IO
        # meta: pop location
        # meta: pop location
        $(Expr(:inbounds, :pop))
        return (Base.print)(SSAValue(0), "hello world", $(QuoteNode('\n')))::Void
    end))=>Void

julia> @code_typed f()
CodeInfo(:(begin 
        $(Expr(:inbounds, false))
        # meta: location coreio.jl println 5
        SSAValue(0) = (Core.typeassert)(Base.STDOUT, Base.IO)::IO
        # meta: pop location
        $(Expr(:inbounds, :pop))
        return (Base.print)(SSAValue(0), "hello world", $(QuoteNode('\n')))::Void
    end))=>Void

Does this mean the input function is run before the calling function?

In this special case, it's hard to answer, the calling function g was optimized out by the compiler at compile time, so there was no g at runtime. :P Let's add some extra contents to g:

julia> g(any_func::Function) = (println("I don't wanna be optimized out!"); any_func())
g (generic function with 1 method)
julia> @code_lowered g(f)
CodeInfo(:(begin 
        nothing
        (Main.println)("I don't wanna be optimized out!")
        return (any_func)()
    end))
julia> @code_typed g(f)
CodeInfo(:(begin 
        $(Expr(:inbounds, false))
        # meta: location coreio.jl println 5
        SSAValue(0) = (Core.typeassert)(Base.STDOUT, Base.IO)::IO
        # meta: pop location
        $(Expr(:inbounds, :pop))
        (Base.print)(SSAValue(0), "I don't wanna be optimized out!", $(QuoteNode('\n')))::Void
        $(Expr(:inbounds, false))
        # meta: location REPL[2] f 1
        # meta: location coreio.jl println 5
        SSAValue(1) = (Core.typeassert)(Base.STDOUT, Base.IO)::IO
        # meta: pop location
        # meta: pop location
        $(Expr(:inbounds, :pop))
        return (Base.print)(SSAValue(1), "hello world", $(QuoteNode('\n')))::Void
    end))=>Void

This example shows the content of g would be run exactly before f, so the answer is YES. Follow the same pattern, it's easy to check out Liso's example:

julia> f = x->x+1
julia> fun(f::Function, a) = f(a)

# nothing new here, `f(9)` was called before `fun` 
# and the return value was passed to `fun` with a binding name `a`.
julia> @code_lowered fun(f, f(9))  
CodeInfo(:(begin 
        nothing
        return (f)(a)
    end))

# to verify your second question:
julia> foo(f, x) = x
foo (generic function with 1 method)

julia> @code_lowered foo(f, 1)
CodeInfo(:(begin 
        nothing
        return x
    end))

Does the input function only get called if the calling function calls it specifically?

So yes, the example above shows that if f is not called by the calling function foo, it will be directly optimized out.

Unlike other languages, Julia is not just a magic black box which is opaque to users, sometimes it's efficient and effective to open the box and be self-taught. BTW, There are two more stages(@code_llvm, @code_native) in Julia, you might need to dump these low-level codes for some advanced investigation, refer to Stefan's great answer in this post: What is the difference between @code_native, @code_typed and @code_llvm in Julia? for further details.

UPDATE:

what is different between these functions: g1() = f() , g2(f::Function) = f(). Results for both of them are the same, so What is different between them?

julia> @code_lowered g1()
CodeInfo(:(begin 
        nothing
        return (Main.f)()
    end))

julia> @code_lowered g2(f)
CodeInfo(:(begin 
        nothing
        return (f)()
    end))

The lowered code tell that g1() always returns Main.f(), here the Main.f is the f in the Main module, but g2 returns f() where f is the function you passed to it. To make this clear, we could define g2 as:

julia> g2(argumentf::Function) = argumentf()
g2 (generic function with 1 method)

julia> @code_lowered g2(f)
CodeInfo(:(begin 
        nothing
        return (argumentf)()
    end))

g2 is "pass a function as an argument of another function", g1 can be considered as an alias for Main.f. Does this make sense to you? @ReD

like image 113
Gnimuc Avatar answered Oct 20 '22 03:10

Gnimuc


Julia has always supported first-class as well as higher-order functions. This means that yes, functions can be arguments to other functions. In addition, the language has also always had support for anonymous functions and closures.

Originally, calling functions that were passed into other functions came with a performance penalty. However, as of v0.5, this is no longer an issue, and functions you pass into other functions as arguments will run just as quickly as functions from base julia. There is more reading on this here.

A simple example of passing functions follows:

julia> f() = println("hello world") #Define a simple function f
f (generic function with 1 method)

julia> g(any_func::Function) = any_func() #Define a function g that accepts any function as input and then calls that function
g (generic function with 1 method)

julia> g(f) #Call g with f as the input
hello world

What is happening here is that I build a function f in the first line, which, when called via f(), will print "hello world". In the second line I build a function g which accepts any function as input, and then calls that function. In the third line, I call my function g using my function f as input, and g then runs the function f, which, in this case, will print "hello world".

The example above may seem trite, but in more complicated scenarios, being able to pass functions around like this is incredibly useful.

As the other answerer points out, base julia exploits this feature with many native functions that accept other functions (including anonymous functions) as input, such as map, filter, etc.

Most readers can stop here. What follows is only relevant to those who use eval a lot...

There is one exception to the above, which the average user is unlikely to ever encounter: If a function is built by parsing and evaluating a string at runtime, and this function is called during the same runtime, then the call will fail with an error message that will say something about "world age". What is actually happening here is that because the function has been created at runtime, the compiler does not have an opportunity to reason about the behaviour of the function, e.g. input and output types, and so it "prefers" not to call it. Nonetheless, if you really want to you can get around this by calling it via the Base.invokelatest method, but this means the function call will not be type-stable. Basically, unless you really know what you're doing, you should avoid messing around with this.

like image 3
Colin T Bowers Avatar answered Oct 20 '22 03:10

Colin T Bowers