2 title: "Syntactic Unification Is Easy!"
8 summary: "Last year I tried implementing syntactic unification and got completely stuck on the algorithms. Last week I read the MicroKanren paper and discovered that unification could be very simple. Let me show you how!"
11 Syntactic unification is the process of trying to find an assignment of variables that makes two terms the same.
12 For example, `(X, Y) = (1, 2)` would give the substitution `{X: 1, Y: 2}`, but `(X, X) = (1, 2)` would fail to unify.
13 Unification is used to implement two things I'm very interested in: type systems, and logic programming languages.
15 Earlier last year I tried implementing unification in order to implement a toy prolog, and I got completely stuck on the algorithms.
16 I eventually puzzled throught it by reading lots of papers and articles, but I don't recall understanding it fully.
17 You can take a look at [the equations that the Wikipedia article uses][wiki] to understand what I was dealing with---it's certainly intimidating!
19 Cut to last week, I happened to read [the MicroKanren paper] (which is very readable) and discovered that unification could be very simple!
22 [wiki]: https://en.wikipedia.org/wiki/Unification_(computer_science)#A_unification_algorithm
23 [the microkanren paper]: http://webyrd.net/scheme-2013/papers/HemannMuKanren2013.pdf
25 ### The Terms To Unify
27 We have to define what kinds of things we're going to unify.
28 There are a few basic things we *need* to have.
30 - Variables, because otherwise unification is just equality comparison.
31 - Terms with multiple subterms, because otherwise unification is just.
32 - Other atomic "base" terms are good to have, because they let you write more interesting terms that are easily understandable.
34 I'm going to write my examples in Python, so the types I'm going to work with are:
36 - Variables: A `Var` class defined for this purpose.
37 - Composite terms: Tuples and lists of terms.
38 - Atomic terms: Any Python type that can be compared for equality.
40 The scheme example they demonstrate in the MicroKanren article uses variables, pairs, and arbitrary objects.
41 The Rust implementation I made initially just has variables, pairs, and integers.
42 Exactly what you want to work with is completely up to you: hopefully at the end of this, you'll be able to figure out how to do it for some custom type.
44 My `Var` type is very simple, with only one custom method to make the printing more compact:
47 from dataclasses import dataclass
49 @dataclass(frozen=True)
57 And everything else is base Python types, so we're done with this part.
61 Let's work through unifying things.
62 We want to unify two terms, and return a substitution that makes them equal, which I'll call the "environment."
63 If they can't possibly be equal, we'll return `None`.
64 Let's start out simple: two things can be unified if they're already equal.
74 The variable cases can also be solved very easily.
75 Trying to unify a variable with another term, we can just substitute that for the variable.
76 When unifying `x = 1`, we get back `{ x: 1 }`.
79 if isinstance(a, Var):
81 if isinstance(b, Var):
85 Finally, let's handle tuples.
86 For this, we want to unify each individual component of the tuple.
87 We want the union of each of these unifications, so we'll update the dictionary each time.
88 We have to check if the sub-unification returned None and pass that along.
91 if isinstance(a, tuple) and isinstance(b, tuple):
95 for (ax, bx) in zip(a, b):
103 Now you might see where this could go wrong, and we'll get to that in a moment with an example.
104 Repeating that function all in one piece:
110 if isinstance(a, Var):
112 if isinstance(b, Var):
114 if isinstance(a, tuple) and isinstance(b, tuple):
118 for (ax, bx) in zip(a, b):
127 Now we can unify stuff:
130 >>> x, y = Var("x"), Var("y")
135 >>> unify((x, 2), (1, 2))
137 >>> unify((x, y), (1, 2))
139 >>> unify((x, x), (1, 2))
144 That last one is wrong!
145 So what can we do to fix that?
147 Well, we need to take into account context.
148 Let's change our `unify` function to accept an environment to work inside.
149 I'm gonna make this a pure function, so we don't update the environment, we just make a new version of it.
150 Some things are changed a bit when doing this.
151 We include `env` in all of our outputs, instead of starting from nothing.
155 def unify(a, b, env={}):
158 if isinstance(a, Var):
160 if isinstance(b, Var):
162 if isinstance(a, tuple) and isinstance(b, tuple):
165 for (ax, bx) in zip(a, b):
166 env = unify(a, b, env)
173 This still doesn't do the right thing though, because we're not actually looking in the environment.
174 We need to look stuff up, so let's write a function to do that.
175 We'll just always call it, and if the term isn't a key for the environment, we'll just return it unchanged.
184 Now we can add these two lines at the beginning.
191 With these modifications, we correctly handle the case we were stuck on earlier.
194 >>> unify((x, x), (1, 2))
195 >>> # look ma no solution!
198 It's still not perfect though.
199 It doesn't properly reject this case where the conflict is two steps away:
202 >>> unify((x, y, x), (y, 8, 9))
206 Here the problem is that we have two facts around, that `x = y`, and that `y = 8`.
207 Even so, it doesn't see that that means `x = 8` when it gets to the assertion of `x = 9`.
208 Luckily this is another simple fix.
209 We just keep walking through the environment as long as we have a binding.
210 Just change the `if` to a `while`!
219 And now this unifier should be able to handle any hard case you throw at it!
221 ### The Whole Unifier
223 Here's the whole program we wrote in one piece, in just 30 lines.
226 from dataclasses import dataclass
228 @dataclass(frozen=True)
239 def unify(a, b, env={}):
244 if isinstance(a, Var):
245 return { **env, a: b }
246 if isinstance(b, Var):
247 return { **env, b: a }
248 if isinstance(a, tuple) and isinstance(b, tuple):
251 for (a, b) in zip(a, b):
252 env = unify(a, b, env)
259 Consider writing your own in your language of choice, or adding support for new datatypes.
260 It would be nice to be able to unify namedtuples and dictionaries and things, or do unification in the web browser.
261 Try it out for yourself, because next time I'll be talking about how you can go from here and use unification to build logic programs.