Serious question: As someone whose never used ed but uses vim on a daily basis, is it worth learning ed? I’m not a regex master by any means but I feel like I grok them enough that I wouldn’t want to learn ed as a way to get better at regexes (emphasized in the description).
No, I don’t think learning ed will make you better at regexes. For me it’s helpful to know if you ever find yourself in an environment where you don’t have access to other tools like vi. Ie, the OpenBSD install media or possible single user mode on some gnu/linux distro.
Agree with the larger point, but have trouble imagining the first suggestion being maintainable – projects already have trouble keeping docs up to date and keeping feature nubs (which are likely tightly tied to other implementation details) around seems quite difficult.
Also not sure the initiative would be there on the user side, given the article’s (accurate) statements around people mostly choosing to open up a codebase when they need it to do something it doesn’t already
..projects already have trouble keeping docs up to date and keeping feature nubs around seems quite difficult.
It shouldn’t be hard to check for consistent feature nubs in CI. And really that’s all it takes. Python’s doctests have shown that it is possible for documentation to be kept up to date. The mainstream now finds updating code without tests to be unacceptable. Outdated documentation will hopefully soon get the same bar. Maybe it takes a little extra effort, just like it takes extra effort to write tests. But both yield dividends in the long term.
not sure the initiative would be there on the user side, given the article’s (accurate) statements around people mostly choosing to open up a codebase when they need it to do something it doesn’t already
OP doesn’t take a position on whether the desire people crack open a codebase for is small or large. I’ve certainly tried many times to navigate the code for open source projects like gcc, vim and firefox. I wasn’t expecting it to be easy. I would totally have accepted subgoals of understanding something over a few weeks.
From my understanding feature nubs would need to be coupled to implementation and in a separate branch/commit/etc. Folks have a lot of trouble keeping functional patches up do date with master branch (e.g. a lot of the work neomutt has had to do). Asking a OSS team (whose code constitute most of the code one might read) to do so seems like it might be a pretty large burden on top maintaining a project.
Well he may not talk about the number of people who want to read code (not positive I got what you’re saying there), but he does open with “Most programmers agree that we don’t read enough code”. Yes, but it is important to note that you are someone who cares about reading code (you hosted this article and write about this on your website). I think once you look at the subset of people who read code, then the subset of those who might do these excercises, and then the subset of those who would go on to contribute to the project, it may start to feel like a pretty high cost.
There’s room for maneuver with the tooling. @smalina and I hack on a project written in a form of Literate Programming where every feature is in a separate file. Just one example of how it’s possible to reduce the management burden for such scaffolding. If we decide it’s useful we can solve these problems.
Even with a separate branch, it should be useful even if it’s not up to date with master, right? Better than nothing? I think we make bigger compromises everyday with conventional workflows.
I think once you look at the subset of people who read code, then the subset of those who might do these excercises, and then the subset of those who would go on to contribute to the project, it may start to feel like a pretty high cost.
If you start with the premise that the goal is to combat personnel churn, it may well be worthwhile. Think of companies or open source projects as pipelines turning smart noobs into experts on their code.
Paraphrasing myself, I’m much less sure of the solution than I am of the problem. I apologize that I’m about to address your points out-of-order but I think your second point is actually more general.
On the topic of the subset of people who would actually go to the trouble to go through a project’s exercises, I’m as skeptical as you are that any of this will turn un-motivated people into avid code explorers. But that’s OK with me! If I can recommend things that actually make reading programs more pleasant for people like myself, akkartik, you, and many other Lobsters readers I suspect, that’s a huge win in my book.
You make another good point that it’ll be hard to get maintainers to write exercises or “maps”. I agree that getting people to keep normal project docs up-to-date is hard already, and if I were arguing maintainers should just do more documenting and more explaining it would be a pipe dream (it may still be). However, what I may not have emphasized enough in the post is that I think it would be a net win if some projects did less module-level documenting in exchange for more high-level mapping and commit pointing. In other words, same amount of work just a different emphasis. Two other points about commit katas as I imagine them:
I think it comes down to, if someones reading your code, they’re trying to fix a bug, or some other wise trying to understand what it’s doing. Oddly, a single, large file of sphaghetti code, the antithesis of everything we as developers strive to do, can often be easier to understand that finely crafted object oriented systems. I find I would much rather trace though a single source file than sift through files and directories of the interfaces, abstract classes, factories of the sort many architect nowadays. Maybe I have been in Java land for too long?
This is exactly the sentiment behind schlub. :)
Anyways, I think you nail it on the head: if I’m reading somebody’s code, I’m probably trying to fix something.
Leaving all of the guts out semi-neatly arranged and with obvious toolmarks (say, copy and pasted blocks, little comments saying what is up if nonobvious, straightforward language constructs instead of clever library usage) makes life a lot easier.
It’s kind of like working on old cars or industrial equipment: things are larger and messier, but they’re also built with humans in mind. A lot of code nowadays (looking at you, Haskell, Rust, and most of the trendy JS frontend stuff that’s in vogue) basically assumes you have a lot of tooling handy, and that you’d never deign to do something as simple as adding a quick patch–this is similar to how new cars are all built with heavy expectation that either robots assemble them or that parts will be thrown out as a unit instead of being repaired in situ.
You two must be incredibly skilled if you can wade through spaghetti code (at least the kind I have encountered in my admittedly meager experience) and prefer it to helper function calls. I very much prefer being able to consider a single small issue in isolation, which is what I tend to use helper functions for.
However, a middle ground does exist, namely using scoping blocks to separate out code that does a single step in a longer algorithm. It has some great advantages: it doesn’t pollute the available names in the surrounding function as badly, and if turned into an inline function can be invoked at different stages in the larger function if need be.
The best example of this I can think of is Jonathan Blow’s Jai language. It allows many incremental differences between “scope delimited block” and “full function”, including a block with arguments that can’t implicitly access variables outside of the block. It sounds like a great solution to both the difficulty of finding where a function is declared and the difficulty in thinking about an isolated task at a time.
It’s a skill that becomes easier as you do it, admittedly. When dealing with spaghetti, you only have to be as smart as the person who wrote it, which is usually not very smart :D.
As others have noted, where many fail is too much abstraction, too many layers of indirection. My all time worst experience was 20 method calls deep to find where the code actually did something. And this was not including many meaningless branches that did nothing. I actually wrote them all down on that occasion for proof of the absurdity.
The other thing that kills when working with others code is the functions/methods that don’t do what they’re named. I’ve personally wasted many hours debugging because I skipped over the funtion that mutated that data it shouldn’t have, judging from it’s name. Pro tip; check everything.
Or you can record what lines of code are actually executed. I’ve done that for Lua to see what the code was doing (and using the results to guide some optimizations).
Well, I wouldn’t say “incredibly skilled” so much as “stubborn and simple-minded”–at least in my case.
When doing debugging, it’s easiest to step through iterative changes in program state, right? Like, at the end of the day, there is no substitute for single-stepping through program logic and watching the state of memory. That will always get you the ground truth, regardless of assumptions (barring certain weird caching bugs, other weird stuff…).
Helper functions tend to obscure overall code flow since their point is abstraction. For organizing code, for extending things, abstraction is great. But the computer is just advancing a program counter, fiddling with memory or stack, and comparing and branching. When debugging (instead of developing), you need to mimic the computer and step through exactly what it’s doing, and so abstraction is actually a hindrance.
Additionally, people tend to do things like reuse abstractions across unrelated modules (say, for formatting a price or something), and while that is very handy it does mean that a “fix” in one place can suddenly start breaking things elsewhere or instrumentation (ye olde printf debugging) can end up with a bunch of extra noise. One of the first things you see people do for fixes in the wild is to duplicate the shared utility function, and append a hack
or 2
or Fixed
or Ex
to the function name and patch and use the new version in their code they’re fixing!
I do agree with you generally, and I don’t mean to imply we should compile everything into one gigantic source file (screw you, JS concatenators!).
I find debugging much easier with short functions than stepping through imperative code. If each function is just 3 lines that make sense in the domain, I can step through those and see which is returning the wrong value, and then I can drop frame and step into that function and repeat, and find the problem really quickly - the function decomposition I already have in my program is effectively doing my bisection for me. Longer functions make that workflow slower, and programming styles that break “drop frame” by modifying some hidden state mean I have to fall back to something much slower.
I absolutely agree with you that when debugging, it boils down to looking and seeing, step by step, what the problem is. I also wasn’t under the impression that you think that helper functions are unnecessary in every case, don’t worry.
However, when debugging, I still prefer helper functions. I think it’s that the name of the function will help me figure out what that code block is supposed to be doing, and then a fix should be more obvious because of that. It also allows narrowing down of an error into a smaller space; if your call to this helper doesn’t give you the right return, then the problem is in the helper, and you just reduced the possible amount of code that could be interacting to create the error; rinse and repeat until you get to the level that the actual problematic code is at.
Sure, a layer of indirection may kick you out of the current context of that function call and perhaps out of the relevant interacting section of the code, but being able to narrow down a problem into “this section of code that is pretty much isolated and is supposed to be performing something, but it’s not” helps me enormously to figure out issues. Of course, this only works if the helper functions are extremely granular, focused, and well named, all of which is infamously difficult to get right. C’est la vie.
Anyways, you can do that with a comment and a block to limit scope, which is why I think that Blow’s idea about adding more scoping features is a brilliant one.
On an unrelated note, the bug fixes where a particular entity is just copied and then a version number or what have you is appended hits way too close to home. I have to deal with that constantly. However, I am struggling to think of a situation where just patching the helper isn’t the correct thing to do. If a function is supposed to do something, and it’s not, why make a copy and fix it there? That makes no sense to me.
It’s a balance. At work, there’s a codebase where the main loop is already five function calls deep, and the actual guts, the code that does the actual work, is another ten function calls deep (and this isn’t Java! It’s C!). I’m serious. The developer loves to hide the implementation of the program from itself (“I’m not distracted by extraneous detail! My code is crystal clear!”). It makes it so much fun to figure out what happens exactly where.
A lot of code nowadays (looking at you, Haskell, Rust, and most of the trendy JS frontend stuff that’s in vogue) basically assumes you have a lot of tooling handy, and that you’d never deign to do something as simple as adding a quick patch
I do quick patches in Haskell all the time.
Ill add that one of the motivations of improved structure (eg functional prigramming) is to make it easier to do those patches. Especially anything bringing extra modularity or isolation of side effects.
I think it’s a case of OO in theory and OO as dogma. I’ve worked in fairly object oriented codebases where the class structure really was useful in understanding the code, classes had the responsibilities their names implied and those responsibilities pertained to the problem the total system was trying to solve (i.e. no abstract bean factories, no business or OSS effort has ever had a fundamental need for bean factories).
But of course the opposite scenario has been far more common in my experience, endless hierarchies of helpers, factories, delegates, and strategies, pretty much anything and everything to sweep the actual business logic of the program into some remote corner of the code base, wholly detached from its actual application in the system.
I’ve seen bad code with too many small functions and bad code with god functions. I agree that conventional wisdom (especially in the Java community) pushes people towards too many small functions at this point. By the way, John Carmack discusses this in an old email about functional programming stuff.
Another thought: tooling can affect style preferences. When I was doing a lot of Python, I noticed that I could sometimes tell whether someone used IntelliJ (an IDE) or a bare bones text editor based on how they structured their code. IDE people tended (not an iron law by any means) towards more, smaller files, which I hypothesized was a result of being able to go-to definition more easily. Vim / Emacs people tended instead to lump things into a single file, probably because both editors make scrolling to lines so easy. Relating this back to Java, it’s possible that everyone (with a few exceptions) in Java land using heavyweight IDEs (and also because Java requires one-class-per-file), there’s a bias towards smaller files.
Yes, vim also makes it easy to look at different parts of the same buffer at the same time, which makes big files comfortable to use. And vice versa, many small files are manageable, but more cumbersome in vim.
I miss the functionality of looking at different parts of the same file in many IDEs.
Sometimes we break things apart to make them interchangeable, which can make the parts easier to reason about, but can make their role in the whole harder to grok, depending on what methods are used to wire them back together. The more magic in the re-assembly, the harder it will be to understand by looking at application source alone. Tooling can help make up for disconnects foisted on us in the name of flexibility or unit testing.
Sometimes we break things apart simply to name / document individual chunks of code, either because of their position in a longer ordered sequence of steps, or because they deal with a specific sub-set of domain or platform concerns. These breaks are really in response to the limitations of storing source in 1-dimensional strings with (at best) a single hierarchy of files as the organising principle. Ideally we would be able to view units of code in a collection either by their area-of-interest in the business domain (say, customer orders) or platform domain (database serialisation). But with a single hierarchy, and no first-class implementation of tagging or the like, we’re forced to choose one.
Storing our code in files is a vestige of the 20th century. There’s no good reason that code needs to be organized into text files in directories. What we need is a uniform API for exploring the code. Files in a directory hierarchy is merely one possible way to do this. It happens to be a very familiar and widespread one but by no means the only viable one. Compilers generally just parse all those text files into a single Abstract Syntax Tree anyway. We could just store that on disk as a single structured binary file with a library for reading and modifying it.
Yes! There are so many more ways of analysis and presentation possible without the shackles of text files. To give a very simple example, I’d love to be able to substitute function calls with their bodies when looking at a given function - then repeat for the next level if it wasn’t enough etc. Or see the bodies of all the functions which call a given function in a single view, on demand, without jumping between files. Or even just reorder the set of functions I’m looking at. I haven’t encountered any tools that would let me do it.
Some things are possible to implement on top of text files, but I’m pretty sure it’s only a subset, and the implementation is needlessly complicated.
IIRC, the s-expr style that Lisp is written in was originally meant to be the AST-like form used internally. The original plan was to build a more suggared syntax over it. But people got used to writing the s-exprs directly.
Exactly this, some binary representation would presumably be the AST in some form, which lisp s-expressions are, serialized/deserialized to text. Specifically
It happens to be a very familiar and widespread one but by no means the only viable one.
Xml editors come to mind that provide a tree view of the data, as one possible alternative editor. I personally would not call this viable, certainly not desirable. Perhaps you have in mind other graphical programming environments, I haven’t found any (that I’ve tried) to be useable for real work. Maybe you have something specific in mind? Excel?
Compilers generally just parse all those text files into a single Abstract Syntax Tree anyway
The resulting parse can depend on the environment in many languages. For example the C preprocessor can generate vastly different code depending on how system variables are defined. This is desirable behavior for os/system level programs. The point here is that in at least this case the source actually encodes several different programs or versions of programs, not just one.
My experience with this notion that text is somehow not desireable for programs is colored by using visual environments like Alice, or trying to coerce gui builders to get the layout I want. Text really is easier than fighting arbitrary tools. Plus, any non text representation would have to solve diffing and merging for version control. Tree diffing is a much harder problem than diffing text.
People who decry text would have much more credibility with me, if they addressed these types of issues.
That’s literally true! I am work with some of the old code and things are really easy. There are lots of files but all are divided into such an easy way.
On the other hand, new project that is divided into lots of tier with strick guidelines, it become hard form me to just find a line from where bug occur
I’m hearing echoes of Seymour Papert’s theories of psychological constructivism: https://en.wikipedia.org/wiki/Constructionism_(learning_theory)
Indeed. Also Peter Naur’s “programming as theory building”. There’s a CS subculture here that is woefully under-appreciated.
Yes! I’m less familiar with Constructionism itself, but I love Mindstorms.
I noticed that the author mentions TLA+ on his about page and was surprised he didn’t mention it in the post. For the lobsters users who have some experience with TLA+, does it lend itself to being handwritten? I understand it’s not equivalent to code logic, but I’m curious.
It’s pretty easy to write TLA+ operators, especially given the number of mathematical symbols it uses, but I wouldn’t say it’s better handwritten than typed. Operators are somewhat harder, pluscal and models are moderately harder. In contrast, I find it a lot easier to write J programs by hand and then type them up vs typing them in from the start.
Thanks for the reply. I went and read one of Lamport’s papers where he describes TLA+ [0] in the meantime and it seems like there’s another benefit to working with TLA+ on the computer, the ability to drill-down or up on sections of a proof. Not sure how big a difference it makes in practice.
Oh, the big benefit to writing TLA+ on the computer is that you can use the TLC model checker to find spec violations, which IMO is what makes TLA+ worth learning in the first place. Having a clean way to express systems one thing, having a clean way to validate them is quite another.
A different, more imperative approach: A paper algorithm notation, by Kragen Javier Sitaker.
I tried writing a suffix tree class with Kragen’s paper algorithm notation the other day. The problem is that suffix trees have many nested layers of ifs so I ended up running out of horizontal space and then getting frustrated. I barely understand suffix trees well enough to hold them in my head, so I’m thinking I should start by writing out something I understand better. That’ll separate the concern of developing muscle memory and fluency for the notation from the concern of improving my understanding of suffix trees.
As a side note, writing a suffix tree in J would be even more painful.
How are you reading the “Algorithm Design Manual”? Are you doing all of the exercises, a subset, or none? I’m curious because I went through it, doing some of the exercises earlier this year.
So far I’ve been sampling the exercises, pausing to do those that seem interesting. The exercises are pretty good, but I feel that I would get bogged down if I tried to do all in each chapter, hence my current strategy.
Did you finish all the chapters? I was thinking of putting up coding solutions on github as a way to archive my working. I know I will lose a notebook if I start one :/
I didn’t finish all of them. Stopped after graphs’ exercises. I wrote mine in a notebook and while I still have it, it’s not easily shareable.
How did André do this, with no tool but a pencil?
I wish the authors of this and other similar posts (this one about Knuth, ctrl-f “single compilation”) would dig deeper into how this type of thinking functions and how it relates to modern development. Does anyone on Lobsters do this for fun or as an exercise? My closest experience to this is white board interviews but that rarely involves managing sub-components of a larger design.
Disclaimer: I have only programmed in the age of relatively fast compiles and feedback cycles so punch card programming has an air of mystery surrounding it.
Agreed, there’s a lot worth investigating here.
I did something like it for fun, a very long time ago, but I was a kid, and didn’t finish the effort. I was trying to program on a pocket calculator (the TI-85), during a few weeks I spent physically isolated from other computational devices. The calculator had very bad editing capabilities, so I wound up doing a lot of the work on paper.
I think it’s realistic that people could impose similar constraints on themselves if sufficiently motivated. I don’t know if I’d recommend it; it’s incredibly tedious by today’s standards.
Haha, I have exactly the same experience, though I was writing code for the Casio FX9860. I think I may still have my sheets of hand written code somewhere around here still. :)
He just does the work in his head. It’s like math when you move from using your fingers to your head or do algebra that way. I would keep things simple, look at the structure, look at how I connected or transformed things (esp follow rules), plug in values periodically to sanity check, and keep some stuff on paper that’s either intermediate values too big for head or temporary answers.
In high-assurance security or systems, they teach us to do the same with programs using formal specifications, simplified structuring, and careful analysis of it. Started with Dijkstra with Cleanroom and others building on it. My Cleanroom link especially will help you see how it’s done without heavy math or anything.
Let’s take a quick example. I had to do a Monty Hall implementation in a hurry after losing programming to a brain injury. I chose FreeBASIC since it was close to pseudocode and memory safe. I used a semi-formal spec of requirements and initial design forcing precision. I carefully looked at value range, interfaces, and structure. Spotted a moronically-bad decision in structure that led to refactor. Rechecked. Good. Produced FreeBASIC implementation directly from specs that closely corresponded to them sometimes 1-by-1. Worked on first compile and every other run.
Dijkstra THE Multiprogramming System
Cleanroom Low-Defect Methodology
nickpsecurity, top-notch response as always.
EWD has many similar stories, even if they aren’t written with the whimsy and splendor of tvv’s André story - but his writings like the EWD#-series of papers form a masterpiece in their own right, on a Knuth TAOCP level, and sometimes more interesting for enthusiasts because of his unique and unusual philosophies.
“Selected Writings on Computing: A Personal Perspective” should be included with every computer sold as mandatory reading.
Thank you. :) Far as EWD, I’ve read some of them but not all. I should read more some time. He had a great mind. His only problem was a great ego where he’d sometimes dismiss rational arguments to take time to slam something. I thought he may have done that with Margaret Hamilton’s tool for highly-assured software. Led me to write a counterpoint essay to his since he likely either didn’t get the method or was doing the ego thing. Stranger still since it copied his and Hoare’s techniques to a degree.
His methods of declaring the behavior of functions, decomposing them, and hierarchical flow of control with no feedback loops became a cornerstone of designing highly-assured kernels for INFOSEC purposes. The method allowed each component to be tested in isolation. Then, the integration itself was more predictable. It was basically interacting FSM’s like in hardware design and verification. Hoare’s verification conditions combined with Dijkstra’s structuring is still used today for best assurance of correctness. They mostly stopped coding the stuff if not performance-critical: the code is produced by an extraction process from the specs. Others still do hand-coding against specs for efficiency with equivalence checks against specs or just partial correctness with conditions similar to EWD’s in THE Multiprogramming System. SPARK is good example of latter.
Btw, if you like reading papers like that, check out Hansen’s which are great esp in language reports. Plenty of clarity with a mix of innovation and pragmatism. He invented nucleus concept of operating systems and some others things like race-free concurrency in statically-scheduled software. Hoare stole one apparently. Related to this thread, he often wrote his programs in ALGOL on a board that he checked by hand and eye before directly translating it into assembly. They couldn’t build ALGOL compiler for those machines at the time but he said it caught more bugs you wouldn’t see in assembly alone. Clever, interesting researcher that did great work in quite a few areas.
I think there are four prerequisites to accomplishing something like this:
In most modern dev jobs, I suspect the job itself precludes one through three. Thus, four is almost impossible to evaluate, so I won’t comment on it other than to say that people have decried the ills of the younger generations for literally all of human history and been wrong the whole time. I suspect three isn’t actually any faster or likely to yield correct results than iterating on partially designed and implemented code, as well, but I don’t know of any actual research to indicate either way.
I just read Joe Armstrong’s interview in coders at work, and there’s a similar story. Do fast compiles and feedback cycles make us better? It seems we get answers faster with less thinking. And there’s a temptation to stop thinking as soon as we get an answer.
Perhaps an experiment: add a five minute sleep to the top of your build. Add another sleep to the top of your program. Resist, as much as possible, the temptation to just immediately remove them, but instead take a moment to consider if you really want to start a build or spend another minute reviewing your change. After a week, more or less productive?
Many times, I’ve gone through several compile/run cycles for a stubborn test failure, finding and fixing bugs each time through—only to discover that due to a misconfiguration I was just running the original binary the whole time! So I know I would find plenty of bugs if I just read the code for a while and didn’t even bother to run it.
I date from a time when compile cycles were minutes, not seconds, so I already know this…but apparently I forgot.
“Do fast compiles and feedback cycles make us better? It seems we get answers faster with less thinking.”
Maybe. I read PeopleWare along with looking at studies on LISP and Smalltalk vs other languages. I saw that productivity was going up with defect rate still lower than most 3GL’s. The trick was people were focusing on the solution to their problem more than trivial crap. The iterations were faster. In PeopleWare and some other resources, they also claimed this increased the use of a focused state of mind they called “flow” where people were basically in the zone. Any interruptions, even slow compilers, could break them out of it.
Given that, I’m not going to buy the idea we’re thinking less with faster feedback. Instead, evidence indicates we might be thinking more so long as we aren’t skipping the pause and reflect phase (i.e. revision, quality checks). I think most problems come from skipping the latter since incentives push that.
To the extent I’m conscious of being in the zone, I’m not compiling. The zone is the code writing period. I probably could do it with paper and pencil.
Perhaps I’m particularly bad, though I think not necessarily, but I’ve noticed I immediately reduce, if not halt, thinking about a problem when I have a solution that appears to work. If I revisit it, it’s almost certainly out at lunch or “in the shower” or whatever. And from observing others, I really don’t think I’m alone giving up the thinking when I get something “working”.
I’ve been thinking a lot about this this week, and I think I’m going to try my little experiment with introduced delays. Current hypothesis: better results.
You’re describing a common issue that is partial inspiration for having separate people do code reviews. If one person, then one could build it into version control where whatever is checked in isn’t final until it’s reviewed a certain amount of time later. Also, if you get in the zone coding, then dynamic languages might be a big boost for you. Or Wirth-style system languages with a short, compile cycle. They compile about as fast as people think. So, more thinking, less compiling. :)
The flip side of this is the “I have only proved it correct, not tested it” problem. There is a lot of broken code from (generally very smart) “rationalist” programmers–those who place a high value on understanding what’s happening inside all the relevant “black boxes,” and write code based on that mental model with the goal of initial correctness, with limited testing and experimentation.
It’s my opinion that many or most modern systems (especially those that span multiple languages, processes, servers, etc.) are too complicated to yield to this approach, because no model that can fit in one person’s head (no matter how smart) can be sufficiently accurate. Only an empirical approach is capable of reigning in the chaos. (Full disclosure: years of Lisping have left me entirely hooked on REPLs and REPL-integrated development environments like SLIME and Cider, where short feedback cycles are taken to the extreme, so perhaps there is an element of self-justification here.)
The value in the rationalist approach, I think, is when it can be used to constrain the level of allowed complexity in the first place, as perhaps in this story. In a world of long feedback cycles, there is no choice but to build systems that can fit in your head. But generally speaking we don’t live in that world anymore.
I encountered that problem when I first started in BASIC with 3rd-party code. The solution was to use a subset you can understand, create a model of behavior it should follow, make tests for that, and run them to be sure it does what it’s supposed to do. One can then compose many such solutions to get similar results. Error handling for the corner cases.
Also, DSL’s can do this stuff even in complicated applications. Sun Microsystems had a DSL (DASL?) for three-tier web apps that could easily describe them with all the messy stuff synthesized. The example I saw was 10kloc in DSL turned into 100+Kloc of Java, Javascript, and XML. So, we can constrain what’s there to fit with our models and we can synthesize messy stuff from cleaner tooling.
Have you read through this yet? If so, do you recommend it? Do you recommend going through it in the order they present it or jumping around?
I’m about half done Part I. It’s a little rough around the edges, but it’s alright so far. There’s some statements that cause me to raise an eyebrow, but there are others I find myself nodding in agreement with. At the very least, there’s some decent history bits.
I plan to write a mini-review when I finish each part and will post it to the site.
Ah sorry I missed that one. I guess I’ll just have to post more cog sci articles :) so I can use the tag!
It depends. In theory, I like it. I (especially my introvert, monastic side) like the idea of diving into something, not understanding it, and being able to build understanding purely through exploring the codebase. In practice, I’m continuously frustrated by how difficult I find building this understanding and the discrepancy between how much understanding I get from reading code and from talking to its original author (hat tip to akkartik for showing me Programming as Theory Building, which explores this phenomenon in detail).
I also go back and forth on whether I think code-reading independent of interacting with the code via testing it, running it, and using other tooling on it is valuable for building a useful mental model of it (usefulness measured by ability to modify the code).
The pain with legacy code bases isn’t reading them, it’s reading them so you can meet a deadline by extending it.
Although I definitely don’t consider myself wise or an intermediate programmer (~2 years of experience in industry qualifies me as a novice in my opinion), I think about deliberate practice’s relationship to programming often. When I read Peak a few months ago, I noticed that Ericsson felt that most professionals hadn’t figured this out. I remember him distinguishing between brain surgeons and radiologists for example - surgeons improved over their careers whereas radiologists (used as a representative for most other types of doctors) actually got worse [1]. This also matches my observations of the lawyers I know. Each hit a certain level of competency a few years into their career and plateaued and potentially even degraded. This means we have to be discerning when looking for examples from other professions.
In my own search for people who think about this in a programming-relevant way, I’ve found a few helpful or at least thought-provoking resources:
- Cal Newport, a tenured Computer Science professor at Georgetown writes about how he applies deliberate practice on his blog and applied Ericsson’s theoretical framework to modern knowledge work in his book Deep Work. More discerning readers will notice he mentions rubber-duck proving with his dog. From Cal, I’ve adopted the practice of blocking out time where I’m working on one difficult but focused task and ignoring distractions. In an ideal world, this includes all communications and the internet as much as possible. In reality, the open office environment doesn’t always lend itself to hours of distraction-less work, but I do my best.
- This response to a Quora question about tips for advanced writers feels relevant to programming as well. In particular, this quote [2] resonated with me. Thinking about the programming equivalent of rewriting, (refactoring?) makes me wonder whether those who focus on algorithms (interview-like) problems or even coding “katas” as “deliberate practice” are missing out on the practice required to master programming in the large. Clearly, algorithms and other types of clever programming require deliberate practice and skill, but I question whether these skills transfer to architecting a large system, designing an API, or innovating on a legacy codebase. As I’ve gone from noob to novice (first I knew nothing, now I know enough to know how much I don’t know), I’ve become much less interested in excelling at programming in the small and much more interested in excelling at programming in the large, but deliberate strategies for the latter have eluded me thus far. I suspect that strategies for the latter set of skills will leverage or at least acknowledge the iterative nature of these activities.
- Mastery by Robert Greene provides a set of case studies and an (opinionated) framework for how one goes about become a master in a discipline. Worth it for the historical anecdotes alone, this book outlines how to, over the course of an entire career, internalize the practice of a discipline until it becomes a part of you. I’ve included some choice quotes below [3]. I recommend ignoring the teleological stuff and focusing on how well Greene understands Ericsson’s notion of mental representations and single-tasking deep focus being the key to mastery. From Mastery, I gained a somewhat corny but useful perspective on my work. When I’m not cursing at my keyboard, lamenting the mess we’re in, I view programming as a long-term, fulfilling activity which I’d like to pursue for a long time. Mastery also led me to eschew jumping from one trendy framework to another in favor of trying to build an understanding of what’s come before in our discipline.
A few questions that I’m left with even after reading through the above materials and comments on this post:
- To what degree is separating research from programming useful? I’ve recently experimented with writing down I things I would’ve looked up on StackOverflow previously and continuing programming. I later go back and look up questions on StackOverflow in batch and retroactively apply what I find to my work. This feels like a good way to further narrow the scope of my practice, but I’m curious to hear others' thoughts.
- Where do non-programming activities fit into deliberate practice for programmers? For example, reviewing code and writing design docs both tax my intellect and my working memory when done with full attention. On the other hand, I often question whether it’s possible to build new mental representations when doing design or whether designing draws purely from already developed knowledge representations. Similarly, I question whether reviewing code as typically done purely consists of applying existing pattern recognition brain modules to someone else’s code.
[1] I just discovered this Harvard Business Review article which confirms that he drew the distinction between typical radiologists and brain surgeons.
[2]
The HUGE difference between everyday writing that everybody does and serious writing is the proportion that is re-writing. I’d estimate that for non-writers, rewriting accounts for maybe 10-20% of their writing.
For serious writers, it accounts for anywhere between 50-90% depending on how critical the particular piece is. This Quora answer is not very critical for me, so I’d say it’ll hit 50% by the time I am done. There are single paragraphs in my book though that took 5 minutes to write down initially, and then cost me hours to whip into shape, so that’s like 99% rewriting. For my for-pay work, I probably average about 75%. For my own blog, I am erratic. Some pieces hit book-like 99% levels. Other pieces are at 70%. I don’t think I’ve ever hit more than 65% on Quora.
People often ask me how I am so prolific. To be blunt, that’s so easy for me, it is not much harder than just breathing. But rewriting is hard. It is torture. Since one measure of rewriting progress is words eliminated, I often joke that I write for free but charge for eliminating words.
But rewriting is the only kind of writing that counts. If you aren’t rewriting, you aren’t developing as a writer.
When you hit 10,000 hours of rewriting, you’ll be a skilled writer or a skilled thinker with the written word. If you want to be both, it’ll take you 20,000 hours.
[3]
All of us have access to a higher form of intelligence, one that can allow us to see more of the world, to anticipate trends, to respond with speed and accuracy to any circumstance. This intelligence is cultivated by deply immersing ourselves in a field of study and staying true to our inclinations, no matter how unconventional our approach might seem to other. Through such intense immersion over many years we come to internalize and gain an intuitive feel with the rational processes, we expand our minds to the outer limits of our potential and are able to see into the secret core of life itself. We then come to have powers that approximate the instinctive force and speed of animals, but with the added reach that our human consciousness brings us. This power is what our brains are designed to attain, and we will naturally led to this type of intelligence if we follow our inclinations to their ultimate ends.
&
You must avoid at all cost the idea that you can manage learning several skills at a time. You need to develop your powers of concentration, and understand that trying to multitask will be the death of the process.
I’d say that the analogy to rewriting is adding features and refactoring. Get some practice with building a dead-simple prototype of something, then add features to it until parts of the architecture start feeling getting creaky, then refactor appropriately. All of the little decisions involved in that are important to practice - how to get an initial prototype of something small into production as fast as possible by not over-designing it, how to decide when the “creakiness” and future plans of an app justify refactoring, how to create an improved architecture for the new size of the problem and move the app to it smoothly. Not to mention designing test suites that help instead of hindering refactoring and knowing the realistic scale of the app and what issues are and aren’t a concern at that scale.
As far as I can tell, this concept’s mostly been applied to gaming. I’m curious how well it would apply line-of-business or other application areas with different data access patterns. I suspect the high-level thinking (a focus on data layouts that are correct and efficient by construction) would transfer, but some of the patterns would shift. The prevalence of memory-abstracted languages like Java and C# in other areas would also reduce the relevance of many of these patterns..
Has anyone read or had experience with applying this concept outside of game development?
In the introduction it does say “This book is a practical guide for serious game developers. It is for game developers working to create triple A titles across multiple platforms, for independent developers trying to get the most out of their chosen target hardware, in fact for anyone who develops cutting edge software in restrictive hardware.”
Do lobsters users find the Pomodoro technique yields the stated benefits - efficiency and consistent flow? I’ve experimented with it in the past and found it useful for tasks for which I experience resistance. Despite that I’ve always wondered whether it would interrupt my flow for tasks I can’t easily break down into smaller chunks. I’d love to be proven wrong though.
There’s a phenomenal sci-fi short story, the namesake of Stories of Your Life And Others, which uses the Sapir Whorf hypothesis as its base. I highly recommend it.
Apparently, it’s being turned into a movie which debuts later this year.
I do actually enjoy to write quite a bit of code before I hit compile. It depends on the problem at hand, and for some things surely it makes sense to have a faster feedback loop.
But having just spent half a year working on a game every day, there are cases when I write a bunch of complicated code for even an hour, without getting anywhere near to running it and testing. Some code has to be written in larger chunks.
A lot of times I don’t need to run my code to know that it is correct. Surely I could make a mistake, but I try to program somewhat defensively, throwing asserts whenever there’s a precondition that isn’t completely trivial. People frown upon asserts these days, saying that TDD is the holy grail … but what if the code I need to write is just a 300 lines of an algorithm that can’t simply be split into tiny 3 line methods that are red-green-refactored? What if the problem being solved is more conscise and readable as a 300 line bunch of switch/if statements and bare for loops with indices? Do I really need to run it every 5 seconds to stay on track?
Say that you’re writing a basic pathfinder, starting from scratch. First you probably want to have some representation of the graph, so you pick one and code it. There’s a clear idea of what the data structure looks like, what the invariants should be, and you just write it. Then when you’re writing the traversal algorithm, you just write down the algorithm with all the invariants as asserts. Surely it’d be nice to have some tests along the way to verify that the code is correct, but those don’t have to be run when writing those 50 lines.
One might suggest you’d want tests early if you don’t know if the code is really solving the problem you’re trying to solve. I’m not going to argue with that, but there’s great value in looking at a piece of code and thinking about it, instead of just running it and seeing what happens.
In my opinion, this is what it comes down to. Programmers these days like to run code instead of just looking at it and creating a mental model of what it does. Surely your mental model can be wrong, and you should definitely run and test the code, but you should first be fairly confident in what it will do. There’s nothing wrong with writing 100 lines in one go, reading them over and thinking to yourself “yes, this is definitely correct, I don’t need to run it for now”.
What if the code has a bug in it? People immediately reach for a debugger and try to step through the code to find the error, instead of just reading the code without executing it first.
This all breaks down especially in cases when you can’t actually run/test the code and you’re left with just your mind, trying to read the code and make sense of where the error might be. The debugging code by reading it line by line might be the only thing you have left in a lot of cases, and I’d much rather prepare myself to solve the difficult problems, than to optimize for the easy ones.
Is it more valuable to have a super fast feedback cycle when writing trivial lines of code, or to train yourself in keeping a strong mental model, so that you can use it when the time comes and you can’t use your fancy tools anymore?
The fact that you occasionally spend more time thinking/coding than running/debugging doesn’t negate the need for fast compiles and, ideally, hot code swapping of some kind.
I’ve written games with pathfinding and complex fiddly bits of logic and such before. In my experience, even if your code is 100% correct on the first try, games need lots of rendering hooks in to subsystems for visualizing what’s happening. Maybe the AI does something surprising and you want to understand why it does it. Or maybe performance isn’t great and you want to watch the navmesh while you tweak the assets to minimize the work the engine is doing. Being able to change a color or a formula or something, then Edit & Continue is an incredible time saver that leads to better outcomes.
I’m sympathetic to this argument, but have historically struggled to develop this skill deliberately, not for lack of trying. Any tips for working towards mastering this skill, outside of just doing it in your day-to-day? In particular, I find the right modality for building my mental model of the code typically falls somewhere between verbal and visual, but I’m not sure whether this is habit or optimal.
I was excited to see this post so I look forward to hearing any tips or further reading you’d recommend on this topic.
Generally you’d want to have a clear idea of what you want to write before you write it. Almost to the point when you could just tell a robot the instructions and he could do it for you. The mental model doesn’t have to be 1:1 copy of the code though, it may be more abstract, but you should be able to extrapolate the code from it.
If you’re having trouble imagining how an existing codebase works, then that’s a problem in and of itself, and making changes to it while instantly checking if they’re correct is bound to introduce bugs at some point.
Say that you want to make a change in a piece of code someone else wrote. You’re not exactly sure what it will do, so you change it, run the tests and see what happens. If it does the right thing, you might convince yourself that the change is correct and move on … but it might also be the case that the tests just don’t cover a new type of bug you’ve introduced.
The problem was a bit earlier when you start thinking “I’m not exactly sure what this does”. Don’t make changes to code you don’t have a clear mental model of (unless in hurry hehe). Build the mental model first, read through the code and try to understand how it works. You should be able to answer questions like “are there any invariants that I might be in danger of breaking?”.
It takes a lot of patience to do this, especially the first time around a particular type of code it might take a while before you get used to thinking about it. There are no magic bullets really, you just have to get familiar with the type of program you’re working with.
One thing that I find very helpful at times is to read the code outside of an editor. If it’s short enough, you can print it out and read it on a paper while scribbling on it with a pencil. Or you can just use a phone/tablet, or just another computer that doesn’t have an editor and read it in a web browser or something.
Not being able to make changes will force you to focus on what is important.
I love how a well-regarded software developer uses learning a new instrumental composition, rather than building software, to illustrate their point.
I once had an instrument-specific instructor who liked to stress that the point of teaching was to train you to teach yourself. It was expected that I would be able to identify the hardest parts of a composition by analysis, prior to even touching my instrument, and tackle them on my own. Much like Joe, after tackling the hardest sections individually I was required to play the piece in it’s entirety, though I was also required to provide observational analysis on the performance to myself.
Once a week we would meet and I would play a piece I learned. Often, he’d proffer a piece of information unrelated to my actual performance but something which would nevertheless improve my understanding of what I was playing and how to improve my interpretation. My favorite example of this was with a piece by Bottesini (Elegy No. 1 in D).
After finishing what I thought was a good performance, he asked whether I knew that Bottesini actually wanted to be an opera composer. I was befuddled. Who cares about the ambitions of a composer long past? He then remarked that if you look at the piece, the juxtaposition of passages played in the upper and lower ranges almost seems like a duet between a male and female singer (see: ~2:40 in the video). This little fact changed my entire view of the piece. No longer was it simply a composition played on an instrument but rather an elegy for the love between two people, both parts playing out on a single instrument.
Sometimes the technically difficult parts are actually the easiest. Anyone can create software, good or bad. It’s the context that gives what you created power and the interpretation that gives it meaning. Go out and build whatever your going to build in whatever language or systems you want. Bottesini did. He couldn’t write an opera but he could sure play the bass like no other. He instead made the best opera he could using what was available and the result was an enduring piece in the classical repertoire.
the point of teaching was to train you to teach yourself
Hah. I’ve had this thought but with regard to therapy. Where it’s also true, except therapists generally don’t seem to know it. I guess it applies to a lot of things. :)
Thank you for the anecdote, it’s moving and relevant. And got me to read the article, which I’d probably have skipped otherwise.
I’m happy someone read it! While it doesn’t address everything in Joe’s post directly, I thought it might be a interesting companion piece for someone.
I agree that the idea of teaching yourself how to teach yourself seems to be almost universally applicable. I would really like to read any formalized studies on the topic. There seems to be many books, especially self-help text, that focus on specific aspects but I don’t think I’ve come across anything about the subject as a whole. Maybe auto-didacticism is a mystical art.
I have found the books that deal with the specific portions of teaching yourself to be very helpful, however. I’ve recently been re-reading The Inner Game of Tennis
, which focuses on achieving a flow state by silencing negative criticism given to yourself by your self. It also makes a few references to Zen in the Art of Archery
, which apparently is a seminal text on a similar subject. it’s on my to-read list.
If anyone has any similar recommendations, I’d love to hear them!
I recently read The Inner Game of Tennis
as well and have found it helpful in my physical pursuits. Have you found any applications related to programming and learning? I struggle to apply his ideas to programming because I find my internal dialogue generally functions as the middleman between my unconscious and conscious when programming. Analytical tasks (at least in my brain) are mediated through a combination of visual and verbal thoughts. If anything, I often find something like Rubber Duck Debugging helps me simplify or understand complicated solutions.
Just wanted to add a short note that I’ve been using kitty at home and at work and I slightly prefer to iTerm2, which I was using previously.
To address cbdev’s criticisms, almost all of the benefits I’ve experienced have kitty having a significantly lower startup latency than iTerm2 and the default Terminal app. I also slightly prefer the window and tab management shortcuts kitty provides by default to iTerm2’s.