How to use higher-order functions in Julia properly?

As was pointed out by Steven G. Johnson, higher-order functions are great part of Julia language (topic for another big discusion) and often much more suitable to solve problems that metaprogramming tools, which are for many method of choice.

I want to learn more about them and how I should use them in Julia, but I new to this topic and I don’t know where to start.

As a side note, searching of both Documentation and YouTube about metaprogramming in Julia give you few answers immediately, when my searching about higher-order functions in documentation seems to found mostly pages with words like “higher dimensions” (I didn’t check all results). And one YT video from 2017. This is unfortunate, since as Tamas Papp points out, metaprogramming is often “first trick in the bag”, when should be the last, left for advanced users.

1 Like

There are a few words and examples about this here: Functions · The Julia Language

I don’t know of a document that lists all there is to know about the use of functions as values.

The main points I see are:

  • A function can be used like a regular value: assigned to a variable, passed as argument, etc.

  • Such function values can be created dynamically as anonymous functions, e.g. f = x->2x, or more verbose: f = function(x) 2x end. The function can refer to (capture) variables available in the scope where it’s defined.

  • Passing functions as values is most common when working with map, filter and similar functions, as in many languages.

  • Julia has some less common uses of functions as values including

    • Composition operations that return a new function:

      julia> f = cos ∘ sin ∘ (x->2x)
      #62 (generic function with 1 method)
      
      julia> f(pi/2)
      1.0
      
    • Negation operator returning a new function:

      julia> notletter = !isletter
      #64 (generic function with 1 method)
      
      julia> notletter('2')
      true
      
    • Partial applications returning new functions:

      julia> under10 = <(10)
      (::Base.Fix2{typeof(<),Int64}) (generic function with 1 method)
      
      julia> under10(3)
      true
      
      julia> hasG = contains('G')
      (::Base.Fix2{typeof(contains),Char}) (generic function with 1 method)
      
      julia> hasG("hello")
      false
      

      A more practical example would be filter(contains('G'), myStringArray).

  • All functions have a type, which can be used for dispatch. As with any value, you can get the type with typeof:

    julia> f = x->2x
    #1 (generic function with 1 method)
    
    julia> g(func::typeof(f)) = "That's f!"
    g (generic function with 1 method)
    
    julia> g(f)
    "That's f!"
    
    julia> g(sin)
    ERROR: MethodError: no method matching g(::typeof(sin))
    

    An interesting example of dispatch on functions is : which is actually a function of type Colon. You can use it like this:

    julia> h(::Colon) = "called with :"
    h (generic function with 1 method)
    
    julia> h(:)
    "called with :"
    

    This is how syntax like a[:] works:

    julia> a = [1,2,3];
    
    julia> @less a[:]
    

    shows that the relevant getindex method is defined as:

    function getindex(A::Array, c::Colon)
    ...
    
23 Likes

That’s a very nice writeup! Please consider making a PR to the docs (I am happy to review).

7 Likes

I’m not sure what would be a good place in the documentation. Do you think it would belong in its own section on Functions · The Julia Language , although it overlaps with #man-anonymous-functions and #Function-composition-and-piping?

I would put it after sections which build up prerequities (similarly to Design Patterns with Parametric Methods), so practically after Function composition and piping.

2 Likes

OK I’ve put it in my Julia backlog which I hope to clear around Christmas. :slight_smile:

4 Likes