Using Methods As If They Were Functions

As shown in Figure [fig:useMethodsLikeFuncsListMapScaladoc], have you noticed that the Scaladoc for the List class map method clearly shows that it takes a function?

The map method of Scala’s List class.

But despite that, you can somehow pass it a method, and it still works, as shown in this code:

// [1] create a method
scala> def doubleMethod(i: Int) = i * 2
doubleMethod: (i: Int)Int

// [2] supply the method where a function is expected
scala> List(1,2,3).map(doubleMethod)
res0: List[Int] = List(2, 4, 6)

The intent of this lesson is to provide a brief explanation of how this works, and because it works, how it affects your Scala/FP code.

I only cover this topic lightly in this lesson. If you want more details after reading this lesson, see the appendix, “The Differences Between val and def When Creating Functions.”

I think it’s safe to say that most Scala/FP developers prefer to define their “functions” using the def keyword. Although the result isn’t 100% exactly the same as writing a val function, Scala lets you treat both approaches the same, such as when you pass a def method into another function. Therefore, because the syntax of def methods seems to be more comfortable for developers to read and write, most developers use the def approach.

From the previous lessons, you know that this val isEven example is an instance of the Function1 trait:

scala> val isEven = (i: Int) => i % 2 == 0
isEven: Int => Boolean = <function1>

However, when you write the same algorithm using def, the REPL output shows that you have created something else:

scala> def isEven(i: Int) = i % 2 == 0
isEven: (i: Int)Boolean

The REPL output for the two examples is clearly different. This is because a val function is an instance of a Function0 to Function22 trait, but a def method is … well … when you’re not working in the REPL — when you’re writing a real application — it’s a method that needs to be defined inside of a class, object, or trait.

While this reality is “fudged” a little bit inside the REPL, when you are writing Scala code in a real application, that statement is correct: the only way you can define def methods is within a class, object, or trait.

You can easily demonstrate the differences. First, create a file named Methods.scala and put this code in it:

class Methods {
    def sum(a: Int, b: Int) = a + b
}

If you compile that code with scalac:

$ scalac Methods.scala

and then run javap on the resulting Methods.class file you’ll see this output:

$ javap Methods
Compiled from "Methods.scala"
public class Methods {
    public int sum(int, int);
    public Methods();
}

sum is clearly a method in the class named Methods. Conversely, if you create a sum2 function in that same class, like this:

class Methods {
    def sum(a: Int, b: Int) = a + b
    val sum2 = (a: Int, b: Int) => a + b
}

and then compile it with scalac and examine the bytecode again with javap, you’ll see that a val function creates something completely different:

public scala.Function2<java.lang.Object, java.lang.Object, java.lang.Object> sum2();

This lesson explores these differences, particularly from the point of view of using def methods just as though they are functions.

In addition to showing that def methods are different than val functions, the REPL also shows that a method is not a variable that you can pass around. That is, you know that you can assign an Int to a variable name:

scala> val x = 1
x: Int = 1

and then show information about that variable:

scala> x
res0: Int = 1

You can also define a function and assign it to a variable:

scala> val double = (i: Int) => i * 2
double: Int => Int = <function1>

and then show information about it:

scala> double
res1: Int => Int = <function1>

But if you define a method using def:

scala> def triple(i: Int) = i * 3
triple: (i: Int)Int

and then try to show that method’s “variable,” what you’ll actually get is an error:

scala> triple
<console>:12: error: missing arguments for method triple;
follow this method with `_' if you want to treat it as a partially applied function
       triple
       ^

The REPL shows this error because the triple method is not a variable (field name) in the same way that an Int or a function is a variable.

Not yet, anyway. Very shortly I’ll demonstrate how you can manually create a variable from a method.

The reason I show these examples is to demonstrate that until you do something like passing a method into a function, a def method is not the same as a val function. Despite that, we know that somehow you can later treat a method as a function.

Which leads to the next question …

In the appendix, “The Differences Between val and def When Creating Functions,” I show in detail how the Scala compiler lets you use def methods just like val functions. Without repeating too much of that information here, you’ll find that the solution is hinted at in Version 2.9 of The Scala Language Specification:

Eta-expansion converts an expression of method type to an equivalent expression of function type.”

What that means is that when the Scala compiler is given these two lines of code:

def isEven(i: Int) = i % 2 == 0   // define a method
val evens = nums.filter(isEven)   // pass the method into a function

it uses this “Eta Expansion” capability to automatically convert the method isEven into a function — a true Function1 instance — so it can be passed into filter.

This happens automatically during the compilation process, so you generally don’t even have to think about. In fact, I used Scala for almost a year before I thought, “Hey, how is this even working?”

To give you an idea of how Eta Expansion works, let’s use the earlier triple example. I first defined this method:

scala> def triple(i: Int) = i * 3
triple: (i: Int)Int

and then when I tried to show its value in the REPL, I got this error:

scala> triple
<console>:12: error: missing arguments for method triple;
follow this method with `_' if you want to treat it as a 
partially applied function
       triple
       ^

The error message states that you can follow this method with an underscore to treat the method as a partially applied function. That is true, and I demonstrate it in the next lesson. But for this lesson, the important thing to know is that doing this creates a function from your method.

To demonstrate this, go ahead and do what the error message says. Follow the method name with an underscore, and also assign that result to a variable name:

scala> val tripleFn = triple _
tripleFn: Int => Int = <function1>

Notice that the signature of this result is Int => Int. This means that tripleFn is a function that takes one Int as an input parameter, and returns an Int result. The REPL output also shows that tripleFn has a value <function1>, which means that it’s an instance of the Function1 trait. Because it’s now a real function, you can display its value in the REPL:

scala> tripleFn
res0: Int => Int = <function1>

This new function works just like the method works, taking an Int input parameter and returning an Int result:

scala> tripleFn(1)
res0: Int = 3

To confirm that this manually-created function works as advertised, you can pass it into the map method of a List[Int], which really does expect a function, not a method:

// create a List[Int]
scala> val x = List(1,2,3)
x: List[Int] = List(1, 2, 3)

// pass in the `tripleFn` function
scala> x.map(tripleFn)
res1: List[Int] = List(3, 6, 9)

This is a short example of what Eta Expansion does for you behind the scenes, during the compilation process.

To sum up this point, this process happens automatically when you pass a def method into a function that expects a function. It also lets you use def methods just like they are functions in many other situations.

For much more information on this process, see the appendix, “The Differences Between val and def When Creating Functions.”

It’s hard to really “prove” in the REPL that this is what happens because I don’t know of any way to disable Eta Expansion. But if you could disable it, you would find that the method would not work with map, and the function would work with it.

While you can’t prove it in the REPL, you can show what happens behind the scenes with the Scala compiler. If you start with this class:

class EtaExpansionTest {

    def double(i: Int) = i * 2

    def foo = {
        val xs = List(1,2,3)
        xs.map(double)     // pass the `double` method into `map`
    }
}

and then compile it with this command:

$ scalac -Xprint:all Methods.scala 

you’ll see a lot of output, and if you take the time to dig through that output, you’ll be amazed at what the compiler does to the xs.map(double) code by the time it’s done with it. I won’t go into all of that here, but if you’re interested in how this process works, I encourage you to dig into that output.

In the previous lesson I showed that you can define functions and then store them in a Map. Can you do the same thing with methods?

Well, if you define two methods like this:

def double(i: Int) = i * 2
def triple(i: Int) = i * 3

and then try to store them in a Map, like this:

val functions = Map(
    "2x" -> double,
    "3x" -> triple
)

you’ll get the following error messages:

<console>:13: error: missing arguments for method double;
follow this method with `_' if you want to treat it as a 
partially applied function
           "2x" -> double,
                       ^
<console>:14: error: missing arguments for method triple;
follow this method with `_' if you want to treat it as a 
partially applied function
           "3x" -> triple
                       ^

Before this lesson those errors might have been a head-scratcher, but now you know how to solve this problem — how to manually convert the methods into functions by following the method invocations with an underscore:

val functions = Map(
    "2x" -> double _,
    "3x" -> triple _
)

That syntax converts the double and triple methods into functions, and then everything works as shown in the previous lesson, which in this case means that you can get a function back out of the Map and use it:

scala> val dub = functions("2x")
dub: Int => Int = <function1>


scala> dub(3)
res0: Int = 6

The reason I showed everything in this lesson is because most developers prefer the def method syntax over the val function syntax. That is, given the choice to write an algorithm using either approach, developers seem to prefer the def approach, and I believe that’s because the def syntax is easier to read.

Because of this, in the rest of this book I will often write def methods and refer to them as functions. Technically this isn’t accurate, but because (a) methods can be used just like functions, and (b) I don’t want to have to keep writing, “A method that acts like a function,” I will now start using this terminology.

Here’s a summary of what I showed in this lesson:

  • The Scaladoc for collections methods like map and filter show that they take functions as input parameters.

  • Despite that, somehow you can pass methods into them.

  • The reason that works is called “Eta Expansion.”

  • I showed how to manually convert a method to a function (using the partially-applied function approach).

  • As a result of Eta Expansion, you can use def to define methods, and then generally treat them in the same way that you use val functions.

In this lesson I only covered the basics of how a “def method” is like a “val function.” For more details on the differences, see the appendix, “The Differences Between val and def When Creating Functions.”

In this lesson I showed that you can generally treat a def method just like a val function, and not have to worry about the differences between the two. I also showed that if the compiler doesn’t take care of that process for you automatically, you can handle it manually.

In the next lesson you’ll see how to write functions that take other functions as input parameters. With this background, you know that this also means that those functions will be able to take methods as input parameters as well.

results matching ""

    No results matching ""