Our stacker language worked fine. But the way we implemented the stack wasn’t as sleek as it could’ve been. The relevant excerpt from the source code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | (define stack empty) (define (pop-stack!) (define arg (first stack)) (set! stack (rest stack)) arg) (define (push-stack! arg) (set! stack (cons arg stack))) (define (handle [arg #f]) (cond [(number? arg) (push-stack! arg)] [(or (equal? * arg) (equal? + arg)) (define op-result (arg (pop-stack!) (pop-stack!))) (push-stack! op-result)])) (provide handle) |
What’s the problem here? Our handle function uses two techniques that are common in other programming languages, but discouraged in Racket:
Relying on a variable outside the function, also known as state. Here, handle relies on stack, which is a variable that lives outside the function.
Changing a value from afar, also known as mutation. Here, handle uses push-stack! and pop-stack! to add & remove items by changing the value of stack.
Racket, instead, encourages a style called functional programming. This doesn’t mean merely “programming with functions”. (Everyone does that.) It means designing our functions so that they’re self-contained. Rather than relying on state, everything the function needs to do its job is passed into the function as input. Rather than relying on mutation, the function provides its result as a return value, which entirely replaces the old value. So instead of updating a single element of an existing list, we would return a whole new list.
Functional programming is big in Racket for two reasons.
First, because it dovetails with Racket’s expression-based structure. Expressions can be freely combined like Legos, allowing us to join elements that wouldn’t fit together in other languages. For instance, we can wrap if around an argument in an arithmetic operation:
Or we can wrap it around the operator:
Or the result:
What makes these combinations possible is the fact that every expression is self-contained. In Racket, this virtue goes by the slightly mathy name composability. When we use functional programming, we sever the tentacular connections implied by state and mutation, thereby improving the composability of our functions. + Composability is also a design virtue of many Unix-derived text-based command-line tools, which are designed to be chained together.
The other reason functional programming is big in Racket is because of its roots as a research language. Researchers like to be able to prove things about functions. But when a function operates outside its boundaries, or is affected by data far away, it becomes harder to reason about the function’s behavior. All those external possibilities need to be considered. It’s easier to prove that a self-contained function is correct.
When we say a function is more easily proved correct, this also implies that it can be more easily tested. We’re not going to dive into Racket’s testing facilities yet (though the curious can peek ahead). But the discipline of functional programming tends to reveal bugs earlier.
That said, do we always have to use functional programming in Racket? No. Sometimes, it can be more efficient or readable to use state or mutation. In those cases, we should feel free.
Nor are we standing on a mountaintop, shouting “you’re doing it wrong” at the languages below. It’s simply an issue of idiomatic usage. Functional programming is pervasive within Racket. If we take the time to understand functional programming at the beginning of our Racket journey, a lot of things will make sense. If we don’t, we’ll have the uncomfortable feeling of moving against the grain.
So let’s create a little variant of stacker called funstacker, designed to introduce some functional-programming concepts.