Second edition with machine learning, deep learning, LLMs & AI available now! Buy now
« Back to contents

Programming languages

Introduction

What is a programming language?

A programming language is a formal language for writing programs. It defines the syntax you are allowed to write, the semantics that give that syntax meaning and the abstractions available to the programmer.

If that sentence sounded suspiciously neat, don’t worry. The rest of the chapter is about unpacking the words hidden inside it.

A programming language is a language designed for specifying programs. We instruct computers to understand these languages and perform the computations they specify. The branch of computer science that studies programming languages is known as programming language theory (PLT). It sits at a really interesting intersection of computing, linguistics, logic and mathematics.

The standard academic analysis of programming languages generally involves working up from logical first principles. We’ll approach the subject from the other direction by making a survey of a few popular programming languages. We’ll begin by looking at how programming languages can be understood in terms of their syntax and semantics. Then we’ll examine how they can be categorised into different paradigms according to a few important, distinguishing features. Type systems are important and interesting enough to merit their own section at the end of the chapter.

Let’s begin our survey by looking at what Wikipedia has to say about JavaScript, Go and Haskell, three popular and distinctive languages:

JavaScript is a high level, just-in-time compiled, object-oriented programming language that conforms to the ECMAScript specification. JavaScript has curly-bracket syntax, dynamic typing, prototype-based object-orientation, and first-class functions.

Go is a statically typed, compiled programming language designed at Google … Go is syntactically similar to C, but with memory safety, garbage collection, structural typing, and CSP-style [communicating sequential processes] concurrency.

Haskell is a general-purpose, statically typed, purely functional programming language with type inference and lazy evaluation.

What’s remarkable about all three of these descriptions is that they are at once highly informative to an experienced reader and utterly baffling to everyone else. By the end of this chapter, you’ll understand every bit of that techno-babble!

Defining a programming language

Programming languages are examples of formal languages: languages defined by a formal specification. They are neat and logical, in comparison with natural languages (e.g. English, Mandarin). Grammar defines the elements of the language and how they can be combined into correct statements. The rules of the grammar are known as the language’s syntax. Knowing these rules, we can write syntactically valid source code and the computer can analyse it to derive the structure of the specified program. We’ll cover this process, compilation, in much more depth in chapter ten.

Syntax is the surface level of how the language looks and feels. Programmers generally like their languages to feel familiar and so certain syntax patterns are common across languages. C uses curly brackets to delimit blocks of code:

1 int function square(int n) {
2   return n * n;
3 }

Both JavaScript and Go have a similar syntax rule. This is what Wikipedia means when it says that JavaScript has “curly-bracket syntax” and Go has “C-like syntax”. This is the same function in Go:

1 func square(n int) int {
2   return n * n;
3 }

There are a couple of minor differences but structurally they look very similar. The same function in Haskell, which is influenced by a different family of programming languages, has completely different syntax:

1 square :: Int -> Int
2 square n = n * n

Syntax only tells us whether a bit of code is a valid fragment of the language. It doesn’t tell us anything about what should happen when that fragment is executed. The behaviour of the language is known as its semantics. The semantics of C, Go and Haskell all specify that we have defined a function called square that takes an integer called n and returns another integer that is the square of n. Even though they are syntactically different, all three functions have identical semantics.

The smallest semantic unit is the expression. An expression is anything that can be evaluated, or computed, to a value. 3 * 3 and square(3) are both expressions that evaluate to 9. Expressions can be composed to create more complex expressions: square(3 * 3).

Many languages have the concept of a statement: an executable chunk of code that might contain expressions but does not itself evaluate to a value. Statements are useful because they produce behaviour known as side effects. For example, an if statement usually has the following structure:

1 if (EXPRESSION) { A } else { B }

The statement controls whether A or B is executed next, depending on whether EXPRESSION evaluates to true or false. Statements such as if, while, for and return control the flow of execution through the program. Other important categories of side effects include mutating program state and input/output. These operations are all useful because of the change in system state they effect, not because they evaluate to a useful value.

If you’re unsure whether something is an expression or a statement, ask yourself whether it is meaningful to assign it to a variable. Generally, assignment only allows expressions to be assigned. This JavaScript example doesn’t work because the if statement doesn’t evaluate to anything status could hold:

1 var status = if (user.isValid) { user.status } else { "no user" }

Origins and communities

Designing a language is fundamentally a creative act. Which syntax rules do you choose and what are the semantics? What are the core concepts underpinning the language and how do they all fit together? There is no single, correct answer to any of these questions and so there is no single, “best” programming language. What is an expression of sublime beauty to one person is a confusing and ugly mess to another. What works wonderfully in one problem space might be hopelessly awkward in another.

The combination of syntax and semantics forms the specification of a language. It defines what is considered syntactically valid code and how that code should behave when executed. The exact format of the specification varies. Commonly, languages start out relying on a single reference implementation. This will be a compiler or interpreter, most likely written by the language’s initial creator(s), that is seen as definitive. The language is defined by how the reference implementation behaves. Later, if the language gains popularity and multiple implementations arise, it might prove useful to write a standardised specification document so that everyone has an agreed understanding of how the implementations should behave.

This is the path Ruby has taken. It began with a reference implementation called Matz’s Ruby Interpreter (MRI) written by Ruby’s creator, Yukihiro “Matz” Matsumoto. It has since evolved into multiple implementations standardised by a formal specification. An interesting thing about Ruby is that the specification itself is written in Ruby as a suite of unit tests. If a program successfully passes all of the tests, then it’s a valid Ruby implementation!

Every language has its origin story. Sometimes the language is written by a lone developer, as Ruby was by Matz and Python was by Guido van Rossum. Sometimes a company might put together a team specifically to create a new language, as Google did with Go. No matter who the creators are, programming languages are made to fix a perceived deficiency. Other languages are too slow, too complex, too simplistic, too lacking in support for this or that idea.

There are many, many programming languages out there and very few of them escape obscurity, let alone become popular. What determines whether a language “makes it”? Sometimes it’s a case of being the right tool for the task. A language’s semantics might make it especially well suited to a particular problem or environment. C originally became popular because it made it easier to write performant code for multiple architectures. Technical merits are not always sufficient. C also enjoyed an association with the killer app of the day: the Unix operating system. Go gained some initial name recognition thanks to its association with Docker and Kubernetes at the start of the container hype cycle.

Technical merits aside, Go undoubtedly owes much of its current popularity to the fact that it is backed by Google and has a paid team of developers working on its core libraries and tooling. Languages without corporate sugar daddies have to rely on their creator successfully building an enthusiastic community of volunteer contributors who will create the necessary ecosystem themselves. The social, community element of programming languages is just as important as it is with natural languages.

The purpose is always to communicate. Writing code is far more than just instructing the computer. That’s the easy part, really. It’s also a communication to all the other developers, current and future, who will read and interact with your code. Using the right language helps us to better express complex, abstract concepts in a way that is readily understandable by both people and machines.

Programming language concepts

In this section, we’ll consider a few questions that will help us understand what distinguishes one programming language from another.

Inside the complete chapter

What you’ll learn

Continue reading

Finish the Programming languages chapter

Get the complete chapter in The Computer Science Book, along with twelve more chapters covering the foundations from computer architecture to modern AI.

Buy the ebook - $19.99

The ebook includes PDF and EPUB formats and a 28-day money-back guarantee.

Not ready to buy yet?