1. Overview

In this tutorial, we’re going to look at partially-applied functions, function currying, partial functions, and functions in general.

We’ll begin by understanding functions in Scala and how the Scala compiler treats different kinds of functions.

2. Functions

In Scala, functions are first-class values in the sense that they can be:

  • passed to and returned from functions
  • put into containers and assigned to variables
  • typed in a way similar to ordinary values
  • constructed locally and within an expression

Functions are majorly defined by using a def or a val, as explained in this article. def‘s evaluate each time while val‘s evaluate once:

val dummyFunctionVal : Int => Int = {
  println(new Date())
  num => num
}

def dummyFunctionDef(number : Int) : Int = {
  println(new Date())
  number
}

println(dummyFunctionVal(10)) // prints out the date and then the result :10
println(dummyFunctionVal(10)) // doesn't print out the date and just returns 10

println(dummyFunctionDef(10)) // prints out the date and then the result :10
println(dummyFunctionDef(10)) // prints out the date and then the result :10

When we define a function with def, we call it a “method”. These need to be defined within an object or class. They also have an implicit reference to the class instance in which we defined it. They are technically not values and do not have a type.

On the other hand, when we define a function with val, we call it a “function value”. These are specialized forms of the built-in Scala class FunctionN, where N can range from 0 to 22.

These functions have some extra methods defined on them, such as andThen and compose, as seen in the official docs.

Let’s see the difference when we try and compose function values or methods:

val getNameLengthVal : String =>  Int = name => name.length
val multiplyByTwoVal : Int => Int = num => num * 2

getNameLengthVal.andThen(multiplyByTwoVal) // compiles

def getNameLengthDef(name : String) : Int = name.length
def multiplyByTwoDef(number : Int) : Int = number * 2

getNameLengthDef.andThen(multiplyByTwoDef) // doesn't compile

Here’s an example of converting a method to a function value:

val getNameLengthDefFnValue = getNameLengthDef _
val multiplyByTwoDefFnValue = multiplyByTwoDef _ 

getNameLengthDefFnValue.andThen(multiplyByTwoDefFnValue) // compiles

The conversion process, also known as eta expansion, is a technique in Scala that converts methods into function values.

3. Partially-Applied Functions

Partially applied functions, as its name implies, describes a function with its arguments partially applied. It gives us the ability to create more specific functions from general functions as well as avoid repetitive code.

What this means is that, when applying a function, we don’t pass in arguments for all of the parameters defined by the function, but only for some of them, leaving the remaining ones blank.

What Scala returns is a new function whose parameter list only contains those parameters from the original function that were left blank in their respective order.

It’s important to understand that partially applying a function always results in a new function. The original function is only evaluated when all the arguments are fully applied.

When we partially supply arguments to a function, Scala returns a function which then accepts as arguments the remaining parameters.

3.1. Avoiding Repetitive Code

Let’s take an example where we’ll need to build a URL given the protocol and the domain. We’ll simplify this example just to concatenate the protocol and the URL.

Let’s first try a naive and repetitive approach:

def createUrl(protocol: String, domain : String) : String = {
    s"$protocol$domain"
}

val baeldung = createUrl("https://","www.baeldung.com")
val facebook = createUrl("https://","www.facebook.com")
val twitter = createUrl("https://","www.twitter.com")
val google = createUrl("https://","www.google.com")

Looking at this code, we can easily see that the DRY principle is being violated. We’re continually supplying the same protocol https:// all throughout the code.

We can partially apply the protocol, which should give us a new function that takes only one argument, which is the domain. The function returned as a result of the partial application of the protocol argument is a function value.

It can then be treated as any other value in the program in that it can be assigned to variables or passed in and out of functions.

To partially apply a function in Scala, we simply supply the arguments we want to and use an underscore _ with the appropriate type for arguments not supplied:

def createUrl(protocol: String, domain : String) : String = {
  s"$protocol$domain" 
} 
val withHttpsProtocol: String => String = createUrl("https://", _: String)
val withHttpProtocol: String => String = createUrl("http://", _: String)
val withFtpProtocol: String => String = createUrl("ftp://", _: String)

In this example, our original method createUrl can be thought of as having a type that takes two strings and returns a string which is written like this (String, String) => String. By partially applying a single argument, we’re left with one unsupplied argument, hence the return type String => String. 

We can now use our new function value withHttpsProtocol to construct URLs without having to repetitively supply the protocol https://:

val baeldung = withHttpsProtocol("www.baeldung.com")
val facebook = withHttpsProtocol("www.facebook.com")
val twitter = withHttpsProtocol("www.twitter.com")
val google = withHttpsProtocol("www.google.com")

It’s obvious how we avoided repetition by the use of partially applied functions.

3.2. Partially-Applied Function Variations

Let’s take a more practical example. In this example, we’ll write a function that formats HTML:

def htmlPrinter(tag: String, value: String, attributes: Map[String, String]): String = {
  s"<$tag${attributes.map { case (k, v) => s"""$k="$v"""" }.mkString(" ", " ", "")}>$value</$tag>"
}
htmlPrinter("div","Big Animal",Map("size" -> "34","show" -> "true")) 
// prints <div size="34" show="true">Big Animal</div> 

We designed a function that can construct an HTML tag with attributes. If we wanted to write a couple of HTML elements with the same attribute, we could do:

val div1 = htmlPrinter("div", "1", Map("size" -> "34")) 
val div2 = htmlPrinter("div", "2", Map("size" -> "34"))
val div3 = htmlPrinter("div", "3", Map("size" -> "34")) 
val div4 = htmlPrinter("div", "4", Map("size" -> "34"))

Or, we could also avoid repetition by partially applying functions:

val withDiv: (String, Map[String, String]) => String = htmlPrinter("div", _:String , _: Map[String,String])
val withDivAndAttr: String => String = withDiv(_:String, Map("size" -> "34"))
val withDivAndAttr2: String => String = htmlPrinter("div", _: String, Map("size" -> "34"))

The reason we can partially apply withDiv is that it’s technically still a function.

By partially applying some arguments, we were able to reduce a lot of boiler-plate. We can also see that we were able to partially apply single or multiple arguments, as well as different variations in which we can partially apply the argument

When all arguments are fully supplied, only then us the function body in a partially applied function evaluated

4. Function Currying

A curried function informally describes a function that takes multiple argument groups one at a time.

Given a function with three arguments groups, the curried version will take one argument group and return a function that takes the next argument group, which returns a function that takes the third argument group.

This is similar to partially applied functions. We supplied some arguments and got back a function that accepts the remaining elements.

The concepts of currying and partially-applied functions are similar, but they aren’t technically the same.

Function currying enables us to transform a function (A, B, C, D) into (A => (B => (C => D))) .

In Scala, we define curried functions by the use of multiple parameter groups rather than a single one.

def dummyFunction(a: Int, b: Int)(c: Int, d: Int)(e: Int, f: Int): Int = {
   a + b + c + d + e + f
 }
  
val first: (Int, Int) => (Int, Int) => Int = dummyFunction(1,2)
val second: (Int, Int) => Int = first(3,4)
val third : Int =  second(5,6)

We’ve defined a function with multiple parameter groups, and as we supply each parameter group, we get back a new function that accepts the next parameter group.

We may say to ourselves, isn’t this overkill? We could simply use a single parameter group and save ourselves the stress of adding more brackets.

Looking at the Scala doc for Future, we see that its apply method which is used to construct a Future uses a curried function where the second parameter group is a single argument of an implicit execution context.

A future with a single parameter group will not be as aesthetically pleasing as seen in this example:

val executionContext: ExecutionContext = ExecutionContext.fromExecutorService(Executors.newCachedThreadPool())
// invalid!
val future1 = Future({ 
  Thread.sleep(1000) 
  2 }, executionContext)

val future2 = Future {
  Thread.sleep(1000) 
  2 
}(executionContext)

We can see the difference between future1 and future2, one is curried while the other is not, which results in the braces within a bracket as the function body within the braces is not the only argument to the apply method.

Another area where curried functions are beneficial is in type inference when using generics.

Let’s imagine that the foreach method didn’t exist on sequences, and we attempt to write something similar:

def withListItems[T](list : List[T],f : T => Unit) : Unit = {
   list match { 
     case Nil => () 
     case h :: t => 
       f(h)
       withListItems(t,f) 
   } 
 } 
withListItems(List(1,2,3,4), number => println(number + 2)) // does not compile

We can see that the last expression does not compile as expected. We expect the compiler to automatically infer that our type T is an Integer and has the method +. The Scala compiler cannot infer the type this way.

Let’s compare the same function, but curried and see how our compiler reacts:

def withListItems[T](list : List[T])(f : T => Unit) : Unit = {
    list match {
      case Nil => ()
      case h :: t =>
        f(h)
        withListItems(t)(f)
    }
  }

withListItems(List(1,2,3,4))(number => println(number + 2)) // compiles

We see that the last expression compiles as the type can be inferred.

The compiler can’t use the inference of one parameter to help resolve another unless those parameters are in different parameter groups.

Type inference flows left-to-right from parameter group to parameter group**. Type inference within a parameter group happens at the same time for all parameters.**

In the first example, the compiler couldn’t infer the function argument number as Integer because, as explained above, the Scala compiler cannot use a single parameter within a parameter group to infer the type of another parameter within the same parameter group.

In the second example, the first parameter group evaluates and enables the compiler to infer the variable number as an Integer, thus identifying the type T as an Integer. That information was then used by the compiler to infer that the function argument number is, in fact, an Integer.

5. Partial Functions

Partially-applied functions define a function that will only work for a subset of possible input values. It allows us to make informed decisions based on input parameters.

We define partial functions in Scala by creating instances of the PartialFunction trait:

val isWorkingAge : PartialFunction[Int,String] = new PartialFunction[Int,String] {
    override def isDefinedAt(x: Int): Boolean = if(x >= 18 && x <= 60) true else false

    override def apply(v1: Int): String = {
      if (isDefinedAt(v1)) {
        s"You are $v1 years old within working age"
      }else{
        s"You are $v1 years old and not within working age"
      }
    }
  }

isWorkingAge(12) // You are 12 years old and not within working age isWorkingAge(22) // You are 22 years old within working age

The partial function trait provides two methods, isDefinedAt, which defines our acceptable subset of values, and an apply method where the logic of our function goes. It enables us to isolate conditional statements from the actual logic of our code.

We often use partial functions even without knowing. Whenever we do a pattern match, the case statements we make are partial functions.

Our previous example was verbose. Let’s try and improve it with case statements:

val isWorkingAge : PartialFunction[Int,String] = {
    case age if age >= 18 && age <= 60 => s"You are $age years old and within working age"
    case other => s"You are $other years old and not within working age"
}

We may ask, where’s the isDefinedAt method, well, we cleverly did that by defining the only cases or single case in our example and then provided a catch-all clause through the other case statement. This represents the subset of values our function doesn’t cater for.

Similar to function values, partial fractions also offer composability. It provides some methods such as orElse, andThen amongst others that help to achieve comparability.

5.1. Composing Partial Functions

Let’s modify our example to use multiple partial functions chained together:

val isWorkingAge : PartialFunction[Int,String] = {
  case age if age >= 18 && age <= 60 => s"You are $age years old within working age"
}

val isYoung : PartialFunction[Int,String] = {
  case age if age < 18 => s"You are less than 18, and not within the working age"
}

val isOld : PartialFunction[Int,String] = {
  case age if age > 60 => s"You are greater than 60 and not within the working age"
}

val verdict = isWorkingAge orElse isYoung orElse isOld

verdict(12) // You are less than 18, and not within the working age
verdict(22) // You are 22 years old within working age
verdict(70) // You are greater than 60 and not within the working age

We were able to compose several partial functions together using the orElse method, which passes the supplied argument to the next partial function if the current partial function cannot handle the argument or is not defined for that particular argument.

In technical terms, orElse, when used by a partial fraction, passes the supplied argument to the next partial fraction if its isDefinedAt method returns false for that particular argument.

Unlike other programming languages, Scala extensively uses partial functions. It’s used in the collect method of Sequences and also in Actors.

6. Conclusion

In this article, we’ve taken a look at different types of functions and their usage. We’ve also seen how we could properly utilize these types of functions to write cleaner, elegant, and less tedious code.

Code snippets and examples can be found over on GitHub.


« 上一篇: Scala 字符串简介
» 下一篇: Scala 中的范围