Functions and Polymorphic Methods

So far, the methods we've dealt with have had strictly defined types. Parameters for these methods were specified in terms of Int, Double, String, etc. Now we are going to look at what it means for a method to be polymorphic, and what functions are in Scala.

If you have any issues understanding the material in this unit, please ask questions in the discord channel I've set up for teaching Scala, or you can discuss on scala-users.

Please remember to run the provided examples via the Scala console unless told otherwise. If you've forgotten how to launch and/or shut down the Scala console, please refer here.

Functions

We've used methods extensively in the previous units. As you may remember, methods are always bound to the instance of a type, are invoked with the . operator and providing parameters (if needed) to the () operator, and allow you to define re-useable aliases for sections of code. Methods are not the only way to define re-useable code though; there exists expressions in Scala that create re-useable sections of code that are not bound to an instance of a type, and which can be passed around your codebase. These expressions are called functions. Let's look at the syntax for a function expression:

(<name>: <type>, <name>: <type>) => <expression>

This looks in some ways like a method definition. The first part in parentheses () is where parameters for the function are put. You can have 0 or more parameters. After the parameters section ends, you have a =>, indicating the transition to the function body, which is the expression in this diagram. The expression can be whatever expression you like, code block, if-expression, for-expression, you name it. The result of this syntax is also an expression! It will return data, but with a type you have not seen before...

scala> val add = (a: Int, b: Int) => a + b
val add: (Int, Int) => Int = Lambda$1285/0x000000080107d000@ff21443

The type of a function expression takes the form of:

(type, type) => type

You might also notice that we don't have a nice output for the function in our console. Unlike 0 or hello, Lambda$1285/0x000000080107d000@ff21443 is fairly unintelligible. This is because a function is not data in the traditional sense, it's an executable section of code that can be passed around, but representing it with text is... not really feasible.

A function, once used as the definition of a value, can be used just like a method:

scala> add(4,2)
val res0: Int = 6

You can vary the parameters needed for the function as much as you want too:

scala> val sayHello = () => println("hello")
val sayHello: () => Unit = ...

scala> sayHello()
hello

scala> val addThree = (a: Int, b: Int, c: Int) => a + b + c
val addThree: (Int, Int, Int) => Int = ...

scala> addThree(1,2,3)
val res0: Int = 6

They can also be passed to methods, on account of them being expressions:

scala> def flexibleMethod(a: Int, b: Int, fn: (Int, Int) => Int): Int = fn(a,b)
def flexibleMethod(a: Int, b: Int, fn: (Int, Int) => Int): Int

scala> flexibleMethod(4,5,(a: Int, b: Int) => a * b)
val res0: Int = 20

scala> flexibleMethod(6,100, (a: Int, b: Int) => b - a)
val res1: Int = 94

One thing to note about functions, and something that makes them extra nice, is that if the type of the function is already specified, you do not need to specify it again when writing out the function expression:

scala> flexibleMethod(4,5,(a,b) => a * b)
val res0: Int = 20

scala> //we didn't need to write the type for a and b, because it's defined in flexible method for the fn input

scala> val addFive: (Int) => Int = (a) => a + 5
val addFive: Int => Int = ...

scala> //same for addFive. We knew the type, so the type didn't need to be specified in the expression

This leads us to the shortcuts that can be taken when writing a function...

Shortcuts

There are a number of shortcuts that can be taken when writing functions. If Scala knows the type of the function, and it almost certainly will if you're passing it to a method, then you can choose not to specify the types for the parameters, changing our function expression syntax to:

(<name>, <name>) => <expression>

If you have only one input parameter and no need to specify it's type, you can further simplify the expression by dropping the parentheses ():

<name> => <expression>

In the case of a single parameter, the type of the function can be expressed similarly; Int => Int instead of (Int) => Int.

Methods to Functions

Methods in Scala can automatically be turned into functions if you just pass their name into something expecting a function:

scala> def mul(a: Int, b: Int) = a * b
def mul(a: Int, b: Int): Int 

scala> flexibleMethod(4, 5, mul)
val res0: Int = 20

This lets you use methods directly instead of defining a wrapper function for the method like:

scala> flexibleMethod(4,5,(a,b) => mul(a,b))
val res0: Int = 20

Practice

  1. Write a function that divides two Ints
  2. Write a function that takes an Int and produces the String "I'm years old!"
  3. What are the minimum number of parameters that a function can have?
  4. Do you always have to specify types when writing a function expression?
  5. Do you always have to write parentheses around the parameters of a function?
  6. Is a function an expression or a statement?

Polymorphic methods

So far, we've only used methods with pretty specific types. The take method on List only takes an Int parameter for example. However, sometimes a function needs to be able to deal with many different kinds of types with one definition. A method that has been written to do this is called a polymorphic method.

The definition of a polymorphic method is slightly different than the methods we're used to:

def <name>[<typeParameterName>, <typeParameterName>](<name>: <type>) = <expression>

The syntax for a polymorphic method has a new section where type parameter names can be input, between [] square brackets. A polymorphic function will have one or more type parameter names between the [] square brackets. Let's look at how this works in practice:

scala> def meth[A](a: Int) = a

Here, a single type parameter name was provided A. This creates a new type named A that can be used in the rest of the method as you please:

scala> def meth[A](a: A): A = a

But what exactly is A? It's the first type input between the square brackets when you call the method:

scala> meth[Int](4)
val res0: Int = 4

scala> meth[Double](4.0)
val res1: Double = 4.0

scala> meth[String](4.0)
-- [E007] Type Mismatch Error: -------------------------------------------------
1 |meth[String](4.0)
  |             ^^^
  |             Found:    (4.0d : Double)
  |             Required: String

Since you have A also associated with the data input of the method, you can choose to not pass in a type parameter, and let Scala work out what A should be instead:

scala> meth(4)
val res0: Int = 4

scala> meth(4.0)
val res1: Double = 4.0

scala> meth("hello")
val res2: String = hello

As you can see, our method can take all sorts of data when you make it polymorphic. There's literally no restriction on the kind of data that can be passed in to meth.

scala> meth(()=> println("hello"))
val res0: () => Unit = ...

However making this method do something useful is quite difficult. You have to prove to Scala the code you write will work for all data, and this is incredibly difficult...

scala> def meth[A](a: A) = a * a
-- [E008] Not Found Error: -----------------------------------------------------
1 |def meth[A](a: A) = a * a
  |                    ^^^
  |value * is not a member of A, but could be made available as an extension method.

Programming these kinds of methods will be the subject of a later unit, but you should be aware of what it means when a method is polymorphic and how that changes its signature.

Type parameter restrictions

Type parameters can be restricted in order to make programming a polymorphic method easier. When you restrict the type parameter, Scala is better able to know what your type parameter is, and what you can and can't do with it.

There are two main major restrictions that are used frequently with type parameters: <: and >:. <: is used to create a restriction that says the type parameter is the child of another type. For example A <: Int would say that A has to be Int, or a child of Int. >: has the opposite effect, it says that A is a parent of another type. In essence, A >: Int says that A either has to be Int or its parent.

What exactly a parent or child of a type means is something we'll go over in a later unit, and the types we're dealing with at the moment don't have meaningful parents or children, so when you see these restrictions when defining a type parameter, we'll think of them in a simplified way:

  • A <: Int means A is Int
  • A >: Int means A is Int

You can think of these rules as like Bohr's model of an atom, they are imperfect simplifications of the truth that will be helpful to you at these beginner stages, but will need to be abandoned as you dive deeper into Scala. Keep that in mind.

Helpful polymorphic methods of Lists

List is a polymorphic type. That means that List has a named type parameter just like a polymorphic method. The name of that type parameter is A, and a number of methods on List instances use this type parameter, or an additional one that exists only for that method.

A few extremely valuable polymorphic methods on List are:

  • List.empty
  • map
  • filter
  • sortBy
  • reduce
  • flatMap
  • contains

List.empty

List.empty is a polymorphic method bound to List that gives back an empty List of the type specified. It's signature is def empty[A]: List[A] and invoking looks like the following:

scala> List.empty[Int]
val res0: List[Int] = List()

The type parameter can be dropped if the type of empty list is already defined:

scala> val emptyIntList: List[Int] = List.empty
val emptyIntList: List[Int] = List()

map

map is one of the most useful methods on List, and it actually exists on a lot of other types too. The signature of map is def map[B](f: A => B): List[B] where A is the type parameter of the List instance. So if you invoke map on a List[Int] its signature will change a bit to def map[B](f: Int => B): List[B].

map takes a function from the user, and applies that function to each element of the List it's invoked from, creating a new List in the process. This method lets you modify and update all of the contents of your list in place. For example, if you wanted to add one to every number in a List[Int], you'd use map:

scala> val nums = List.range(0,3)
val nums: List[Int] = List(0,1,2)

scala> nums.map(a => a + 1)
val res0: List[Int] = List(1,2,3)

flatMap

flatMap works very similarly to map, except its signature is a bit different: def flatMap[B](f: A => List[B]): List[B]. flatMap expects the function you give it to give it a List, and then flattens the List down.

A comparison of results makes the difference between these methods much clearer:

scala> nums.map(a => List(a))
val res0: List[List[Int]] = List(List(0), List(1), List(2))

scala> nums.flatMap(a => List(a))
val res1: List[Int] = List(0,1,2)

res1 here is just a simple List[Int], because the Lists returned from each element got flattened into one unified List. res0 came from map, which does not flatten, so its type is List[List[Int]].

The flattening behavior of flatMap can be used to filter Lists:

scala> nums.flatMap(a => if a > 0 then List(a) else List.empty)
val res0: List[Int] = List(1,2)

However, the method filter is better for that purpose.

It can also be used to grow Lists:

scala> nums.flatMap(a => List.range(0,a+2))
val res0: List[Int] = List(0,1,0,1,2,0,1,2,3)

contains

contains is a method on List instances with the signature def contains[A1 >: A](elem: A1): Boolean. Don't worry about the >: A restriction on A1 right now. You should just imagine that A1 was written as A, making the signature of contains for List[Int] def contains(elem: Int): Boolean.

contains takes an expression and checks if the data returned by that expression is stored in the List. Let's see how it works:

scala> val nums = List.range(0,3)
val nums: List[Int] = List(0,1,2)

scala> nums.contains(1)
val res0: Boolean = true

scala> nums.contains(-1)
val res1: Boolean = false

As you can see, contains takes pretty much any type of data, and checks to see if it's in your List. If it is, it will return true, otherwise it will return false.

filter

filter is a method on instances of List that can be used to create a new List with only some of the elements of the original. Its signature is def filter(p: A => Boolean): List[A], where A is the type parameter of the List. That means for List[Int] the signature of filter is def filter(p: Int => Boolean): List[Int].

filter takes a function and processes every element in the List with it. If the function returns true, the element will stay in the new List, otherwise it will be missing. Let's look at an example:

scala> val nums = List.range(0,3)
val nums: List[Int] = List(0,1,2)

scala> nums.filter(a => a > 1)
val res0: List[Int] = List(2)

scala> nums.filter(a => a <= 1)
val res1: List[Int] = List(0,1)

sortBy

sortBy is related to sorted, in that it sorts your List. However, sorted sorts only based on the data in the List, while sortBy can sort by the properties of the data in your List. Its signature is roughly def sortBy[B](f: A => B): List[A], which means for List[Int] its signature changes to roughly def sortBy[B](f: Int => B): List[A].

sortBy takes a function that transforms the data in your List into the data you want to perform the sort with. This transformation is thrown away after the sorting is done, and you have a sorted List with your original data in the order you wanted.

Let's look at a useful application of sortBy: sorting Strings by number of letters.

scala> val favoriteShows = List("Ozark", "Stranger Things", "Better Call Saul", "Squid Game")
val favoriteShows: List[String] = List(Ozark, Stranger Things, Better Call Saul, Squid Game)

scala> favoriteShows.sortBy(str => str.size)
val res0: List[String] = List(Ozark, Squid Game, Stranger Things, Better Call Saul)

scala> favoriteShows.sorted
val res0: List[String] = List(Better Call Saul, Ozark, Squid Game, Stranger Things)

Using sortBy we were able to sort the names of shows based on shortest name to longest name, while sorted would sort the shows by dictionary order (lexicographic order), putting Better Call Saul first because it starts with a B.

The True signature of sortBy

Earlier, I said that the signature of sortBy is roughly def sortBy[B](f: A => B): List[A]. That's because the true signature is def sortBy[B](f: A => B)(implicit ord: Ordering[B]): List[A]. This implicit section is new to us, and it's more complicated than the other things we're discussing here. What it does is ask for proof that the type you get back from the function has information on how to order it. All of the type's we've dealt with so far have this info, but some in the future may not. For example, there is no information on how to sort a List[Any].

If you get an error like

1 |List(1,2,3).sortBy(_ => (): Any)
  |                                ^
  |No implicit Ordering defined for Any..

Then your issue is that there is no order information available for the type in question. We'll learn how to provide that information later, just be aware of that restriction.

reduce

reduce is an extremely helpful method on instances of List, allowing you to turn List[A] into A. Basically, if you have a List[Int], you would be able to use reduce to make it into a single Int (assuming the List is not empty). The sum method we mentioned in a previous unit is a reduction; it adds all the elements of a List together to get a single Int. Similarly, one can use the reduce method to calculate a the sum of a List fairly easily.

The signature of reduce is def reduce[B >: A](op: (B, B) => B): B, as I've stated before, at this point you can imagine that the type parameter restriction B >: A just means B, so you can imagine the signature as def reduce[B](op: (B,B) => B): B.

The function passed to reduce is run on pairs of elements from the List that you invoked reduce on. If the List has only one element, that element is just returned by reduce, otherwise it chains invocations of the function op to get a result. Lets look at what that might look like:

scala> val op = (a: Int, b: Int) => a + b
val op: (Int, Int) => Int = ...

scala> List(1,2,3).reduce(op)
val res0: Int = 6

As you can see, by passing in a simple addition function, we reduced List(1,2,3) to 6. Lets take a look at how that answer is reached behind the scenes:

List(1,2).reduce(op) ==
op(1,2) ==
3

List(1,2,3).reduce(op) == 
op(op(1,2),3) ==
op(3,3) == 
6

List(1,2,3,4).reduce(op) ==
op(op(op(1,2),3),4) ==
op(op(3,3),4) ==
op(6,4) ==
10

Once you understand how reduce chains your function call on all of the list's elements, it becomes much simpler to understand.

One thing to note about reduce is that it will fail if the List in question is empty:

scala> List.empty[Int].reduce((a,b) => a + b)
java.lang.UnsupportedOperationException: empty.reduceLeft
  at scala.collection.IterableOnceOps.reduceLeft(IterableOnce.scala:727)
  at scala.collection.IterableOnceOps.reduceLeft$(IterableOnce.scala:724)
  at scala.collection.AbstractIterable.reduceLeft(Iterable.scala:919)
  at scala.collection.IterableOnceOps.reduce(IterableOnce.scala:698)
  at scala.collection.IterableOnceOps.reduce$(IterableOnce.scala:698)
  at scala.collection.AbstractIterable.reduce(Iterable.scala:919)
  ... 26 elided

Practice

  1. Add one to all elements of a List without using a for-expression
  2. Reduce a List of numbers by multiplying the numbers together
  3. Filter a List of Strings to only include the Strings that are bigger than 3 letters long.
  4. Reduce a List of numbers to the largest number in the list.
  5. distinct exists for Strings, just like Lists, and it produces a String with no repeat characters. Define a method named uniqueLetters that uses sortBy, map, distinct and size to sort a List[String] by the number of unique letters in a String

Answers

Functions

  1. Write a function that divides two Ints
  2. Write a function that takes an Int and produces the String "I'm years old!"
  3. What are the minimum number of parameters that a function can have?
  4. Do you always have to specify types when writing a function expression?
  5. Do you always have to write parentheses around the parameters of a function?
  6. Is a function an expression or a statement?

Helpful polymorphic methods of Lists

  1. Add one to all elements of a List without using a for-expression List(1,2,3).map(a => a + 1)
  2. Reduce a List of numbers by multiplying the numbers together List(1,2,3).reduce((a,b) => a * b)
  3. Filter a List of Strings to only include the Strings that are bigger than 3 letters long. List("hello", "a", "b", "world").filter(a => a.size > 3)
  4. Reduce a List of numbers to the largest number in the list. List(1,2,3).reduce((a,b) => if a > b then a else b)
  5. distinct exists for Strings, just like Lists, and it produces a String with no repeat characters. Define a method named uniqueLetters that uses sortBy, distinct and size to sort a List[String] by the number of unique letters in a String
    def uniqueLetters(strings: List[String]) = 
       strings.sortBy(a => a.distinct.size)
    

Did you find this article valuable?

Support Mark Hammons by becoming a sponsor. Any amount is appreciated!