Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the performance characteristics between curried, partially applied, and 'normal' functions in Scala?

I took a look here: Scala currying vs partially applied functions, but the answers there answer more about the functional and semantic differences between currying, partial application, and normal functions in Scala.

I'm interested in learning whether there are any performance considerations between these different techniques that can be used on functions, namely...

If we use the performance of a normal function as a base:

def add3(a: Int, b: Int, c: Int) = a + b + c
add3(1, 2, 3)

And then compare to:

// First
(add3 _).curried(1)(2)(3)

// Second
val add2 = add3(1, _: Int, _: Int)
val add1 = add2(2, _: Int)
add1(3)

// Third
def add3(a: Int)(b: Int)(c: Int) = a + b + c
add3(1)(2)(3)

What are the things I might want to be mindful of if I've identified some poorly performing code (either in terms of speed or memory usage) and I see a lot of currying or partial application happening in said code segment?

In Haskell I would, for instance, be looking at how many thunks are being generated and hanging around. I'd presume Scala uses a similar sort of method for passing around partially applied and curried functions, and the details of how Scala handles these things would be valuable to know.

like image 285
josiah Avatar asked Apr 13 '16 15:04

josiah


People also ask

What is currying in Scala with example?

Currying Functions in Scala with Examples. Currying in Scala is simply a technique or a process of transforming a function. This function takes multiple arguments into a function that takes single argument. It is applied widely in multiple functional languages. Syntax.

What are partially applied functions in Scala?

An important concept to discuss here is partially applied functions. When we apply a function to some of its arguments, we have applied it partially. This returns a partially-applied function that we can use later. Let’s take an example. Here, the underscore is a placeholder for a missing value. Let’s begin with Scala Currying Function example.

What can you do with PAFs in Scala?

As a fun example of some things you can do with PAFs, the “partially-applied functions” section of the Scala Exercises website demonstrates that you can create curried functions from “normal” Scala functions. For instance, you can start with a “normal” one-parameter group function like this:

What is underscore in Scala?

Let’s take an example. Here, the underscore is a placeholder for a missing value. Let’s begin with Scala Currying Function example. Remember the other piece of syntax we looked at ?


1 Answers

I used my scala-to-java tool to convert your snippets to java, and here are results:

  1. Simple function call is transpiled to similar function call:

    def add3(a: Int, b: Int, c: Int) = a + b + c
    add3(1, 2, 3)
    

    Result:

    public final class _$$anon$1 {
        private int add3(final int a, final int b, final int c) {
            return a + b + c;
        }
    
        {
            this.add3(1, 2, 3);
        }
    }
    
  2. "First" snippet: (add3 _).curried(1)(2)(3) is transpiled essentially into into this:

    ((Function3<Integer, Object, Object, Object>)new _$$anon$1$$anonfun._$$anon$1$$anonfun$1(this)).curried().apply(BoxesRunTime.boxToInteger(1)).apply(BoxesRunTime.boxToInteger(2)).apply$mcII$sp(3);
    

    I omitted boilerplate. What happens here is wrapper function class is created around add3, then method curried of that class is called, then apply is called three times on the result of the previous application. You can check the sources of curried here. What it does, it just returns several high-order functions (function that return function).

    So, comparing to "case 0", several extra function wrappers are created for single curried call.

  3. "Second":

    // Second
    val add2 = add3(1, _: Int, _: Int)
    val add1 = add2(2, _: Int)
    add1(3)
    

    I will not provide the whole page of transpiled output. Check it here if you want. Essentially what happens is for add2 and add1 functional wrapper class is generated with single method that takes corresponding number of arguments. So, when you call add1(3), it calls add2 which calls add3. Generated wrappers are effectively singletons, so the overhead is limited to several function calls.

  4. Third:

    def add3(a: Int)(b: Int)(c: Int) = a + b + c
    add3(1)(2)(3)
    

    Is transpiled to simple function call again:

    public final class _$$anon$1 {
        private int add3(final int a, final int b, final int c) {
            return a + b + c;
        }
    
        {
            this.add3(1, 2, 3);
        }
    }
    

    but if you try to use it in curried way, for example, like this val p = add3(1) _, a functional wrapper class will be additionally generated.

like image 102
Aivean Avatar answered Nov 15 '22 06:11

Aivean