Simple programming languages

I like simple programming languages ​​like Gleam, Go and C. I know I'm not the only one. There's something wonderful about working with a simple language: what it's like to read it, use it in a team, come back to it after a long time, etc.

In this post I want to clarify what this simplicity is, highlight a couple of reasons why it is so important. I will suggest five key ideas that should be implemented in a simple programming language:

  1. Opportunities that are always at hand
  2. Fast Iteration Loops
  3. Uniformity in doing everything
  4. Principles of working with functions
  5. Simple Static Type Systems

We will discuss each of these ideas in detail below.

Always at hand

The philosophy of technology distinguishes between two very useful concepts: presence-at-hand and readiness-at-hand. A concept is considered present if it currently occupies your thoughts and is in your working memory. A concept is considered to be at hand if we may not even be aware of its presence until we try to use it. For example, when we enter our kitchen for the thousandth time, we don’t quite accurately imagine what exactly is in all these cabinets, bags, drawers of food, on the table, what decorations there are in the kitchen and anything else. I compare entering the kitchen like this to spontaneously looking into the refrigerator to grab a snack. On the other hand, when I sit down at the table, it is the table and chairs that pass into the category of cash for me, although they were just handy. At this moment, the refrigerator fades into the background and becomes handy.

Just because you use something doesn’t mean it automatically goes from handy to cash. For example, even when you read with glasses, the glasses remain handy and not cash because your brain processes what you see without thinking about the glasses at all. However, if during operation improvised items fail, they immediately become cash. Continuing with the example: if there is a dirty speck on the glass of your glasses, your brain will immediately clearly realize that, in fact, you were reading the book with glasses on.

This idea is closely related to the limitations of human working memory. Memory limits the number of concepts you can handle simultaneously, but it can generalize, helping to filter out the noise (in which we constantly exist) from all the informative data we need.

I've explored this idea of ​​availability here because in simple programming languages ​​it's often true that There is There are many such features that are specially implemented so as not to “clog the airwaves” when we do not use them. For example, Gleam, Go and C are characterized by strong cross-platform functionality, and supporting multiple platforms is a big chunk of work that has to be done when programming on them. When you need to make your code work in a browser, or on a Raspberry Pi, or on a smartphone, or on a server, specific features are added to the language for this purpose, which, however, do not harm its simplicity. Another example is support for the Language Server Protocol (LSP), which receives a lot of attention among developers for Gleam and Go, and in C this protocol is supported very well, despite the age of the language.

I won’t dwell on this too much; I believe that in the following sections it will become a little clearer why I gave this particular example. I recommend you read this article that explores the above ideas about the philosophy of technology in more detail.

Iteration loops

A very fast iteration cycle (usually iteration is equal to compile time) is exactly the aspect that developers of most simple languages ​​strive to implement. If the iteration is fast, then the cost of prototyping and experimentation is very small, and the developer can stay in the flow state.

Obviously, C lacks a little polish in this regard, since the language was originally designed as a one-pass compiler, but in general the entire structure of the language is very well designed for this limitation. You can tell as much as you want how annoying you are when working with header files – given these circumstances, they seem very ergonomic to me. They give us the ability to randomly execute commands, which we take for granted, but such an opportunity should definitely be included in the “handy” category.

But in Gleam and Go, the compiler performance is one of the best in its class. Go is famous for this, so I won't go into too much detail here. The Gleam compiler is written in Rust, and its developers have made it clear that it will not be self-hosted, as this will reduce performance and complicate distribution. Whenever possible, file parsing and processing is parallelized, and at a minimum, my Gleam projects compile instantly.

It is also worth mentioning the dependency system in Gleam, it is extremely nice. It works with the Hex package manager used in Erlang and Elixir, so it generates neat documentation pages in HexDocs format. So you can easily find libraries, and good documentation becomes the norm. To see how much more convenient everything is done in Gleam, let's look at what options are provided when I enter gleam into the command line and press enter:

 $ gleam

gleam 1.0.0

USAGE:
    gleam <SUBCOMMAND>

OPTIONS:
    -h, --help       Print help information
    -V, --version    Print version information

SUBCOMMANDS:
    add        Add new project dependencies
    build      Build the project
    check      Type check the project
    clean      Clean build artifacts
    deps       Work with dependency packages
    docs       Render HTML documentation
    export     Export something useful from the Gleam project
    fix        Rewrite deprecated Gleam code
    format     Format source code
    help       Print this message or the help of the given subcommand(s)
    hex        Work with the Hex package manager
    lsp        Run the language server, to be used by editors
    new        Create a new project
    publish    Publish the project to the Hex package manager
    remove     Remove project dependencies
    run        Run the project
    shell      Start an Erlang shell
    test       Run the project tests
    update     Update dependency packages to their latest versions

Lots of completely straightforward and convenient subcommands! I've been working with Gleam for several months now, published a couple of packages, and added many others to my projects, and so far I'm happy with everything.

Uniformity in doing everything

When you design a language for fast compilation, this often means that you have to do without frills. For example, there are no plans to add metaprogramming to Go; moreover, for a long time there were not even generics provided there.

But often in such languages ​​it is argued as follows: by sacrificing something for the sake of performance, we make individual components of the language only better. In Go, it is customary to wrap all cyclic code in a for loop, all “this or that” code should be contained in if statements, and any “select from a set” code should be contained in switch statements. This is why Go's for loops and switch statements are a bit unusual, and why there is no while loop at all. The story of concurrency in Go follows one approach, but in Rust it is completely the opposite. To some extent, you can write code here in a functional style, but writing lambda expressions in Go is a real hemorrhoid. In the Go type system, any non-trivial problems are solved using interfaces.

In Gleam this idea is further developed. He has a functional pedigree, so there No cyclic structures, only recursion and things like map and fold. Tail call optimization is applied, so code like this compiles much as if it were enclosed in a while loop. Moreover, Gleam doesn't even have if! On the contrary, there is only a (powerful) pattern matching mechanism in the presence of (powerful) constraint conditions. The calculation of the Fibonacci series could be programmed like this:

pub fn fib(n: Int) -> Int {
  case n < 2 {
    True -> n
    False -> fib(n - 1) + fib(n - 2)
  }
}

Pattern matching against True and False works exactly like an if statement, so in practice this “limitation” is never particularly annoying.

In addition, Gleam imposes a snake case (snake_case) when naming variables and functions, and types are named in the Pascal style (PascalCase). Additionally, Gleam has a great system for formatting code dogmatically (just like Go). When you run a project in Gleam, by default it runs the github action to check formatting, among other things. Yes, that is right! The restrictions are such that you are quickly forced into a specific programming style that all your colleagues use.

Gleam openly aims to create a small synergistic set of capabilities, optimize work in favor of fast learning and lightness code reading. According to the motto, this language can be learned in an evening. This focus is very important and definitely resonates with the features that I particularly like about Go. It's hard to imagine how useful this is until you've worked with the language a little yourself.

As AI-enabled code completion gains popularity, this one-directional approach becomes all the more valuable.

Generative AI seems to me to be aesthetically oriented in a philosophical sense, both because of its nature of processing code “word by word” (rather than as a stream of consciousness) and also because of its statistical basis. Thus, when working with simple languages ​​like C, Go and Gleam, programs in which are always written the same way, AI suggestions will be highly accurate. In these languages, the “aesthetics” of the code from a human point of view and from a computer point of view are largely consistent. Above I have provided a function for calculating the Fibonacci series, and it was almost entirely generated by Claude, without any editing, just in the process of preparing the code base for this post (we are talking about a small to medium-sized application on Gleam). I'm pretty sure that there was little or no Gleam code in the Claude training set, and it would be easy to confuse the language with Rust, since the syntax of these languages ​​is (intentionally) similar. But the AI ​​still did a very decent job.

Principles of working with functions

Language used in academia

O.B.J.

, as conceived by the developers, is a functional language without lambda expressions (strictly speaking, it is a language with “term rewriting”). Its scientific creators insist that it is difficult for humans to reason about higher-order functions, so they offer interesting ways to convey in other ways much of the expressiveness that is usually contained in closures. Such methods can be conditionally called “slightly object-oriented”.

C and Go clearly stand out from other languages ​​in this regard. Both of these languages support higher-order functions (Although, in C, closures are organized in a very DIY way, which is not surprising), but this style of code is not at all idiomatic. As I said above, loops should be provided with ready-made loop constructs, and dynamic behavior should generally be achieved in other ways. This is almost obvious when writing code in Go and C, and in Go it is definitely done more for ideological than technological reasons. Lambda expressions in Python are structured in approximately the same way, but such features are less pronounced there.

You might think that Gleam doesn't fit well into this category because it is a purely functional language, but its structure provides mechanisms for working in this style. Local variable bindings in Gleam are not recursive, this is clearly done in order to encourage the programmer to raise functions to the top level. Gleam uses the |> operator, which makes higher-order code much easier to read and judge. Gleam's (cool!) use syntax covers most of the practical uses of lambda expressions in functions, so it feels like you're writing convenient, simple imperative code. For example, you could program something like this, reminiscent of for loops:

import gleam/int
import gleam/list
import gleam/io

/// для каждого i в списке выводим на экран i+1
pub fn print_all_plus_one(l: List(Int)) {
  // этот пример надуманный;  как правило, приходится использовать всего один цикл
  //один цикл:
  let res = {
    use i <- list.map(l)
    int.to_string(i + 1)
  }
  // другой цикл:
  use s <- list.each(res)
  io.print(s)
}

Note that the code in this style is a bit sloppy. In this case, you usually don't have to use use; moreover, you can get by with calls to list.each in the normal order. I just wanted to show how use turns higher order functions into a kind of imperative code. Indeed, I came across code of this type from time to time in the code bases on Gleam.

If these aspects of language design interest you, then I think you will also enjoy this is a cool post.

Type systems

You may wonder why Python is not on my list. The reason writing code in Python is so different is because

refactoring

. I find the type systems from Gleam, Go and C very helpful when I have to make major changes to my code; this way I don't have to keep a lot of unnecessary information in my head. In Python, I find myself wandering through descendants, left wondering what the next type system error I'll stumble upon at runtime. Python doesn't do much to make it easier for me to manage a project, so it's just scary to touch the project in any significant way. Optimizing for readability usually goes hand in hand with optimizing for refactorings.

On the other hand, there may be readers who would add Haskell to my list. Firstly, there is practically nothing in it except lambdas, right? Yes, simple language. Honestly, I don't think anyone would think to add Haskell to this list. Due to the way the type system is structured (and the vicious culture of “taking random sequences of characters and using them to express complex ideas – all in order to make any function one-liner”), Haskell is in no way simple. There are many ways to write code in it, and it's incredibly difficult to read (though a lot of fun once you get the hang of it).

Simple languages ​​amazingly balance on the line between expressiveness and constraint. C, Go, and Gleam all offer dynamic typing in one form or another, clearly designed for a very limited scope. Plus, they all have some quirkiness to express the things you want without using dynamic typing. In Go this is done using interfaces, in Gleam using powerful polymorphism, and in C using preprocessor macros and casts. After all, type systems are very concise and restrictive. The balance achieved in simple programming languages ​​is very pleasing in practice. It’s the same as how a wise family manages to find the golden mean between freedom for the child and reasonable control. The child is not in danger, and at the same time he is happy.

Conclusion

I hope you enjoyed this article. I know how many people there are who value simple languages ​​precisely for their simplicity, but I have almost never seen articles attempting such systematization as I have done here.

As you may already guess, I am very interested in simple programming languages ​​and am planning to write a small language myself in which the above ideas would be developed even more than in Gleam. This language should not have lambdas, just like OBJ. In addition, I have some ideas on how to organize work without a garbage collector, ideas for this I will borrow from the Mojo language, which has an interesting borrowing check system that is convenient for interacting with Python.

In general, I think that the ease of use of the language, high iteration speed, uniformity in all operations, the principles of working with functions described above, and simple static type systems are key ideas that should guide the design of new languages. In this case, you should not strive to invent a new Haskell and Rust.

PS
Please note that we are having a sale on our website.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *