Connecting...

Ximedes Expanding W855h425@2x

Methods as functions (or, what exactly is “eta expansion”?) by Sinisa Louc

Ximedes Expanding W855h425@2x

In this article by Sinisa Louc he talks about the difference between no-parenthesis and empty-parenthesis methods and the role this difference plays when converting methods to functions. 


'A few days ago I stumbled upon a StackOverflow question regarding this stuff and while writing my answer I noticed that I wrote a respectable chunk of text. Which is a good indicator for a blog post. Oh and by the way, in case you’re not familiar with any of my previous work, this post will be mostly revolving around Scala (although the concepts presented are quite general in functional programming).

First we will see what’s the difference between no-parenthesis and empty-parenthesis methods. We will see what role this difference plays when converting methods to functions. Eta-expansion as a mechanism for transforming methods into functions will be explained, as well as techniques for manual conversion. Finally, partially applied functions and curried functions are explained in the context of the problem at hand.


If it’s empty, it still exists

Let’s say we have these two creatively named methods:

def methodA(s: String) = ???
def methodB(f: () => String) = ???

We want to see which invocations of these methods pass and which fail when we feed them with different values for parameters.

Let’s start from this very simple method f:

def f = "foo"
methodA(f)
methodB(f) // error!
methodA(f()) // error!
methodB(f()) // error!

Since methodA() takes a String, it will have no problems taking f as a parameter. Instead of as a method, our f can basically be viewed as a value whose evaluation is performed every time it’s accessed. If we had a valinstead, it would only be evaluated once (and lazy val would postpone that evaluation until the point of usage). This is what they teach you in every Scala book. If f had a side-effect, that effect would be performed twice in our example, whereas having f as a val would result in side-effect being performed only once — at the point of definition of f. OK, nothing new here.

Onto method B. Since it’s expecting a function (although a weird one, from “nothing” to String), it cannot tolerate our pure String being passed. Compiler’s message is pretty clear: “Type mismatch, expected () ⇒ String, actual: String”.

The other two invocations are completely wrong. When we say f(), compiler thinks we are trying to access a certain char within f (because f is actually a string “foo”) and tells us that methods A and B take a string, not a char. And even if they were taking a char, we would need to provide an index of the char we are trying to fetch from f.

Now, let’s make things more interesting. Let’s add a seemingly trivial and irrelevant addition to our method f (we can call the new version f2):

def f2() = "foo"

We added the empty parenthesis. What implications does this have? Well, first of all, we can actually invoke our method the good old fashioned way — by writing f2(). We couldn’t have done this with f because our f was treated like a string value, but things are a bit different now. Expression f2() behaves exactly as you would expect it to — it invokes the f2 method and results in the string “foo”.

What if we write just f2? Hold on, let’s try to invoke our evaluation methods and see what happens:

methodA(f2())
methodB(f2()) // error!

methodA(f2)
methodB(f2)

OK, first two invocations should be pretty obvious. Invoking f2() results in a string, which is fine for method A, but not fine for method B which takes a function.

However, other two invocations both worked fine! If this is not a surprise to you, well, either you are already familiar with this mechanism in Scala or you are not paying enough attention :) I mean, these two methods have parameters of different types, and yet they can both take the same value. Is our value f2 both a string and a function at the same time? Of course not. Actually, it’s neither. It’s not really a value at all; it’s a method that returns a string. In theory, neither of these two invocations should have succeeded. This is where Scala compiler comes into play.

First invocation, methodA(f2), works because the compiler assumes that when we said f2 what we actually wanted was f2(). It gives a warning “empty-paren method accessed as parameterless”. While having a method f and accessing it as f() results in a completely different thing (invoking apply() on a String, which results in a Char once we provide it with index), having it the other way around — method f2() accessed as f2 — results in the same thing as f2(). This is defined in the style guide, but is not really encouraged, and with a good reason — it can confuse people. To spare yourself the confusion, my advice is to always invoke the method as it is defined. If parameterless, invoke it without parenthesis. If empty-parenthesised, invoke it with empty parenthesis. Former is commonly used for getters of class fields whose value needs to be re-calculated every time (such as currentAccountBalance) and latter for functions with side-effects (such as println()).


Eta-conversions

That was the first invocation, methodA(f2). Now, about that second one, methodB(f2). This is where things get a bit more interesting. What Scala compiler just performed for us is called eta-expansion. There are two directions for this “eta operation”, as you will see soon. In literature, direction of eta-expansion is also sometimes called eta-abstraction, opposite direction is called eta-reduction, and they are both referred to under common term eta-conversion.

The idea behind eta-expansion is pretty simple. Having a function f(x), we normally refer to the function itself as f. For example, imagine we need to pass it to another function; we would pass simply f. Now, if instead of passing f we passed x ⇒ f(x), nothing would change, would it? Take the square function for example. We give it a 4, it returns a 16. Having this function is completely the same as having function x ⇒ sqr(x). We give it a 4, it returns a 16. We just “wrapped” our sqr function with another layer, resulting in another function. We can do this indefinitely:

val sqr = (x: Int) => x * x
val sqr2 = (x: Int) => sqr(x)
val sqr3 = (x: Int) => sqr2(x)
val sqr3_expandedVersion = (x: Int) => ((x: Int) => sqr(x))(x)

println(sqr(4))
println(sqr2(4))
println(sqr3(4))
println(sqr3_expandedVersion(4))

They all print 16. Eta-reduction is simply going in other direction — from sqr3towards sqr. By the way, don’t mind the “expanded version” in the code; it looks a bit complicated, but if you take a closer look you’ll see that it is merely injecting the definition of sqr2 into sqr3 so that it only depends on sqr. It’s just showing how the function grows more and more, but retains the same functionality.

Eta-expansion is what compiler does “behind the scenes” when it notices that you need a function, but are provided a method. Here’s a simplified version of what it does:

// given a method:
def someMethod() = ???
// it's easy to convert it to a function using eta-expansion:
val someFunction = () => someMethod()

// if there are parameters, it's still the same principle:
def someMethod(x: Int, y: String) = ???
val someFunction = (x: Int, y: String) => someMethod(x, y)

Simple, but powerful.


Partially applied functions

There are times when compiler is not expecting a function so it will not automatically convert the method into a function. For example, let’s revisit our method f2() and try to define a corresponding function with same functionality:

def f2() = "foo"
val f2fun = f2 // f2fun is a string, not a function!

What happened? If you remember that stupid confusing automatic conversion from earlier, you will recall that it is allowed to invoke a parameterless method (that is, an empty-parenthesised method) without the parenthesis. OK, we agreed we would avoid doing this deliberately in our code in order to avoid confusion, but what do we do about the existing convention in the compiler that interprets our f2 as f2()? How do we turn the method f2() into a function value f2?

Well, there are two things we can do:

  • explicitly declare the type of value to be a function
  • treat the method as a partially applied function
def f2() = "foo"
val f2fun1 = f2               // f2fun is a string, not a function
val f2fun2: () => String = f2 // however, this works!
val f2fun3 = f2 _             // this too!

Explicitly declaring the type of value is a nice way to go. You are telling the compiler “cut it out with your automatic parameterless invocation mumbo-jumbo and turn this method into a function”.

But treating the method as a partially applied function is, in my opinion, an even more elegant solution. If you’re not familiar with partially applied functions (and a closely related technique, currying), then it would be a wise thing to consult some online sources.

Shortly put, partially applying a function is a way to… well, partially apply a function :) Seriously though, if we have a function that takes three parameters, x, y and z, we can only apply the first one and as a result get a function of two parameters. Or we can apply the first two and get a function of only one parameter. For example, having a function that takes two integers and adds them, we can apply only the first one, e.g. 42, and as a result we will get a function that adds the input number to 42:

val add: (Int, Int) => Int = (a: Int, b: Int) => a + b
val add42: Int => Int = add(42, _)
println(add42(8)) // 50

We “fixed” the first parameter and only allowed the second one to be specified by the user. Note that if you don’t explicitly define add42 to be of type Int => Int, you will need to explicitly define the type of the unused parameter: add(42, _: Int).

Currying is a similar principle. Main idea with currying is to separate a function of n parameters into n functions of one parameter. This is how all functions with 2-or-more parameters are treated in Haskell; you cannot have a function of more than one parameter. If you need a function of, say, three parameters, what you do is you define a function of one parameter returning a function of one parameter that returns a function of one parameter. Function f(a, b, c) in Haskell must be defined as a function returning a function which returns a function, and it’s invoked as f(a)(b)(c).

OK, small digression, back to Scala. Currying works just the same in Scala, but is not mandatory. Note that if we curry a function, there’s no need for the underscore thingy; with partially applied functions it’s needed because otherwise compiler will rightfully complain that add() takes two parameters, but with currying we are simply providing only the first parameter, which is completely valid:

val add: Int => Int => Int = (a: Int) => (b: Int) => a + b

val add42: Int => Int = add(42)
println(add42(8)) // 50

val add42and8: Int = add(42)(8)
println(add42and8) // 50

One more thing to keep in mind (I’m risking to lose you here, but here goes) — if you have a method with empty parenthesis and you want to turn it into a function, either of these two techniques will work. However, if you have a method without parenthesis, then explicit type won’t work:

def foo() = "foo"
val foo2 = foo _
val foo3: () => String = foo

def bar = "bar"
val bar2 = bar _
val bar3: () => String = bar // error

Don’t get discouraged; there’s no need to remember these compiler quirks. Important things that I’m hoping you will get out of this article are listed below.


Summary

Alright, here’s a short recap of the information presented in this article:

  • Methods are not the same as functions: functions are values just like integers, strings or other objects and they can be passed around, returned, kept inside collections etc. Methods on the other hand are not values; they don’t have a type and cannot exist on their own (they are an attribute of a structure in which they are defined, e.g. a class, object or trait). Note that methods must be defined using the def keyword, while functions can be defined like any other value — by using def, val or lazy val, in which case the keyword determines whether the value is evaluated every time, only once or only once but at the point of usage.
  • There is a difference between methods with no parenthesis and methods with empty parenthesis: former are basically values, but re-evaluated upon every access, while latter are methods as we know them. My strong advice is to invoke them as they are defined (if there are empty parens in definition, put them in invocation too; this way you don’t have to remember what happens if f is invoked as f() and vice versa).
  • Eta-expansion is a simple technique for wrapping functions into an extra layer while preserving identical functionality (e.g. from sqr to x ⇒ sqr(x)) and it’s performed by the compiler to create functions out of methods.
  • When automatic eta-expansion fails, you can use two techniques to covert a method into a function: explicitly declare type of value to be a function or treat the method as a partially applied function by putting an underscore after method name, which means all of its parameters are transferred to function parameters.
That’s all for now. As usual, I’m looking forward to your comments and feedback on sinisalouc@gmail.com. Also feel free to hit me up on Twitter.'

This article was written by Sinisa Louc and posted originally on Medium.com