Can anyone give a definitive answer on how by-name parameters => T
and Function0
parameters () => T
are transformed into one another by the Scala compiler? I know they are not the same, but the difference is very subtle as they can be interchangeably used in many scenarios.
Example: if I define
def someFunction: Int = 2
def f(x: => Int): Unit = println(x)
then I can successfully call
f(2)
f(someFunction)
How is () => Int
an acceptable replacement for => Int
?
More generally, is () => T
a universally acceptable replacement for a by-name => T
parameter?
Also, please correct me if I'm wrong on the following reasoning: => T
is never an acceptable replacement for () => T
because the first is a value type (T
), the other is a function type. That is, if I have def f(x: () => Int)
, I'll never be able to pass an Int
, or a lazy Int
(doesn't even make sense anyway as there are no lazy types).
Alright, here's the full breakdown.
def value: Int = ???
def method(): Int = ???
def f1(f: () => Int) = ???
def f2(f: => Int) = ???
f1(value) // fails
f1(method) // works
f2(value) // works
f2(method) // works with a warning "empty-paren method accessed as parameterless"
f1(value)
This one fails because f1
is expecting a Unit => Int function, but is given an Int value.
f1(method)
This one works because f1
is expecting a function, and is given a method. Here's the difference: method is not a value in Scala; it cannot exist on its own and is an attribute of the context it's defined in (class, trait, object etc.). Function is a value; it can be kept in a collection, taken as argument in another function, returned from a function etc. When the compiler is expecting a function and is given a method, it performs eta expansion. Given a function e.g. f: (a: Int, b: Int) => Int
, eta expansion is a process of creation of another layer around that while preserving the signature, so it becomes (a: Int, b: Int) => f(a, b)
. This technique is useful because we can turn methods into function. Given some method def f(a: Int): Int = ???
, we can perform eta-expansion to create a function of type Int => Int out of it: (a: Int) => f(a)
. I wrote a blog post about this some time ago if you're interested.
f2(value)
Works without surprises, but pay attention to the fact that the passed value is accessed every time it's used in the function body.
f2(method)
Works, but with a warning that we are invoking a method that is defined with empty parenthesis by using no parenthesis. Good practice is to use methods without parenthesis (e.g. f
) when they simply represent a value, but one that is recalculated every time it's accessed, e.g. numberOfUpvotes
, and to use methods with empty parenthesis (e.g. f()
) when some kind of side-effect is performed and hence the method isn't idempotent, e.g. createSnapshot()
(again, this should not be present in purely functional code).
Word of advice: don't encumber your mind with what's a replacement for what. Don't use replacements. If something needs a function, provide it a function. If it needs a value, provide a value. If a method is defined without parens, invoke it without parens. If it has parens, invoke it with parens.
If you need to go from method to function and compiler is expecting a function, eta-expansion will happen automatically. If it's not expecting a function, you need to do it manually.
def f(): Int = ???
val a = f // no function context; a is a string
val b: () => Int = f // b is a function Unit => Int
val c = f2 _ // c is a function Unit => Int
Last case is a partially applied function. I feel like I'm going too broad now so I will stop here. I hope this helped.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With