alice maz



how I think when I think about programming

a schematic overview of charles babbage's analytical engine

this is a post I've wanted to write for some time. in it, I attempt to explain, on a very basic level, how I think about programming. my hope is to give newer programmers some background knowledge and bigger-picture context. to help give form and structure to what is often presented as just a bunch of shit to remember

I don't attempt to teach programming per se. I don't cover all the details needed to write a program that compiles and runs. there are very many guides for that. I don't discuss computer science, nor do I get into type theory, though both of these topics can serve as a highly eluciditory basis for understanding programming. I talk about "fundamentals" a lot, but my fundamentals aren't a set of overarching theories. just a bag of tools

my goal is to impart some general heuristics about how to think about programming for the subset of people who happen to think like me

I got started programming by just messing around. making tiny modifications to existing javascript twitter bots to fashion my own, with a "fuck it, whatever works" kind of attitude. gradually my modifications got more ambitious, and after a couple months I could write the programs myself. a couple months after that, I found someone doing freelance webdev willing to take me on as a junior partner. and then from there I just kinda learned on the job

I generally consider theory to be useful background knowledge but fastidious study a poor substitute for firsthand experience. I "learn with my hands," is the way I've always put it. it has its upsides and downsides

getting up to "working knowledge" on something is usually trivial for me. I can speedrun in weeks what might take others months, or months what could take them years. I just bang my head on the problem until it (the problem, ideally) breaks

on the other hand, it means that I usually power through things in manic bursts. I easily get bored once progress becomes more incremental, when patience and diligence pay higher dividends than brashness and force. I go back and forth on whether I can and should train myself to be more disciplined, or if I am just a natural crosspollinator-type human and should just embrace it

anyway, if this description of how I learn feels familiar to you, it is likely that the way I think about this stuff will be useful. if you learn better in a formal classroom setting, or with guided mentorship, or through diligent study, or by leaning on carefully composed theoretical foundations, then it's possible the only value this post can provide is a voyeur's glimpse into how the fauves live

I've given bits and pieces of these lectures to at least a dozen people who've come to me at various times for advice about how to get started with programming. I deliver a rundown of the fundamental abstractions used in imperative programming. then I give a glimpse above, at abstractions that can be constructed out of them, and a peek below, at what they themselves abstract over

"abstraction" in mathematics means to use a higher-level concept that carries fewer assumptions but covers a broader superset of cases. complexity is removed for the sake of generalizability. "abstraction" in programming means to paper over the underlying workings of a system with shorthands or conveniences. complexity is added so you can pretend it isn't there. no matter how you dress it up, when you're using a computer, you are always somewhere in the jenga pagoda

master the fundamentals, understand how they're composed into abstractions, and you can pick almost anything up as you go. I've gotten hired for jobs where I didn't know the language I'd be writing, expecting to learn it on their dime in the first couple days. it's easy once you stop seeing syntax and features like "so what does this thing do..." and start seeing "ah, this is what they call that." not a ubiquitous skill in software, but not an uncommon one

the volume of software in existence is massive. hundreds of ecosystems, thousands of tools, millions of libraries. if you tried to learn them all as individual things, it would be impossible. but they're all built out of the same basic parts

I remember when I first started programming, looking at job postings and seeing dozens of languages, tools, acronyms, jargon rattled off as necessary requirements. java c# php perl js ajax ruby go mysql postgres angular react agile scrum aws azure. it was daunting! I barely knew one thing, how was I going to learn 10 or 15, just to get started working?

now I roll my eyes at these lists. I know what they all are, I've dabbled in many, I can easily learn any of them to professional proficiency. a lot of them are particular cases of general things, a lot of them are different words for the same thing, a lot of them are just bullshit. but all of them can be comprehended at a glance and learned by doing

learning a new technology, for an experienced programmer, usually doesn't look like concerted study, thwarted attempts, struggle and triumph. it looks like reading a three-paragraph summary of it, coding in or against it based on vibes, checking documentation for syntax, and bitching about how dumb the guy who designed it must have been

the title of the post is "how I think about programming," not "how you should think about programming." therefore I will brook no criticism. I'm a strong believer in learning styles, and I think people should naturally adopt what is most comfortable. if everything that follows rings false, we probably just have different perspectives

the material should be reasonably accessible to anyone who possesses general familiarity with a curlybrackets language. I admit sometimes I may move too fast, and other times I may get bogged down in detail. I do my best to explicitly introduce technical terminology before making it load-bearing. but pedagogy is hard, and unless you're constantly working with new learners, it's very difficult to keep in mind all the little quirks and customs that you've come to take for granted

if this post feels valuable, but parts are inaccessible, it may be worth revisiting as you gain more experience. I'm trying to bridge gaps between concepts, more than I am trying to actually teach the concepts themselves. form, rather than content

I've been programming for about six years now. before that, I cooked pancakes for a living. don't believe anyone who says you need a fancy degree, specialized training, or exposure since childhood to code. you can pick up programming just by being clever and stubborn

kata one

variables, flow control, scope, functions

the first set of basics are assignment, flow control, and scope. all examples are in c unless otherwise stated. don't be scared if you're used to python or js. c is very easy to write. it's only hard to write correctly

assignment is conceptually very simple. you are storing a value in some named piece of storage:

int a = 2;

here, the value 2 is being stored in a, an integer variable. you can access the value via the name you've given your storage:

printf("%d\n", a); // prints 2

and you can use it for further assignments:

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

the algebraically confusing second line is not an equation. the value of a, then 2, is loaded out of its storage space, and added to the value of b. the sum is stored back in the space b, replacing the 3 that used to be there

the thing to remember is that a and b are not objects. they are names of boxes in which we deposit objects

normally a program proceeds top to bottom, line by line, executing each statement and moving on to the next. it does this forever, until it encounters a statement that tells it to stop, or a kind of error that forces it to stop. but a program that can only proceed or halt cannot do much that is really interesting

programs become truly expressive once you have a way to change the future order of execution based on some present state. mechanisms that do this are called control-flow constructs, and the two most common are branches and loops

this is a branch:

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

the condition is checked, and based on the result, one of two different blocks will be executed. the code in the blocks can be arbitrarily complex. you can have several conditions checked in sequence, or more complicated constructs with fallthrough rules, but they are computationally equivalent

this is a loop:

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

the block is executed once unconditionally, then the condition is checked, and based on the result, the block will either be run again, or flow will proceed past the loop to continue as normal. depending on what condition you set, the loop can execute arbitrarily many times, possibly indefinitely. there are other loop forms that check the condition at the top, or lack a condition and force you to explicitly abort inside a nested branch, but they are computationally equivalent

in c, coherent units of execution are grouped together in blocks. blocks are self-contained, enclosed in curlybrackets, and can have their own isolated storage space. when the end of a block is reached, program flow continues in the context it was contained in. in this way, blocks are nested inside each other, ultimately all nested in the main function. a block can access storage areas from its outer containers, but once execution moves outside of a block, all of that block's storage is lost, though changes it made to outside storage is kept

the area of influence a block has, including storage it owns and storage it can access, is called its scope. a block that has a name and can be invoked from somewhere else and passed information is called a function

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

for(int i = 0; i < 10; i++) {
    int x = timestwo(i);
    printf("%d\n", x);
}

here, the increasing values of i generated by the for loop are passed in as an argument to the function int timestwo(int). an x is calculated inside the scope of the function and returned to its caller. it then is assigned to x outside, a completely different storage location which just happens to share a name. the storage that was internal to int timestwo(int) is now gone

it's worth noting that other languages, notably javascript, have a somewhat different notion of scope than c. the point I would make is that scope, like many things in programming, is a convention, not a fact of reality. most of the rules don't really "mean" anything on a deep level. they don't transcend the contexts in which they were implemented

type theorists, computer scientists, and category theorists would have a very different view on such an assertion. but that isn't relevant to us right now. we fuck around and find out

learn the rules, how to play within their bounds, and how any when to break them. existentially meaningless but internally consistent is the way to approach. if you obsess over what everything "really means," you will get lost. this is useful for navigating many systems designed by humans besides programming languages

digression one

what is "kata"

I like to think of basic programming concepts as akin to martial arts kata. those of us who tried karate as kids probably remember going in expecting to be doing flips and flying kicks from day one. we were instead instructed to perform the same punch, over and over, without variation. this is kata: the most elementary building block of technique

kata is, I think, the secret to how you actually get good at programming. learning the big fancy stuff—baroque libraries, sprawling plugin ecosystems, towering frameworks—to a very deep level is the province of domain specialists. I respect what they do when they're good at it, but it is not how I work

the way I see it, to be really good, you don't need to study any particular big thing. you need to practice and understand the small things, over and over, until they're second nature. a 100ms lag time on an interface is the limit below which response will feel instantaneous. you want to be able to work that fast with the absolute basics. fast enough that you don't have to think about them at all

there is a wonderful quote by bruce lee that I love to deploy whenever appropriate:

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 you understand a field, you are mired in sameness. everything is opaque and undifferentiated. as you learn more, you begin to distinguish elements, group them into classes, construct a taxonomy. when you really, deeply understand something, it gets harder to hold to such strict distinctions, but easier to feel. you can discuss and build models, but you hold those models as contingent and insufficient

there is something deep in the tension between analysis and synthesis that goes way beyond this topic. I feel like that tension is one of the really important ideas about thought, people, cultures, the world. I can't really articulate the weight of it properly. it's just a feeling

having only holism, you are a rube, a sitting duck. you know not. armed with reason, the ability to categorize, to distinguish and dissect, you are dangerous, a weapon. you think you know, but you still don't. it is essential to relearn holism with a grounding in reason to be able to move past this, to feel the world without turning what you touch into ash

there's a stereotype that a scholar-bureaucrat adheres to confucian doctrine in his working years, then indulges in daoism upon retiring. it is probably true that pushing past petty analysis makes it harder to tolerate bullshit, to be a cog in an ugly system, to accept duty and sacrifice. but it also makes you frighteningly effective at work, for those things you do deem valuable enough to work on

that is the higher goal I'm setting here, for this one little field, that itself has remarkably little to say about life. don't get stuck in the models. see past them. and then you can see, a kick is just a kick

memory semantics one

stack and heap

we've been talking about variables in terms of placing values into boxes that we access with convenient names. the place where all those boxes reside is called memory

memory has no inherent structure or meaning to it. it is a very long series of one-byte (eight binary bits, max value 255) boxes, numbered starting from 0, all the way up into the billions. all meaning that the sequences of bytes in memory contain is imposed by the programmer, and thus the programs they write and run on top of

when a program is started, the operating system provides it with the illusion that it is the only program on the machine. its address space starts from the special 0 address—unwritable and used as a standard null value—and nothing else tramples upon its preserve

it needn't be this way: msdos famously passed full control of all memory to any program, meaning everything ran as root and multitasking didn't exist. but unix-style virtual memory allows each program to arrange its own memory however it sees fit while protecting other programs and the os from misbehavior. by convention, usually memory has a handful of read-only sections, plus two major writable sections, called stack and heap

the stack is a last-in, first-out queue that starts at the last address in memory, and grows downward, toward the lower addresses. every time you call a function, one stack frame is added on top of the stack. a stack frame is a chunk of space where one function's arguments and internal storage live. when a function returns, its stack frame is disregarded, and the new top of the stack is once again the function that originally called it

the heap starts from somewhere after the 0 address and grows up toward higher addresses. it is persistent storage, controlled by the programmer directly, and unrelated to where in the function stack you are. you directly ask the operating system for a chunk of memory of a specific size via the function malloc:

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

here, enough space for 16 integers is allocated. each integer is multiple bytes, needed to store numbers larger than 255. our chunk of memory is called a "buffer," and malloc returns a pointer to its first byte

unless released, our buffer will persist for the entire life of the program. when we're done with it, we tell the operating system, so it can recycle the space back into the pool it allocates memory from:

free(a);

the reason why stack and heap grow in opposite directions (towards each other) is so programs have use of all memory available to them if they need to allocate much more on one than the other

kata two

indirection and recursion

as we've seen, every location in memory has an address, and the named boxes we've been storing data in are really just pieces of memory. we can take any of our boxes and get the address its storage starts at as a value we can then store somewhere else:

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

the asterisk in the type of p indicates that this variable stores a pointer, ie a memory address. the ampersand is the address-of operator, returning the address of the storage used by a variable

the address can then be dereferenced, ie the pointer can be followed and the value at the address it points to retrieved:

printf("%p: %d\n", p, *p); // prints something like "0x7ffd12bd3958: 3"

the function malloc returns the type void *, that is, a pointer that could point to anything. traversing pointers to use or manipulate values stored at their address is often called "indirection" because it is an indirect mechanism of data access

pointers can nest arbitrarily deep. you can have pointers to pointers, and so on, forming large, abstract graphs. the don't truly "nest." fundamentally pointers are just numbers, stored in memory, like large integers. it's only because they're interpreted as memory addresses that they have any meaning

because strings in c are just continuous series of characters, a collection of strings may be represented by the double pointer char **. it's useful to use double pointers to resizable containers so functions can reallocate storage implicitly when needed without breaking references. complex objects are typically represented by structures that point to structures which themselves point to structures. and so on

recursion is the idea of a function calling itself, effecting sequential repeated execution in a manner not unlike a loop. the classic examples of this are the factorial and fibonacci functions, where any result depends on all previous results of the same calculation. this allows them to be expressed succinctly and naturally (albeit often horrifically inefficiently) via recursion:

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

here, if you were to call factorial(5), the function would fall into the else branch, and attempt to calculate 5 * factorial(4). this would necessitate another call into the function, and another, until hitting the "base case" of n == 0. then a series of returns effectively unwind all the function calls made thus far, producing the result 120

clever use of recursion allows you to iterate in much more complex ways than a simple loop. it is much easier, for instance, to continuously subdivide a group to perform some task on the subsets with recursion than with looping. various means of sorting collections, or searching for items in complicated nested hierarchies, are naturally suited to being subdivided or accumulated in this manner

things get even more interesting in a language with first-class functions. that is, functions that can be assigned to variables, passed as values, and constructed at runtime (crucially, capturing the environments in which they are constructed). c has the first two properties but not the third. moreover, its syntax for function pointers is notoriously cumbersome, and the language is just not suited for this kind of programming. javascript, being at its core the worst dialect of scheme, has all the necessary features

in a language with first-class functions, you can easily construct functions that accept and apply other functions as arguments, recursively. these, being functions of functions, are called higher-order functions

hofs allow you to very simply express concepts like "apply this operation to all members of a list," "remove all members from the list that fail some boolean test," or "combine all the elements of a list into a result using some specified operation." there are various reasons one would want to do this, but the most obvious is they allow iteration that depends on the structure of what is being iterated over, in a way that doesn't require explicitly tracking position or mutating existing data

these functions are also "pure," meaning they operate data-in, data-out, with no side effects. this makes it very easy to chain operations together into pipelines, and even to regard those pipelines as objects in their own right. stream programming, which regards the pipelines as the units of concern, and data merely as what flows through them, offers a drastically different perspective on what's really "happening" in a program

it is absolutely essential to have an intuitive grasp on indirection and recursion. this is perhaps the most important factor distinguishing a clumsy programmer from a deft one. indirection and recursion are harder at first than storage and flow because their usage forms conceptual graph structures that you must be able to see in your head to manipulate and traverse

remember, the golden rule is to know the basics so well that you shift from conscious thought to reflex action. it took me a long time after learning about these concepts before I could properly feel them. there's no shortcut around repeated exposure

memory semantics two

structs, arrays, enums

a struct is a blueprint for a composite datatype, consisting of a series of named fields of particular types:

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

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

on the surface, a struct is a way of grouping together several variables into one structure that can be accessed and passed around as a unit. underneath, a struct is just a buffer. it is just enough space for the sum of its field sizes, allocated contiguously in memory. every type you include in a struct must have a fixed size known at compile time. this means that the struct itself is of a fixed size

because all field sizes are known in advance, you can access struct fields by fixed offsets. the struct itself can be reduced to a pointer to its first field, or, equivalently, the memory address of the start of the buffer. you can use this plus the offsets to access particular fields:

assert(user.pin == &user + sizeof(char *)); // true

it's worth noting the word "struct" is pulling double duty. it may refer either to the definition of the fields that constitute the type. or it may refer to an instantiation of that definition, a particular buffer somewhere in memory. the definition is just a blueprint for making any number of separate buffers

a struct is very conceptually similar to a tuple, an idea more common in functional programming, which is just a specific sequence of datatypes without any field names. in fact, some languages only have tuples, and provide a struct-like record syntax that desugars to simple tuple member access

an array is a series of boxes all of the same datatype, backed by a single buffer (which is, as always, contiguous in memory): a series of integers can be represented by an int array. strings in c are represented by an array of chars, terminated by a null symbol

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

an array can be reduced to a pointer to its first element, ie the start of the buffer. with this, you can do pointer math to access arbitrary elements. because every slot is the same size, an item is just an offset multiplied by the slot size:

assert(evens[3] == evens + 3 * sizeof(int)); // true

arrays are not the same concept as lists, although people will sometimes refer to them offhand as that. a list is a noncontinguous series of boxes, each of which contains a value and a pointer to the next box:

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

the last box in the list points to the null address to indicate the end. lists are convenient if you want to add new items without having to reallocate existing storage, because you just have to fix up some pointers. to access any particular item, however, you have to traverse pointers from the start until you find it

if you change both fields in such a struct to void pointers, you get the humble cons cell, the famously versatile lisp primitive:

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

cons cells are flexible enough to construct essentially arbitrary data structures. the elegance of lisp comes from the fact that it represents its own code with cons cells, which are in and of themselves working parse trees. because lisp can manipulate and run lisp code as easily as it can manipulate any data, the language is perhaps uniquely suited for metaprogramming

enums are a way of representing a very simple set of mutually exclusive choices, where any enum value is one of those choices:

enum Color {
    Red,
    Green,
    Blue,
};

underneath, an enum is represented as a single integer. there is a more complex construct, called the union, which can represent choices between two different concrete datatypes, which may include structs. a common pattern, called a tagged union, combines the two, using an enum to indicate which struct variant a union represents

to step back from c, it should be appreciated that these concepts represent something more fundamental than some language's arbitrary mechanism for declaring datatypes. a struct or tuple represents a collection of different types grouped together in a particular order. a tagged union, or equivalently an enum that can carry a value (as it can in some languages), represents a mutually exclusive choice between different types. these are called product types and sum types respectively, and can be nested recursively to make types of arbitrary complexity

to wit, our c types in haskell:

data User = User { username :: String
                 , pin :: Int
                 , active :: Bool
                 }

data List = Node { x :: Int
                 , next :: Node
                 } | Nil

data Cons a b = Cons a b

data Color = Red | Green | Blue

paradigms

imperative and oop

the point of all that was to show the simple primitives that everything builds on top of. variables, functions, arrays, structs, loops, branches, pointers, and recursion. whatever complicated concepts you encounter in higher-level languages, they are more or less composed of these basic elements. often quite literally: the primary implementation language for higher-level languages is c. understanding what building blocks an author had to work with often makes it much easier to draw conclusions about how their work is likely put together

with this in mind, we will explore how to assemble a stripped-down version of the core abstractions of another style of programming, using our simple imperative toolbox

object-oriented programming is a paradigm that is easily accessible to imperative programmers because most of the popular languages for it now can be summed up as "imperative programming plus some extra stuff." there is no reason why it had to be this way. if languages like simula, smalltalk, and eiffel (especially eiffel, google "design by contract" sometime) had become the foundation of modern oop, we would be having a very different conversation right now. but, as it turned out, these first oop languages ended up serving as sources of ideas for languages otherwise grounded in the imperative paradigm

all mainstream object-oriented programming derives from c++. c++ was created as the successor to a language called "c with classes" and might itself be appropriately named "c with everything not in c." c++ is not a strict superset of c, but most reasonable c programs are valid c++ as well

java descends directly from c++, originally written to replace it in contexts where it was too unwieldy, and adopts its curlybrackets syntax and many of its basic assumptions. javascript was heavily inspired by scheme, but had a curlybrackets syntax bolted onto it, and now has oop-friendly class-based inheritance syntax to hide its more unique prototype-based internals. c#, don't let the name fool you, is microsoft java. objective c was the only language in common use that followed the spirit of smalltalk, but it was criticized and eventually deprecated in part because people found it "too weird"

if you are a new programmer, don't feel you have to understand any of the previous three paragraphs. it is more an apologia for the sweeping generalizations I'm about to make

there was a period in the 90s and 00s when java was the standard teaching language for intro cs students. this was always contentious and criticized, and I think that taking oop abstractions as a baseline truth, rather than grounding yourself in the kata I've presented, is a mistake. I usually deliver this example of oop abstractions built from imperative fundamentals as a corrective to people with "everything's an object" brainworms. the object abstraction is totalizing in oop, but once you slice it open and flip it inside out, it's easier to see as a simple organizational tool

going between seeing objects as organization schemes and objects as objects is kind of like watching the optical illusion dancer gif spin one way or the other. declarative programming, functional programming, continuation-passing style, and various other formalisms have this same kind of feel, once you know them well. you can think in them, or about them, at will, and feel that shift in your perception as you go from one to the other

fundamentally, an object is a... thing... that bundles state (variables) and functionality (functions) together. a class is a blueprint for a type of object, describing what state variables it has, and what functions, called methods, can operate on that state. and then an object is a concrete instantiation of that class, with its own storage, independent from all others

no outside entities can read from or write to an object's internal state. it is, as the parlance goes, encapsulated. methods are the sole means of access or mutation. the point of all this is to force all stateful computation through a small set of gatekeeper functions, implemented all in one place, so the program is easier to refactor (rearrange) and reason about

unfortunately, we don't have anything like classes or methods in our imperative language. we only have structs and functions

oop examples are always like, corporate database stuff, or cars for some reason, so I'm consciously fighting that impulse:

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

the typedef is just cute syntax to let us write the type as Android instead of struct Android

then with that struct definition, we can initialize new instances of such a struct:

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

again, a struct is nothing special. it's syntax for reserving the correct amount of space for a series of variables, accessing fields via pointer offsets, and some compile-time type safety checks

if we want to frequently make androids, and especially if we want to persist them and pass them around, we may want a function that does the setup for us:

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);

when you want to make a new android, you just pass in its names. the rest is standard and only needs to be written once

note that a_two is a stack-allocated struct, which means it will go out of scope when the block it was created in is exited. two_b and nine_s are stack-allocated pointers to heap-allocated structs. they can be accessed as long as a copy of the pointer is retained somewhere and are never deallocated unless explicitly freed. the strings likewise never scope out. their data is compiled directly into the binary, and the char pointers are just the address where that data resides

struct data can be modified anywhere with no constraints:

// send 2B on a mission
two_b->on_mission = true;

// 2B scores a kill
two_b->kills++;

this can make it hard to track down bugs, if your program is passing around pointers to these structs and mutating their contents anywhere. it becomes a thousand times worse if concurrency is involved because then things might get changed in the middle of other things using them

one solution to this is to be very strict with ourselves that we'll never touch struct data except in special functions written for this purpose. this gives us one place to put correctness checks so we never forget them, and one place to put debug logging when trying to diagnose a problem

we only need to write each function once. we have them accept a pointer to a struct, and then we modify whichever struct is given to us:

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 invented the basics of object-oriented programming. by enshrining our formalism in the language, we can stop viewing it as a handful of conventions employed when working with structs and functions. we can instead roll all the pieces into a single abstraction, hide the details under syntax, and think of the abstraction as the 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 kills\n", this->kills);
            }
            else {
                this->kills++;
            }
        }
};

android *two_b = new android("2B", "2E");

two_b->go_on_mission();
two_b->score_kill(false);

the above code is c++. despite that, there is nothing truly new. all the functionality is, from a user's perspective, largely the same as the preceding snippets of c

the c++ class contains both the fields of our c struct and the functions we wrote that accept a pointer to the struct as a parameter. the c++ function android is a constructor, and identical to the c function make_android, except the heap allocation is now implicit. the class constructor is invoked implicitly via the new keyword

the functions go_on_mission and score_kill likewise are the same, except now they are methods. they still take a pointer as their first argument! but this argument is hidden from the function signature, and its value is accessed by the this identifier

and finally, because the data fields are not marked public like the methods, nothing outside of the class can access them. if we were to write something like two_b->kills, it would result in a compiler error

there is so, so much more in oop, but none of it is truly fundamental. I don't mean to pick on the paradigm, but for various reasons it tends to be the worst in terms of making simple things complicated. java, in particular, became in the 00s a way to deskill programming and render the programmer fungible, and so accumulated slogans and stereotypes meant to make it easier to cycle engineers through projects

inheritance, interfaces, and abstract classes are ways to add more methods to things implicitly. operator overloading, generics, (subtype) polymorphism, overrides, and reflection are ways to make functions more flexible or mess with function dispatch. there is an entire cult of "design patterns" drawn from an ominously named book people just call "gang of four" that has an entire eldritch taxonomy of things your enterprise company boss wants you to memorize for half of an entry-level tech salary. visitor patter, observer pattern, decorator pattern, singleton, factory beans factory factory

you do not need to learn any of this shit unless a client is paying you for it. and even then, consider it an occupational hazard

all these details are not really important, and if you are already familiar with oop, this may be horribly long-winded. my actual point is this: there is nothing special about the object abstraction. it's just built up out of our fundamental kata. but when you come up with an abstraction, and you roll it up to be watertight, it lets you stop thinking about the details underneath. none of what we do as programmers would work if we had to think about the implementation details of everything all the time

the danger is that people who come along later may not know what happens underneath. they come to think in terms of the abstraction alone. so when the abstraction fails them, as it always inevitably does, they don't have the context to even understand what is happening

oop is the worst culprit of this kind of thinking because objects present themselves as literal objects. it is very seductive to see them as real things that actually exist. people come to think in the metaphor and the metaphor becomes real

I wanted to do a similar construction of functional programming. start with simple recursion, build up to maps and folds, restrict ourselves to single-assignment (immutability). but it would be another tour like this, and I hope I've made my point. no abstraction is magic, they are almost always conceptually very simple, and it can all be learned, easily, if you know the basics

the last thing I will say about oop is, if you absolutely must niche into it, go for c++, not java. java engineers are made to be replaceable. learn template metaprogramming and give yourself job security until you die

digression two

odds and ends

before getting into the last major technical section, I wanted to share some practical advice on a variety of topics in no particular order. in other words, I want to ramble

I said this is not a tutorial on how to program. so, how the hell do you learn to program?

first, I would say pick a language and stick with it until you are at least at an intermediate level. some are better than others, but in the long run it doesn't matter. there is an idea held by some that starting with java ruins you, or starting with basic ruins you, or whatever. this is not true, unless you niche into it and refuse to broaden your understanding as you broaden your experience

the most important thing I want to get across with this entire article is that "programming" is nothing special. it's a form of applied abstract symbol manipulation. different programming languages do the same things in different ways, and once you get good, you can move fluidly between them. if you have a faculty, whether innate or acquired, for abstract symbol manipulation, you can learn to program

an important collorary to this is that if you can learn to program, you can learn tons of other things just as easily. there are thousands of opinionated programmers online who every day insist on presenting themselves as living counterexamples to this assertion, but nevertheless I am resolute. the only reason why programming is so commonly self-taught is because it is a system that is unusually suited to trial and error by people with no specialized knowledge or specialized equipment. once you master your learning style, as long as you are bright and curious and determined, you can learn anything

so, get reasonably good at one language before branching out. but, do branch out! when you know a language, you think in terms of that language. once you know several in one paradigm, you think in terms of the paradigm. once you know the paradigms, you can just think in programming

this attitude does not come for free. you need to be able to actively relate material across the conceptual gaps, or you will be stuck in these specialized silos like so many people are. this is how college teaches you to think: in terms of tracks and prerequisites

maybe some people really do think best like this. maybe most! I don't want to make any normative claims. but if you have stuck with my bullshit this long, we are probably enough alike that I can be a bit presumptuous

the way I catalog knowledge is more like one big graph. giant, sprawling, with no organization to it, interlinked loosely in some places and tightly in others. when I encounter new topics, I never really worry if I "don't get" most of it. I just look for ways I can relate it back to my graph. if I read a text outside of my depth and barely understand it, it still lays groundwork, a skeleton structure that I can later flesh out with more material

I think the root of creativity is not inventing novel ideas out of nothing, but simply drawing novel edges on a large graph. I think a lot of adult loss of neuroplasticity, adult retreat to conservatism, adult inability to enjoy new art or music, all these various hatreds of newness are just people who decided to stop learning, to stop adapting, and thus lost the ability to do so

I think the "software engineer" sans specialty is gradually losing ground and will be relegated to low status clerical labor within our lifetimes. tooling has made great strides toward the deskilling of rote implementation and maintenance work, and human-in-the-loop systems may well finish the job. the world will always need more crud apps and saas platforms, and it gets easier for the average dev to 80/20 their way to something serviceable enough every day. many professional programmers actually contribute negative value to their orgs. it's just, in a large enough company, it's hard to tell who they are, and no one has incentive to care anyway

but that's for making more versions of things that basically already exist. for new things, for science and art and novel engineering, I think the norms surrounding programming will become more like writing. a tool of expression, a tool you use to enact your knowledge about some procedural or scientific topic. the means, not the end

it can already be used this way, of course, even if the majority of code now written is shitwork. code is not the next mass literacy. most people will never learn, although they may learn just enough to stumble through running the machines that others invented. my expectation, or maybe my hope, is that it can at least be the next latin, the next classical chinese. a tool for a minority, but a large minority, to express their wills in a way that transcends their native culture

the 21st century will have two lingae francae. english, to say. and code, to do

I think the future belongs to people who can use code to gain leverage on the world and to create new things that are more than just software. I think being a domain expert who can program well will be more valuable than a great programmer with no other talents, or a specialist who is incompetent in software. I think being a great programmer, but also able to work across multiple fields, bridge gaps, draw novel inferences, is and will continue to be a superpower

so be good to your graph

anyway. the overarching theme I have been trying to impart with the kata idea is simple: feedback, response. the tighter you make the feedback-response loop, the faster you can work, the faster you can think, the more natural and effortless everything feels. the exact same principle applies to debugging

no one really teaches you how to debug. you just kind of have to figure it out. this is a shame, because debugging is conceptually pretty simple

I think what happens is people learn to program, work by themselves on programs small enough to keep in their heads, and can get by debugging most things just by thinking really hard. then, eventually, they get a job working on a large codebase, or their projects get too big for them to track, or attract contributors, and they are suddenly forced to sink or swim. they then throw together the basic principles of debugging from scratch to solve their problem and then forget they ever didn't know it

there is a remarkable story by dan luu that I think about often, about a computer engineering class. this was infamous as a weed-out class, separating the men from the boys, and was feared for its legendary difficulty. the author breezed through most of it on talent, while students failed left and right at each turn, until the class final which overwhelmed him as well. struggling into the night before the deadline, making no progress, eventually it turned out the only real challenge was to invent debugging. in effect, the kids who realized in time you could systematically test every connection to find the one that needed to be fixed, passed. the rest failed. no one had ever taught them to do this

there are all kinds of fancy tools for debugging, and many of them are quite good, but the basic principle is again: feedback, response. for this, the easiest place to start without having to learn anything new is the humble print statement

my first "real" job, after writing little crud toys in the single-digit-thousands of lines of codes, was a 1.5 million line, 15-year-old, object-oriented perl codebase. if that means nothing to you, know that everyone else winced. printf was my best friend. just put that shit anywhere. print the value that's changing but shouldn't, that should change but doesn't, just scatter prints until you find where it happens. when you find it, print more

change stuff just to see what changes as a result. and you can see if it changes by printing it. imagine you are the world's dumbest scientist. you don't need a strong hypothesis if you can run a new experiment every 20 seconds. feedback, response

don't try to analyze code if it feels overwhelming. the most important skill you learn on your first big codebase is how to not read code that isn't important. just relax, focus on the one thing you care about, and do anything that gives you more information about it

if building and running the code takes too long, try and find a simpler case that produces the same error. print stack traces so you can see where things are coming from. just iterate, focus on getting more information, faster, and narrowing your focus. feedback, response

use version control. that means use git. you can develop niche opinions about weird version control systems when you are old and wizened. (hopefully by that time the pijul rewrite will be usable, and we can share a beer over that...)

git is very simple. in your code directory, git init makes a repository. git add FILENAME stages a file for commit, ie does nothing but you have to do it anyway. git commit creates a new commit with your staged files, which is like a checkpoint of where you're at. git remote add origin URL sets up the place where your repository get stored for safekeeping/sharing. git push origin BRANCH where branch is probably main or master puts your repository on the internet. if you ever have problems, copy the entire project directory somewhere for safekeeping before you fuck around with it. you now know more about version control than the typical cs undergrad who didn't just self-teach already

version control is a gamechanger for various reasons and I don't understand how nontechnicals live without it. you have backups of everything, files are error-checked for consistency, you can see when changes were made, you can describe why you made those changes, you can experiment with changes and back them out trivially. use it

text editors. you can learn vim or emacs if you want. I use vim and would die without it. but honestly at this point sublimetext and vscode are fine and don't have the same steep learning curve. people rag on ides partly out of elitism and partly because ides used to be horrible monolithic corporate shit. sublimetext and vscode are fine

use linux. ok, ok, you can use osx if you really insist, but don't blame me when getting software to work is annoying. don't use windows, you're making your life hell for no reason. in software, we distinguish between normal places, and "windows shops." everything important targets linux first, so everything on windows is completely different, and almost always inferior

as for what distro, ubuntu is fine. I could bitch endlessly about it, but if you're new to unix, ubuntu is fine. you can develop strong opinions about linux distros much earlier than you can about version control, don't worry

this is not to say linux is strictly the best os for everything. linux sucks for most people. telling designers to get off osx or non-tech business guys to get off windows is nonsense. their platforms work better for them, they have mass adoption, they have all the best tools for those workflows. switching would give them a vastly inferior experience, for what, bragging rights? among people they don't have any reason to want to impress? but for software work, linux is just the best. there's no debate here

learn the command line. it's a fucking pain in the ass to get started, everything is inscrutable because all this stuff was thrown together by a group of guys in the 70s at one company for one machine, not expecting it to be the basis of all modern computing. the original unix clock ran at 60hz and would have rolled over after 2.5 years if they hadn't changed it, that's how far ahead they were thinking. sorry, this is just how it is. the command line is too useful to neglect

the best way to learn to program, I think, is by coming up with some small project that seems tractable and figuring out whatever you need to solve it. this is easier said than done, because new programmers have the hardest time of anyone at thinking up things to attempt and judging whether they're doable. once you are are good at programming, you will have literally hundreds of ideas of things to do but lack the time to do more than two or three at once. so enjoy it while it lasts

javascript or python are probably the most reasonable beginner languages. they both have their quirks, but they have a wealth of learning material, and most importantly, comprehensive library support for anything you might want to do. following examples in a book to make the console print sunny or cloudy based on random number rolls bores most people to tears. you want to build something that you enjoy, that makes you feel like you've created something of value, no matter how silly or small. and then just build up from there. feedback, response

kata three

registers, jumps, calling convention

it was fun to take apart the object-oriented paradigm and see how someone raised in its bosom may be seduced by it abstractions. unfortunately, we have been seduced as well. objects, methods, inheritance, and design patterns are not real things. but neither are variables, loops, structs, or functions. it's abstractions all the way down

programming languages are a set of techniques provided for the benefit of the human programmer. but to run them, they must be compiled to machine code, which is nothing but a long sequence of binary digits. interpreting these number streams is famously difficult for humans, though not impossible. programmers who wrote raw binary or could read coredumps in hex are part of the lore of the field. our culture-heroes

a set of human-readable mnemonics for machine code, plus a handful of simple labor-saving shortcuts, is called an assembly language. assembly was one of the first inventions for making programming more efficient for human operators, before compilers and before high-level languages. examples in assembly suffice to demonstrate what is "really" going on underneath the code, to further develop intuitions on what is possible in computers, and to develop more flexibility in adapting to new systems

I firmly believe every high-level programmer worth their salt needs to understand c, and every c programmer needs to understand assembly. the mediocre dive into their abstraction and make a career out of it and it alone, shutting their eyes to anything outside their walled garden. but to actually be good, you need to know what's happening underneath your feet

machine code is not the last abstraction in the pile. but below there you get into hardware engineering, and below that electrical engineering. these abstractions are not airtight either. but they're outside my paygrade. google "rowhammer attack" or "spectre attack" or "acoustic cryptanalysis" sometime if you want to peer into the void

on a basic level, a computer can be thought of as a cpu that executes instructions, a set of boxes called registers that hold one value each, and a long, homogeneous series of bytes that constitute main memory

"arrays" are a shorthand for manipulating sections of memory that happen to be laid out in a particular way. "structs" are likewise a convention imposed on homogenous and undifferentiated memory. "stack" and "heap" are conventions. "buffers" are conventions too: malloc and free get sections of memory from the operating system, and annotate them with metadata, but writing outside of them is exploitable, not impossible. even "code" is just bytes in memory

when you invoke a program, an operating system program called the loader puts all its data into memory. the program has an entrypoint, which is an address to start interpreting code from. instructions are executed in sequence, one after the other, with the current execution address being stored as a value in a register called the instruction pointer (ip) or program counter (pc). the cpu executes whatever is pointed at and moves the pointer to the next instruction. branch instructions simply modify the ip register, indirectly causing the cpu to execute instructions somewhere else instead

the distinction between "code" and "data" is arbitrary, only enforced as a safety mechanism after a series of famous exploits that revolved around tricking programs into executing user-supplied data as code. taking control of the instruction pointer is the same as taking control of the entire program. it's all just bytes in memory

revisiting our original c examples, we can see how some simple operations compile (with optimizations disabled) down to assembly:

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

corresponding to:

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 is a lot new here to understand

values in assembly are typically rendered in hexadecimal, for human convenience. hex is a base-16 counting system, with digits ranging 0-9 followed by a-f. that is, 0x9 is 9, 0xa is 10, 0xf is 15, and 0x10 is 16. the 0x prefix is a convention indicating what counting system we're using. hex is convenient because a digit represents exactly four bits, so two digits is always a byte

the numbers in the first column are addresses. just the number of bytes from some fixed 0 address, much like main memory. the second column with individual hex digit pairs is the exact machine code in the binary. note the third line has three bytes in the second column and an address ending in 19. the address on the next line ends in 1c, three bytes higher

the third and fourth columns are mnemonics for the machine code. there's only two kinds of instruction in this simple example, mov and add

mov is an entire spectrum of instructions that (contrary to the name) copy data from one location to another. in this syntax, the destination is to the left of the comma, much as the assigned variable in c is to the left of the equals. the first two lines move the hardcoded values 2 and 3 into particular locations on the stack, identified by offsets from the base pointer, rbp, a register that contains the address of the bottom of the current stack frame

the third line copies the value of b into a general-purpose register called eax. the reason for this is add can only operate on a register. the next line adds the value of a into the eax register, and the final line copies that value back to b on the stack

aside from the instructions themselves, we also use three different kinds of operands. the source operands on lines one three and five are 0x2, DWORD PTR [rbp-0xc], and eax respectively. the first is an immediate, ie the number 0x2 simply represents the value 2. the second is an indirect memory access: indirect in the exact same way as indirection. an address is calculated as an offset from the base pointer and dereferenced to produce a value. the third is a register name, and the value is retrieved from that register

and finally, note that this and all assembly examples are in what's called intel syntax. there is an alternate syntax called at&t that looks rather different, and has operands in the opposite order. but it is just a way to display machine code for human convenience. the machine code bytes are exactly the same

blocks and higher-level flow-control constructs don't exist in assembly. all flow control compiles down to simple jumps:

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

corresponding to:

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

here, on the first line, a comparison is performed between the value in a and the number 2. this comparison sets a bit in a special register called the status register, which is evaluated by the jle instruction. jle is a conditional jump, "jump if less or equal," which skips to the assignment of 4 on the last line of the assembly. if the jump is not taken, ie if a's value is greater than 2, control simply proceeds to the next line. the assignment of 3 is performed, and then an unconditional jump skips past the assignment of 4

a loop looks roughly similar:

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

corresponding 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 

the loop begins on line two, and three instructions perform the add, and a comparision and conditional jump determine whether to repeat the action. a while loop, by contrast, would perform the comparison up top and jump past the end if it failed

it is worth noting that c actually has a jump of its own, called goto. it is unfairly maligned

the last thing to understand here is that the function, just like the branch or the loop, is just another kind of jump:

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

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

corresponding 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 
40113e:	89 45 f4             	mov    DWORD PTR [rbp-0xc],eax

the main block is straightforward. 5 is stored on the stack, then copied to the register edi for argument passing, and then our timestwo function is called. call is a special kind of unconditional jump that first pushes a return address onto the stack

and this is the assembly for the timestwo function, in its entirety, including setup/teardown I've been skipping:

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    

the first thing a function does is store the current base pointer on the stack and reset the base pointer to the current stack pointer. it then copies the value in edi to the stack. in other words, function arguments are just normal stack variables. so far, this has all been standard setup for a function to execute. how this is done is basically arbitrary, as long as everything does it the same way. the set of steps taken to set up and tear down a function call on a particular platform is known as its "calling convention"

from there, the actual computation proceeds. the multiplication by 2 is performed by a left shift operator, moving all bits to the left by one, effectively doubling the number. that is, multiplication by 2 in decimal is equivalent to multiplication by 10 in binary. the result is moved into eax, the register used for a function's single return value. and then the caller's base pointer is restored to rbp and the address stored on the stack is popped and jumped back to with ret

as an aside, every function call adds a stack frame, and if you run out of space, the program will either preemptively crash, or attempt to write past its bounds and be killed by the operating system with an error like "segmentation violation." overrunning the stack space is called a stack overflow, and in general invalid memory access is called a segfault. but, interestingly, some functional programming languages have a feature called tail-call optimization. these languages can detect that some recursive sequences don't actually need their stack frames, and optimize them to a simple loop. a series of jumps modifying a value in a register. it's all just jumps

this has been a lot of information to dump. if there is any section of this rather long post that is ok not to grasp, it's this one. but with this knowledge of the underlying systems, we can demystify the abstractions above, and also understand why certain things work the way they do

a machine register typically holds a single machine word. on x64 this is a qword, which is eight bytes. sometimes programmers from higher-level languages wonder if you can return a struct, or an array, or multiple values in c. this makes it obvious why you cannot: a register is used, so the only possible return is a single value. if a higher-level language wants to return something special, it probably returns a pointer and fixes up some things behind the scenes, just as call and ret fix up things of their own

it also explains why data does not persist on the stack, and why arguments are primitive values or pointers, and why the maximum addressable memory of a system is determined by its word size. again, I hammer one of my central points: it's not about understanding the particular details of a particular system. it's about simultaneously being able to think in terms of an abstraction which is inherently meaningless, and look beyond it at lower abstractions which are themselves meaningless

to not get trapped in the web, but to glide over it, as the spider, and in due time spin your own

further reading

I said in a previous section that javascript or python are fine starting languages. eloquent javascript seems like a decent enough resource for the former. it doesn't assume any existing knowledge, and I particularly like how it goes out of its way to provide background information. I don't know any python resources to recommend them offhand

another path is the classic of mit students of yore: sicp. sicp starts you in a simple dialect of scheme and has you off and programming in under an hour. the book is a classic, so you can google "sicp scheme" to find a myriad of easy to install execution environments for it. scheme's core syntax can be learned in an afternoon. the material it presents is accessible, yet deceptively advanced. it tries to follow an approach like I do, of starting with basics and building concepts up out of those simple pieces

scheme was replaced by java as the primary teaching language because of perceptions that scheme was "useless" and that languages used in industry were more "practical." you will probably never have to write scheme in a job. but my point, as always, is that you must be able to generalize concepts across boundaries. if you can't do that, then just learn what you need to eat and try to stay afloat

for a certain type of person, c is a perfectly fine language to start with. if you go that route, there is still no better resource than k&r. I don't recommend starting with c unless you're strongly drawn to it, but if you are, don't let anyone talk you out of it

I think most people want to learn top-down, making useful things that amuse them, picking up fundamentals as they go. for them, javascript or python. but some people don't care about seeing immediate results, and are more gratified by understanding the system from the start, and will learn higher-level concepts when they need to. for them, scheme or c

if you want to learn about type theory, tapl is a great intro. it starts with maybe the simplest programming language on earth, the untyped lambda calculus, and builds up types brick by brick on top of that foundation

if you want to learn operating systems, my favorite resource is ostep. broadly it's structured around the tentpole topics of virtualization, concurrency, and persistence, and goes into extensive technical detail on all of them

if you want to learn cs fundamentals, I don't have a book in mind but you can google "algorithms and data structures" and find something probably. honestly I just learned that stuff as I went, and I'd only bother to cram it if I needed to pass a meme whiteboard interview

neither do I have a text in mind on formal grammers and parsing them, but this is a topic that ought to be understood by more working programmers than it is. you don't need to be able to implement an efficient parser (until suddenly you do), but you should understand the chomsky hierarchy and its correspondence to computational complexity

I also can't recommend anything for ops or infosec, though I can't imagine learning these topics in any way except by doing them

as for hands on material, by far my favorite thing to recommend is microcorruption. it has you writing exploits for a 16-bit microcontroller, giving you a grand tour through low-level hacker shit, set up perfectly to teach it. the final test is fiendish, and will make you do more work than even feels reasonable, but the feeling of conquest when you beat it is unmatched

I also very much like cryptopals, which walks you through breaking practical cryptosystems in a long series of challenges. many of these will require you to write a lot of code, and to think very hard, but other than reasonable cleverness and a bit of algebra, there are no real prereqs to tackle these

a lot of people enjoy advent of code, an annual series of 25 toy problems to solve in code, with answer-checking on the site. the 2021 challenges are available to play still. this is how I learned a good deal of cs fundamentals: by having to reinvent basic algorithms and data structures to cope with problems designed to be computationally intractable any other way. I found the challenges fun before I had the background knowledge to know how to solve them, but tedious after. I suspect the only reason to do these if you know cs is to compete on time or pad a github for job applications

the last fun code thing I like is nand2tetris, which starts you at an even lower level than I finished on. you build the jenga pagoda level by level, starting by putting together boolean logic out of nand gates, then an adder and flash memory, a toy cpu, an assembler for a basic assembly language, a compiler for a simple oop language, and then finally programs in that language you build with your compiler. depending on how much you already know about compilers, the later stages can get tedious, but it's fine to start at the beginning and quit when you get bored

there is a whole host of cultural material I could link, but I will limit myself as best I can. if you are an outsider to software, and get good at it, there is still a complex signalling game to be played so that people know you're good at it. I used to think this was not the case, because acceptance seemed to come very naturally and easily to me, but I didn't understand then that acting like you're not playing a signalling game is part of the game

there's a lot to digest if you want to look like you "belong," and this isn't something I can guide you through. it's not strictly necessary to participate in tech culture or open source, but it does make getting hired easier. I don't know how to tell people how to break into tech because the first job is the hardest to get and every "non-traditional" path is different and impossible to follow exactly. I can't say "here is how to do it." the best I can offer is "here is how to be the kind of person something like it can happen to"

the important thing to remember is that it's not enough to be valuable. you must be legibly valuable. this trips up people who are not used to cultivating an appearance

but to link three cultural gems, chosen for their merit and by my love for them, rather than base utility: the most important far is esr's hacker howto. a short tour of the culture and the spirit of the work, it is a classic, and I believe the core of it will never become obselete. the attitude of curiosity, tenaciousness, freedom, and boredom he espouses is truly the golden path

the second is dijkstra's classic lecture on the cruelty of really teaching computer science. dijkstra is always erudite and a purist at heart. here he presents a very different vision of programming than what most of us practice, one grounded in formal methods and mathematics. I suspect he would have nothing but contempt for my primitive and slovenly ways. read him to expand your mind

and the third is in the beginning there was the command line by neal stephenson. a sprawling nonfiction tour through old school computing and then-modern operating systems, but also much more. the difference between tools that feel powerful and tools that are powerful. the things that are hidden away from most people, behind a curtain that we are preparing to sneak behind. the proper attitude to take when designing a world

I hope this post proves valuable to some. feel free to shower me with love or hate via twitter or email. if you're reading this because I linked it to you personally, I believe you can do it. but whether I linked it or not, may fortune be with you in all your endeavors