Functional Programming in Scala - Day 3
Read from section 2.5.2 to end of chapter 2.
Summary
Section 2.5.2 Calling HOFs with anonymous functions
This section was just going over what anonymous functions are in Scala. An example of an anonymous function would be (x: Int) => x == 9, which is a function that takes in an integer and checks to see if it is 9. Of course, much like everything in Scala, these also have types as well. When checked in the REPL, an anonymous function like (x: Int, y: Int) => x == y would have a type of (Int, Int) => Boolean = <function2>.
I think the most interesting part of this chapter though, was to learn that this way of writing anonymous functions, is just syntactic sugar! For example, the following 2 implementations are equivalent.
1
2
3
4
5
val checkEqualsAnonymous = (x: Int, y: Int) => x == y
val checkEqualsExplicit = new Function2[Int, Int, Boolean] {
def apply(x: Int, y: Int) = x == y
}
In other words, when a function literal is defined, what Scala actually does is it creates an object with a method called apply. Scala has a special rule such that objects with the apply function can be called as if they were themselves a method. Taking the function checkEqualsAnonymous as an example, the following invocations of it are equivalent as well.
1
2
val result1 = checkEqualsAnonymous(1, 2)
val result2 = checkEqualsAnonymous.apply(1, 2)
Note that Function2 is just a trait (i.e. what Scala calls interfaces) provided by the Scala standard library. There are also others like Function1 and Function3. Since functions in general are just ordinary objects, they are called first-class values.
Section 2.6: Following types to implementations
This last section was heavier on exercises and explanations on an introduction to thinking about the types first then deriving the implementations. The first example that the book looks at is the def partial1[A, B, C](a: A, f: (A, B) => C): B => C function. This is an example of a function that only has one possible implementation that would compile.
1
2
3
def partial1[A, B, C](a: A, f: (A, B) => C): B => C = {
b => f(a, b)
}
The trick is to start from the return type - B => C - of the function and work from there. The return type is B => C or a “function that takes in an object of type B as an input, and returns an object of type C”. Then just by following that, the function can be implemented.
The following exercises for currying, un-currying and compose can also be done in a similar way.
Exercise 2.3: Currying
1
2
def curry[A, B, C](f: (A, B) => C): A => (B => C) =
a => b => f(a, b)
This is an interesting method because this is effectively a way to transform the function f so that it can operate on partial input later down the line. For example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def makeCurry(a: Ingredient, b: Ingredient): Curry = { ... }
def finishMakingCurry(numberOfPeople: Integer, addToCurry: Ingredient => Curry) = {
val variableIngredient = IngredientBasedOnTotalPeople(numberOfPeople)
addToCurry(variableIngredient)
}
def main(args: Array[String]): Unit = {
val makePartialCurry = curry(makeCurry);
val fixedIngredient = Ingredient("carrot")
finishMakingCurry(makePartialCurry(fixedIngredient));
}
I don’t quite understand the point of the curry function though, because why not just gather all of the inputs before the function f is called? I think there must be some other building blocks where this curry function would be the only function that would fit in nicely. I hope that this will be explained down the line.
On a side note, at first I thought that the term “curry” came from the food curry, since the food is usually made up by mixing a bunch of stuff together. In a similar way, I thought you were “mixing” up the functions by throwing in one ingredient into the function to produce a function that’s almost there but only one ingredient away. Turns out that this is just named after the mathematician Haskell Curry. According to the book, Moses Schoenfinkel also independently discovered it, but Schoenfinkelization is not a very catchy name I suppose.
Exercise 2.4: Un-currying
1
2
def uncurry[A, B, C](f: A => B => C): (A, B) => C =
(a, b) => f(a)(b)
Exercise 2.5: Compose
1
2
def compose[A, B, C](f: B => C, g: A => B): A => C =
a => f(g(a))
The last compose function is such a common thing to do that Scala’s standard library provides compose as a method on Function1. You can simply write f compose g or g andThen f to apply the function g and then the function f.
I think that section 2.6 introduced some pretty interesting concepts in the Functional Programming world. That being said, the concepts are so abstract right now, I wouldn’t know how to actually use these in a real world code base. I’m really looking forward to the design aspects of a larger scale codebase so that I can see how these building blocks fit together.
Notes
Section 2.5.2 Calling HOFs with anonymous functions
- The syntax
(x: Int) => x == 9is a function literal or an annonymous function. - If you were to check the type of this using the REPL, then you get something like the following
1 2
scala> (x: Int, y: Int) => x == y res0: (Int, int) => Boolean = <function2>
- This
<function2>notation indicates that the value ofres0is a function that takes two arguments.
- When a function literal is defined, what is actually happening in Scala is that an object with a method called
applyis being done. - For example, when a function literal like
(a, b) => a < bis defined, this is really syntactic sugar for the following object creation.1 2 3
val lessThan = new Function2[Int, Int, Boolean] { def apply(a: Int, b: Int) = a < b }
lessThanhas a typeFunction2[Int, Int, Boolean], which would usually be written as(Int, Int) => Boolean.- The
Function2interface has anapplymethod, so when thelessThanfunction is called withlessThan(10,20), it’s really syntactic sugar for calling itsapplymethod, likelessThan.apply(10,20). - In this way, we can see one of Scala’s special rules, where objects that have an
applymethod can be called as if they were themselves a method.- Again, since
lessThanhas theapplymethod, you can call it likelessThan(10, 20)(treating the object itself as a method) rather than doinglessThan.apply(10,20)(which is equally valid syntax).
- Again, since
- Now
Function2is just an ordinary trait (interface) provided by the Scala standard library.- There are also other variants like
Function1andFunction2. - Since functions are just ordinary Scala objects, they’re called first-class values. In the book, depending on context, a “function” can refer to either such a first-class function or a method.
- There are also other variants like
Section 2.6: Following types to implementations
- By implementing polymorphic functions, the universe of possible implementations for that polymorphic function significantly decreases.
- It took me a while to understand this statement, because the universe of possible implmentations for the logic that you want cannot decrease just by implementing a polymorphic function.
- As in, just because the polymorphic function is implemented, it’s not as if the other implementations disappear from the world completely as if they cannot and never existed.
- However, the book is talking about the function written polymorphically. As in, when the signature for the polymorphic function is written, then there can be only a few ways that it can be implemented such that the program compiles. (e.g. the
partial1[A,B,C](a: A, f: (A, B) => C): B => Cexample function) - The original sentence is “As you might have seen when writing
isSorted, the universe of possible implementations is significantly reduced when implementing a polymorphic function.”- I think this is a poorly written sentence because when you read “the universe of possible implementations” the subject is unclear.
- The way that this is written makes it seem like the subject is “of the functions that would achieve what you want to do”. Logically, this is always going to be infinite and will never decrease, so it doesn’t make sense.
- A better way to write this sentence would be “As you might have seen when writing
isSorted, when thinking about the implementation for a polymorphic function, the universe of possible implementations for that function is significantly reduced in comparison to the monomorphic version(s) of that function.”
- I think this is a poorly written sentence because when you read “the universe of possible implementations” the subject is unclear.
- It took me a while to understand this statement, because the universe of possible implmentations for the logic that you want cannot decrease just by implementing a polymorphic function.
- Next, we will take a look at
def partial1[A, B, C](a: A, f: (A, B) => C): B => Cwhich is an example of a polymorphic function that can only be implemented in one way.- The implementation will look like the following.
1 2 3
def partial1[A, B, C](a: A, f: (A, B) => C): B => C = { b => f(a, b) }
- This is essentially a function (
partial1) that takes a function (f) as an input, and returns that same input function (f) with one of its arguments (a) fixed. - Note that the return value could also be implemented as
(b: B) => f(a, b), but the type ofbcan be omitted because Scala can infer that from the return type (B => C) of the functionpartial1.
Exercise 2.3, 2.4, 2.5: Currying, Un-currying, and Compose
- These were fun little exercises to do. Pretty easy to reason about if you just started out at the return type.
1 2 3 4 5 6 7 8
def curry[A, B, C](f: (A, B) => C): A => (B => C) = a => b => f(a, b) def uncurry[A, B, C](f: A => B => C): (A, B) => C = (a, b) => f(a)(b) def compose[A, B, C](f: B => C, g: A => B): A => C = a => f(g(a))
- The last
composefunction is such a common thing to do that Scala’s standard library providescomposeas a method onFunction1.- You can simply write
f compose gorg andThen fto apply the functiongand then the functionf.
- You can simply write