Making Cars Move Fluidly with Alice Programming: Understanding Core Programming Principles

This is a topic I’ve been eager to explore: how the fundamental concepts of programming underpin even seemingly complex tasks like making a car move smoothly in a virtual environment, especially within a beginner-friendly platform like Alice. My aim is to provide aspiring programmers, particularly those interested in domains like game development or simulation, with a foundational understanding and a broader perspective. Instead of overwhelming you with isolated facts, I want to weave together the essential threads that form the fabric of programming.

This isn’t a programming tutorial in the traditional sense. You won’t find step-by-step instructions to compile and run code here. Numerous resources already excel at that. Nor will we delve into the depths of computer science or type theory, although both offer incredibly insightful frameworks for grasping programming. My focus is on “fundamentals,” but not in a theoretical, academic way. Think of it as a practical toolkit, a collection of heuristics that guide my approach to programming challenges.

Ultimately, my goal is to share a way of thinking about programming that resonates with those who learn and problem-solve in a similar style to mine.

My own programming journey began with playful experimentation. I started by tinkering with existing Javascript Twitter bots, tweaking and modifying them with a “let’s see what happens” attitude. Gradually, these modifications grew more ambitious. Within a few months, I was writing programs from scratch. Shortly after, I connected with a freelance web developer who took me on as a junior partner. From there, my learning was largely on-the-job and experience-driven.

I believe that theoretical knowledge is valuable background, but hands-on experience is irreplaceable. I learn best by doing, by immersing myself in the problem and working through it directly. This approach has its advantages and disadvantages.

I can quickly achieve a working understanding of new technologies. I can often compress months of learning into weeks, or years into months, simply by relentlessly tackling the problem until a solution emerges.

However, my learning style also has its limitations. I thrive in the initial stages of rapid progress but can become restless when progress becomes incremental. Patience and meticulousness, qualities that yield high returns in later stages, are not my natural strengths. I often question whether I should cultivate more discipline or simply embrace my nature as someone who thrives on cross-pollination and broad exploration.

If this description of my learning style resonates with you, then the way I approach programming might be particularly helpful. If you learn best through structured classroom settings, guided mentorship, diligent study, or a strong theoretical foundation, this article might offer a different, perhaps intriguing, perspective on how programming can be approached. It’s a glimpse into the mindset of someone who learns by doing and intuition.

Over time, many individuals seeking guidance on starting their programming journey have come to me. To them, I’ve offered a breakdown of the fundamental abstractions in imperative programming. I’ve shown them the layers above, the abstractions built upon these foundations, and the layers below, the underlying mechanisms being abstracted away.

“Abstraction” in mathematics aims for broader applicability with fewer assumptions, simplifying complexity for generality. In programming, “abstraction” uses shortcuts and conveniences to mask underlying system complexities. Complexity is added to make it seem like it’s not there. No matter how elegantly presented, programming always involves navigating layers of abstraction, a bit like a Jenga tower.

By mastering the fundamentals and understanding how they compose into abstractions, you gain the ability to learn almost anything as needed. I’ve been hired for roles where I was unfamiliar with the required language, confident in my ability to learn it rapidly. The key is shifting from seeing syntax and features as isolated “things to memorize” to recognizing them as implementations of familiar concepts. This skill, while not universal, is not uncommon in software. The sheer volume of software, with its countless ecosystems, tools, and libraries, can seem overwhelming. Trying to learn each as a separate entity is impossible. However, they are all constructed from the same basic components.

I remember when I first explored programming job postings, I was overwhelmed by lists of languages, tools, acronyms, and jargon: Java, C#, PHP, Perl, JS, AJAX, Ruby, Go, MySQL, Postgres, Angular, React, Agile, Scrum, AWS, Azure. It felt daunting! I barely knew one or two things; how could I possibly learn ten or fifteen just to get started?

Now, those lists elicit a different reaction. I recognize them all, have worked with many, and can confidently learn any to a professional level. Many are specialized instances of general concepts, different names for similar ideas, or simply marketing buzzwords. But all are comprehensible and learnable through practice.

For an experienced programmer, learning a new technology typically isn’t a struggle of intense study, failures, and breakthroughs. It’s often reading a concise summary, coding based on intuition, checking documentation for syntax, and perhaps complaining about design choices.

The title of this piece is “how I think about programming,” not “how you should think about programming.” I embrace diverse learning styles and believe individuals should naturally gravitate towards what feels most comfortable. If what follows doesn’t resonate with you, it simply means we have different perspectives.

The material should be reasonably accessible to anyone with basic familiarity with a curly-braces language. I may sometimes move quickly or get bogged down in details. I’ll do my best to introduce technical terms clearly before they become essential. But teaching is challenging, especially when you’ve internalized concepts to the point of taking them for granted.

If this article is valuable but parts are unclear, consider revisiting it as your experience grows. My aim is to bridge conceptual gaps, emphasizing the form of programming thought rather than just the content of specific technologies.

I’ve been programming for about six years now. Before that, I was a pancake cook. Don’t believe anyone who says you need a formal degree, specialized training, or childhood exposure to code. Programming is accessible to anyone with curiosity, cleverness, and determination.

Kata One: The Foundational Movements of Programming

Variables, Flow Control, Scope, Functions

The first set of fundamental concepts revolves around assignment, flow control, and scope. We’ll use C examples for clarity, but don’t be intimidated if you’re more familiar with Python or JavaScript. C, at its core, is straightforward to write; the challenge lies in writing it correctly. These concepts are universally applicable and crucial for tasks ranging from basic data manipulation to complex simulations, like fluid car movement in Alice. Imagine these as the very basic instructions you’d give Alice to start controlling objects in her world.

Assignment is conceptually simple: storing a value in a named storage location.

int a = 2;

Here, the integer value 2 is stored in a variable named a. You can access this value using its name:

printf("%dn", a); // Prints 2

And use it in further assignments:

int b = 3;
b = b + a; // b is now 5

The second line, which might seem algebraically confusing, is not an equation. The value of a (which is 2) is retrieved from its storage, added to the current value of b, and the result is stored back into b, replacing the original value of 3.

Remember, a and b are not objects themselves, but names for storage containers holding values. In Alice programming, think of these variables as properties you assign to your virtual car – its speed, position, or color.

Normally, a program executes line by line, from top to bottom, until it encounters a stopping instruction or an error. However, a program that only proceeds sequentially is limited. To create truly dynamic and interactive programs, like simulations of car movement, we need to alter the execution path based on conditions. This is where control flow constructs come in. The two most common are branches and loops.

Here’s a branch example:

if (a > 2) {
  a = 3;
} else {
  a = 4;
}

The condition (a > 2) is evaluated. Based on the result (true or false), one of two code blocks is executed. These blocks can contain arbitrarily complex code. You can have nested conditions and more intricate structures, but computationally, they are equivalent to these basic branching mechanisms. In Alice, you might use conditional statements to determine if a car should accelerate based on user input or sensor readings.

Here’s a loop example:

int a = 0;
do {
  a++;
} while (a < 10);

The code block within the do...while loop executes at least once. Then, the condition (a < 10) is checked. If true, the block executes again; otherwise, execution proceeds past the loop. Loops enable repetitive actions, essential for animations or continuous updates in a car movement simulation. Imagine a loop in Alice constantly updating the car’s position to create the illusion of fluid motion. There are other loop types (like while and for loops), but they are fundamentally similar in their ability to repeat code execution based on conditions.

In C, code blocks are grouped using curly braces {}. Blocks create their own isolated storage space, known as scope. When a block finishes, execution returns to the enclosing context. Blocks can be nested, with all code ultimately nested within the main function. A block can access storage from its outer scopes, but its own internal storage is discarded when execution leaves the block, though changes made to outer scope storage are retained.

Scope refers to the region of influence of a block, including the storage it owns and can access. A function is a named block that can be invoked from elsewhere and can receive input data (arguments).

int timestwo(int n) {
  int x = n * 2;
  return x;
}

for (int i = 0; i < 5; i++) {
  int x = timestwo(i);
  printf("%d times two is %dn", i, x);
}

In this example, the for loop generates increasing values of i, which are passed as arguments to the timestwo function. Inside timestwo, a variable x is calculated and returned. This returned value is then assigned to a variable x in the outer scope. Note that these two x variables are distinct storage locations, even though they share the same name. The storage internal to timestwo is discarded after the function completes.

It’s important to note that scope rules can vary across languages, like JavaScript’s slightly different approach compared to C. The key takeaway is that scope is a convention, a set of rules defined by the language, not an inherent property of reality. These rules are designed to manage variable visibility and lifetime, helping to organize code and prevent naming conflicts. In Alice, understanding scope is crucial for managing variables within different parts of your animation or interactive scene.

Type theorists and computer scientists might offer different, more formal perspectives on scope. But for our practical purposes, it’s about understanding the rules of the game, how to work within them, and sometimes, how to bend or break them when necessary. Programming languages, like many human-designed systems, are existentially meaningless but internally consistent. Obsessing over the ultimate “meaning” of every rule can be unproductive. Focus on practical application and understanding the conventions.

Learn the rules, master playing within them, and understand when and how to break them. Approaching programming with this mindset – embracing the rules while remaining flexible – is key to creating fluid and dynamic car movements in Alice, or any programming task.

Digression One: Programming Kata – Mastering the Fundamentals

What is “Kata”?

I find it helpful to think of basic programming concepts as akin to martial arts kata. Many who tried karate as children likely expected to be performing impressive flips and kicks from day one. Instead, they were instructed to repeat the same punch, over and over, with unwavering precision. This repetition, this focus on fundamental movements, is kata: the elementary building block of technique.

Kata, in my view, is the secret to achieving true proficiency in programming. Learning complex libraries, sprawling ecosystems, and towering frameworks in depth is the domain of specialists. I respect their expertise, but that’s not how I personally operate.

To become truly skilled, you don’t necessarily need to master any single “big thing.” Instead, you need to practice and deeply understand the small things, the fundamentals, until they become second nature. Consider the 100ms lag time threshold for instantaneous interface response. You want to be able to work with the absolute basics at that speed, so effortlessly that you don’t even have to consciously think about them.

There’s a wonderful quote by Bruce Lee that I often find relevant:

Before I learned the art, a punch was just a punch, and a kick, just a kick. After I learned the art, a punch was no longer a punch, a kick, no longer a kick. Now that I understand the art, a punch is just a punch and a kick is just a kick.

Before understanding a field, everything seems undifferentiated, a blur of sameness. As you learn, you start to distinguish elements, categorize them, and build a taxonomy. With deep understanding, rigid distinctions soften, replaced by a more intuitive feel. You can still analyze and create models, but you hold these models lightly, recognizing their inherent limitations.

There’s a profound tension between analysis and synthesis that extends far beyond programming. This tension, I believe, is central to understanding thought, people, cultures, and the world. It’s a feeling more than a clearly defined concept.

Lacking analysis, you’re naive, vulnerable. Reason and categorization provide power, making you a sharp tool. You think you know, but true understanding is still elusive. Reintegrating holism, grounded in reason, is essential to move beyond this stage, to experience the world without reducing everything to mere components.

There’s a stereotype of the Confucian scholar-bureaucrat who adheres strictly to doctrine during their career, then embraces Daoism in retirement. Perhaps pushing beyond mere analysis makes it harder to tolerate superficiality, to be a cog in a flawed system, to accept duty and sacrifice blindly. But it also makes you incredibly effective in pursuits you deem truly valuable.

This is the higher goal I’m setting here, even within the limited scope of programming, a field that, in itself, has little to say about the grand questions of life. Don’t get trapped by the models. See beyond them. Then, you can truly see: a punch is just a punch, a kick is just a kick, and a variable is just a variable – but understood at a fundamental, almost instinctual level. This mastery of the basics is what allows for fluid, intuitive programming, whether you’re animating a car in Alice or building complex systems.

Memory Semantics One: The Landscape of Data Storage

Stack and Heap

We’ve been discussing variables as named boxes for storing values. The place where these boxes reside is memory.

Memory, in its raw form, is unstructured, without inherent meaning. It’s essentially a vast sequence of one-byte boxes, each addressable by a number starting from 0 and extending into billions. The meaning we ascribe to sequences of bytes in memory is imposed by the programmer, through the programs they write and run.

When a program starts, the operating system creates the illusion that it’s the only program on the machine. Its address space begins at address 0 (typically reserved and unwritable, serving as a null value) and is protected from interference by other programs.

This wasn’t always the case. MS-DOS, for example, granted programs direct control over all memory, leading to a lack of multitasking and inherent security risks. Unix-style virtual memory, however, allows each program to manage its own memory space while safeguarding other programs and the OS. By convention, memory is typically divided into read-only sections and two primary writable sections: the stack and the heap.

The stack is a last-in, first-out queue. It starts at the highest memory address and grows downwards, towards lower addresses. Each function call adds a stack frame to the top of the stack. A stack frame is a block of memory where a function’s arguments and local variables are stored. When a function returns, its stack frame is discarded, and the stack pointer reverts to the frame of the calling function. Think of the stack as temporary workspace for functions as they execute.

The heap, in contrast, starts at a lower address and grows upwards, towards higher addresses. It’s persistent storage, directly managed by the programmer, and independent of the function call stack. You explicitly request a block of memory of a specific size from the operating system using the malloc function:

int *a = malloc(sizeof(int) * 16);

This allocates enough space for 16 integers. Each integer requires multiple bytes to store values larger than 255. The allocated block of memory is called a “buffer,” and malloc returns a pointer to its first byte. This buffer persists for the duration of the program unless explicitly released. When you’re finished with it, you must tell the operating system to recycle the space back into the memory pool:

free(a);

The stack and heap grow in opposite directions (towards each other) so that programs can effectively utilize all available memory, allocating more to one or the other as needed. Understanding stack and heap memory is crucial for writing efficient and reliable programs, especially when dealing with dynamic memory allocation, which is common in game development or simulations where objects and data structures are created and destroyed frequently. In Alice, while memory management is largely hidden, understanding these concepts provides a deeper insight into how the virtual world is managed behind the scenes.

Kata Two: Navigating Data and Logic

Indirection and Recursion

As we’ve seen, every memory location has an address, and variables are essentially names for memory locations. We can obtain the memory address of a variable and store it as a value in another variable. This is the concept of pointers and indirection.

int i = 3;
int *p = &i;

The asterisk * in the type declaration of p indicates that p is a pointer variable, storing a memory address. The ampersand & is the “address-of” operator, returning the memory address of the variable i.

This address can then be dereferenced, meaning we can follow the pointer to access the value stored at that memory location:

printf("%p: %dn", p, *p); // Prints something like "0x7ffd12bd3958: 3"

The function malloc returns a void *, a pointer that can point to any data type. Using pointers to access or manipulate data is called “indirection” because it’s an indirect way of accessing data through a memory address rather than directly by name.

Pointers can be nested to arbitrary depths, creating complex data structures like graphs. However, at their core, pointers are just numbers, stored in memory, representing memory addresses. Their meaning arises from how they are interpreted by the program.

C strings are contiguous sequences of characters. A collection of strings can be represented by a double pointer char **. Double pointers are useful for managing resizable containers, allowing functions to reallocate memory as needed without breaking existing references. Complex objects are often represented by structures containing pointers to other structures, and so on, creating interconnected data networks. Imagine representing a car in Alice. You might use pointers to link different components like wheels, engine, and body, allowing you to manipulate the car as a whole or access individual parts.

Recursion is a programming technique where a function calls itself, creating a sequence of repeated executions similar to a loop, but often in a more elegant and concise way for certain problems. Classic examples are factorial and Fibonacci functions, where each result depends on previous results of the same calculation. Recursion allows for succinct and natural (though sometimes inefficient) implementations:

int factorial(int n) {
  if (n == 0) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

Calling factorial(5) will lead to a chain of function calls: 5 * factorial(4), 4 * factorial(3), and so on, until the base case n == 0 is reached. Then, the series of return statements unwinds the calls, ultimately producing the result 120. Recursion is particularly useful for tasks that involve breaking down problems into smaller, self-similar subproblems, such as traversing tree-like data structures or implementing algorithms like quicksort. In Alice, recursion could be used for tasks like creating fractal patterns or implementing complex animation sequences.

Clever use of recursion allows for more intricate iteration patterns than simple loops. For instance, recursively subdividing a group to perform operations on subsets is often easier to implement than using loops. Sorting collections or searching nested hierarchies are tasks naturally suited to recursive approaches.

Languages with first-class functions, where functions can be assigned to variables, passed as arguments, and created at runtime (capturing their creation environment), offer even more powerful recursive possibilities. C supports the first two properties but not the third, and its function pointer syntax is cumbersome. JavaScript, inspired by Scheme, possesses all these features.

In languages with first-class functions, you can create higher-order functions, which accept and apply other functions as arguments, often recursively. Higher-order functions allow you to express concepts like “apply this operation to every item in a list,” “filter a list to keep only items that pass a test,” or “combine all list elements into a single result using a given operation.” They enable iteration that adapts to the structure of the data being processed, without explicitly managing indices or modifying data in place.

These functions are often “pure,” meaning they operate on input data to produce output data without side effects. This makes it easy to chain operations together into pipelines, and even treat these pipelines as objects themselves. Stream programming, which focuses on pipelines as primary units and data as flowing through them, offers a fundamentally different perspective on program execution.

An intuitive grasp of indirection and recursion is essential. It’s a key differentiator between a clumsy programmer and a deft one. These concepts initially seem harder than basic storage and flow control because they involve conceptual graph structures that you must visualize and manipulate mentally. Mastering them, like mastering kata, requires repeated exposure and practice until they become reflexive. It took me time after learning these concepts to truly feel them, and there’s no shortcut around that process of repeated engagement and practice. Developing this intuition is crucial for creating fluid and responsive interactions in Alice or any programming environment.

Memory Semantics Two: Structuring Data in Memory

Structs, Arrays, Enums

A struct (structure) is a blueprint for creating composite data types. It groups together named fields, each of a specific data type:

typedef struct User Android;
struct User {
  char *username;
  int pin;
  bool active;
};

struct User user = { "alice", 1234, true };

The typedef simplifies type declarations, allowing us to use Android instead of struct Android. A struct definition groups variables into a single unit, accessed and manipulated as a whole. Underneath, a struct is just a contiguous block of memory, large enough to hold all its fields. Each field within a struct must have a fixed size known at compile time, ensuring the struct itself has a fixed size.

Because field sizes are fixed and known, struct fields can be accessed using fixed memory offsets. A struct can be thought of as a pointer to its first field, or the starting memory address of its buffer. Offsets are then used to access specific fields relative to this starting address:

// While not directly compilable C code, this illustrates the concept conceptually
// assert(offsetof(struct User, pin) == sizeof(char *)); //Conceptual assertion

The keyword “struct” is used in two ways: to define the structure’s fields (the blueprint) and to create instances of that structure (actual memory buffers). The definition serves as a template for creating any number of independent struct instances.

A struct is conceptually similar to a tuple, a data structure common in functional programming, which is an ordered sequence of data types without named fields. Some languages only offer tuples, providing struct-like syntax that internally translates to tuple element access. In Alice, you might use structs to represent complex objects like cars, with fields for color, speed, and position.

An array is a contiguous sequence of elements of the same data type, all stored in a single, contiguous memory buffer:

int evens[] = { 2, 4, 6, 8 };
char name[] = { 'a', 'l', 'i', 'c', 'e', '' };

An array, like a struct, can be treated as a pointer to its first element, i.e., the start of its memory buffer. Pointer arithmetic can then be used to access any element. Since all elements are the same size, accessing an element is simply a matter of calculating an offset based on the element size and index:

// Conceptual assertion, not directly compilable C
// assert(&evens[3] == (char*)evens + 3 * sizeof(int));

Arrays are not the same as lists, though the terms are sometimes used interchangeably. A list is a non-contiguous sequence of elements, where each element contains a value and a pointer to the next element in the list:

struct Node {
  int x;
  struct Node *next;
};

The last node in a list points to a null address to mark the end. Lists are advantageous for dynamic data structures where elements can be added or removed without reallocating the entire structure. However, accessing a specific element in a list requires traversing the list from the beginning until the desired element is found. In Alice, you might use arrays to store a sequence of animation frames or a list of objects in a scene.

If you generalize the fields in the Node struct to void *, you get the versatile cons cell, a fundamental building block in Lisp-like languages:

struct Cons {
  void *car;
  void *cdr;
};

Cons cells are flexible enough to represent nearly any data structure. Lisp’s elegance stems from representing its own code using cons cells, which inherently function as parse trees. Because Lisp can manipulate and execute Lisp code as easily as data, it’s exceptionally well-suited for metaprogramming.

Enums (enumerations) are used to represent a simple set of mutually exclusive choices, where a variable can hold only one of the defined choices:

enum Color { Red, Green, Blue };

Internally, an enum is represented as an integer. A more complex construct, the union, can represent choices between different concrete data types, including structs. A common pattern, the tagged union, combines enums and unions. An enum is used to indicate which struct variant is currently active within the union.

Stepping back from C, these concepts – structs, arrays, enums – represent fundamental data structuring principles that transcend specific languages. A struct or tuple is a product type, representing a collection of different types combined in a specific order. A tagged union or an enum that can carry a value (sum type) represents a choice between different types. These product and sum types can be nested recursively to create data structures of arbitrary complexity.

To illustrate, here are our C types expressed in Haskell:

data User = User { username :: String , pin :: Int , active :: Bool }
data List = Node { x :: Int , next :: List } | Nil
data Cons a b = Cons a b
data Color = Red | Green | Blue

These data structuring concepts are essential for organizing and managing data in any program, including Alice animations and interactive worlds. Understanding how to structure data efficiently is key to creating complex and well-organized programs that can simulate fluid car movements or any other dynamic behavior.

Paradigms: Approaches to Organizing Code

Imperative and OOP

The preceding sections aimed to illustrate the basic building blocks of programming: variables, functions, arrays, structs, loops, branches, pointers, and recursion. Complex concepts in higher-level languages are ultimately built from these primitives, often quite literally, as C is frequently the implementation language for higher-level languages. Understanding these foundational elements makes it easier to grasp how higher-level constructs are assembled and how they operate.

With this foundation, we can explore how to construct a simplified version of object-oriented programming (OOP) abstractions using these imperative tools.

Object-oriented programming is a paradigm that feels accessible to imperative programmers because many popular OOP languages are essentially “imperative programming plus some extra features.” This wasn’t inevitable. If languages like Simula, Smalltalk, and Eiffel (particularly Eiffel, with its “design by contract” philosophy) had become the foundation of modern OOP, our current understanding and practice of OOP might be very different. However, these early OOP languages primarily served as sources of ideas for languages rooted in the imperative paradigm.

Modern mainstream OOP largely descends from C++. C++ was created as an evolution of “C with Classes” and could aptly be named “C with everything not in C.” While not a strict superset, most reasonable C programs are also valid C++.

Java is a direct descendant of C++, originally intended to replace it in situations where C++ proved too complex. Java adopted C++’s curly-braces syntax and many core assumptions. JavaScript, while inspired by Scheme, grafted curly-braces syntax and class-based inheritance (to mask its prototype-based internals) onto its core. C#, despite its name, is essentially Microsoft’s version of Java. Objective-C was unique in its adherence to the spirit of Smalltalk, but was criticized and eventually deprecated partly for being perceived as “too weird.”

If you’re new to programming, don’t worry about understanding these historical details. It’s more of a justification for the generalizations I’m about to make about OOP.

In the 1990s and 2000s, Java became the standard introductory language for computer science students. This choice was always debated, and I believe that taking OOP abstractions as fundamental truths, rather than grounding oneself in the programming kata I’ve presented, is a mistake. I offer this example of building OOP abstractions from imperative fundamentals as a corrective for those who think “everything is an object.” The object abstraction is pervasive in OOP, but examining its underlying construction reveals it as a helpful organizational tool, not an inherent truth.

Shifting between seeing objects as organizational structures and objects as concrete entities is akin to observing the optical illusion of the spinning dancer, perceiving it rotating in either direction at will. Declarative programming, functional programming, continuation-passing style, and other programming paradigms offer similar shifts in perspective once you become familiar with them. You can think in them or about them, consciously switching between viewpoints.

Fundamentally, an object is a… “thing”… that combines state (data, variables) and behavior (functions). A class is a blueprint for a type of object, specifying its state variables and the functions (methods) that operate on that state. An object is a concrete instance of a class, with its own independent storage.

Encapsulation is a key OOP principle: an object’s internal state is protected. External entities cannot directly read or modify it. Methods are the sole interface for accessing and changing an object’s state. The goal of encapsulation is to channel all stateful operations through a controlled set of methods, defined within the class itself, making the program easier to refactor and reason about.

However, our basic imperative language doesn’t natively have classes or methods. We only have structs and functions.

OOP examples often involve corporate databases or, for some reason, cars. Let’s consciously avoid those clichés:

typedef struct Android Android;
struct Android {
  char *name;
  char *secret_name;
  unsigned int kills;
  bool on_mission;
};

The typedef allows us to use Android as a shorthand for struct Android. Using this struct definition, we can create instances:

Android a_two = { "A2", "No2", 972, true };

Again, a struct is simply a way to reserve memory for a group of variables, access fields using pointer offsets, and provide some compile-time type checking.

To streamline Android creation, especially if we need to persist and pass them around, we can create a constructor function:

Android* make_android(char *name, char *secret_name) {
  Android *a = malloc(sizeof(Android));
  a->name = name;
  a->secret_name = secret_name;
  a->kills = 0;
  a->on_mission = false;
  return a;
}

Android *two_b = make_android("2B", "2E");
Android *nine_s = make_android("9S", NULL);

Now, creating a new Android instance is simpler, requiring only the names. The rest of the setup is standardized and written only once.

Note that a_two is stack-allocated, meaning it goes out of scope when the block it’s created in ends. two_b and nine_s are stack-allocated pointers to heap-allocated structs. They persist as long as a copy of the pointer is retained and are explicitly deallocated using free. The strings are also persistent, their data compiled directly into the binary, with the char pointers holding the memory addresses of this data.

Struct data can be modified directly and arbitrarily:

// Send 2B on a mission
two_b->on_mission = true;
// 2B scores a kill
two_b->kills++;

This unrestricted modification can make debugging difficult, especially in larger programs where pointers to structs are passed around and modified in various places. Concurrency exacerbates this issue, as data can be changed unexpectedly while other parts of the program are using it.

One approach to address this is to strictly limit direct struct data modification. We can enforce a convention that struct data is only modified through specific functions designed for that purpose. This centralizes data access and modification, providing a single point to implement correctness checks and debugging logs.

We create functions that accept a pointer to an Android struct and modify the struct’s data:

int go_on_mission(Android *a) {
  if (a->on_mission) {
    fprintf(stderr, "%s is already on a mission!n", a->name);
    return 1;
  }
  a->on_mission = true;
  return 0;
}

void score_kill(Android *a, bool was_friendly) {
  if (was_friendly) {
    printf("redacting combat logs...n");
  } else {
    a->kills++;
  }
}

go_on_mission(two_b);
score_kill(two_b, false);

We have now essentially recreated the basics of object-oriented programming using imperative C. By formalizing these conventions within the language itself, we move beyond seeing them as just coding practices. We can encapsulate these concepts into a single abstraction, hiding the underlying details behind syntax, and interact with the abstraction as an agent:

class android {
  string name;
  string secret_name;
  unsigned int kills;
  bool on_mission;

public:
  android(string name, string secret_name) {
    this->name = name;
    this->secret_name = secret_name;
    this->kills = 0;
    this->on_mission = false;
  }

  int go_on_mission() {
    if (this->on_mission) {
      fprintf(stderr, "%s is already on a mission!n", this->name.c_str());
      return 1;
    }
    this->on_mission = true;
    return 0;
  }

  void score_kill(bool was_friendly) {
    if (was_friendly) {
      printf("redacting combat logs... %d corrected killsn", this->kills);
    } else {
      this->kills++;
    }
  }
};

android *two_b = new android("2B", "2E");
two_b->go_on_mission();
two_b->score_kill(false);

This C++ code, while seemingly different, is fundamentally doing the same things as the preceding C code snippets.

The C++ class combines the fields of our C struct and the functions that operated on it. The C++ android function is a constructor, analogous to the C make_android function, but with implicit heap allocation through the new keyword. go_on_mission and score_kill are methods, similar to our C functions, but now associated with the android class. They still implicitly take a pointer to the object as their first argument (accessed via this). Data fields, not marked public, are now inaccessible from outside the class, enforcing encapsulation. Trying to access two_b->kills directly would result in a compiler error.

OOP encompasses much more, but these are the core concepts. I don’t mean to criticize OOP, but it often tends to complicate simple things. Java, in particular, became in the 2000s a way to standardize programming roles, making programmers more interchangeable, leading to slogans and stereotypes that simplified project staffing.

Inheritance, interfaces, and abstract classes add layers of method extension. Operator overloading, generics, polymorphism, overrides, and reflection enhance function flexibility or method dispatch. The “Gang of Four” design patterns book codified an entire taxonomy of patterns, often memorized for entry-level tech roles. Visitor pattern, observer pattern, decorator pattern, singleton, factory beans factory factory – these are examples of patterns that, while sometimes useful, can also add unnecessary complexity.

You don’t need to learn these advanced OOP concepts unless specifically required for a project or role. Even then, consider it a potential occupational hazard.

These details are not fundamentally important. My point is that the object abstraction, while powerful, is built upon our fundamental programming kata. Abstractions are valuable because they allow us to work at a higher level, without constantly dealing with underlying implementation details. However, the danger arises when later programmers, unfamiliar with the underlying layers, begin to see the abstraction as reality itself. When the abstraction inevitably fails, they lack the context to understand why.

OOP, with its “objects as real things” metaphor, is particularly prone to this kind of thinking. The metaphor becomes mistaken for reality.

I considered doing a similar breakdown of functional programming, starting with recursion and building up to maps and folds with immutability. But I hope this OOP example effectively demonstrates my point: abstractions are not magic. They are built from conceptually simple components, and understanding these components is key to truly mastering programming and avoiding being limited by the abstractions themselves.

The last point about OOP: if you must specialize in it, choose C++ over Java. Java engineers are often treated as replaceable. Mastering C++ template metaprogramming, on the other hand, can offer greater job security and more control over your work.

Digression Two: Practical Advice and Perspectives

Odds and Ends for the Aspiring Programmer

Before moving to the final technical section, I want to share some practical advice on various programming-related topics, in no particular order – a bit of a ramble.

I’ve stated that this isn’t a programming tutorial. So, how do you learn to program, especially to the point where you can make a virtual car move fluidly in Alice or build other interactive experiences?

First, choose a language and stick with it until you reach at least an intermediate level. Some languages are better starting points than others, but in the long run, the specific language matters less than the fundamental concepts you learn. The idea that starting with Java or Basic “ruins” you is false, unless you become narrowly specialized and resist broadening your understanding as you gain experience.

The most crucial message I want to convey is that “programming” is not inherently special or mystical. It’s a form of applied abstract symbol manipulation. Different languages achieve the same things in different ways, and with experience, you can move fluidly between them. If you possess an aptitude for abstract symbol manipulation, whether innate or developed, you can learn to program.

A corollary to this is that if you can learn to program, you can learn many other complex skills just as easily. While some programmers might present themselves as counter-examples, I firmly believe this. Programming is uniquely suited to self-teaching because it’s a system where trial and error is effective, even without specialized knowledge or equipment. Once you understand your learning style, and if you are curious and determined, you can learn anything.

So, gain a solid foundation in one language before branching out. But do branch out! When you know only one language, you tend to think within its limitations. Knowing several languages within a paradigm broadens your perspective to the paradigm itself. Knowing multiple paradigms allows you to think in terms of programming as a fundamental skill, independent of any specific technology.

This broader perspective doesn’t come automatically. You must actively connect concepts across different systems, or you risk becoming trapped in specialized silos, as many programmers unfortunately are. Traditional education often reinforces this siloed thinking through tracks and prerequisites.

Perhaps some people learn best in this linear, structured way. Maybe most people do! I’m not making a normative claim. But if you’ve followed my thoughts this far, we likely share a similar approach to learning and knowledge.

My own knowledge organization is more like a vast, sprawling graph. It’s largely unorganized, with varying degrees of interconnectedness. When I encounter new topics, I don’t worry if I initially understand only a fraction of it. I focus on finding connections to my existing knowledge graph. Even if I read something beyond my immediate grasp, it still lays a foundation, a skeletal structure that I can later flesh out with more detail.

I believe creativity isn’t about inventing entirely new ideas from nothing, but rather about creating novel connections within a vast knowledge graph. The loss of neuroplasticity in adulthood, the tendency towards conservatism, the resistance to new art or music – these might be linked to a decision to stop learning, to stop adapting, and consequently losing the ability to do so.

I suspect the generalist “software engineer,” without deep specialization, is gradually becoming less relevant and may be relegated to lower-status, routine work. Tooling advancements are automating rote implementation and maintenance tasks, and human-in-the-loop systems may further accelerate this trend. There will always be demand for basic applications and SaaS platforms, and increasingly, average developers can use readily available tools to create serviceable solutions. Many professional programmers already contribute negative value to their organizations. In large companies, it’s hard to identify them, and there’s often no incentive to do so.

However, for genuinely new creations, for scientific and artistic innovation, for novel engineering, I believe programming norms will evolve to resemble writing. Code will become a tool for expression, for enacting knowledge about procedural or scientific domains – a means to an end, not the end itself.

Code is not the next mass literacy. Most people will never learn to program, although they may learn enough to operate machines created by others. My hope is that it can become the next Latin, the next Classical Chinese – a tool for a minority, but a significant minority, to express their will in ways that transcend cultural boundaries.

The 21st century will have two linguae francae: English for communication, and code for action.

I believe the future belongs to those who can leverage code to influence the world and create things that are more than just software. A domain expert with strong programming skills will be more valuable than a brilliant programmer lacking domain knowledge, or a specialist incompetent in software. A great programmer who can also work across disciplines, bridge gaps, and make novel connections will be, and continue to be, exceptionally powerful.

So, nurture your knowledge graph.

The core theme I’ve emphasized through the kata analogy is feedback and response. The tighter your feedback loop, the faster you learn, the faster you think, and the more effortless programming becomes. This principle is equally crucial for debugging.

Debugging is rarely formally taught. People often learn it through trial and error. Initially, programmers work on small projects they can mentally track, debugging largely through careful thought. Then, they encounter larger codebases, projects that outgrow mental capacity, or collaborative projects. Suddenly, they’re forced to develop debugging strategies to survive. They reinvent basic debugging principles to solve immediate problems, often forgetting they ever lacked these skills.

Dan Luu’s remarkable story about a challenging computer engineering class illustrates this point. This class, notorious for its difficulty and high failure rate, was designed to “weed out” students. The author initially excelled through innate talent, while many students struggled and failed. However, the final project overwhelmed even him. Working late into the night, making no progress, he realized the real challenge was to invent debugging. Students who figured out they could systematically test each connection to isolate the problem passed. The rest failed. No one had explicitly taught them this essential skill.

While sophisticated debugging tools exist, the fundamental principle remains: feedback and response. For beginners, the simplest tool, requiring no new skills, is the humble print statement.

In my first “real” job, working on a 1.5 million line, 15-year-old, object-oriented Perl codebase (a daunting prospect even for experienced programmers), printf (or its Perl equivalent) became my best friend. I liberally sprinkled print statements throughout the code – printing variable values that were unexpectedly changing or not changing, scattering prints to pinpoint the location of the issue. When you find the general area, add more prints.

Experiment by changing code and observing the results. Use print statements to see the effects of your changes. Approach debugging like a very basic scientist. You don’t need complex hypotheses if you can run a new experiment every 20 seconds. Feedback, response.

Don’t get bogged down trying to analyze overwhelming code. The most vital skill in navigating a large codebase is learning to ignore irrelevant code. Relax, focus on the specific issue, and do whatever gives you more information about it.

If building and running the code is slow, try to create a simpler, minimal case that reproduces the error. Print stack traces to understand the execution path leading to the error. Iterate, prioritize getting faster feedback and narrowing your focus. Feedback, response.

Use version control. Specifically, use Git. You can explore niche version control systems later. (Hopefully, by then, the Pijul rewrite will be mature, and we can discuss its merits over a beer…)

Git is straightforward to start with. In your project directory, git init creates a repository. git add FILENAME stages a file for commit (a necessary but initially unclear step). git commit creates a snapshot of your staged files, like a checkpoint. git remote add origin URL sets up the online repository location. git push origin BRANCH (usually main or master) uploads your repository to the internet. If you encounter problems, copy the entire project directory to a safe location before making further changes. You now know more about version control than many CS undergraduates who haven’t self-taught.

Version control is transformative for many reasons. It provides backups, ensures file consistency, tracks changes, documents reasons for changes, and enables easy experimentation and rollback. It’s invaluable, and I wonder how those outside technical fields manage without it.

Text editors: You can learn Vim or Emacs if you wish. I use Vim and find it indispensable. But Sublime Text and VS Code are excellent and have a gentler learning curve. Criticism of IDEs is partly elitist and partly based on outdated monolithic corporate IDEs. Sublime Text and VS Code are perfectly fine for most programming tasks.

Use Linux. Okay, macOS is acceptable if you insist, but expect occasional software compatibility issues. Avoid Windows for development; it often adds unnecessary complexity. In software, we distinguish between “normal” environments and “Windows shops.” Critical software targets Linux first, making Windows development often different and often less efficient.

For Linux distributions, Ubuntu is a good starting point. While I have my criticisms, it’s beginner-friendly. You’ll likely develop strong Linux distribution preferences sooner than version control system preferences.

This isn’t to say Linux is universally the best OS. For most users, it’s not. Pressuring designers to switch from macOS or business professionals from Windows is pointless. Their platforms are better suited for their workflows, have wider adoption, and offer the best tools for their needs. Switching would likely be detrimental for them. But for software development, Linux is simply the superior choice.

Learn the command line. It’s initially frustrating; the interface is cryptic because it was developed in the 1970s by a small group for a specific machine, not anticipating its central role in modern computing. The original Unix clock, running at 60Hz, would have overflowed after 2.5 years if not modified – that’s how far ahead they were thinking. Despite its quirks, the command line is incredibly powerful and essential for software development.

The best way to learn programming, in my opinion, is to conceive a small, achievable project and learn whatever you need to complete it. This is easier said than done, especially for beginners who struggle to generate project ideas and assess their feasibility. Once you become proficient, you’ll have countless project ideas but limited time. So, enjoy this phase of exploration.

JavaScript or Python are reasonable beginner languages. Both have quirks, but they offer abundant learning resources and comprehensive libraries for diverse tasks. Following textbook examples that print “sunny” or “cloudy” based on random numbers is often demotivating. Aim to build something personally engaging, something that feels valuable, however small or simple. Then, build upon that. Feedback, response. Perhaps start by creating a simple Alice world where you program a car to move forward and backward, then gradually add complexity to achieve fluid, realistic motion.

Kata Three: The Machine’s Perspective

Registers, Jumps, Calling Convention

It was insightful to dissect the object-oriented paradigm and see how its abstractions can become ingrained. However, we must also recognize that our own framework—variables, loops, structs, functions—is also built upon abstractions. It’s abstractions all the way down.

Programming languages provide tools for human programmers. To execute programs, they must be compiled into machine code: long sequences of binary digits. Interpreting these binary streams directly is extremely difficult for humans, though not impossible. Programmers who could write raw binary or decipher core dumps in hexadecimal are legends in the field, cultural heroes of a bygone era.

Assembly language is a human-readable representation of machine code, with mnemonics for instructions and some basic conveniences. Assembly was an early step towards making programming more efficient for humans, predating compilers and high-level languages. Examining assembly code clarifies what happens “under the hood,” deepens our intuition about computer capabilities, and enhances our adaptability to new systems.

I firmly believe that every high-level programmer should understand C, and every C programmer should understand assembly. Mediocre programmers remain within their chosen abstraction, becoming specialists within a narrow domain. But true mastery requires understanding what’s happening beneath the surface.

Machine code isn’t the ultimate bottom level. Below it lies hardware engineering, and below that, electrical engineering. Even these levels are abstractions, though beyond my expertise. (Explore “Rowhammer attack,” “Spectre attack,” or “acoustic cryptanalysis” to glimpse the complexities at these lower levels).

At a basic level, a computer consists of a CPU that executes instructions, registers (fast storage locations within the CPU) that hold single values, and main memory (a large, slower storage area).

“Arrays,” “structs,” “stack,” and “heap” are all programmer-imposed conventions on raw, undifferentiated memory. Even “code” itself is just bytes in memory.

When a program is executed, the operating system’s loader places its data into memory. The program has an entry point, a memory address where instruction execution begins. Instructions are executed sequentially, with the current instruction address stored in a register called the instruction pointer (ip) or program counter (pc). The CPU executes the instruction pointed to by ip/pc, then increments the pointer to the next instruction. Branch instructions simply modify the ip register, causing the CPU to execute instructions elsewhere in memory.

The distinction between “code” and “data” is arbitrary, enforced primarily as a security measure after exploits that involved tricking programs into executing user-provided data as code. Gaining control of the instruction pointer effectively means gaining control of the entire program. It’s all just bytes in memory.

Let’s revisit our initial C examples to see how simple operations compile into assembly code (with optimizations disabled):

int a = 2;
int b = 3;
b = b + a; // b is now 5

This C code compiles to the following assembly (x86-64 architecture):

40110b:  c7 45 f8 02 00 00 00  mov DWORD PTR [rbp-0x8],0x2
401112:  c7 45 f4 03 00 00 00  mov DWORD PTR [rbp-0xc],0x3
401119:  8b 45 f4              mov eax,DWORD PTR [rbp-0xc]
40111c:  03 45 f8              add eax,DWORD PTR [rbp-0x8]
40111f:  89 45 f4              mov DWORD PTR [rbp-0xc],eax

There’s much new terminology here.

Assembly values are typically represented in hexadecimal (base-16), for human readability. Hex digits are 0-9 and a-f (a=10, f=15). 0x prefix indicates hexadecimal. Hex is convenient because each digit represents exactly four bits (half a byte), so two digits always represent a byte.

The numbers in the first column are memory addresses, byte offsets from a fixed starting address. The second column shows the actual machine code bytes in hexadecimal. Note the instruction lengths vary. The third and fourth columns are human-readable mnemonics for the machine code. In this example, we see mov (move/copy data) and add (addition) instructions.

mov instructions copy data between locations. In Intel syntax (used here), the destination operand is on the left, similar to assignment in C. The first two lines mov the immediate values 2 and 3 into memory locations on the stack, addressed as offsets from the base pointer register rbp (which points to the bottom of the current stack frame).

The third line movs the value of b (from memory) into the eax register, a general-purpose register. add can only operate on registers. The next line adds the value of a (from memory) to the value in eax. The final line movs the sum back from eax to the memory location for b.

Operands can be immediate values (like 0x2), memory addresses (like DWORD PTR [rbp-0xc], an indirect memory access), or register names (like eax).

This is Intel syntax; AT&T syntax is an alternative with different operand order and formatting. The underlying machine code bytes are identical in both syntaxes.

Blocks and high-level control flow constructs don’t exist in assembly. Control flow is achieved through jumps:

if (a > 2) {
  a = 3;
} else {
  a = 4;
}

Compiles to:

401119:  83 7d f8 02           cmp DWORD PTR [rbp-0x8],0x2
40111d:  0f 8e 0c 00 00 00     jle 40112f <main+0x24>
401123:  c7 45 f8 03 00 00 00  mov DWORD PTR [rbp-0x8],0x3
40112a:  e9 07 00 00 00        jmp 401136 <main+0x2b>
40112f:  c7 45 f8 04 00 00 00  mov DWORD PTR [rbp-0x8],0x4

The cmp instruction compares a with 2 and sets flags in a status register. jle (jump if less or equal) is a conditional jump. If the condition (a <= 2) is true, it jumps to address 40112f, skipping the a = 3 assignment. Otherwise, execution continues to the next line, executing a = 3, and then jmp (unconditional jump) jumps past the a = 4 assignment.

Loops also compile to jumps:

int a = 0;
do {
  a++;
} while (a < 10);

Compiles to:

40110b:  c7 45 f8 00 00 00 00  mov DWORD PTR [rbp-0x8],0x0
401112:  8b 45 f8              mov eax,DWORD PTR [rbp-0x8]
401115:  83 c0 01              add eax,0x1
401118:  89 45 f8              mov DWORD PTR [rbp-0x8],eax
40111b:  83 7d f8 0a           cmp DWORD PTR [rbp-0x8],0xa
40111f:  0f 8c ed ff ff ff     jl 401112 <main+0x7>

The loop starts at 401112. Instructions increment a, then cmp compares a with 10. jl (jump if less) conditionally jumps back to 401112 if a is less than 10, repeating the loop. A while loop would perform the comparison before the loop body, jumping past the body if the condition is initially false.

C’s goto statement is a direct representation of assembly jumps, though often discouraged in high-level C programming for readability reasons.

Functions, like branches and loops, are also implemented using jumps:

int timestwo(int n) {
  int x = n * 2;
  return x;
}

int a = 5;
int x = timestwo(a);

Compiles to:

40112f:  c7 45 f8 05 00 00 00  mov DWORD PTR [rbp-0x8],0x5
401136:  8b 7d f8              mov edi,DWORD PTR [rbp-0x8]
401139:  e8 c2 ff ff ff        call 401100 <timestwo>
40113e:  89 45 f4              mov DWORD PTR [rbp-0xc],eax

Main block: 5 is stored, then moved to register edi for argument passing, then call timestwo is executed. call is a special jump that pushes the return address onto the stack before jumping to the function’s start.

Assembly for timestwo function (including setup/teardown):

401100:  55                    push rbp
401101:  48 89 e5              mov rbp,rsp
401104:  89 7d fc              mov DWORD PTR [rbp-0x4],edi
401107:  8b 45 fc              mov eax,DWORD PTR [rbp-0x4]
40110a:  c1 e0 01              shl eax,0x1
40110d:  89 45 f8              mov DWORD PTR [rbp-0x8],eax
401110:  8b 45 f8              mov eax,DWORD PTR [rbp-0x8]
401113:  5d                    pop rbp
401114:  c3                    ret

Function entry: push rbp saves the caller’s base pointer, mov rbp, rsp sets up the current function’s base pointer. mov DWORD PTR [rbp-0x4], edi copies the argument from register edi to the stack. Function arguments are essentially stack variables. This setup/teardown sequence is the calling convention, a platform-specific standard.

Computation: shl eax, 0x1 (shift left by 1 bit) is equivalent to multiplying by 2. The result is moved to eax, the register for return values. pop rbp restores the caller’s base pointer. ret pops the return address from the stack and jumps back to the caller.

Stack overflow errors occur when function calls exhaust stack space. Segmentation faults (segfaults) occur due to invalid memory access. Tail-call optimization in some functional languages transforms certain recursive calls into loops, avoiding stack growth. Ultimately, it’s all just jumps and register manipulations.

This assembly overview is dense. It’s acceptable if this section isn’t fully grasped on a first reading. However, understanding these underlying systems demystifies higher-level abstractions and clarifies why things work as they do.

Machine registers typically hold a machine word (e.g., 8 bytes on x64). This explains why C functions can only directly return a single value (register-sized). Returning structs or multiple values usually involves returning pointers and compiler-managed behind-the-scenes operations, similar to how call and ret handle function call mechanics. It also explains stack-based local variable lifetime and argument passing, and the word-size limit on directly addressable memory.

The key is to simultaneously think in terms of high-level abstractions (which are inherently simplified models) and be aware of the underlying, equally abstract, machine-level realities. Avoid getting trapped within any single abstraction; learn to glide over them, like a spider on its web, and eventually, to spin your own. This flexible, multi-layered understanding is essential for truly mastering programming and for being able to create fluid and complex behaviors, like realistic car movements in Alice, or any other software system.

Further Reading: Expanding Your Programming Horizons

I suggested JavaScript or Python as good starting languages. Eloquent JavaScript is a solid resource for JavaScript. It assumes no prior knowledge and provides excellent background context. I don’t have a specific Python recommendation offhand.

Another classic starting point is the MIT classic: SICP (Structure and Interpretation of Computer Programs). SICP uses Scheme, a Lisp dialect, and gets you programming very quickly. Scheme’s syntax is simple and can be learned rapidly. SICP is accessible yet covers advanced concepts, building from fundamentals to complex ideas, similar to the approach in this article. Search “SICP Scheme” to find easy-to-install Scheme environments.

Scheme was replaced by Java at MIT due to perceptions of Scheme being “impractical” and industry-relevant languages being more “useful.” You likely won’t use Scheme professionally. But the core point, as always, is to generalize concepts across boundaries. If you can’t, then just learn the bare minimum to get by.

For those drawn to a more foundational approach, C is a fine starting language. K&R (The C Programming Language) remains the definitive C resource. I don’t recommend C as a first language unless you are strongly inclined towards it, but if you are, don’t be dissuaded.

Most people prefer a top-down approach, building interesting projects and learning fundamentals along the way. For them, JavaScript or Python are good choices. Others prefer understanding the system from the ground up, learning fundamentals first and tackling higher-level concepts as needed. For them, Scheme or C might be more suitable.

For type theory, TAPL (Types and Programming Languages) is a great introduction. It starts with the untyped lambda calculus (a minimal programming language) and builds up type systems layer by layer.

For operating systems, OSTEP (Operating Systems: Three Easy Pieces) is excellent. It covers virtualization, concurrency, and persistence in detail.

For CS fundamentals (algorithms and data structures), searching “algorithms and data structures” online will yield many resources. I learned this material as needed and only recommend focused study if required for job interviews.

Similarly, for formal grammars and parsing, online searches will find resources. Understanding the Chomsky hierarchy and its relation to computational complexity is valuable for programmers.

For operations (Ops) and information security (Infosec), hands-on practice is essential.

For practical exercises, Microcorruption is highly recommended. It involves exploiting vulnerabilities in a 16-bit microcontroller, providing a hands-on introduction to low-level hacking. The final challenge is demanding but rewarding.

Cryptopals is another excellent resource, guiding you through breaking real-world cryptosystems through a series of coding challenges. It requires coding and analytical thinking, but no advanced prerequisites beyond basic algebra and cleverness.

Advent of Code is an annual series of 25 programming puzzles. The 2021 challenges are still available. It’s a fun way to learn CS fundamentals by reinventing algorithms and data structures to solve computationally challenging problems. It can be enjoyable even without prior CS knowledge, but may become tedious for those with a strong CS background, except for competitive speed-solving or portfolio building.

Nand2Tetris starts even lower than assembly, guiding you to build a computer from NAND gates up to a compiler and operating system. It covers boolean logic, memory, CPU design, assembly language, a simple OOP language, and a compiler. The later stages (compiler construction) can be tedious depending on your background, but starting from the beginning and stopping when interest wanes is fine.

Regarding software culture, ESR’s Hacker Howto is a classic guide to the hacker ethic and mindset. It emphasizes curiosity, tenacity, freedom, and a healthy dose of boredom.

Dijkstra’s “On the Cruelty of Really Teaching Computer Science” presents a more formal, mathematically grounded vision of computer science, contrasting with more pragmatic approaches. It’s valuable for broadening your perspective.

Neal Stephenson’s “In the Beginning…Was the Command Line” is a sprawling essay exploring old-school computing, modern operating systems, and the essence of powerful tools versus merely “powerful-seeming” tools. It delves into hidden complexities and the right mindset for designing systems.

I hope this article is helpful. Feel free to share feedback via Twitter or email. If you’re reading this because I sent it to you personally, I believe you can succeed in programming. And regardless, I wish you the best in all your endeavors.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *