What To Think When You See That Opening Curly Brace (Scala 3 Video)
During the process of writing this book I had to get away from Scala for a while, and when I came back to it one of the first things I saw was some code that looked like this:
val x = FOO {
// more code here
}
More accurately, the code I saw looked like this:
val x = FOO { (s: State) =>
// more code here
}
Right away I had to ask myself, “Okay, Al, what in the world is FOO
, and how does this code work?” It turns out that depending on exactly what the code looks like, FOO
can be one of several things.
Goal
This style of code is used a lot by experienced Scala developers, so if you don’t know what FOO
is, it’s another potential stumbling block to learning Scala/FP. Therefore, this lesson is about what FOO
can possibly be when you see code that looks like that.
The short answer
The short answer is that FOO
in this example:
val x = FOO {
// more code here
}
can be several things:
- An anonymous class
- A function that takes a by-name parameter
If the code is slightly different and looks like this:
val f = FOO { (a: String) =>
// more code here
}
it can be:
- A class that takes a function parameter
- A function that takes a by-name parameter
I’ll show examples of each of these in this lesson.
1) An anonymous class
In this code:
val mary = new Person {
val name = "mary"
val age = 22
}
I create an instance of Person
using Scala’s “anonymous class” syntax. There’s no way for you to know it by looking at this code, but I could have defined Person
as either a trait, abstract class (or less likely as a class), but for this example I created it as a trait:
trait Person {
def name: String
def age: Int
override def toString = s"name: $name, age: $age"
}
When you paste that trait
into the Scala REPL, and then also paste in this code:
val mary = new Person {
val name = "mary"
val age = 22
}
println(mary)
you’ll see this output:
name: mary, age: 22
Discussion
One thing you know about Person
is that it’s not a case
class. case
classes don’t allow the use of the new
keyword, and they must also have constructor parameters. Both of these things tell you that Person
isn’t a case
class.
It’s also unlikely, though possible, that Person
is defined as a class
. If Person
is a class with name
and age
fields, those fields would require an override
qualifier when mary
is created. The only way Person
can be a class in this example is if name
and age
are not defined in the class.
2) A function that takes a by-name parameter
A second thing that can look like this FOO
example:
val x = FOO {
// more code here
}
is a function that takes a by-name parameter.
In the Scala Cookbook I shared a timer
function that can be used like this:
val (result, time) = timer {
// some long-running block of code here
// ...
}
Here’s a simple example of a block of code you can use with timer
:
val (result, time) = timer {
Thread.sleep(1000)
42
}
When I run that code I get a result like this:
result: 42, time: 1004.819575
As that example shows, the timer
function does three things:
- Accepts a block of code
- Runs the block of code
- Returns the result of that code along with the length of time it took to run
In the Cookbook I showed that the timer
code is defined to accept a by-name parameter named blockOfCode
:
def timer[A](blockOfCode: => A) = {
val startTime = System.nanoTime
val result = blockOfCode
val stopTime = System.nanoTime
val delta = stopTime - startTime
(result, delta/1000000d)
}
For the purposes of this lesson, this timer
code:
val (result, time) = timer {
Thread.sleep(1000)
42
}
shows that a function that takes a by-name parameter is a second way to let people write code that looks like this pattern:
val x = FOO {
// more code here
}
3) A class that takes a function parameter
Another code pattern that you’ll see that looks similar to the first two examples is this:
val f = FOO { (a: String) =>
// more code here
}
In this code, FOO
is either:
- a
case
class that takes a function input parameter (FIP) - a function that takes a FIP
Furthermore, in this example that FIP must be defined to take one input parameter, which is either a String
or a generic type.
As an example, you can create a case
class that meets that criteria like this:
case class StringToInt(run: String => Int)
In this code, run
is declared to be a FIP that transforms a String
to an Int
.
You can create an instance of that class by giving it an anonymous function, like this:
val stringToInt = StringToInt { s: String =>
// algorithm can be as long as needed ...
s.length
}
Now that you’ve created the stringToInt
variable, you can call the run
function on it at any later time:
// prints "7"
println(stringToInt.run("bananas"))
Note 1: Blurring out some code
One key to understand what’s happening with those curly braces is to recognize that the code shown in this image:
(The image is shown in the video.)
is a function literal that takes a single input parameter (a String
), and returns an Int
(because the last line of the code block evaluates to an Int
).
I find that “blurring out” code like that in my mind is a helpful technique to see the function literal when I see curly braces used like this.
Note 2: run
is just a field in the class
Another important point to understand is that because run
is a constructor input parameter:
case class StringToInt(run: String => Int)
it also becomes a public field in the StringToInt
class. It’s a field in that class just like name
is a field in this Person
class:
case class Person(name: String)
Because name
and run
are both fields of their classes, this code:
val p = Person("mary")
println(p.name)
is similar to this code:
val stringToInt = StringToInt { s: String =>
s.length
}
println(stringToInt.run("bananas"))
Note 3: Passing in a real function
If what’s happening in the StringToInt
code isn’t clear, you may find that it’s easier to pass in an instance of a named function as opposed to using an anonymous function. To demonstrate this, here’s the StringToInt
class again:
case class StringToInt(run: String => Int)
Next, rather than giving it an anonymous function, define a “regular” named function that matches run
’s signature, i.e., a function that transforms a String
to an Int
:
def len(s: String) = s.length
Now you can create a new instance of StringToInt
by passing len
into StringToInt
’s constructor:
val stringToInt = StringToInt(len)
This is the same approach as before, except this time I declared len
as a “regular”, named function. Now I can write the last two lines of code as before:
// prints "5"
println(stringToInt.run("scala"))
Note 4: A more complicated example
Here’s a slightly more complicated example of this technique. The following code shows a class that is defined to take a function (a FIP) that has two input parameters of generic type A
, and transforms those parameters into a potentially different type B
:
case class Transform2ParamsTo1Param[A, B](fun: (A, A) => B)
To be clear, here’s the FIP signature by itself:
fun: (A, A) => B
Now I can write code like this to create an instance of Transform2ParamsTo1Param
:
val x = Transform2ParamsTo1Param { (a: String, b: String) =>
a.length + b.length
}
Then I can call fun
on x
like this:
// prints "6"
println(x.fun("foo", "bar"))
Because Transform2ParamsTo1Param
defines its function input parameter fun
to take generic types, I can also write code like this to take two Int
values and return an Int
:
val y = Transform2ParamsTo1Param { (a: Int, b: Int) =>
a + b
}
// prints "3"
println(y.fun(1, 2))
While this might seem a little unusual if you come from an OOP background, this is just another example of a class that takes a function input parameter, i.e., an example of passing functions around.
If code like this isn’t comfortable right now, fear not, it wasn’t comfortable to me initially either. In my experience, I never got comfortable with it until I started using the technique in my own code. (Pro tip: Write a lot of code. As the saying goes, “One learns by doing the thing.”)
4) A function that takes a function input parameter
Getting back to my FOO
examples ... another code pattern that can look like this:
val f = FOO { (a: String) =>
// more code here
}
is when a function takes a function input parameter. I’ll show a variation of that in this section.
The following code looks like the FOO
and Transform2ParamsTo1Param
examples:
val res = s2i("hello") { s: String =>
s.length
}
However, in this case s2i
is a case
class or a function that has two parameter groups. This code tells you that s2i
’s first parameter group must take a String
parameter (hello
in this case), and the second parameter group takes a function that transforms a String
to an Int
(or it may use generic types).
While s2i
can be implemented as either a function or as a case
class, it’s most likely a function. I show both approaches next.
a) Implemented as a function
The function approach will have two parameter groups that look like this:
def s2i (s: String)(f: String => Int) = f(s)
You can verify this by pasting the following code into the Scala REPL:
def s2i (s: String)(f: String => Int) = f(s)
val res = s2i("hello") { s: String =>
s.length
}
println(res)
That last line will print the number 5
. This is a straightforward implementation of s2i
.
b) Implemented as a case
class
While this solution is a little more convoluted, it’s possible that s2i
can be implemented as a case
class, like this:
case class s2i (s: String)(_fun: String => Int) {
def fun = _fun(s)
}
val res = s2i("hello") { s: String =>
s.length
}
println(res.fun)
As the code shows, the primary difference to the consumer of s2i
is that for the class you must call println(res.fun)
to see the result, as opposed to calling println(res)
with the function.
Aside: A practical use of multiple parameter groups
In the Scala Cookbook I shared a practical example of the multiple parameter group approach with this using
code:
using(io.Source.fromFile("example.txt")) { source =>
for (line <- source.getLines) {
println(line)
}
}
using
is cool because it automatically calls the close
method on the resource that’s passed into it in the first parameter group. As you’ll see in the code that follows, it calls close
after the function that’s passed into the second parameter group is run. This is a smart and useful application of this technique.
In this case, using
is defined as a function, like this:
def using[A <: { def close(): Unit }, B](resource: A)(f: A => B): B = {
try {
f(resource)
} finally {
resource.close()
}
}
I first learned about the
using
control structure in the book, Beginning Scala. See the Scala Cookbook for a more thorough discussion of this code. Somewhere in history, someone named this code a “Loan Pattern,” and Joshua Suereth implemented the same technique in his Scala ARM Library.
5) A non-FP possibility: Reassignable Properties
There’s another technique that enables code like this, but a) the technique is rarely used, and b) it would never be used in functional programming. Therefore, I won’t write about it here, but if you’re interested in a technique that lets you write Scala/OOP code like this:
def top = new MainFrame {
title = "First Swing App"
contents = new Button {
text = "Click me"
}
}
see my article, Reassignable variables and properties.
Keys to remember
The following code examples provide a summary of the key points of this lesson.
Anonymous class
This code shows the creation of a Person
instance using the “anonymous class” technique:
val mary = new Person {
val name = "mary"
val age = 22
}
In this specific example, Person
may be defined as a trait or abstract class, or (much less likely) as a class.
A function that has a by-name parameter
In this example, timer
is implemented as a function that declares a by-name parameter, and therefore takes a block of code:
val (result, time) = timer {
// some long-running block of code here
// ...
}
I implemented timer
like this:
def timer[A](blockOfCode: => A) = {
val startTime = System.nanoTime
val result = blockOfCode
val stopTime = System.nanoTime
val delta = stopTime - startTime
(result, delta/1000000d)
}
A case class that takes a function parameter
In this code, StringToInt
is implemented as a case
class that has a function input parameter (FIP), and that FIP apparently transforms a String
to an Int
:
val stringToInt = StringToInt { s: String =>
s.length
}
For that code I defined this case
class:
case class StringToInt(run: String => Int)
(It’s possible that the FIP also uses generic types.)
A case class that takes a FIP that has multiple input parameters
In this example:
val x = Transform2ParamsTo1Param { (a: String, b: String) =>
a.length + b.length
}
Transform2ParamsTo1Param
is defined as a case
class that has a FIP, and that FIP must take two input parameters itself:
case class Transform2ParamsTo1Param[A, B](fun: (A, A) => B)
----------------
A function or class that has multiple parameter groups
I also showed that when you see code like this:
val res = s2i("hello") { s: String =>
s.length
}
s2i
will be a function or case
class that has two parameter groups. It’s much more likely to be a function, which will be implemented like this:
def s2i (s: String)(f: String => Int) = f(s)
It’s possible that it could also be a case
class, which will be written like this:
case class s2i (s: String)(_fun: String => Int) {
def fun = _fun(s)
}
Pattern recognition
I wrote this lesson because over the last few years I’ve begun to look at computer programming as a form of pattern recognition. This was first made clear to me when I was reading a book about Lisp titled, The Little Schemer, where the authors state:
“The goal of this book is to teach the reader to think recursively ... It is our belief that writing programs recursively in Scheme is essentially simple pattern recognition.”
Some patterns are easy for most programmers to understand, and they’re adopted in many programming languages. Other patterns are easy for some programmers to recognize, but not for others. As I noted in this lesson, I stumbled on this pattern when I came back to Scala after a brief absence:
val x = FOO {
// more code here
}
As a result, I tried to understand all of the things that FOO
could possibly be, and I shared that information here.
A final reason I mention this specific pattern is because Scala/FP developers seem to use it very often. Because you’ll see it a lot in the code that follows, I wanted to be sure I covered it here.
Update: All of my new videos are now on
LearnScala.dev