Create your own
Lesson illustration

Prolog's Legacy: Pattern Matching and Unification in Modern Languages

Hello! Welcome back to our course on programming theory.

Introduction

In our last lesson, we explored the Expression Problem, which highlighted a fundamental trade-off between object-oriented and functional paradigms. We saw that pattern matching is a cornerstone of the functional approach, allowing for elegant extension with new operations.

Today, we'll dig into the intellectual origins of this powerful feature. We will analyze the influence of the logic programming paradigm, as exemplified by the language Prolog, on two key features of modern languages:

  1. Advanced pattern matching.
  2. Unification-based type inference, such as the Hindley-Milner system used in Haskell and ML.

This lesson directly addresses the learning outcome: Analyze the influence of logic programming (Prolog) on modern language features like pattern matching and unification-based type inference. By the end, you will understand how a single, powerful concept—unification—born from logic, serves as the engine for both querying knowledge bases and automatically inferring types in sophisticated compilers.

1. Logic Programming and Prolog

Logic programming is a declarative paradigm, but it differs significantly from the functional programming we've focused on. Instead of defining functions that map inputs to outputs, you define a world of facts and rules and then ask the language to find answers to queries based on that world. Prolog is the most famous logic programming language.

To get a feel for Prolog's core concepts, please watch the beginning of the following talk. It introduces Prolog's execution model, which is built on four simple pieces: logical AND/OR, term rewriting, and unification.

"Production Prolog" by Michael Hendricks

This video, 'Production Prolog' by Michael Hendricks, provides a concise and practical introduction to the language's core ideas.

Please watch from 00:49 to 07:18. Pay close attention to: The core execution model (AND/OR, term rewriting, unification). How unification (=) works with variables (capitalized) and atoms (lowercase). The concept of defining a 'relationship' rather than a function, as shown with the append example, which can be queried in multiple directions.

As you saw, the heart of Prolog is its ability to find values for variables that make a statement true. This mechanism is called unification, and it's a more powerful and general form of the pattern matching we've seen before.

2. Unification: More Than Pattern Matching

In functional languages like Haskell or ML, pattern matching is typically a one-way process: a concrete value is deconstructed and its parts are bound to new variables.

Prolog's unification, on the other hand, is two-way. It is a process of finding a substitution for variables that makes two terms structurally identical. This allows it to not only deconstruct terms but also to construct them based on logical constraints.

The following discussion from Stack Overflow provides an excellent comparison.

Pattern Matching - Prolog vs. Haskell

This Stack Overflow thread, 'Pattern Matching - Prolog vs. Haskell', clearly articulates the fundamental differences between the two mechanisms.

Please read the top four answers (by users LeleDumbo, false, CapelliC, and m09). Focus on: The distinction between one-way matching (Haskell) and two-way unification (Prolog). How Prolog's length/2 predicate can be used to both find the length of a list and generate a list of a given length. The idea that unification can 'build' terms as well as 'deconstruct' them.

To formalize this, let's look at a definition of unification.

Unification and Resolution | Programming Languages

This article, 'Unification and Resolution', provides a more formal definition of unification, which will connect to its use in type inference later.

Please read the sections 'Unification', 'A unification algorithm', 'Most general unifier', and 'Occurs check'. Focus on understanding: Unification as finding a substitution to make two terms equal. The concept of a most general unifier (MGU) as the least restrictive solution. The purpose of the occurs check to prevent infinite terms like X = f(X).

The key takeaway is that Prolog's pattern matching is unification. This bidirectional power allows for writing extremely general predicates that define relationships, a feature that has influenced, even if not fully replicated, the design of pattern matching in many modern languages. Python's structural pattern matching (PEP 634), for instance, while not full unification, moves beyond simple destructuring towards this more expressive style.

3. Unification's Second Act: Type Inference

The influence of unification extends far beyond pattern matching on values. It is the core algorithm behind the Hindley-Milner (HM) type inference system, which is a hallmark of the ML language family (SML, OCaml) and Haskell.

Your background in mathematics and stochastic systems will be useful here. Think of type inference as solving a system of equations. The compiler analyzes an expression, generates a set of constraints on the types involved, and then uses unification to solve for the type variables.

For example, in the expression f(x), the compiler generates constraints like:

  • x has type .
  • f has a function type .
  • The input type of f must be compatible with the type of x, so must unify with .
  • The type of the whole expression is the return type of f, which is .

The goal is to find the most general unifier for all these type variables, which results in the most general type for the expression.

The following video provides a fantastic walkthrough of building a type inference algorithm from scratch, with unification at its center.

f(by) 2019 - Christoph Hegemann, TYPE INFERENCE FROM SCRATCH

Christoph Hegemann's talk, 'TYPE INFERENCE FROM SCRATCH', demystifies the process by implementing it in Haskell. The core logic is what matters, not the Haskell syntax.

Please watch from 20:27 to 26:04. This is the most critical part of the talk for our purposes. Focus on: The definition of unify: finding a substitution to make two types equal. The unification rules for function types, variables, and base types. The role of the 'occurs check' to prevent infinite types (e.g., a = a -> b), which is the exact same problem we saw in Prolog's unification. How the inference rule for function application uses unify to connect the type of the function and the type of its argument.

As the video demonstrates, the algorithm for unifying types is structurally identical to the one for unifying terms in Prolog. It's the same powerful idea applied in a different domain: the domain of types instead of the domain of values.

This connection can be made even more explicit. Because Prolog is so good at expressing rules and relationships, you can actually write a type checker in Prolog.

Caption: A simple type inference system implemented in Prolog. The rules define the types of expressions. The first two lines are facts (`i` is an integer, `x` is a real). The next two are rules for addition. Prolog's engine can then use these rules to either check a type (`type(+(i,x), real).`) or infer a type (`type(+(x,x), T).`), finding the substitution `T = real`.

This image perfectly illustrates the connection: the logic for type inference can be expressed as a set of facts and rules, which a logic programming language can then execute to find a solution.

Conclusion

Today we traced the influence of a single concept, unification, from its home in logic programming to its application in two of the most powerful features of modern functional and multi-paradigm languages.

Key Takeaways:

  • Logic Programming (Prolog): A declarative paradigm where programs consist of facts and rules. Queries are answered by searching for logical consequences.
  • Unification: The core mechanism of Prolog. It is a bidirectional process that finds the most general substitution of variables to make two terms structurally identical. This is more powerful than the one-way pattern matching common in many functional languages.
  • Influence on Pattern Matching: Prolog's unification-based approach inspired the powerful structural pattern matching features found in modern languages, enabling complex data deconstruction and validation in a single expression.
  • Influence on Type Inference: The Hindley-Milner type inference algorithm is a direct application of unification to the domain of types. The compiler generates type constraints and uses unification to solve for unknown type variables, inferring the most general type for an expression.

You've now seen how ideas from different programming paradigms cross-pollinate, leading to innovation. The abstract logical process of unification provides a concrete, powerful algorithm that compilers use to make programming safer and more expressive.

Preview of the Next Lesson:

In our next lesson, we will continue our exploration of paradigm choices by examining a critical area of modern software: concurrency. We will contrast the functional approaches (like actors in Erlang and immutability in Clojure) with the traditional imperative models of threads and locks.

Can't find a good explanation? Sign up and we'll make it for you

Sign up