Evolution of Programming Languages

Functional Programming and Multi-Paradigm Languages


Learning Objectives

  • You know the core ideas of functional programming and how they differ from imperative programming.
  • You know of programming languages that have influenced the development of functional programming.

Functional Programming Fundamentals

Functional programming (FP) — that we’ve had quite a lot of fun with — treats computation as the evaluation of mathematical functions while avoiding changing state or mutable data. At the core, FP emphasizes the use of pure functions — functions that, given the same input, always return the same output and produce no side effects. Side effects include modifying external state, such as global variables, or performing I/O operations.

Pure Functions and Immutability

A pure function does not modify any external state. For example, consider the following Python function add. The function always returns the sum of two numbers and does not rely on or modify any external state:

def add(a, b):
  return a + b

result = add(3, 4)
print("Result of pure add:", result)

In contrast, an impure function like add_and_update below might depend on or modify external state, which can make the behavior of functions unpredictable:

global_sum = 0

def add_and_update(a):
  global global_sum
  global_sum += a
  return global_sum

print("First call:", add_and_update(3))
print("Second call:", add_and_update(4))

Higher-Order Functions and Function Composition

FP uses first-class functions, meaning that functions can be passed as arguments, returned from other functions, or assigned to variables. This allows for powerful abstractions. For example, Python’s built-in map and filter functions allow you to process collections in a functional style:

numbers = [1, 2, 3, 4, 5]

squared = list(map(lambda x: x ** 2, numbers))
print("Squared numbers:", squared)

evens = list(filter(lambda x: x % 2 == 0, numbers))
print("Even numbers:", evens)

Recursion and Pattern Matching

Many functional languages also favor recursion over traditional looping constructs. Again in Python, for example, recursion would look as follows:

def factorial(n):
  if n <= 1:
    return 1
  else:
    return n * factorial(n - 1)

print(factorial(5))

Pattern matching is another key feature of functional languages, allowing developers to match data against specific patterns. As we’ve seen earlier in the course, we could use pattern matching — say in Gleam — to implement the above factorial function:

import gleam/int
import gleam/io

pub fn factorial(n) {
  case n {
    0 -> 1
    _ -> n * factorial(n - 1)
  }
}

pub fn main() {
  let result = factorial(5)
  io.println(int.to_string(result))  // Output: 120
}

By focusing on immutability and stateless computation, functional programming also makes concurrent and parallel programming safer. As an example, Erlang — a functional language that Gleam builds on top of — is known for its robust support for concurrency and fault tolerance in distributed systems.

Loading Exercise...

Landmark Languages

The evolution of functional programming is closely tied to landmark languages that have influenced how developers think about computation.

The first functional programming language, Lisp, introduced in the late 1950s, popularized many functional programming concepts such as treating code as data, recursion, and dynamic typing. A classic Lisp example is a recursive factorial function:

(defun factorial (n)
  (if (<= n 1)
      1
      (* n (factorial (- n 1)))))

(print (factorial 5))  ; Output: 120

Lisp syntax is based on symbolic expressions, or s-expressions, which represent both code and data in the same format. Everything in Lisp is a list, where the first element is a function and the rest are arguments. As an example, the expression (factorial 5) calls the factorial function with the argument 5.

Scheme, a dialect of Lisp, introduced lexical scoping and first-class continuations, which influenced many modern languages. The above factorial function could be written in Scheme as follows:

(define (factorial n)
  (if (<= n 1)
      1
      (* n (factorial (- n 1)))))

(display (factorial 5))  ; Output: 120

Scheme has also been widely used as an introductory programming language in academia, although it has been transitioned away from in favor of languages like Python and Java that are more widely used in industry.

Scheme was also used as an introductory programming language at Aalto University for quite some time.

ML is another influential functional language that introduced strong static typing, type inference, and algebraic data types. Here’s the factorial function in ML:

fun factorial 0 = 1
  | factorial n = n * factorial (n - 1)

print (Int.toString (factorial 5))  (* Output: 120 *)

Haskell, on the other hand, is one of the first purely functional languages. Haskell’s approach to handling side effects through constructs like monads has significantly influenced modern functional programming. Here’s another version of the factorial function in Haskell:

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

main :: IO ()
main = print (factorial 5)  -- Output: 120

Multi-Paradigm Languages

While functional programming languages have dedicated communities, many modern programming languages blend functional programming techniques with imperative and object-oriented styles. These multi-paradigm languages offer developers the flexibility to choose the best approach for a given problem.

Languages like JavaScript have evolved to include functional programming features such as lambda expressions, where many array operations are also handled in a functional style:

const numbers = [1, 2, 3, 4, 5];

const squared = numbers.map(x => x * x);
console.log("Squared numbers:", squared);

const evens = numbers.filter(x => x % 2 === 0);
console.log("Even numbers:", evens);

Benefits and Challenges

One major advantage of functional programming is the ability to reason about code in a more mathematical and declarative way. Pure functions and immutable data structures lead to fewer side effects, making it easier to test and debug code. Additionally, immutability simplifies concurrent programming because immutable data can be safely shared across threads without synchronization issues.

However, adopting functional programming comes with a set of challenges. Developers who are accustomed to imperative or object-oriented styles may find concepts such as higher-order functions, recursion, and monads initially difficult.

Further, functional programming needs support from the tools and libraries available in the language ecosystem. As an example, as we have previously discussed, recursion can benefit significantly from tail-call optimization, which is not always available in all languages. As an example, although one might be tempted to write the factorial function in Python as follows:

def factorial(n):
  if n <= 1:
    return 1
  else:
    return n * factorial(n - 1)

Python does not perform tail-call optimization (without extra modules), which can lead to problems for large inputs when trying to follow functional style of programming. That is, when adapting programming styles, one needs to also understand the limitations and capabilities of the language being used.

Loading Exercise...