Preface
Before you start reading this book, it is important to think about the distinction between programming languages and programming language features. I believe that developers benefit from being able to rely on an extensive set of programming language features, and that a solid understanding of these featuresin any languagewill help them be productive in a variety of programming languages, present or future.
The world of programming languages is varied, and continues to evolve all the time. As a developer, you are expected to adapt, and to repeatedly transfer your programming skills from one language to another. Learning new programming languages is made easier by mastering a set of core features that todays languages often share, and that many of tomorrows languages are likely to use as well.
Programming language features are illustrated in this book with numerous code examples, primarily in Scala (for reasons that are detailed later). The concepts, however, are relevantwith various degreesto other popular languages like Java, C++
, Kotlin, Python, C#
, Swift, Rust, Go, JavaScript, and whatever languages might pop up in the future to support strong typing as well as functional and/or concurrent programming.
As an illustration of the distinction between languages and features, consider the following programming task:
Shift every number from a given list by a random amount between 10 and 10. Return a list of shifted numbers, omitting all values that are not positive.
A Java programmer might implement the desired function as follows:
Java
List randShift(List nums, Random rand) { var shiftedNums = new java.util.ArrayList(nums.size()); for (int num : nums) { int shifted = num + rand.nextInt(-10, 11); if (shifted > 0) shiftedNums.add(shifted); } return shiftedNums;}
A Python programmer might write this instead:
Python
def rand_shift(nums, rand): shifted_nums = [] for num in nums: shifted = num + rand.randrange(-10, 11) if shifted > 0: shifted_nums.append(shifted) return shifted_nums
Although they are written in two different languages, both functions follow a similar strategy: Create a new empty list to hold the shifted numbers, shift each original number by a random amount, and add the new values to the result list only when they are positive. For all intents and purposes, the two programs are the same.
Other programmers might choose to approach the problem differently. Here is one possible Java variant:
Java
List randShift(List nums, Random rand) { return nums.stream() .map(num -> num + rand.nextInt(-10, 11)) .filter(shifted -> shifted > 0) .toList();}
The details of this implementation are not important for nowit relies on functional programming concepts that will be discussed in . What matters is that the code is noticeably different from the previous Java implementation.
You can write a similar functional variant in Python:
Python
def rand_shift(nums, rand): return list(filter( lambda shifted: shifted > 0, map( lambda num: num + rand.randrange(-10, 11), nums)))
This implementation is arguably closer to the second Java variant than it is to the first Python program.
These four programs demonstrate two different ways to solve the original problem. They contrast an imperative implementationin Java or in Pythonwith a functional implementationagain, in Java or in Python. What fundamentally distinguishes the programs is not the languagesJava versus Pythonbut the features being used imperative versus functional. The programming language features used in the imperative variant (assignment statements, loops) and in the functional variant (higher-order functions, lambda expressions) exist independently from Java and Python; indeed, they are available in many programming languages.
I am not saying that programming languages dont matter. We all know that, for a given task, some languages are a better fit than others. But I want to emphasize core features and concepts that extend across languages, even when they appear under a different syntax. For instance, an experienced Python programmer is more likely to write the example functional program in this way:
def rand_shift(nums, rand): return [shifted for shifted in (num + rand.randrange(-10, 11) for num in nums) if shifted > 0]
This code looks different from the earlier Python codeand the details are again unimportant. Notice that functions map
and filter
are nowhere to be seen. Conceptually, though, this is the same program, but written using a specific Python syntax known as list comprehension, instead of map
and filter
.
The important concept to understand here is the use of map
and filter
(and more generally higher-order functions, of which they are an example), not list comprehension. You benefit from this understanding in two ways. First, more languages support higher-order functions than have a comprehension syntax. If you are programming in Java, for instance, you will have to write map
and filter
explicitly (at least for now). Second, if you ever face a language that uses a somewhat unusual syntax, as Python does with list comprehension, it will be easier to recognize what is going on once you realize that it is just a variation of a concept you already understand.
The preceding code examples illustrate a contrast between a program written in plain imperative style, and one that leverages the functional programming features available in many languages. I can make a similar argument with concurrent programming. Languages (and libraries) have evolved, and there is no reason to write todays concurrent programs the way we did 20 years ago. As a somewhat extreme example, travel back not quite 20 years to 2004, the days of Java 1.4, and consider the following problem:
Given two tasks that each produce a string, invoke both tasks in parallel, and return the first string that is produced.
Assume a type StringComputation
with a string-producing method compute
. In Java 1.4, the problem can be solved as follows (do not try to understand the code; it is rather long, and the details are unimportant):
Java
String firstOf( final StringComputation comp1, final StringComputation comp2) throws InterruptedException { class Result { private String value = null; public synchronized void setValue(String str) { if (value == null) { value = str; notifyAll(); } } public synchronized String getValue() throws InterruptedException { while (value == null ) wait(); return value; } } final Result result = new Result(); Runnable task1 = new Runnable() { public void run() { result.setValue(comp1.compute()); } }; Runnable task2 = new Runnable() { public void run() { result.setValue(comp2.compute()); } }; new Thread(task1).start(); new Thread(task2).start(); return result.getValue();}
This implementation uses features with which you may not be familiar (but which are covered in Here are the important points to notice:
One reason such old-fashioned features are still covered in this book is that I believe they help us understand the richer and fancier constructs that we should be using in practice. The other reason is that the concurrent programming landscape is still evolving, and recent developments, such as virtual threads in the Java Virtual Machine, have the potential to make these older concepts relevant again.