Today we will wrap up this unit by stepping back and considering what makes the Scheme language special. We started by focusing on the syntax of the language, but now we can start to think about what Scheme does interestingly in terms of its semantics.
Summary:
- Declarative vs iterative languages
- Functional programming
- First-class functions
- Immutability
- Homoiconicity
What is Scheme? Why are we learning it?
You’ve likely noticed that programming in Scheme feels quite different from programming in languages like Java, C++, or Python. This is because Scheme belongs to a different programming paradigm.
Scheme is a declarative language, and we typically use a functional programming style when writing Scheme code. This approach emphasizes what to compute, not how to compute it, by combining expressions and treating functions as a primary data type.
Declarative vs. Imperative Programming
Programming paradigms are a way of classifying languages based on their features and style. The most common paradigm, which you’re already familiar with, is imperative programming.
- Imperative Programming (e.g., C++, Java, Python): This style focuses on describing how a program operates. You write a sequence of commands that explicitly change the program’s state. Think of it like giving someone turn-by-turn directions. You don’t necessarily need to say where they are going, you just say how to get there in terms of each individual step.
In programming terms, an imperative language has you (the programmer) managing variables, memory, loops, and data structures explicitly. You tell the code how to do each step and generally have maximal control over how it works.
- Declarative Programming (e.g., SQL, Scheme, Haskell, …): This style focuses on expressing the logic of a computation without describing its control flow. You describe what you want the result to be. Think of it like giving a taxi driver an address. You don’t tell them which streets to take; you just declare the destination, and the driver (the language’s evaluator) figures out the best way to get there.
In Scheme, an expression like (+ (* 2 5) 3) declares a desired result: “the sum of 3 and the product of 2 and 5.” You aren’t giving step-by-step instructions for a CPU to load numbers into registers and perform arithmetic. You are simply stating the mathematical relationship.
The Functional Programming (FP) Style
Functional programming is a style of declarative programming where computation is treated as the evaluation of mathematical functions. The core idea is to build programs by applying and composing functions. This style avoids changing state and mutable data.
Here are the key principles of the functional style you’ve seen in Scheme:
1. Functions are First-Class Citizens
This is the most important concept in functional programming. It means that functions are treated just like any other data type (like integers, strings, or lists). You can:
- Pass functions as arguments to other functions.
- Return functions from other functions.
- Store functions in data structures, like a list.
The lambda keyword you’ve used is the primary tool for this. It creates an anonymous function on the fly.
;; Define a variable 'double' that holds a function
(define double (lambda (x) (* 2 x)))
;; Call the function stored in the variable
(double 5) ; returns 10
2. Emphasis on Immutability
In pure functional programming, data is immutable — it cannot be changed after it’s created. If you have a list '(a b c) and want to add an element, you don’t modify the original list. Instead, you create a new list.
The cons procedure does exactly this. It constructs a new list without altering any existing ones.
(define my-list '(b c))
(define new-list (cons 'a my-list))
;; new-list is now '(a b c)
;; my-list is still '(b c)
This is very different from an imperative language where you might list.append(newItem) which modifies the list in place. Immutability makes programs easier to reason about and to compose sub-expressions inside each other without worrying about the side effects of the order things are evaluated.
The fancy term for this in a programming language is referential transparency: the idea that a piece of code (such as an expression) has the same meaning regardless of where it shows up.
This limitation on the programmer (don’t change the values of variables) actually gives more freedom to the interpreter or compiler, which can more effectively optimize or sometimes parallelize functional programming code.
In fact, Scheme is the second language we have seen which features immutable variables. The first is LLVM IR itself, where the SSA (Single Static Assignment) property says each register can only be assigned once.
3. Recursion over Iteration
Since variables and data structures are often immutable, traditional loops (like for or while loops that rely on updating a counter variable) are not the primary way to perform repeated operations.
Instead, functional programming relies heavily on recursion. A recursive procedure calls itself with a modified input until it reaches a base case. You’ve seen this with car (get the first element) and cdr (get the rest of the list), which are the building blocks for recursively processing lists.
How This All Connects in Scheme
Let’s tie these concepts back to the Scheme features you’ve already learned:
-
Nesting Expressions: The syntax of Scheme, with its nested parentheses, naturally encourages the composition of functions. The result of an inner expression
(* 2 3)becomes the input for an outer expression(+ ... 4). This is a direct representation of functional composition. -
cons,car, andcdr: These are the fundamental pure functions for list manipulation. They take lists as input and produce new data (a new list or an element) as output without causing any side effects. -
quote(‘): Quoting is a powerful feature that tells the Scheme evaluator to treat the following expression as literal data instead of code to be executed. This ability for a language to treat its own code as data is called homoiconicity and is a hallmark of the Lisp family of languages (which includes Scheme). It blurs the line between code and data, which you’ll see is incredibly powerful later on.
Side note: Languages are multi-paradigm
Although the functional programming style is heavily favored and natural in Scheme programming, it isn’t strictly required by the language. Like most modern general-purpose languages, Scheme is actually a “multi-paradigm” language that lets the programmer decide how they want to write their code.
So yes, you can actually reassign the values of variables in Scheme with
the set! command, you can run multiple statements in a row with a
begin block, and Scheme even has a do loop which does something
similar to a for loop in other languages.
Conversely, you can also do functional programming in Java or C++. For instance, Java has lambda expressions that let you create anonymous functions on the fly kind of similar to Scheme. But they are more restricted from the lambda functions we can make in Scheme, due to the legacy of how Java works internally where everything is a class or an instance of a class.
The point is, modern languages let us program in many different ways, but they might be more natural or more awkward depending on how the language itself is designed. Scheme was designed to support functional programming, so that style of problem solving “feels” good in Scheme, whereas something like object-oriented design “feels” natural in Java.
(In a few weeks, we will also see how to make objects in Scheme!)