- Noah Jacobs Blog
- Posts
- On Coding in Tongues
On Coding in Tongues
How I taught myself three programming languages in a month.
On Coding in Tongues
LVI
2024.07.21
Learning things is not usually as hard as people act like it is. I just taught myself three programming languages in a month, and you can, too.
JavaRisp
Over the past month, I’ve taught myself JavaScript (JS), Rust, and Lisp. The latter two are a bit more esoteric and considered more “challenging” languages to learn.
To clarify, when I say “taught,” I mean that I was capable of writing a non trivial amount of code for a project in each of them. You can see the product of the JS and Lisp on my website; the home page has a cool knowledge graph you can play with that runs a language model in your browser.
Even if you’ve never written a lick of code, this post is still relevant for you. The same principles I’ve followed below work for anything, whether it be spoken languages, martial arts, selling, writing, or yoga.
The strategy I used is simple:
Latent Information Collection: Learn and integrate the subject into your worldview.
Execute With Principles: Practice the skill guided by the information you’ve learned.
Methodically Overcome Roadblocks: Methodically overcome your knowledge gaps.
I seriously believe that the barriers people put around learning are oftentimes artificial and made out to be higher than they really are. What follows is how I find myself tending to overcome those barriers.
Note One: While prior to this, I had been coding in Python for 2.5 years and had limited experience in C++, I had not touched Rust or Clojure and had edited maybe a total of 5 lines in JS.
Note Two: Clojure is the dialect of Lisp I learned, so I will use Lisp and Cojure interchangeably below.
Latent Information Collection
When I want to learn about something, I’ll let my curiosity run rampant and consume as much information about it as I can. I’ll question experts about it, read books and blogs discussing it, and listen to youtube videos or podcasts pertaining to it.
For whatever I’m learning, my information consumption is so voracious that at least three dates this year have caught me reading textbooks while I was waiting for them; collecting latent information is good for my learning, but not for my love life.
While the process is largely unstructured and very flowing, the goal is to integrate the skill or topic into my worldview. What things that I know are true elsewhere are true in this new subject, and what things that I know are true elsewhere are seemingly different in this new subject?
In the case of Lisp, the book Structure and Interpretation of Computer Programs has been incredibly helpful, as it doesn’t just talk about the language, but it helps you restructure your view of programming more to think in terms of increasing layers of abstraction.
I’ll relate this to something like Jiu Jitsu, where early on, I had to learn how to make myself heavy when I'm on top of someone. Now, though, that fundamental thing is second nature and I can focus on “higher level” things, like how to isolate a limb. This mirrors what I just learned about Lisp, which is abstracting away lower level things, like the operation of adding two data structures together so you don’t have to think about it anymore and can, likewise, focus on higher level things.
On the other hand, with Rust, the notion of strong typing and data ownership is very alien to any understanding that I had from something like Python and object oriented programming. It’s something I’m still integrating as I learn more about functional programming.
The point is, though, that I’m not trying to figure out if you declare a function with “fn,” “function,” or “defn,” just like I wouldn’t be trying to figure out if my first sales meeting in a process was technically “customer discovery” or a “pilot meeting.” I’m looking for the strong, underlying principles that will impact my world view and mingle with the things I know and the experiences I have.
Notice that I didn’t bring up as much JS here. Admittedly, I did not take this approach as intentionally with that language, and, if you look at my JS code vs my Clojure or Rust code, you’d pretty quickly be able to see that.
Execute With Principles
Each of the languages I learned had a project I was trying to complete with them:
Lisp: Build my website; later, build a WordPiece tokenizer and gpt api handler
Rust: Run a language model
JS: Visualize my blog posts in a knowledge graph; later, run a language model
Execution is so important that I almost put it before the part about collecting latent information. The above tasks gave me something objective and concrete to work towards with each of the languages I was learning. It gave me a chance to take my abstract understanding derived from the above learning and test it against the world.
Reps, reps, reps. Try as you might, you’re not going to get swole by reading a textbook on weightlifting. Your knowledge means nothing unless it is stress tested against the cold, unforgiving universe. The only way you’ll find out if the information you have will help you do the thing you want is by using it to try to do the thing you want to do and seeing if it works.
Statue at BAPS Swaminarayan Akshardham—the fella is crafting the man he wants to be through repetition and labor. The Lisp programmer is within, hidden beneath stone.
If you want to write, just write. If you want to run, just run. If you want to sell, just sell. In my case, when I wanted to learn Lisp, I started coding my website in Lisp. And, if you don’t know how to get started, it would almost be foolish at this point to not leverage tools like GPT and Claude. Of course, while doing this, you should keep referring back to those critical principles you are integrating with your world view.
As an example with Lisp, when I was writing the WordPiece Tokenizer, I asked GPT to produce the code. It did, but it wrote the code with a very python like structure with nested loops. Once I saw this, I reminded myself that recursion is a critical building block of Lisp, and wrote out pseudo code for the function:
;Given one pre processed word, returns the token
(defn tokenize-word [text vocab]
let tokens be empty list
anonymous match token function
takes in text and vocab
if text is empty
add unknown to token list and kill recursion
else if text in vocab
add matching token to tokens
remove the matched characters from text
call the anon function on it again )
There was a lot of naivety here, as we’re missing a necessary nested recursion to navigate the entirety of the string. But, using a core lisp principle, I was able to get on the right track with this pseudo code. Then, after feeding the pseudocode into GPT and continuing to try to run it, edit it and then run it again, I kept learning more about tokenizers and lisp and hardened it into something that works just fine.
It’s not just about executing, it’s about executing with reference back to those core principles you’ve learned about.
All of this is without mentioning the notion of Deep Work–one hour of fully focusing on learning the skill during the day is worth three or four hours of partially focusing on it.
Overcoming Roadblocks
When you’re learning something new like programming languages, it can feel like there are an overwhelming amount of ways for it to go wrong. When things do go wrong, I think it’s useful to group it into a few buckets, though:
You know how to get to the answer but haven’t realized it.
You’ve don’t know that you’ve made a silly mistake.
You’ve don’t know that there’s something fundamental that you’re missing.
The goal of constantly referring back to the big principles when you’re executing the skill is to reduce the probability of the first thing being a problem. You want to make sure that you’re constantly going back to the most critical principles above to reduce the likelihood of not retrieving the relevant information.
In regards to the latter cases, this is where it gets the most challenging. However, I think this is where the increasing power of tools like LLM’s really come in handy, especially to free yourself from the silly mistakes.
Back to the above example of me writing the pseudo code for the lisp tokenizer and gpt fleshing the draft out–part of the reason I did that is because I’m not trying to memorize syntax. I am trying to understand the core principles of the language and how I can leverage those to get the results I want faster.
Of the three languages I learned, each has a different way to declare a function:
Clojure: defn
JS: function
Rust: fn
If I need to declare a function and I don’t have this memorized in any language, I can scroll up in my file and see what I used before. Properly leveraging resources like your own code, the internet, and language models help a lot to reduce the silly mistakes.
It gets a little more nuanced when it’s more than a silly mistake, but I guess that the “hard” part about learning. As an example, you could think that you’re fixing a silly mistake only to realize you’ve been putting bandaids on bandaids because you’ve really missed a fundamental thing.
That being said, you’re not debugging your code, you’re debugging yourself.
This is why you’re continuously collecting information about the thing you’re learning, even after you start executing. If you keep messing up strings in Rust, then your information collection should probably be refocused on learning strings in Rust. The goal of executing is to help you accelerate and direct your learning.
Together, learning and executing helps you notice where your holes are and gives you the direction needed to fill them in.
I’m not going to say it’s “easy,” but it does just take time and intentional practice.
The goal of this post is to remind you that walls are not as hard to get over as you think, as long as you commit to getting over them.
Learn with the goal of integrating the subject with your worldview, do the thing, and make sure that you’re debugging yourself along the way.
That’s it.
Live Deeply,