My career has exposed me to several programming languages. This is a common experience for many software engineers, especially as the modern programming landscape moves toward an increasingly service-oriented world. In this world, working across several systems and language boundaries is par for the course. However, true mastery of a language looks very different from the vague familiarity that may be sufficient for operating in various contexts. For instance, the shallow working knowledge demanded by “component-assembly” style work doesn’t exactly render us polyglots, given that integrating APIs or navigating DSLs, by design, removes us from the core language by a few layers of abstraction. By contrast, developing deep proficiency in a language takes a lot of persistence and practice. For the purpose of mastery, learning how to learn can remove a lot of frustrations that hinder fast uptake.
My (often undesirable) experience context-switching across languages, codebases, and projects has been fraught with a lot of chaos and, what I would consider, “inefficient learning”. This is because I didn’t have the self-awareness to understand how I best learn. In the absence of such awareness, I loaded my self-study curricula with counter-productive methods that worked for others, and berated myself when these strategies failed me. I tried to learn from textbooks I could not retain, programming tutorials that bored my soul, started and abandoned aimless side-projects with little-to-no guidance, and endured painfully uncomfortable pair programming sessions that chipped away at my confidence. The experience of learning new languages, often in professional settings with a less-than-kind audience to watch my lapses, has helped me build a healthy portfolio of failures. That said, continued exposure and pattern recognition has also yielded a deepened understanding of how I best learn, and helps me get faster with every new iteration.
The following discussion outlines a generic framework I use when learning new languages. My hope is that it will be helpful to others, and more selfishly, that sharing my own best practices will start a conversation about what others find useful, giving me ample opportunities to steal and apply ideas that are superior to my own. :)
Step 1: The system of the language
I like to start by learning about the structure of the language. This includes asking: how is a project in this language organized and setup? What is the language’s notion of packages, libraries, or other sub-components? What programming paradigm the language belongs to, and what properties does it have as a result? Am I in a world of typeclasses, or in a world of object-instantiation? What do the configuration files look like, and how do they impact the project?
As a top-down thinker, I need to form a structural understanding of the language. Understanding project structure gives me a clear mental model I can use as the foundation to build on. Without this orientation, diving straight into the syntax or experimenting with trial-and-error implementations is less elucidating.
Step 2: Syntax
Reading textbooks that linearly introduce one concept after another don’t hold my attention. I prefer to use books and tutorials as reference material to consult, and find examples of programs and code blocks I can type into new files, and examine them. Without running the code, I ask myself what I can interpret, and what I can’t. Then I look at the constituent syntactic parts, and try to pinpoint what I need to learn. A lot of syntax is shared between many languages, and chances are you don’t need to re-learn what an
if statement is from scratch, but unfamiliar operators are worth looking up. I keep a glossary of anything new that stands out.
Step 3: Patterns and Idioms
Learning syntax in isolation is less valuable than contextualizing syntactic knowledge by writing out small programs that illustrate its use. I do this by finding common patterns and idioms and typing them out. This allows me to fit syntax into larger and larger components.
Step 4: Tooling and Workflow
Up until this step, my learning has been static. I haven’t run code. But now I want to know: how do I compile and/or run my program? What is the debugging workflow? How do I interpret error messages and react appropriately? How to install packages? How do I navigate documentation? What information do I need to find more information? What other processes need to run, what do they accomplish, and how do they have to be sequenced? This will form the feedback loop necessary to build a deeper understanding of the system and syntax.
Step 5: Drills, Drills, Drills
Building muscle memory and deepening conceptual understanding requires practice. I like to practice by implementing algorithms with increasing complexity in a way that gives me repeated exposure to the components outlined in steps 1-4. This practice helps close the gaps between the organizing principles of the system, the patterns that implementations follow, the syntactic building blocks, and the tools and processes needed to form feedback loops. In this way, I think of my system as a four-strand braid that becomes more and more tightly woven with practice.
Most of all: Use self-discipline, not self-criticism.
Metacognition refers to learning about learning. Increasing this form of self-awareness can dramatically expediate the time it takes to gain a new skill. While programming requires maintaining an acute awareness of our own neuroses, the psychology underlying the uptake of specialized technical skills is not discussed frequently enough. This leads to a lot of time wasted applying techniques that simply do not work. Knowing yourself is powerful. Honoring your needs and limits, rather than fighting them, is the most important precursor to maximizing your efficiency.
Industry leaders love pointing to studies in grit and growth mindset that repeatedly demonstrate that embracing struggle sustains progress throughout the pursuit of meaningful long-term goals. Unfortunately, these phrases are exceedingly co-opted by abusive organizational policies. “Embracing struggle” does not mean welcoming self-mutilation or persevering at all costs. Instead, learn to discern self-discipline from self-criticism. Be disiciplined about tuning out distracting self-criticism and staying focused on the problem. Do not ignore difficulties. Pay attention to the emotional experience, but use it to guide your next move without dwelling on discouragement.