8 Big Ideas From 8 Programming Languages

A tour of some of the defining characteristics and ideas of eight different programming languages

Erik Engheim
Nov 21, 2020 · 18 min read
computer code on a screen
computer code on a screen
Photo by luis gomes from Pexels

I have had an affinity for playing around with different programming languages for over 20 years. This is an attempt at recollecting some of those experiences — ideas which really stood out in each language or which I have in retrospect seen the significance of.

This list is highly subjective. It is a list of features representing ideas that caught my attention. You will always find people who think some other feature or idea is more prominent or important, but that is fine.

Zig programming language logo and wordmark

Zig — Compilation Time Code

In some areas, Zig almost behaves as a dynamic language despite being statically typed. With statically typed languages, no code runs until after you have compiled that code. However, in Zig, you can mark data as known at compile time. If the compiler cannot determine the value at compile time, it is a compilation error.

On the other hand, code that relies exclusively on values known at compilation time is allowed to run at compilation time, rather than at runtime. Look at this innocent example which looks a lot like calling printf in C:

print("number: {} string: {}", .{num, str});

In C, this is a potentially risky function call because at runtime, code has to analyze the format string to see how many arguments to read in. However, the format string may not match the argument list. In fact, this is bad enough that modern C compilers treat this as a special case, which is verified at compilation time. However, this is a special exception only made for the printf function.

In Zig, no special treatment is required. The format string is marked as known at compile time with the special comptime keyword:

print(comptime format: []const u8, args: anytype)

That means if the compiler discovers that it is not known (e.g., because you read that string in from a file), then a compilation error will be produced, and you have to make sure this format string is indeed known at compilation time.

The num and str variables have values that may not be known at compilation time, but their types are known. This allows Zig to execute all the code inside print, which only relies on the format string and knowing the types of the arguments.

You can imagine this involves a bunch of loops and if-statements deciding on what other functions to call depending on the type of the num and str variables. In the finally compiled function, all this code will be gone, and only the code to be executed at runtime will be left.

Julia programming language wordmark

Julia — Multiple Dispatch

There is a lot to say about Julia, but the one feature that sticks out with Julia is that everything is built upon multiple dispatch. What does that mean? In Julia terminology, one can have a function fight, but a function has multiple implementations called methods. So we could have multiple methods (don't confuse with OOP) defined such as:

fight(a::Archer,  b::Knight)
fight(a::Pikeman, b::Archer)
fight(a::Knight, b::Knight)

Unlike object-oriented programming, all the arguments decide what code gets run, not just a special first argument such as this or self. And no, this is not function overloading, as this decision is made at runtime, not at compilation time. You do not need to know the type of a and b upon compilation.

This is of significance in something like a game. At compilation time, you cannot know what type of soldiers will end up fighting each other in the game. Whether an archer fights a knight or a pikeman is a dynamic decision made by players at runtime.

You can use it to define different ways of displaying different datatypes. For example, all data types in Julia use the show function to display any data type on any IO device:

show(io::IO, x)

If you don’t add your own method, it will by default simply use reflection to discover the fields in your object x and display it. But you could define your own custom display of, say, a Point type with:

show(io::IO, p::Point) = print(io, "($(p.x), $(p.y)")

Representing the point in a different manner for another IO device is also possible:

show(io::IOBuffer, p::Point) = print(io, "BufPoint($(p.x), $(p.y)")

Without multiple dispatch, this flexibility would be hard to accomplish. Either the Point type would have to implement some serialization interface or you would have to modify an IO base class and its subclasses to accept the new Point type.

Swift programming language logo and wordmark

Swift — Optionals

While Swift was not the language that pioneered optional types or sum types, nor the first language that exposed me to the idea, it was the first language where I began using this concept regularly. Here from Apple’s Swift documentation is an example explaining the concept:

if let firstNumber = Int("4") {
if let secondNumber = Int("42") {
if firstNumber < secondNumber && secondNumber < 100 {
print("\(firstNumber) < \(secondNumber) < 100")
}
}
}

// Prints "4 < 42 < 100"

Parsing a string "4" to an integer 4 could fail. In Swift, the Int function will return a value of type Int?, which means it could be an integer or a null. The type Int can only be an integer. Types such as Int? and String? cannot be used directly since they could be null. They must be unwrapped in some way. That is what the if let statement in Swift does.

Here is another example which is equivalent:

if let firstNumber = Int("4"), 
let secondNumber = Int("42"),
firstNumber < secondNumber && secondNumber < 100
{
print("\(firstNumber) < \(secondNumber) < 100")
}

// Prints "4 < 42 < 100"

Read more about this in Apple’s documentation. Going from Objective-C to Swift, I was amazed at how many sneaky little bugs I could catch from this. And especially compared to C++, I love how this simplified a lot of functions. No longer did I have to write a reams of defensive code checking for null pointers.

Swift programming language wordmark

Go — Structural Typing

Most people will go on about how programming concurrency is awesome in Go. But for me, structural typing had the biggest appeal. What most of you are familiar with is what is called nominal typing. This is how typing works in C/C++, Java, and C#.

What this gives Go is a form of duck typing very similar to dynamic languages, but checked at compile time. Go does not have implementation inheritance, but you can define structs with methods and you can define interfaces.

diagram illustrating the hierarchical inheritance of methods in the Go programming language
diagram illustrating the hierarchical inheritance of methods in the Go programming language
Pikeman and Knight need to have the methods defined in Soldier to adhere to the Soldier interface.

I can define a function fight which take an argument of type Soldier where Soldier is some interface with a list of methods. fight can then accept objects of type Pikeman or type Knight as long as both of them have all the methods defined in the Soldier interface. Here is an example of part of such an implementation in Go:

type Soldier interface {
damage(amount int)
attack(soldier Soldier)
}
func fight(a Soldier, b Soldier) {
a.attack(b)
b.attack(a)
}

Notice that, when defining Knight, we don’t specify that it implements the Soldier interface; it simply does by implementing its methods:

type Knight struct {
health int
}
func (knight *Knight) damage(amount int) {
knight.health -= amount
}
func (knight *Knight) attack(soldier Soldier) {
soldier.damage(4)
}

We can pass Knight and Pikeman to a function expecting Soldier types, and the Go compiler figures out that the interfaces match structurally.

knight := Knight{12}
pikeman := Pikeman{8}
fight(&knight, &pikeman)

This is similar to, for instance, Python or Ruby. They don’t care what exactly the type of the inputs are, as long as the objects respond to the method calls made on them. Go lets you do that but in a type-safe manner. If the interface type and struct type didn’t match up, you would get a compilation error. For example, if I didn’t implement the damage method for Pikeman, I would get the following error:

cannot use &pikeman (type *Pikeman) as type Soldier in argument to fight:
*Pikeman does not implement Soldier (missing damage method)

Objective-C — Categories

One of the things I really loved about Objective-C when coming from a C++ background was categories. A category allows you to add any number of methods to an existing class without subclassing it, and this can be done in separate libraries.

How is this useful? Imagine you are storing an object-graph to disk representing some intricate user interface and you want to recreate it. You could traverse this object-graph and call, say, a method createUI to create a corresponding UI object for that object.

Here is the problem: An object-graph made up of model objects should not have dependencies to GUI code. You don’t want to link a graphics library to a library containing non-GUI code.

With categories, however, you could add a createUI method to every one of the object types in the object-graph. You can add different code for each object type.

It is an elegant solution that a C++ developer would have to solve with, say, a visitor pattern. Categories are no longer unique to Objective-C. You also have this feature available in Swift. Because Swift has more normal syntax, it can be useful to show an example of a Swift class extension which is similar to categories:

extension Int {
func takeAway(value: Int) -> Int {
return self-value
}
}

let a = 10
let b = a.takeAway(value: 3)
print(b)

In this case, we are extending the Int type with the takeAway method — which is quite useless, but this is just a simple example.

For a more detailed explanation of how to implement the visitor pattern more elegantly in Swift, you can look at this more detailed explanation. It would work equally well in Objective-C.

Imaginary green creature with five eyes and four legs holding a pennant with the work LISP written on it

LISP — Homoiconicity

The LISP programming language and its derivatives have a feature called homoiconicity that has fascinated me for a long time, even if I don’t do regular programming in LISP.

What it means is that code is represented the same way as data and can thus be manipulated in the same way as data.

In LISP the core data structure is a linked list. Here is a simple example of a list with some elements:

(list 43 "hello" true 2.5 'c')

Please note I am using familiar C-like syntax for booleans and characters here. LISP will use different syntax.

A linked list consists of nodes, where each node can point to another node. It also has a cell to store data. But this can also be a pointer to another list. Hence we can have lists of lists, and these lists can also be lists of lists.

(list 34 (list "hello" true) (list 2.5 'c'))

One can express complex data structures and tree structures this way. If you know a little bit about how a compiler works, you know that when it parses code, it produces an abstract syntax tree.

diagram showing an abstract syntax tree of the expression: a + 3 > 4–2b
diagram showing an abstract syntax tree of the expression: a + 3 > 4–2b
An abstract syntax tree of the expression: a + 3 > 4–2b

When coding in LISP, you are creating this abstract syntax tree directly, and you can in principle rearrange and transform such a tree of code just like any other code.

Let us take an example to get across the universality of the LISP approach. If we look at a Hello World program in C, then the abstract syntax tree or the data structure representing this program is not immediately apparent.

#include <stdio.h>

int main () {
printf("hello, world\n");
return 0;
}

However, there are LISP-flavored versions of C which let you write this program in LISP syntax instead (what we call s-expressions).

(import cstdio)

(def main (fn extern-c int (void)
(printf "hello, world\n")))

Let’s take another example to clarify how this works.

struct Point {
int x;
int y;
};

Using LISP s-expressions, this becomes:

(def Point (struct intern (
(x int)
(y int)
)))

Where am I going with this? Why write code in such a seemingly ugly and awkward syntax? With this syntax, everything is entirely regularized. For example, in this definition, one can clearly see that the code is defined as a list, where the first two elements are def and Point. The third element is another list which starts with the elements struct and internt. It has again a third element which is another list containing all of the variable definitions in the struct.

Okay, so what? What does that give me? You could place, for example, this LISP code in a file, and other LISP code could load this code as data and transform it. It can iterate over this code like any other linked list. It could insert and replace elements.

But in LISP you don’t even need to put this in a separate file. Code can be placed as data directly within other LISP code using quoting. For example, in LISP, this means to add 4 to the variable x.

(+ 4 x)

In normal LISP you can declare a variable like this with a value:

(defvar y 10)

And later change the value with setf:

(setf y 5)

But it does not need to contain a number. We can put anything in there, even a list of code. But how do we avoid that the code gets executed? If we do this, we just store 7, in y:

(setf y (+ 3 4))

What we can do is to quote a list, which turns an expression into just regular list data:

(setf y '(+ 3 4))

If I use a LISP REPL environment (interactive command line) such as SBCL, we can inspect this and evaluate it:

sbcl> y
=> (+ 3 4)

sbcl> (eval y)
=> 7

But you can dynamically create such a list of code yourself. The cons function adds a node to the beginning of a list.

sbcl> (cons 3 '(5 8))
=> (3 5 8)

You can use this to combine fragments of code in a list. For example, in the example below, we pick out the first element of y, which is the + operator, and then we prepend that to a list of the numbers 4 and 5.

sbcl> (cons (first y) '(4 5))
=> (+ 4 5)

We can evaluate this new expression that we have created:

sbcl> (eval (cons (first y) '(4 5)))
=> 9

This is obviously a much bigger topic, and I can only scratch the surface. But the point is to show that by putting any code inside s-expressions, you can easily transform and manipulate this code. So you could, for example, have the LISP-flavored C code I showed earlier inside regular LISP code and perform transformations on that code. This code can then be fed to a program such as C-Mera, which will turn the LISP-style C into normal C code that can be compiled.

This was utilized on the Playstation 2 by the Naughty Dog company, which made Game Oriented Assembly LISP to create games such as Jak and Daxter. Basically, this was wrapping PS2 assembly code in LISP syntax, giving the opportunity to write a low-level language in a high-level one.

That is why LISP is often thought of as a language to make languages. Another advantage of the highly regular syntax is that you can use special LISP editors that navigate tree structures rather than lines and columns. Hence you got keyboard combinations that move to the next sibling in the syntax tree, to a parent, or to a child. You can do things such as select all the child nodes.

Pharo programming language logo and wordmark

Smalltalk — Image-Based Development

Smalltalk is not the first language with image-based development. In fact, that was pioneered with LISP. However, Smalltalk is perhaps the language where it has the most natural fit and provides the most power.

Which of course begs the question: What is image-based development?

No, it does not mean you are drawing images to create programs. Instead, we use the word image to mean a serialization of data in memory. You can think of programming Smalltalk much like an interactive session in a REPL environment. If you have programmed JavaScript, Python, Ruby, or Lua, you should be familiar with REPL-based development. You define variables, functions, etc. in an interactive environment.

Smalltalk takes this a step further. Your code isn’t saved in some separate file you may occasionally load up in your REPL environment. No, the whole development process happens in this interactive environment and the whole thing gets dumped to disk. Essentially you have no separate text files that you manage.

Instead, you can think of Smalltalk development as manipulating an object database with an IDE. But it gets crazier than this. This whole interactive environment is itself written in Smalltalk and is part of that same environment.

This is roughly how it works: At the bottom, you got a virtual machine (VM), which loads an image, which is basically an object database. This image contains the whole Smalltalk development environment itself. Through the IDE, which itself exists in the Smalltalk image, you perform actions that add more objects to this image.

The image below is from a modern incarnation of Smalltalk called Pharo. It shows a typical Smalltalk code editor. At the far left, you got packages. Next to the packages, you got a list of classes defined within the selected package. Next, you got categories for methods. And finally, at the far right, you got methods in that category.

Animation showing an example of traversing classes and methods in the Pharo IDE
Animation showing an example of traversing classes and methods in the Pharo IDE
Example of traversing classes and methods in the Pharo IDE. As you can see, even True and False are classes in Smalltalk.

In the lower part, you see the code for that method. However, it is important to realize that this code is stored as data on class. It is not stored in a text file. You cannot scroll further down and see the next method. Each method is viewed separately because they are separate entities.

So what is the significance of this? Why does it matter? What does this approach give us?

It means that a Smalltalk IDE has a full, always up-to-date, in-memory representation of your code. And this representation is Smalltalk objects. So both you and the IDE can traverse all your classes, objects, and methods. It is what gives Smalltalk IDEs such powerful refactoring tools. Doing things such as renaming a method or moving a method isn’t a text substitution operation like in other IDEs. Instead, it is an object manipulation. You manipulate live objects.

Because the whole development environment is made from classes, methods, and objects you can browse from the development environment and which exist live, you can modify your own development environment and see the changes happen immediately. That means you can change behavior and add new functionality. In fact, even the compiler itself is written in Smalltalk, so you can even change how your classes and methods are compiled.

This means that the first time you use Smalltalk, it can be a bit confusing. For example, Pharo begins with a simple launcher program where you select the image to launch. This is natural. Otherwise, how would we deal with you screwing up your whole image since that could mean your whole development environment no longer works?

Fortunately, Pharo has lots of tools for saving images, restoring them, resetting them, and performing version control. Version control is interesting in Smalltalk since in principle there are no text files. So how would you use, for instance, Git and GitHub with Smalltalk?

In Pharo, this is solved with a special Git manager called Iceberg, shown below:

screenshots from Iceberg Git manager
screenshots from Iceberg Git manager
Iceberg is used to interact with Git in Pharo. A special tool is needed since Smalltalk is not file-based.

Iceberg will export the Smalltalk image to a format Git can deal with. This means classes are turned into directories, and each method is turned into a separate file. This allows you to use Iceberg to track code changes at the level of individual methods.

It’s important to realize this is just an export format. You cannot run this directly. You use Iceberg to import changes from Git.

Image-based development gives a lot of interesting opportunities. You can, for example, store your image in the middle of a debug session and later reload your IDE and resume from where you left off.

Animation of an example of saving the state of the IDE in the middle of a debugging session
Example of saving the state of the IDE in the middle of a debugging session. Debugging can be resumed at the same spot later.

You can export just the state of your debugger to a separate file. You can attach this to a bug report and let another developer pick up debugging where you left off.

Lua programming language logo and wordmark

Lua — Everything is a Hash Table

In LISP, everything is a linked list. In Smalltalk, everything is an object. Lua has its own unique twist on this. In Lua, everything is centered around the hash table as the primary data structure.

For example, an array in Lua is just a hash table where the indices are integers. Lua uses hash tables for everything. Namespaces are simply hash tables containing functions, objects, and other hash tables.

These hash tables are quite clever. They can point to meta-hash tables, which again can have their own meta-hash tables in an infinite chain.

If you try to look up an item in a hash table and it doesn’t have that key, it will continue the lookup in its meta-hash table. If that table doesn’t have it, it will ask its meta-hash table, and so on.

This can form hierarchies. Several hash tables could point to the same meta-hash table. And collections of meta-hash tables could all point to another shared meta-hash table. Thus we can create a hierarchical tree structure.

This can be used for many purposes. It gives a way of providing default values when you have not defined a value yet. Lua was originally intended as a configuration language, where you could specify intricate configuration settings.

But you could take it further. You could use this to define an object-oriented system if you like. Say we define a rectangle with a width and height field.

local rectangle = {width = 10, height = 20}

Next, assign a function object to the area key, which takes a rectangle as the argument and calculates the area of the rectangle.

rectangle["area"] = function(rect)
return rect["width"] * rect["height"]
end

But there is an equivalent syntax sugar version of this form, written this way:

rectangle.area = function(self)
return self.width * self.height
end

Here is yet another equivalent form:

function rectangle.area(self)
return self.width * self.height
end

And finally, if we use the : symbol instead of ., then we don't have to write self as the first argument explicitly, even if it is still the first argument:

function rectangle:area()
return self.width * self.height
end

There is a neat parallel to this when accessing the function object in the rectangle hash table and calling it. The two forms below are equivalent:

rectangle.area(rectangle)
rectangle:area()

Now instead of assigning an area function to every rectangle, you can instead assign all the functions you want for every rectangle to another hash table; let’s call it Rectangle. Then we can set the meta-table for rectangle and every other rectangle object to point to this meta-table. That means when you call rectangle:area(), you actually end up locating area on the meta-table. And voila, we got ourselves an object-oriented system with inheritance and everything.

Part of the appeal here is that Lua is a really tiny language. You could define the syntax on a postcard. Yet you have a very flexible and versatile language with a few features.

Final Remarks

There are many other interesting languages out there. While I loved working with, for instance, Ruby and Python in the past, I cannot say they necessarily offered anything revolutionarily new which I had not seen before. Instead, it was more about offering a new package of features that had been explored elsewhere before. For me, Ruby, for example, allowed me to do a lot of the things you can do in Smalltalk in a more pragmatic fashion. Sure Ruby has no image-based development model, but it works with good old files and can be run as a script like bash. Hence you could use it to make simple shell tools. Smalltalk in contrast is not really suited for that. Or at least was not, when I first explored it.

Then there are languages which are truly novel in their own right, such as Haskell, Rebol, and Forth, but which I simply have too little experience with to do justice to.

But I can mention briefly that Haskell takes type systems and functional programming to the next level. Or at least did. There are more modern variants now, like Idris.

Forth is interesting as it is a stack-based language that offers both very high-level and low-level programming. The code looks really oddball for people used to normal programming languages, but it is perhaps the quickest language to implement on a new architecture with only access to assembly code. Hence a lot of microcontroller systems often have a Forth system installed.

Better Programming

Advice for programmers.

Sign up for The Best of Better Programming

By Better Programming

A weekly newsletter sent every Friday with the best articles we published that week. Code tutorials, advice, career opportunities, and more! Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Erik Engheim

Written by

Geek dad, living in Oslo, Norway with passion for UX, Julia programming, science, teaching, reading and writing.

Better Programming

Advice for programmers.

Erik Engheim

Written by

Geek dad, living in Oslo, Norway with passion for UX, Julia programming, science, teaching, reading and writing.

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store