I have been programming in C and C++ for nearly two decades now, so picking up a few insights was my main goal with K&R. I didn't expect to learn a ton of new stuff about the language since it's such a small language and I've been using it for so long. As for The Little Schemer, I have heard so many good things about this book and the Scheme programming language (a dialect of Lisp) that I was excited to see what it was all about. I was surprised by both books, and probably not in ways that you would expect. Let's take a look at both books in more detail.
The C Programming Language
To be honest, I'm sorry to say that I did not enjoy this book. I was certainly expecting to. It is held up as the epitome of programming language texts by many programmers, and in its day, it most certainly was. But it is showing its age, and numerous points in the book are obsolete or incorrect now. This is entirely understandable, considering that the last published edition is nearly three decades old. The question is whether or not it's still worth a read, given its age, and the answer is probably not.
When I read a tech book, I'm always considering two questions. Am I learning enough new things to justify the time to read the book? And if not, who would get the most benefit out of this book? I can't say that I learned a single new thing from this book, although several good programming techniques were recounted. I would say The Pragmatic Programmer or Clean Code would be much better books for learning these techniques, though, and they have held up better over time. As for anyone else, this book is problematic as well. It's too concise to be an introductory programming text, and it is definitely not meant for beginner programmers. The expert C programmer isn't going to get anything out of it. The experienced programmer looking to learn C will run into issues learning things about the language that they should no longer do, miss out on modern C programming practices, and generally have an incomplete picture of modern C programming. That leaves the curious-minded programmer, and for them, it's a pleasantly short book with the main text coming in at less than 200 pages.
K&R is certainly well written, and it's laid out well. The authors do not mince words to pad their page count, and they describe this clear, compact programming language in clear, compact prose and code examples. Their description of this spartan language is almost comical in what it doesn't do:
C provides no operations to deal directly with composite objects such as character strings, sets, lists, or arrays. There are no operations that manipulate an entire array or string, although structures may be copied as a unit. The language does not define any storage allocation facility other than static definition and the stack discipline provided by the local variables of functions; there is no heap or garbage collection. Finally, C itself provides no input/output facilities; there are no READ or WRITE statements, and no built-in file access methods. All of these higher-level mechanisms must be provided by explicitly-called functions.Yet, its small footprint has certain advantages:
Since C is relatively small, it can be described in a small space, and learned quickly. A programmer can reasonably expect to know and understand and indeed regularly use the entire language.Good programming practices are sprinkled throughout the text. Like the program feature descriptions and explanations, the reasoning is short and to the point. For example, here's an excerpt on why magic numbers are a bad idea in code:
We prefer the symbolic constants IN and OUT to the literal values 1 and 0 because they make the program more readable. In a program as tiny as this, it makes little difference, but in larger programs, the increase in clarity is well worth the modest extra effort to write it this way from the beginning. You’ll also find that it’s easier to make extensive changes in programs where magic numbers appear only as symbolic constants.In contrast, some of the weaker areas of K&R include their use of the basic integer types of int, short, and char instead of the more specific and portable int32_t, int16_t, and uint8_t; the common use of short, non-descriptive variable names; and their sometimes circular justification of certain specification decisions. For example, when interpreting code like:
a[i] = i++;
The question is whether the subscript is the old value of i or the new. Compilers can interpret this in different ways, and generate different answers depending on their interpretation. The standard intentionally leaves most such matters unspecified. When side effects (assignment to variables) take place within an expression is left to the discretion of the compiler, since the best order depends strongly on machine architecture. …This seems wrong to me. Wouldn't you want the standard to explicitly specify the order of operations in cases like this so that the behavior of the code remains consistent across machine architectures? Who cares if any given machine can execute code more efficiently in a different order than another machine. If the behavior changes across machines, then to make the code portable, the order of operations must be explicitly stated with separate statements, and the pattern above has to simply be avoided. It cannot be used regardless of whether the machine it's written for will execute it as intended or not because on the next machine it could be different. Besides, if we're going to agree that code dependent on order of evaluation is bad, then we should never write arithmetic expressions like this:
The moral is that writing code that depends on order of evaluation is a bad programming practice in any language. Naturally, it is necessary to know what things to avoid, but if you don’t know how they are done on various machines, you won’t be tempted to take advantage of a particular implementation.
We should write them like this instead:
result = a*x*x + b*x + c
Or like this:
result = ((((a*x)*x) + (b*x)) + c)
That's just absurd.
result = a*x; result *= x; result_b = b*x; result += result_b; result += c;
So I can't recommend K&R to anyone today. If you're curious about some of the history of C and want to see what some of the best writing for programmers thirty years ago was like, this is it. Otherwise, what Kernighan and Ritchie said was true. C can be learned quickly, but it can also be learned from plenty of online resources that will give you a better modern perspective.
The Little Schemer
This book was my first foray into a Lisp programming language. I purposely held off on learning any Lisp (of which Scheme is one dialect) until I could give this book the attention it deserves. I was not disappointed. In fact, this was probably the most fun I've had learning a programming language since my first C/C++ programming course.
Scheme is an incredible language in its simplicity and power. It's so simple, I'll give you a little crash course right now. This is Scheme:
That expression executes function-name with argument1 and argument2 as function arguments. The list of arguments is as long as the function defines it to be. Arithmetic operations are functions. Flow control is functions. Defining a function is a function. Loops are recursive function calls. The details get a little more involved, but that's basically it. When programmers talk about a language not having a lot of syntax, this is what they mean. Do you end up with a ton of parentheses? Yes, but with good formatting style it's really not a problem, and it's surprisingly readable. Scheme is so elegantly simple. It's functions all the way down.
(function-name argument1 argument2)
Like the language, The Little Schemer is also elegantly simple. The entire book is a series of questions and answers. On each page the questions are on the left and the answers to those questions are on the right. The questions have a natural progression as you are presented with and learn new things that you can do with the language. I went through the book with a sheet of paper folded in half width-wise. I would cover up the answers on each page and write what I thought the answers to the questions should be. Then I would check my work. If I ever got stuck, I could take a quick peak at the answer I was working on and see what the authors were after. Sometimes it was a quirky joke because the authors have a somewhat quirky sense of humor.
This process of going through The Little Schemer lead to some very Zen moments. I would routinely get into a flow where I was answering questions and anticipating where the line of questioning was going. It was such an exciting and enjoyable experience, and the build up of capabilities over the course of the book was incredible. It reminded me of the development of Turing Machines in The Annotated Turing, with each function and chapter building on the last one, and by the end of the book you realize that you've been building something quite extraordinary. I won't say what. It's part of the fun of working through the book, and I highly recommend discovering it for yourself.
Part of the beauty of a book like this is that it gives you time to think about what you're learning, time to ponder deeper truths of what it means to solve problems with code, and time to delve into the underpinnings of programming. For instance, arithmetic is something we normally take for granted in programming. We have operators for addition, subtraction, multiplication, division, and possibly higher level operations. We have math libraries for logarithms and trigonometric functions. Yet, all of arithmetic can be built up from three simple functions: zero? (returns true if zero), add1 (add 1 to a number), and sub1 (subtract 1 from a number). What's more, this is only one representation of arithmetic. There are others.
You can also build up arithmetic from the list processing functions of null? (returns true if its an empty list), cons (add one item to a list), and cdr (remove one item from the beginning of a list). It may not be very efficient because in a normal implementation of arithmetic the computer is doing a ton of work down at the transistor level in the processor, but it is still equivalent. It reminds us that there is always more than one way of doing things, and in fact, the computer is not using a representation of numbers and arithmetic that we are accustomed to. It's using still another representation with high and low voltage levels and boolean logic gates.
These are the kinds of things that were running through my mind as I worked through the book, and it was a fascinating trip. I'm looking forward to the other two books in the series, The Seasoned Schemer and The Reasoned Schemer, and if you're looking for a great book on learning about the essence of programming, I highly recommend The Little Schemer. One note of caution. This book is not for the beginner. It assumes a lot of the reader, and it will challenge you. If you don't already have a good understanding of programming fundamentals, it will probably end in frustration. Otherwise, if you already have a couple languages under your belt and you put in the effort, you're in for a treat.
Opposite Ends of a Spectrum
The two programming languages, C and Scheme, are at opposite ends of a spectrum. They are both small, simple languages that are easy to learn and can be completely contained in your head, but that is where the similarities end. C is about as close as you can get to a machine-level language without dropping down to assembly. You have to manage memory at a low level, the I/O is basic and mostly at the byte and bit level, and string processing is tedious without building up a tool set. Because of how closely you are controlling the machine, it is wickedly fast. On the other hand, Scheme is about as close as you can get to pure computation without dropping down to mathematics. Algorithms can be expressed incredibly succinctly, and building tools to solve high-level problems results in elegant solutions. The same problems can be solved in both languages, but the solutions will look very different.
Returning to the books, they are as different as the languages are. The C Programming Language is a direct, methodical tour of C and its features, while The Little Schemer is as much an exploration of computation as it is a book about a programming language. K&R is showing its age and hasn't kept up with modern C programming practices, while The Little Schemer is timeless. If you're looking for a book to reignite your love of programming and stir up new ideas, The Little Schemer is the book to read.