Skip to content

Instantly share code, notes, and snippets.

@xixixao
Last active December 16, 2022 13:28
Show Gist options
  • Star 36 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xixixao/8e363dbd3663b6729cd5b6d74dbbf9d4 to your computer and use it in GitHub Desktop.
Save xixixao/8e363dbd3663b6729cd5b6d74dbbf9d4 to your computer and use it in GitHub Desktop.

From Languages to Language Sets

After working with a lot of languages, writing my own, this is currently what I consider the most useful classification of programming languages, into 4 levels:

  • 4: Interpreted, dynamically typed: JavaScript, Python, PHP
  • 3: Interpreted, statically typed: Hack, Flow, TypeScript, mypy
  • 2: Compiled with automatic memory management (statically typed): Go, Java (Kotlin), C#, Haskell, Objective-C, Swift
  • 1: Compiled with manual memory management (statically typed): Rust, C, C++

There is a 0th level, assembly, but it’s not a practical choice for most programmers today.

Now every language trades off “ease of use” with “performance”. On this hierarchy the higher numbered, “higher level”, languages are easier to use, while the lower numbered, “lower level”, languages are more performant.

I postulate that for most programming, the “business logic” kind of programming, we want to use a language that sits right in the middle of that hierarchy. Looking at the languages listed that’s no revelation. One language could combine the 2nd and 3rd level though. A language that can be interpreted during development for fast iteration cycle, but compiled for better performance for deployment. There isn’t such a language popular today though.

Now let’s address level 4. Big players sit at this level, perhaps the most popular languages by headcount of their programmers. The problem with a lack of static typing is that it’s hard to work on such code in groups and at scale. Every successful business started with those languages eventually rewrites their codebase to use one of the “lower level” languages because big codebases written by many people are hard to maintain and modify without the support of a static type-checker. They are still great languages for solo, small projects, especially if the code can be easily automatically tested.

Now for level 1, Rust has done an amazing job bringing level 1 to a wider audience of programmers. By both being modern, and safe, it allows many more people to write code that requires best possible performance and resource utilization. In such scenarios Rust should be a clear choice. But coding in Rust is not easy, not in the way coding in JavaScript or Python is. The same solution, much more performant, might require many more lines of code.

And so we come to the levels 2 and 3, where most professional programmers today spend their time. The tradeoff between them is clear: The interpreted languages have a faster development cycle because they don’t require the programmer to wait for a compilation step. But this comes at the cost of performance, as the interpreter in general cannot be as good at optimizing and executing the code as the compiler.

The interesting thing is that these languages are almost identical in their expressive power. The only gap between them is that interpreted languages can include “eval” and dynamic meta-programming (modification of program structure at runtime). These features are usually shied away from in production code though, and are more helpful during development, especially for testing.

The discussion here implies that companies need to use at least 3, often 4 different languages in their codebases. This means 4 different toolsets to maintain. Trainings to provide. Experts to hire. And usually disjoint sets of employee programmers who cannot easily jump from one language to the other.

Clearly there will never be a single language that all programmers use. We need to take advantage of the tradeoffs laid out in this hierarchy. But what we could do is to build a language set, which would smooth out the transition between these levels.

As the basis of this set I propose to use Rust. It is a solid low level foundation to build our language set on. It has modern, well thought out language tooling (including things like its syntax).

There will be 3 languages in this set, besides Rust we want a level 2/3 hybrid and level 4 language.

Let’s look at an example to make this concrete. First a program in Rust:

fn main() {
    let rect1 = Rectangle {width: 30, height: 50};
    println!(The area is {}.”, area(&rect1));
}

struct Rectangle {
    width: u32,
    height: u32,
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

Now a program in RustGC, our level 2/3 hybrid:

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!(The area is {}.”, area(rect1));
}

struct Rectangle {
    width: int,
    height: int,
}

fn area(rectangle: Rectangle) {
    rectangle.width * rectangle.height
}

And now a program in RustScript, our level 4 language:

fn main() {
    let rect1 = { width: 30, height: 50 };
    println!(The area is {}.”, area(rect1));
}

fn area(rectangle) {
    rectangle.width * rectangle.height
}

RustScript can be used for heavy prototyping, especially for complicated stateful programming (interactive UIs). RustGC is our workhorse, with great async support, decent performance thanks to a modern garbage collector, but without the mental overhead of fighting the borrow checker. Finally we reach for Rust any time we need maximum performance and 0-cost abstractions.

RustGC comes with a VM that allows instantenous save -> execute dev cycle, but is compiled for deployment to a binary similar to the one that Rust would compile to, but with an accompanying GC runtime.

The best part is that all three languages share pretty much the same syntax, and they are built so that calling from higher level to lower level variant is effortless. This gives us the ability to use the rich Rust ecosystem from a level 2/3 or even level 4 language.

More examples. UI component in RustScript:

fn app() {
  let (state, setState) = useState({
    total: None,
    next: None,
    operation: None,
  });

  let handleClick = |buttonName| => {
    setState(|state| => calculate(state, buttonName));
  };
  
  let value = state.next.or(state.total).unwrap_or("0");
  <div className="component-app">
    <Display value={value} />
    <ButtonPanel clickHandler={handleClick} />
  </div>
}

Async example in RustGC:

async fn main() {
  let user_ids = vec![1, 2, 3];
  let user_names = user_ids.iter().map_async(
    async |id| => fetch_user_name(id).await,
  ).await;
  println!(user_names.join(", "));
}

async fn fetch_user_name(_: int) -> Future<string> {
  // This could be a database request.
  ""
}
@thor314
Copy link

thor314 commented Apr 8, 2022

Reminds me of the homoiconicity arguments for Lisps. It would be great if this is existed! The rub, as it seems likely to me, is that if the flagship language for a language set is at one level, languages at other levels would inevitably get less attention and be less well maintained than their same-level alternatives.

@tiye
Copy link

tiye commented Apr 8, 2022

While this is an interesting idea, I think it will definitely complicate the compiler/interpreters and even IDEs. I don't hold an opinion on this. But it also reminds of some history in CoffeeScript community that sharing same syntax for multiple targets:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment