I've been programming for about 5 years. In that time I've stuck Python for almost everything. This worked wonderfully because Python is so adaptable; it could usually accomplish whatever I needed it to.
But such monogamy leaves a programmer a little sheltered. A couple months back I took a week-long intensive class on the classic Structures and Interpretation of Computer Programs which is completely written in Scheme, a lisp so minimalist you can learn all the syntax in 10 minutes. This experience left me thinking that my ability to reason about computer programs was too influenced by the only language I knew: Python.
So for the last few months I've been attempting to learn a few more programming languages:
- I've started been porting the library from Jimmy Song's Programming Bitcoin to Racket, a super-set of Scheme (code)
- I went through The Go Programming Language book and wrote a Bitcoin crawler in Go (code)
- I went through The Rust Programming Language book and wrote a Bitcoin crawler in Rust (code)
The bitcoin-racket project has really helped me appreciate all the things that Python takes care of for me. For example, a bitcoin library needs to generate random 256 bit numbers in order to produce ECSDA signatures. But racket's random number function maxes out at 34 bits and it's only pseudo-random. There's also a
crypto-random-bytes function (link) but I couldn't find any utility to convert those bytes into a number. I ended up installing this arcane library which has a pseudo-random number generator that can at least spit 256 bit numbers but I have no idea how random they really are.
In summary, more thinking is required when you use a fringe language like racket and as you can see I'm used to being fed a "right way" to do things and didn't fare so well!
I also ended up diving into generics which I'll talk about more in a future blogpost. Generics are how you teach
structs to do various things, like interact with the
+ operator. But they end up really making the code messy. Look at all the crap I do to just get my
(struct field-element (number prime)) to handle the the
+ - * / operators. Instead of leveraging advanced Racket features, I think I'd rather move this library in the other direction and only use the bare-bones Scheme primitives.
In the last few months I've become interested in concurrency, which made Go very appealing. Go has very little syntax for a modern programming language. Here's a complete list of keywords:
Not many, huh? But a few of these built-in keywords are devoted to concurrency. Namely,
go keyword will spawn a "goroutine", which is a light abstraction over operating system "threads".
Since the goroutine concurrency primitive has first-class support in Go, the designers also added "channels" to facilitate communication between goroutines. Here we use channels to pass a value across 10 different goroutines which each increment once.
I find it easier to reason about concurrent programs written in Go because the syntax makes it easy to see what's going on. And concurrency also felt more fun in Go than Python, where you have to spawn threads using very ugly API or run a event loop that spawns a whole different universe than the REPL you're used to.
As a Python programmer, Rust was a shocking experience. I'm used to running my programs and seeing what's wrong with them when they're run. The Rust compiler tells you all kinds of things that are wrong with your program before you ever get to run them. With proper IDE integration (I use ALE + rls on vim) you never even attempt to compile until the compiler errors stop showing up in your editor. The whole developer experience was turned on it's head, in a good way. I saw logic errors earlier in the process than I would with Python.
But with Rust's hardcore type system and demand that every piece of data have exactly one owner (ignoring advanced stuff like Rc<T>), writing new functions felt very arduous. I think at one point my crawler was a 100 line
main() function! But this friction slowly dissipated as I grew more comfortable deciding and defining boundaries between my functions.
It still seems weird that Rust is so low-level. It feels so much closer to Python than C has in the few times I've played with it.
Lastly, the package manager
cargo + package repository crates.io make library authorships much more appealing than Python's pip + PyPi. Also, I really enjoy how Rust (and Go for that matter) made a language-level decision on how documentation should be written.
// is a comment and
/// is documentation (link)! As a result you get wonderful documentation like this auto-generated for you, and so does every other project. This is a great developer UX – you might be using 10 different libraries but the documentation for each is structured the same, unlike Python where each library might have it's own way of presenting documentation.
Until Next Time
I'm going to try to write one little post like this every day. It won't be very polished, but hopefully the discipline will encourage me to learn something every day.