Cloud Parts

Programming languages

What actually separates one language from another in practice: how the code runs, when types are checked, who frees memory, and which styles of program and concurrency it makes easy.

15 technologies on this page ↓

How it works

Syntax is the visible difference, but a few deeper choices decide how a language behaves in production. The first is how the code runs. C, C++, Rust, Go, Zig and Haskell (with GHC) are compiled ahead of time to machine code for one operating system and CPU. Java, Scala and Clojure compile to bytecode for the JVM, C# and F# to intermediate language for .NET, Erlang and Elixir to bytecode for BEAM; a runtime virtual machine then compiles the busy parts to machine code as the program runs. Python’s main implementation, CPython, compiles source to bytecode too, but runs it in an interpreter loop. The second is typing. A statically typed language (Java, C#, Go, Rust, Haskell) checks types at compile time, and many infer them, so let x = 5; in Rust needs no annotation. A dynamically typed language (Python, Clojure, Erlang, Elixir, Racket) attaches types to values and checks them as each line runs. Strong and weak are looser words for how willingly a language converts between types on its own: JavaScript turns "1" + 1 into "11", Python raises a TypeError. The third is memory. In C and Zig the programmer frees what they allocate. Java, Go, Python and most others use a garbage collector. Rust checks ownership rules at compile time and frees each value when its owner goes out of scope, with no collector. The rest is style and concurrency: which paradigms the language favours (object-oriented, functional) and how it runs many things at once (OS threads, async/await, goroutines, lightweight processes).

Think of it as

Ways of getting dinner from a recipe. A compiled language is a meal cooked ahead and frozen: quick to serve, but only in kitchens with the right oven. A language on a virtual machine ships a meal kit that any kitchen with the standard equipment finishes on site, and the cook gets faster at the dishes ordered most. An interpreted language sends the cook along to read the recipe out, step by step, every time.

Reach for it when

  • Choosing a language for a new service: match its runtime, typing and memory model to the job, not just its syntax
  • Explaining why the same algorithm is fast in one language and slow in another, or why one crashes on a type error only in production
  • Picking between a garbage collector (simpler code, some pauses) and manual or ownership-based memory (predictable, more work)
  • Mixing languages: languages that share a runtime (JVM, .NET, BEAM) can call each other’s libraries directly

Think twice when

  • Ranking languages as fast or slow in general: the implementation, the libraries and the workload matter more than the language’s name
  • Treating “compiled” and “interpreted” as fixed labels: CPython compiles to bytecode, and JIT compilers turn VM bytecode into machine code
  • Choosing on paradigm alone: most modern languages mix object-oriented and functional features

A real example

A chat server keeps 100,000 connections open, each mostly waiting. In Erlang or Elixir each connection gets its own BEAM process, which starts at 327 words (2,616 bytes on a 64-bit machine), about 262 MB for all of them, and the runtime switches out any process after 4,000 reductions, roughly function calls, so none can hog a core. In Go each connection gets a goroutine whose stack starts at a few kilobytes and grows only when needed. In Java 21 each can get a virtual thread instead of an OS thread. C and Rust have no such scheduler built into the language, so the usual design there is an event loop in which a few OS threads serve every connection. Same job, four concurrency models, each a consequence of how the language and its runtime were designed.

More on programming languages

Narrower ideas that build on this one.

Kinds of programming languages

Specific products, and what each does differently.

Compiled to machine code →

A compiler turns the whole program into machine code for one operating system and CPU before it runs, so it starts fast and needs no virtual machine.

Run on a virtual machine →

The compiler emits portable bytecode for a runtime such as the JVM, .NET or BEAM, which loads it, compiles the busy parts to machine code and manages memory and threads.

Interpreted →

An interpreter reads the program and carries it out as it goes, with no separate build step, at the cost of speed in tight loops.

Fifteen languages at a glance#

How each one usually runs, checks types and manages memory. “First appeared” is the first public release unless noted.

LanguageFirst appearedCreated byHow it usually runsTypingMemory
C1972 (developed 1969–1973)Dennis Ritchie, Bell LabsCompiled to machine codeStaticManual: malloc and free
C++1985 (work began 1979 as “C with Classes”)Bjarne Stroustrup, Bell LabsCompiled to machine codeStaticManual, made automatic by destructors (RAII)
Erlang1986 (work began); open source 1998Joe Armstrong, Robert Virding and Mike Williams, EricssonBytecode on BEAMDynamicGarbage collected, one heap per process
Haskell1990 (Haskell 1.0 report)A committee, report edited by Paul Hudak and Philip WadlerCompiled to machine code (GHC)Static, inferredGarbage collected
Python1991 (work began December 1989)Guido van Rossum, CWIInterpreted (CPython runs bytecode)Dynamic; type hints not enforcedReference counting plus a cycle collector
Racket1995 (as PLT Scheme; renamed 2010)PLT, led by Matthias FelleisenCompiled to machine code (Racket CS, on Chez Scheme)Dynamic; Typed Racket adds static typesGarbage collected
Java1996 (JDK 1.0)James Gosling, Sun MicrosystemsBytecode on the JVMStaticGarbage collected
C#2002 (C# 1.0)Anders Hejlsberg, MicrosoftIntermediate language on .NETStaticGarbage collected
F#2002 (v0.1); F# 2.0 in 2010Don Syme, Microsoft Research CambridgeIntermediate language on .NETStatic, inferredGarbage collected
Scala2003Martin Odersky, EPFLBytecode on the JVMStatic, inferredGarbage collected (the JVM’s)
Clojure2007Rich HickeyBytecode on the JVMDynamicGarbage collected (the JVM’s)
Go2009 (open source; design began 2007)Robert Griesemer, Rob Pike and Ken Thompson, GoogleCompiled to machine code, runtime linked inStaticGarbage collected
Elixir2014 (1.0; first commit 2011)José ValimBytecode on BEAMDynamic, with gradual types being addedGarbage collected, one heap per process
Rust2015 (1.0; begun 2006)Graydon Hoare; sponsored by Mozilla from 2009Compiled to machine codeStatic, inferredOwnership, checked at compile time
Zig2016Andrew KelleyCompiled to machine codeStatic, with compile-time genericsManual, through allocators passed explicitly
  • “Compiled”, “bytecode” and “interpreted” describe the usual implementation, not the language: ClojureScript compiles Clojure to JavaScript, C# can be compiled ahead of time with Native AOT, and CPython has an experimental JIT.

Typing: static or dynamic, strong or weak, inferred#

Static and dynamic answer when types are checked. Rust is statically typed, so the compiler must know the type of every variable before the program runs, and C# says every variable “has a type known at compile time.” Python checks as it goes: a name can hold an int now and a str later, and annotations are optional, because “the Python runtime does not enforce function and variable type annotations.” Gradual typing sits in between: Elixir is adding set-theoretic types to its compiler, with a dynamic() type for values checked at run time.

Inference removes most of the cost of static types. Haskell, F#, Scala and Rust work out the types of local values from how they’re used, so the code reads almost like a dynamic language while the compiler still checks it.

Strong and weak have no agreed definition. They usually mean how far the language converts values between types on its own. JavaScript’s + converts the other operand to a string if either side is a string, so "1" + 1 is "11". Python refuses the same expression with a TypeError, and Java’s specification calls the language “strongly and statically typed.”

Memory: manual, garbage collected or owned#

In C, memory from malloc stays allocated until the program passes it to free; forgetting leaks it, and freeing the same block twice is undefined behaviour. Zig keeps that model on purpose: the language “performs no memory management on behalf of the programmer,” and every function that allocates takes an allocator as a parameter, which is why Zig needs no runtime and suits kernels and embedded devices. C++ is manual too, but destructors release resources when an object goes out of scope (RAII), which Stroustrup describes as memory management that is “safe and implicit without a garbage collector.”

Most languages hand memory to a garbage collector instead. Java’s specification puts it bluntly: objects “are never explicitly deallocated.” CPython frees most objects as soon as their reference count drops to zero and runs a cycle collector for the rest. BEAM gives every process its own small heap and collects each one separately.

Rust takes a third route. Every value has one owner, the compiler checks the ownership rules, and the value is freed when its owner goes out of scope. There is no collector and, in the Rust book’s words, none of these checks slow the program down while it runs; the cost is learning the rules.

Paradigms and concurrency#

Paradigm is about how a program is organised. Object-oriented languages bundle data with the methods that act on it; functional languages build programs from functions and immutable values. Few languages are pure examples: C# describes itself as object-oriented with features from other paradigms, Scala and F# mix both, and Clojure calls itself a functional language with a dynamic emphasis.

Concurrency is often where languages differ most. Go multiplexes cheap goroutines onto OS threads; Erlang and Elixir run millions of isolated processes that share nothing and exchange copied messages; Java 21 added virtual threads; C# offers async/await over Tasks. The models are compared in Synchronous and asynchronous execution.

In code · python

import dis

def add(a, b):
    return a + b

dis.dis(add)            # CPython compiled add() to bytecode before running it
# Python 3.14 prints (opcodes change from version to version):
#   3           RESUME                   0
#   4           LOAD_FAST_BORROW_LOAD_FAST_BORROW 1 (a, b)
#               BINARY_OP                0 (+)
#               RETURN_VALUE

print(add(1, 2))        # 3: dynamic typing, + on two ints
print(add("1", "2"))    # 12: the same code on two strings
try:
    add("1", 2)          # strong typing: no silent conversion
except TypeError as e:
    print(e)            # can only concatenate str (not "int") to str

Often confused with

Where it came from1972–2015

For the first decades, the main question was how close to the machine to sit. Dennis Ritchie built C at Bell Labs between 1969 and 1973, with its most creative period in 1972, as a systems language for the young Unix; it grew out of the typeless BCPL and B and gave them a type structure. Bjarne Stroustrup started “C with Classes” down the corridor in 1979, and the first commercial C++ came out in October 1985. Both compiled straight to machine code and left memory to the programmer.

Others traded speed for convenience or safety. Guido van Rossum started Python over the 1989 Christmas holidays at CWI, building on his work on the interpreted language ABC, and posted it to Usenet in February 1991. A committee formed in 1987 to unify more than a dozen lazy functional languages published Haskell 1.0 on 1 April 1990. Java (1996) and C# (2002) made compiling to a portable virtual machine with a garbage collector the default for business software.

The 2000s and 2010s brought languages built for specific pains. At Google, Robert Griesemer, Rob Pike and Ken Thompson started Go on 21 September 2007 because they had to choose between efficient compilation, efficient execution and ease of programming; Go aimed for all three, with garbage collection and concurrency built in. Graydon Hoare began Rust in 2006, Mozilla sponsored it from 2009, and Rust 1.0 arrived in May 2015 with memory safety enforced by the compiler instead of a collector.

Why it’s built this way

Compile to the machine, or to a portable virtual machine
Native code starts fast and needs nothing installed, but has to be rebuilt for every platform. Java targeted networked devices where code had to run on any client, so it compiled to bytecode and let the runtime finish the job.
Check types before the program runs, or while it runs
Static checking catches mistakes early and lets the compiler optimise; dynamic typing is quicker to write and change. Go’s designers set out to combine the ease of an interpreted, dynamically typed language with the efficiency and safety of a statically typed, compiled one.
Collect garbage, or prove memory safety at compile time
Garbage collection removed whole classes of memory bugs but cost run time and memory. Rust’s ownership rules give the same safety with checks done entirely by the compiler, at the price of a steeper learning curve.
Keep the language small and leave memory to the programmer
Zig has no runtime and no hidden allocations, so the same code works in kernels, embedded devices and real-time software where a collector or hidden control flow isn’t acceptable.

Timeline

  1. 1972

    C’s most creative year at Bell Labs, as the language of Unix

  2. 1979

    Bjarne Stroustrup starts “C with Classes”, later C++

  3. 1990

    Haskell 1.0 report published on 1 April

  4. 1991

    Guido van Rossum posts Python to Usenet in February

  5. 1996

    JDK 1.0 ships Java on a virtual machine

  6. 2009

    Go becomes open source on 10 November

  7. 2015

    Rust 1.0 released on 15 May

Sources52

Improve this page on GitHub