Overwriting functions

Note: This post is intended as a FAQ entry in wiki format to get improved by the community.

Relevant pages of the Julia Documentation: Functions, Methods

Relevant Julia principles by design:

  • Different types get different treatment.
  • Special gets priority over generic.
  • Julia is not an interpreter able to compile code, but a compiler able to execute arbitrary code (read more).
  • Julia’s compiler is a Just-In-Time (JIT) compiler (read more).

Note: The last two principles are simplified descriptions of a sophisticated multistage system, which in most cases operates transparently to the programmer. For the needs of this post, the simplified descriptions are enough. The details can be explored by following the provided links.

Demonstration

Important: All code sections in this post constitute a single session, the next ones logically following and often depending on the previous ones.

Defining a function

We are going to define a function named myfun. Before naming something, it is a good idea to check if the name is already in use:

julia> myfun
ERROR: UndefVarError: myfun not defined

Functions get usually defined implicitly, together with the definition of their first method. To explicitly define a function:

julia> function myfun end
myfun (generic function with 0 methods)

The phrase “generic function” in REPL’s answer refers to a different concept than the one used here. For the purposes of this post, only one function (a generic function) is used and its various methods may compare to each other as more/less generic or more/less special.

The new function cannot be called, because it has no methods:

julia> myfun()
ERROR: MethodError: no method matching myfun()

However, it is as real as any other function and can be referenced by other code:

julia> f = myfun # assign to a variable
myfun (generic function with 0 methods)

julia> g = (function myfun end) # attempt to redefine
myfun (generic function with 0 methods)

julia> f === g # equivalent to is(f, g)
true

Therefore, overwriting a function in Julia is empty of meaning. Actually, the whole Functions page of the Julia Documentation is about methods. The reason for using “functions” in the title of that page (as well as in the title of the present topic) is to help people coming to Julia from other languages. Overwriting in Julia is fully possible, but it takes place per method and it needs a different perspective to avoid surprises:

Defining methods

Next we are going to define various methods for the myfun function and notice that overwriting is rare in practice, because of Julia’s key feature of multimethods:

julia> myfun() = "no arguments" # define a method of myfun function without arguments
myfun (generic function with 1 method)

julia> myfun()         # call the new method
"no arguments"

julia> myfun(x) = "single argument: $x" # define a second method for an arbitrary argument
myfun (generic function with 2 methods)

julia> myfun(1)        # call method myfun(x) passing an Int
"single argument: 1"

julia> myfun("a")      # call method myfun(x) passing a String
"single argument: a"

julia> myfun()         # method myfun() not overwritten by method myfun(x)
"no arguments"

julia> myfun("a", "b") # call a not-yet-defined method
ERROR: MethodError: no method matching myfun(::String, ::String)

julia> myfun(::String, ::String) = "too many strings" # define a special fix
myfun (generic function with 3 methods)

julia> myfun("a", "b") # try again previously failed call
"too many strings"

julia> myfun("a")      # myfun(x) not overwritten by myfun(::String, ::String)
"single argument: a"

julia> myfun(x1, x2) = "too many arguments: $x1, $x2" # define a generic fix
myfun (generic function with 4 methods)

julia> myfun(1, 2)     # generic fix works
"too many arguments: 1, 2"

julia> myfun("a", "b") # myfun(::String, ::String) not overwritten by myfun(x1, x2)
"too many strings"

julia> methods(myfun)  # review the defined methods of function myfun
# 4 methods for generic function "myfun":
[...]

Here a programmer may complain that the last definition should overwrite the previous one, as they both have the same number of arguments (two). But that would be against Julia’s principle: “Different types get different treatment.” That principle allows overloading beyond arity and enables Julia to optimize code based on each type’s special characteristics, resulting in high performance.

Still the programmer could suggest that at least the generic fix should get priority over the special one, for being more generic. But that would be against Julia’s principle: “Special gets priority over generic.” That principle (which extends beyond Julia’s boundaries) allows programmers to have code for their special custom types take priority over the generic or default/fall-back functionality provided by Julia packages or other libraries, preventing surprising conflicts and resulting in high flexibility.

Still the programmer could argue that such conflicts could get resolved instead by each time giving priority to whatever method got defined last. That way programmers could decide priority for themselves, based on their personal needs. However, although that would free the programmer from previous definitions, it could easily alter the functionality of loaded packages in ways unpredictable by someone who doesn’t know their internals. One of the reasons that packages created by unrelated people are able to work together smoothly, is that priority is governed by the “special over generic” principle, acting like a contract which people can trust to code independently.

Still the programmer in the above session has a specific intent, that all two-argument calls of the myfun function to return the result defined by the code of the myfun(x1, x2) method. Outside interactive coding, that could be achieved simply by removing the code of the special definition, effectively directing all potential calls to the remaining generic method. But when coding interactively (like in Julia’s REPL), past actions remain in effect to the end of the session. In that case, there are various options presented right below.

Overwriting methods

Redefine/overwrite the special method to make it like the generic one or specialize upon it:

julia> myfun(s1::String, s2::String) = "too many strings: $s1, $s2" # overwrite
WARNING: Method definition myfun(String, String) [...] overwritten

julia> myfun("a", "b") # call the redefined method
"too many strings: a, b"

Redefine/overwrite the special method to explicitly invoke the generic one:

julia> myfun(s1::String, s2::String) = myfun(s1, s2) # overwrite, the wrong way
WARNING: Method definition myfun(String, String) [...] overwritten

julia> myfun("a", "b") # call the redefined method, which calls itself forever
ERROR: StackOverflowError:

julia> myfun(s1::String, s2::String) = invoke(myfun, tuple(Any, Any), s1, s2) # overwrite
WARNING: Method definition myfun(String, String) [...] overwritten

julia> myfun("a", "b") # call the redefined method, but get the result of the generic method
"too many arguments: a, b"

Start over by calling the interactive workspace() function, without exiting Julia’s REPL:

julia> workspace()

julia> myfun           # all user definitions are gone
ERROR: UndefVarError: myfun not defined

julia> myfun(x1, x2) = "too many arguments: $x1, $x2" # both function and method defined at once
myfun (generic function with 1 method)

julia> myfun("a", "b") # only a single candidate method now
"too many arguments: a, b"

JIT compiler implications

In both overwriting cases, a warning was raised. There is good reason for that, as overwriting a method directs all its future calls to the new definition, except of those which are already compiled:

julia> myfun(x) = myfun(x, "b") # purposely define myfun(x) so as to call myfun(x1, x2)
myfun (generic function with 2 methods)

julia> myfun("a")               # here myfun(x) gets JIT compiled to depend on myfun(x1, x2)
"too many arguments: a, b"

julia> myfun(x1, x2) = "2: two arguments are fine: $x1, $x2" # overwrite with a new (2nd) version
WARNING: Method definition myfun(Any, Any) [...] overwritten

julia> myfun("a", "b")          # the 2nd version of method myfun(x1, x2) works fine
"2: two arguments are fine: a, b"

julia> myfun("a")               # but method myfun(x) is still compiled to call the 1st version
"too many arguments: a, b"

julia> myfun(x) = myfun(x, "b") # redefine myfun(x) exactly as before
WARNING: Method definition myfun(Any) [...] overwritten

julia> myfun("a")               # now method myfun(x) gets JIT compiled to call the 2nd version
"2: two arguments are fine: a, b"

julia> myfun(x) = myfun(x, x)   # overwrite to hopefully call 2nd version before next change
WARNING: Method definition myfun(Any) [...] overwritten

julia> myfun(x1, x2) = "3: two arguments are mysterious: $x1, $x2" # overwrite with 3rd version
WARNING: Method definition myfun(Any, Any) [...] overwritten

julia> myfun("a")               # but method myfun(x) gets JIT compiled to call the 3rd version
"3: two arguments are mysterious: a, a"

julia> myfun(x) = invoke(myfun, tuple(Any, Any), x, x) # overwrite to depend on function myfun
WARNING: Method definition myfun(Any) [...] overwritten

julia> myfun(x1, x2) = "4: two arguments ought to be enough: $x1, $x2" # overwrite with 4th version
WARNING: Method definition myfun(Any, Any) [...] overwritten

julia> myfun("a")               # 4th version works fine, as function myfun never gets overwritten
"4: two arguments ought to be enough: a, a"

julia> myfun(x1, x2) = "5: two arguments final offer: $x1, $x2" # overwrite with 5th version
WARNING: Method definition myfun(Any, Any) [...] overwritten

julia> myfun("a")               # 5th version works fine, myfun(x) always calls latest version
"5: two arguments final offer: a, a"

julia> myfun(x) = eval( :( myfun($x, $x) ) ) # overwrite to enforce recompilation of the call
WARNING: Method definition myfun(Any) [...] overwritten

julia> myfun(x1, x2) = "4: two arguments ought to be enough: $x1, $x2" # back to 4th version
WARNING: Method definition myfun(Any, Any) [...] overwritten

julia> myfun("a")               # 4th version works fine, as eval() recreates the call every time
"4: two arguments ought to be enough: a, a"

julia> myfun(x1, x2) = "5: two arguments final offer: $x1, $x2" # forward to 5th version
WARNING: Method definition myfun(Any, Any) [...] overwritten

julia> myfun("a")               # 5th version works fine, myfun(x) always calls latest version
"5: two arguments final offer: a, a"

julia> map(myfun, 1:2)          # higher-order function map gets JIT compiled to call current myfun(x)
2-element Array{String,1}:
 "5: two arguments final offer: 1, 1"
 "5: two arguments final offer: 2, 2"

julia> myfun(x) = "single argument: $x" # overwrite with initial definition
WARNING: Method definition myfun(Any) [...] overwritten

julia> map(myfun, 1:2)          # but map is still compiled to call the previous definition
2-element Array{String,1}:
 "5: two arguments final offer: 1, 1"
 "5: two arguments final offer: 2, 2"

julia> map( (x)-> myfun(x) , 1:2) # use an anonymous function to recompile on every call
2-element Array{String,1}:
 "single argument: 1"
 "single argument: 2"

The above should clear up that, as Julia is not an interpreter but a compiler, overwriting a method doesn’t update already compiled definitions. And also that, as Julia’s compiler is a JIT compiler, new definitions get normally compiled not before their first actual usage (precompilation is not examined in this post).

  • The invoke() function is slower than a compiled call, so it should be avoided under usual conditions.
  • The eval() function is even slower, for providing more flexibility.
  • The repeated creation of anonymous functions adds its own overhead, but it works well for interactive calling of higher-order functions.
  • Another work-around in various coding scenarios is to put compiled methods inside modules and reload the latter whenever an update is needed, as described in official Workflow Tips.

Final considerations

The previous demonstration covers the most usual cases. But the implications are more complicated and there are edge cases of undefined behavior, not mentioned here. There is an open issue on Julia’s repository which aims to remove compiler implications from future releases of Julia. The intended behavior is that, every time a method gets redefined, it will recursively invalidate all compiled calls to it, so as on their next execution they get recompiled to call the latest definition. However, that won’t change the principles stated here and every programmer in Julia should be aware of them, no matter if coding interactively or not.

7 Likes

akis,

Your thought process in describing the following is very enlightening to more casual users of Julia like myself

  • compilers vs. interpreters
  • functions vs. methods
  • overwriting methods
  • pre compilation and JIT implications

Unless one digs down to a certain level in Julia these concepts can easily go right over your head.

Thanks for your efforts in documentation…Archie

.

1 Like

My understanding is that fixing #265 would change the principles you demonstrate in the last part: redefining functions will for all intents and purposes be equivalent to recompiling the functions which call them.

1 Like

The post mentions four principles, listed in the beginning for convenience. The last two principles are demonstrated near the end:

The rest of the text contains no principles. Would you suggest some rephrasing to make that clear?

My understanding is that what you demonstrate in the part “JIT compiler implications” is not a consequence of any principle, but the infamous issue #265. This is the way things currently are, and it’s good that you address this in a FAQ item because many people run into it, but it will (hopefully) change soon.

The way I see it, once #265 is fixed, the fact that Julia is a JIT compiler should be irrelevant for function redefinitions.

1 Like

Principles don’t change, but I updated the post to state what should be expected from fixing issue #265. Thanks for the suggestion.

A common work-around for the problem of obsolete cached compiled methods (issue 265) is to put volatile method definitions in a module, then reload the module after editing, as described in the workflow section of the manual. You should be reminded to also reload any modules using the changed one by a single warning (rather than the cascade you get when reloading a file of bare functions). This is not quite as robust as workspace(), but suffices for typical iterative development in Julia v0.5 (and can save a lot of time in a large project).

2 Likes

Wiki updated to include three more user suggestions and extra code. Much appreciated.