Schemescape

Development log of a life-long coder

Future-proof programming languages, part 3

In part 1, I narrowed down a list of popular programming languages to the ones I liked and that I thought would still be useful in the future. In part 2, I recorded some initial impressions, and added a few more languages to the mix.

In this update, I'm recording my experiences with a trivial program in each language, with an eye towards possibly incorporating one or more of these languages into my minimal development environment.

Scenarios and metrics

This post is not an attempt to definitively benchmark or objectively compare programming languages in depth! Instead, I'm playing around with each language and environment to see which ones are even worth my consideration. Specifically, I'm looking at:

Results

For reference, my Alpine Linux baseline image was around 150 MB. Here are the results, ordered by descending SDK size:

Language SDK size Program size Stand-alone size Build on Pi? Cross-compile for Pi?
Rust 650 MB 300 KB ? Probably Mostly easy
Go 550 MB 1.2 MB 1.2 MB Yes Easy
C# 510 MB 75 KB 67 MB No Failed
Zig 335 MB 800 KB 800 KB No Mostly easy
C++ 230 MB 14 KB 940 KB Yes Tedious
C 144 MB 14 KB 13 KB Yes Tedious
Python 75 MB 1 KB 7 MB Yes Unnecessary
JavaScript (Node) 60 MB 1KB 80 MB Yes Unnecessary
Lisp (SBCL) 45 MB 1KB 40 MB Probably No
Tcl 8 MB 1 KB ? Yes Unnecessary
Lua 1 MB 1 KB 1 MB Yes Unnecessary

SDK size

Just looking at SDK size, Rust is an outlier. Even compared to a similarly complex language like C++, the Rust SDK is absolutely huge. At the moment, I don't know why the SDK is so big. Even after allowing for a build tool, custom linker, and standard library, the Rust SDK is disappointingly heavy. This unfortunately matches my previous experience with Rust on Windows. Update: Using the "minimal" profile, the SDK was closer to 650 MB for native compilation and another 150 MB for cross-compilation.

Obviously, it's not fair to compare the compile-to-native languages to scripting languages, but comparisons within each category seem appropriate. Go is the second largest, but it comes with some impressive features: the ability to trivially cross-compile and a large standard library. Go's offering seems on par with C#, but with a better "compile to native code" story (and, obviously, larger differences in the programming languages themselves).

Zig is larger than I expected for a simple language, but it can cross-compile for different architectures (similar to Go), and Zig can even compile C/C++ code, so it's arguably worth the footprint.

Looking at interpreted languages, the results are pretty similar except for Tcl and Lua (both of which don't come with much of a standard library).

"Hello, world!" program size

The Go binary shows one of Go's weaknesses -- it's compiled to native, but it has to include the entire runtime in each binary. This isn't a deal-breaker for me, so I'm not too concerned.

I am curious why the Zig binary is so large, but I haven't investigated any further yet. Given that Zig supports "freestanding" binaries, I'm sure it's possible to create minimal binaries with Zig.

The C++ binary using "iostream" was unacceptably big, but using "stdio.h" gets it back in line with C, in the low double-digit kilobytes.

Stand-alone executable size

Deployment is probably my least favorite part of software. Obviously, there are exceptions, but in most cases, a "program that does X" would be most convenient as a single executable (or at least a single zip file) that just does X. Here, I'm looking at fully statically-linked or bundled binaries.

Native languages generally compile to a single binary. I didn't try to get a fully static binary from Rust, but I've read that it's possible. C certainly is good at producing small executables (in the past, this was a prerequisite!).

The other languages show some interesting results: JavaScript and C# appear to be the most bloated, closely followed by SBCL (a Common Lisp implementation). But for JavaScript there are multiple runtimes--80 MB is for Deno, but QuickJS is under 1 MB--so JavaScript supports indirectly optimizing for either size or speed. Lisp has a similar story, although I didn't investigate it in depth. Python was impressively slim (definitely a surprise for me!).

That leaves C# which, sadly, just appears to be bloated. For the record, I couldn't get a Tcl bundle working, but I suspect it would be < 10 MB.

Developing on a Raspberry Pi

Note: this section is about developing/compiling on a Raspberry Pi B.

The only languages that appeared to not be trivial to compile on a Raspberry Pi B with Alpine Linux were C# (this appears to be an unsupported platform) and Zig (theoretically, you might be able to bootstrap an environment, but I haven't attempted that yet).

Cross-compiling for a Raspberry Pi

Although I'm interested in developing directly on a Pi (as part of my minimal development environment), it's likely I'll end up doing some development on a (faster) desktop, and then want to run the output on a Pi. Results here are easily categorized:

Scripting languages sort of get a free pass here, but there are almost certainly large differences in ease of deployment.

Go and Zig have excellent tooling for cross-compiling. And if Zig can eventually support cross-compiling C/C++ code without requiring a separate toolchain, that would be huge! Update: Cross-compilation for Rust worked well, once I found out how to tell Rust to use its bundled linker.

C/C++ support cross-compiling through a painful process of setting up an entire compilation environment for the target. It's tedious and annoying, and I hope I never have to do it again.

As for C#, I suspect there's some way to get them to successfully cross-compile for a Pi, but neither worked for me. I put SBCL in the last category because it seems to require running on the target in order to produce a bundle.

Analysis

Subjectively, I was least impressed with the following languages, so they've been cut:

I'll tackle the remaining languages in two categories: native/compiled/statically typed vs. interpreted/dynamically typed.

Native languages

Here are the remaining native languages, along with my favorite and least favorite qualities:

C++ and Rust

If I need powerful abstractions, C++ and Rust are the candidates. At the moment, I'm leaning towards giving Rust a try (despite the gigantic SDK). I'm familiar with C++, and it's certainly capable, but Rust's memory safety guarantees are worth exploring in depth. Hopefully that doesn't end up being a mistake...

C, Go, Zig

If I'm just looking for a simple and fast language, that would leave C, Go, and Zig. C is almost certainly future-proof for another decade or two and the SDK is reasonably sized. I'm less sure about Go and Zig, despite their efficient tooling. I'm skeptical of Go's niche (native code, but with a garbage collector). Zig seems too immature, but its C/C++ compatibility is attractive.

Overall, Rust's unique combination of safety and speed is compelling, and I'm sure I'll end up using it eventually. I'll probably also need to use C where it's already entrenched.

Interpreted languages

Here are the remaining interpreted/dynamic languages:

These languages are almost a battery inclusion continuum from "included" to "sold separately".

Python

I don't like Python. But it's everywhere! Now that Python is convenient on Windows, Python is almost a defacto portable shell scripting language (with a lot more). For example, the Zig bootstrap process requires Python! If it had required Node, people would have lost their minds. I think it's high time I give in and just embrace Python.

JavaScript

JavaScript has somehow leap-frogged Python to become a convenient language, and TypeScript makes it even better for large projects. But I will admit that Node's API is irritating, and the NPM ecosystem continually has to grapple with the lack of a standard library. Deno's tooling is excellent, but Deno also breaks compatibility left and right. JavaScript is definitely future-proof, but the non-browser runtimes I'm less sure about.

Lua

Lua is a great language for embedding, but I'm not sure it can compete with the popularity of Python and JavaScript for general software tasks.

All of the above

Realistically, I'm probably going to use all three of these scripting languages, just for different purposes:

That's all!

When I started this investigation, I thought Rust was a shoo-in, but it's big and not as stable/polished as I'd expected. It's still at the top of my "native" list, but only because of its unique combination of memory safety and systems-level design.

Prior to digging in a bit more, I wanted to continue avoiding Python in favor of JavaScript/TypeScript (running under Deno where possible, and Node otherwise). I've finally given in, and I think I'll be writing a lot more Python in the future. I'd also written off Lua, but its minimalist design dovetails nicely with my minimal development environment aspirations, so I might give Lua another shot. I'll happily continue using JavaScript (really TypeScript) for the web, of course.

Addendum/notes

The next section is just a collection of notes I made while testing out languages. It's likely riddled with factual errors.

C#

Go

Lisp (SBCL)

Lua

JS/TS

Python

Rust

Tcl

Zig