/ EXERCISE, PROGRAMMING, STYLE

Exercises in Programming Style with higher-order functions

This week, the chapter is named "Kick forward". The style’s constraint is not to call a function directly, but to pass it to the next function as a parameter, to be called later.

This is the 5th post in the Exercises in Programming Style focus series. Other posts include:

  1. Introducing Exercises in Programming Style
  2. Exercises in Programming Style, stacking things up
  3. Exercises in Programming Style, Kwisatz Haderach-style
  4. Exercises in Programming Style, recursion
  5. Exercises in Programming Style with higher-order functions (this post)
  6. Composing Exercises in Programming Style

Higher-order functions

I’m not sure whether the concept of higher-order functions has its roots in Functional Programming. However, the concept itself is easy to grasp: a higher-order function can be used as a function parameter - or as a return value - of another function. This is available in Java since version 8, but it’s available in Scala and Kotlin since their beginnings.

In Java, they are mainly used in conjunction with streams:

Stream.of("A", "B", "C", "D", "E")
        .map(new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s.toLowerCase();
            }
        });
    }

Stream.of("A", "B", "C", "D", "E")
      .map(s -> s.toLowerCase());

Stream.of("A", "B", "C", "D", "E")
      .map(String::toLowerCase);

All three previous lines do exactly the same, because lambdas are mapped to anonymous classes of functional interfaces.

In Kotlin, this is pretty similar, but for 2 points:

  1. No Kotlin would ever write an anonymous class, precisely because higher-order functions are available since the beginning
  2. The default parameter name of a lambda is it, just as in Groovy
Stream.of("A", "B", "C", "D", "E")
      .map { it.toLowerCase() }


Stream.of("A", "B", "C", "D", "E")
      .map(String::toLowerCase)

Calling the function

The next logical step once we pass a function to another function is to calling it. In Java, one needs to know the type of the higher-order function. For example, to execute a Function, one needs to call apply() and pass a parameter with the expected type; to execute a Predicate, the function is called test(), etc.

Function<String,String> toLowerCase = String::toLowerCase;
toLowerCase.apply("A");
Predicate<String> isLowerCase = s -> s.equals(s.toLowerCase());
isLowerCase.test("A");

Kotlin is more consistent, as there’s a single function called call(), defined on the KCallable interface.

fun isLowerCase(string: String) = string == string.toLowerCase()
val callable: Boolean = ::isLowerCase.call("A")

However, call() accepts any number of arguments of type Any?. It’s up to the caller to pass the correct number and type for parameters. The following compiles but will fail at runtime:

val callable: KCallable<String> = ::isLowerCase.call("A", 2, Any())

To benefit from the compiler, one can use the KFunctionX type with X being the number of parameters that the function accepts. That type provides an invoke() function with the correct number of arguments and their types.

val ok: KFunction<String, Boolean> = ::isLowerCase.invoke("A")             (1)
val ko: KFunction<String, Boolean> = ::isLowerCase.invoke("A", 2, Any())   (2)
1Compiles
2Doesn’t compile

Icing on the cake, invoke() is an operator function which operator is…​ nothing, so that the following syntax is also valid:

(::isLowerCase)("A")

Applying the theory

In the exercise, the entry-point function calls a function that calls a function, etc. down to 7 nested levels. If we change the KCallable to KFunction, the top function has this very "interesting" signature:

fun readFile(
    filename: String,
    function: KFunction2<
            List<String>,
            KFunction2<
                    List<String>,
                    KFunction2<
                            List<String>,
                            KFunction2<
                                    List<String>,
                                    KFunction2<
                                            List<String>,
                                            KFunction2<
                                                    List<Pair<String, Int>>,
                                                    KFunction1<List<Pair<String, Int>>, Map<String, Int>>,
                                                    Map<String, Int>>,
                                            Map<String, Int>>,
                                    Map<String, Int>>,
                            Map<String, Int>>,
                    Map<String, Int>>,
            Map<String, Int>>
): Map<String, Int>

While I believe that static typing has a lot of benefits, this of course doesn’t make the code easier to read. To cope with that, Kotlin offers type aliases. The following snippet makes use of them to improve the situation:

typealias WordFrequency = Pair<String, Int>
typealias WordFrequencies = List<WordFrequency>
typealias Lines = List<String>
typealias Words = List<String>
typealias MapFunction = KFunction1<WordFrequencies, Map<String, Int>>
typealias SortFunction = KFunction2<WordFrequencies, MapFunction, Map<String, Int>>
typealias FrequencyFunction = KFunction2<Words, SortFunction, Map<String, Int>>
typealias RemoveFunction = KFunction2<Lines, FrequencyFunction, Map<String, Int>>
typealias ScanFunction = KFunction2<Lines, RemoveFunction, Map<String, Int>>
typealias NormalizeFunction = KFunction2<Lines, ScanFunction, Map<String, Int>>
typealias ReadFunction = KFunction2<Lines, NormalizeFunction, Map<String, Int>>

fun readFile(filename: String, function: ReadFunction) = function(read(filename), ::normalize)

Here’s an excerpt of the final code:

fun run(filename: String) = (::readFile)(filename, ::filterChars)                               (1)

fun readFile(filename: String, function: ReadFunction) = function(read(filename), ::normalize)  (2)

fun filterChars(lines: List<String>, function: NormalizeFunction): Map<String, Int> {           (3)
    val pattern = "\\W|_".toRegex()
    val filtered = lines
        .map { it.replace(pattern, " ") }
    return function(filtered, ::scan)                                                           (3)
}
1Uses the function reference in conjunction with the shortcut operator for invoke(). The second parameter is the function reference on the next function to call
2The second parameter’s type uses an alias, to make the code easier to read. It calls the invoke() method of the passed function parameter, and pass the next function to call as a reference
3The rest follows the same logic: use a higher-order function as the last parameter, and call the shortcut version of invoke() to continue the chain

Conclusion

As for the introduction, while types are helpful to lower the probability of some bugs, they also decrease readability. Fortunately, Kotlin type aliases are a boon to cope with that.

call() and invoke() are two options to consider when calling functions with reflection. Each of them comes with pros and cons.

The complete source code for this post can be found on Github.
Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Currently working for Exoscale. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More