We’ve looked at inductive definitions for structures like strings and trees and numbers, and how these kinds of definitions support inductive arguments over those structures.
It’s possible to do something similar when defining functions or predicates. (For now, let’s think of predicates as functions whose codomain is 𝟚
, the set of truth-values {true, false}
.) You’ll more often see the label recursive used when talking about this style of defining functions. In fact, there is a distinction to be made, where inductive definitions are a proper subclass of the recursive ones. The examples we’ll be talking about here will all be inductive. But the difference between these isn’t one we’ll be in a position to understand until later; and in lots of literature people just use the broader label “recursive” to talk about functions defined in any of these ways.
If you think of predicates as functions, and you talk about those as being defined “recursively,” you can also think about defining a structure as defining a predicate that’s true of objects iff they have that structure, and so you might also talk about “recursive definitions” of strings and trees and so on. (Partee talks this way in some optional readings I provided.)
Let’s think about the factorial function. We might write its definition like this:
factorial (x ∈ ℕ) =def {
if x is 0, then 1;
if x is k+1, then (k+1) ⋅ factorial(k)
}
The =def
symbol means that we are providing a definition. We are not asking whether or hypothesizing that factorial(x)
may equal the value on the right-hand side of the =def
; instead we are saying This is what we’ll mean by
Our definition applies to any factorial(x)
.x ∈ ℕ
, but not to other arguments.
The definition we go on to provide, inside the {...}
s, is a piecemeal definition. It has multiple clauses, and we have to check the conditions on each clause to see which one applies.
Some of the clauses in such a definition have to give us an answer directly, without invoking the factorial
function again. (In this case, just the first one does so.) If that weren’t the case, then our definition would never “bottom out” and have a defined result. Other clauses (in this case, just the second one) are allowed to invoke the factorial
function again, though it’s important that they do so using different arguments. If they didn’t, then again our definition would never “bottom out” and have a defined result. For most arguments, neither of the following definitions succeed in determining any result:
crazy1 (x) = def {
if x is 0, then crazy1(1);
if x is k+1, then 1 + crazy1(k)
}
crazy2 (x) = def {
if x is 0, then 1;
if x is k+1, then 1 + crazy2(x)
}
You might want to present the factorial function instead like this:
factorial (x ∈ ℕ) =def {
if x is 0, then 1;
if x is k > 0, then k ⋅ factorial(k-1)
}
I presented it with the second clause saying x
was a k+1
, instead of saying it was a k > 0
. I was implicitly assuming that k
is a variable that matched only ℕ
s, and so these two ways of talking are equivalent. There’s a reason to prefer my presentation style, though, in that it more closely matches the way we inductively defined ℕ
s, and so will generalize better to other inductively-defined structures. Sometimes instead of k+1
, we’ll write instead S(k)
, where S( )
expresses the successor function. Thus, my definition could be rewritten as:
factorial (x ∈ ℕ) =def {
if x is 0, then 1;
if x is S(k), then S(k) ⋅ factorial(k)
}
I expect you intuitively understand this definition, but in fact there is a lot of subtlety about what’s happening here. Notice that k
is a variable whose value will depend on what x
is. If x
is 0
, then it won’t satisfy the condition if x is S(k)
. But if x
is 1
, then k
will be 0
; if x
is 2
, then k
will be 1
; and so on. x
is also a variable but it has a different role than k
. x
is a variable given to us from the outside. It’s the one whose factorial
we’re defining. k
is instead a variable that (in some cases) can be derived from x
. The way I’ll talk is that x
is an argument to the factorial
function, and k
is a pattern variable. The sense of these labels will emerge below.
In the conditions, we also see the notation 0
and S( )
. These are not variables. They designate fixed values: the number zero and the successor function.
I’m going to introduce a more regimented way of writing these definitions. You can think of it as a kind of programming language. In principle I could give you a webpage where you entered these definitions and then could apply the defined functions to different arguments to see what the result would be (if any — if you defined the crazy1
or crazy2
functions you wouldn’t get any result). Sadly that webpage does not yet exist. We will just pretend.
In more regimented form, my definition of factorial
looks like this:
factorial (x) =def dissect x {
λ 0. 1;
λ S(k). S(k) ⋅ factorial(k)
} assuming {λ x if x ∈ ℕ}
The assuming {...}
rule is just making explicit that the function we’re defining applies only to arguments that are in ℕ
. I’ll suppress such rules for the time being and leave them implicit. Later we’ll take them up again.
The dissect x {...}
part just says that we’re about to give a piecemeal definition, where the correct result depends on what conditions x
satisfies. Inside the {...}
are several clauses, here one to each line but the important thing is we separate them with semicolons. Each clause begins with a pattern which we’re checking to see if x
matches against. The first pattern is 0
; the second pattern is S(k)
. As before, we understand 0
and S( )
to have fixed meanings, but k
is a variable whose value will depend on x
, in the case where x
is the successor of some other number (so when x > 0
). We precede the patterns with λ
(a Greek lowercase lambda) just as a signal that what comes next is a pattern. After the pattern we have a period, and then the result that the definition returns in case this clause does apply — that is, in case x
“matches” that clause’s pattern. Note that in the part after the period, we can use variables that were introduced in the pattern: thus in the second clause we can say S(k)
and factorial(k)
. Instead of S(k)
we could equally have said x
there, since this expression is used only when k
gets assigned a value making x
equal S(k)
.
Some vocabulary we will use is that when a pattern variable like k
derives its value from what x
’s value is, it is getting assigned or bound to the derived value. And when the second clause is satisfied we will say either that x = S(k)
or that the expressions x
and S(k)
designate the same number, or have the same value.
Does that all make sense?
Using patterns in this way is implicit in lots of mathematical and logical practice. Some programming languages (like OCaml, Haskell, Scheme, and recent versions of Python) are more explicit about it. It may be confusing at first, but in the long run, I think it’ll help us to be explicit about it the way we’re doing here.
Now I want to define a function nbits
where it takes ℕ
s as arguments, and returns how many digits it takes to represent the argument in binary notation. Since zero in binary notation is 0
, nbits(0) = 1
. One in binary notation is 1
, so nbits(1)
is also = 1
. Two in binary notation is 10
and three is 11
, both of which take two digits, so nbits(2) = nbits(3) = 2
. Then nbits(4) = nbits(5) = nbits(6) = nbits(7) = 3
. And so on.
Here is how we define this function in the informal style we began with:
nbits (x ∈ ℕ) =def { if x is 0, then 1; if x is 2j, then 1 + j; if x is k+1, then nbits(k) }
Note that we want the last clause to apply only when the second clause fails to apply. nbits(4)
should have its result determined by the second clause, not the last clause. We could have convention that the clauses get checked in order, and it’s always the first one that matches that applies. But we’ll instead use a different convention, that will force you to think more carefully about what’s going on. We’ll say that in our regimented form, after the pattern we have either a period or a sequence of one or more !
s. (Think of a period as being zero !
s.) Clauses with more !
s always have higher precedence. Clauses with the same number of !
s are assumed to have the same precedence, no matter what order they come in. Generally you’ll want to design equal-precedence clauses in such a way that only one can ever apply. (We’ll talk later about what happens if not.)
Another complication here is that I don’t want to introduce 2j as a new kind of pattern. It’s too complicated, and isn’t part of the inductive definition of numbers in the way that the successor function S( )
is. So instead for that clause, we’ll add an extra twist to our regimented language. We’ll say that between the pattern and the period and/or exclamation points, you can have something of the form if ...
, where inside the ...
you can use variables introduced in the pattern, and check if they satisfy certain predicates. The predicates you can use can be formulated using =
, <
, arbitrary mathematical operations like exponentiation, and any other functions you’ve defined whose results are truth-values. In the ...
you can also use operators like not
, and
, and or
. These if ...
expressions are called guard conditions or guards.
Given these refinements, our nbits
definition can be expressed in regimented form like this. (I suppress the assuming {...}
rule.)
nbits (x) =def dissect x {
λ 0. 1;
λ S(k) if isPow2(S(k)) and k > 0! 1 + nbits(k);
λ S(k). nbits(k)
}
Here I help myself to a predicate isPow2( )
which is understood to return true
for the arguments 1, 2, 4, 8, 16, 32, ...
and false
for all other arguments. Since we no longer have a variable j
getting assigned to the power that x
is equal to 2
raised to, our second clause doesn’t directly tell us what the nbits
is, but instead recurses depending on how many bits x
’s predecessor has. So nbits(1) = nbits(S(0))
which by the third clause = nbits(0) = 1
. And nbits(2) = nbits(S(1))
which by the second clause is 1 + nbits(1) = 2
. And nbits(3)
by the third clause is the same as nbits(2)
. And nbits(4) = nbits(S(3))
which by the second clause is 1 + nbits(3) = 3
. And so on.
Notice that we have to give the second clause higher precedence than the third clause; because cases when the pattern and guard of the second clause are satisfied will also be ones where the pattern of the third clause applies.
We could equivalently have written the first clause as:
λ k if k = 0. 1
And could equivalently have written the second clause as either:
λ S(k) if isPow2(x) and k > 0! 1 + nbits(k)
or:
λ S(k) if isPow2(x) and x > 1! 1 + nbits(k)
The important thing is that we introduced the variable k
in that clause’s pattern so that we can use its value (namely the predecessor of x
) in the recursive invocation nbits(k)
after the !
.
Sometimes we will define functions that take multiple arguments, and then we’ll need to specify which of them we’re “dissecting” or matching against the patterns and guards we specify. Thus we may have definitions like this:
choose (m, n) =def dissect n {...}
But very often our definitions will have the form we see in the definitions of factorial
and nbits
, and then we can help ourselves to some shorthand. Here again is our regimented-form definition of factorial
, with some parts underlined that are ellided in the second shorthand version:
factorial (x) =def dissect x { λ 0. 1; λ S(k). S(k) ⋅ factorial(k) }
factorial =def { λ 0. 1; λ S(k). S(k) ⋅ factorial(k) }
In truth, the (x)
to the left of =def
in the first version, and the (m, n)
to the left of =def
in the sketched definition of choose
, are themselves working fundamentally like patterns. So we could write a definition of choose
like this:
choose =def {
λ (m, n). dissect n {...}
}
This version has one dissect
expression embedded inside another, where the outer one has only a single clause, with the pattern λ (m, n)
. But when we need to define functions like choose
taking multiple arguments, I’ll stick with the earlier style of definition.
All our functions so far have taken numbers as arguments. Let’s extend our apparatus to enable us to work with functions that take strings as arguments. (Some we work with later will take pairs of strings as arguments; others will take pairs of strings and numbers.)
Whereas before we had variables like x
and k
designating numbers, we’ll now instead have variables that designate strings. I am going to adopt the convention of using lowercase Greek variables like α
and β
to designate strings. I want you to follow this convention too. It will help us in later classes if you get used to doing this now. If you can’t easily type a α
, it’s fine to write it out longhand as alpha
; similarly with β
as beta
, γ
as gamma
, δ
as delta
, …
Whereas before we had notation 0
and S( )
with fixed meanings, now we’ll instead have notation like ""
and "abc"
and ⁀
with fixed meanings. The empty string can also be written as ɛ
or empty
; note that however you write it, this is still a fixed expression, not a variable like α
or alpha
.
Variables can designate any string, including the empty string.
The following two clauses will work mostly the same way:
λ "". ...
λ α if α = "". ...
Similarly these will work mostly the same way:
λ "abc". ...
λ α if α = "abc". ...
The only difference is that in each case the second version introduces a new pattern variable α
that the first does not. These pattern variables are in effect only for the guard of that clause (if there is one) and for the result expression that comes after the .
or !
s. They’re not available to be referenced again in other clauses — unless you reintroduce them in the patterns of those clauses (and in those clauses they may be assigned different values).
When we have a pattern like λ α ⁀ "x"
, that will match any string that ends with an "x"
and the variable α
will then be assigned whatever comes before the "x"
. You can also have patterns like λ α ⁀ "x" ⁀ β
; that will match any string that contains (at least one) "x"
, and the variable α
will be assigned what comes before the "x"
(this may be the empty string) and the variable β
will be assigned whatever comes after the "x"
(again, this may be the empty string). You are not allowed to repeat variables in a pattern, so you can’t say this:
λ α ⁀ "x" ⁀ α
But you can get the same effect using a guard, like so:
λ α ⁀ "x" ⁀ β if α = β
What happens if the string value being matched against the pattern has multiple "x"
s in it? That is, what if we match "axbxc"
against the clause:
λ α ⁀ "x" ⁀ β. ...
This will be understood so that both assignments hold. That is, we evalute the ...
both with (i) α
assigned to "a"
and β
assigned to "bxc"
; and also with (ii) α
assigned to "axb"
and β
assigned to "c"
. This is understood the same as if two equal-precedence clauses match the string value. If you get the same result no matter which clause applies, or no matter which way the value "axbxc"
is dissected into an α
and β
, then there is no problem. If you could get different results, then the definition is considered broken. We’ll call these broken definitions stinky. (I made this label up, but there’s a reason for it, that I’ll reveal later.)
If no clause matches some string value, that’s another way for a definition to be stinky.
A definition is considered stinky as a whole if it’s stinky for any argument(s) it’s assumed to apply to. Later, we’ll allow some definitions to explicitly have an undefined
result in some cases. This is different from being stinky. Stinky definitions are bad, and reflect badly on the definition’s designer. Definitions being undefined
for some arguments is not in itself a problem. (Of course, it would be an issue if you wanted the definition to be defined for those arguments.)
Avoid stinky definitions by being sure to use guards and exclamation points so that you never have a case where multiple clauses with the same precedence can deliver different results. And never have a case where a value can be dissected in multiple ways by a single clause, and the result differs depending on the choice. And never have a case where a legal argument might fail to be matched by every clause.
We can rely on some assumptions about which arguments count as “legal.” You don’t have to worry about getting negative numbers, or π
, or a string, if you’re expecting a ℕ
. Or getting a number when you’re expecting a string. But don’t assume that string arguments will always be non-empty, or will always have exactly one "x"
in them, and so on. Your definitions should be designed to deal with all such options explicitly.
If you don’t have any useful result to return in some clause, say the result is undefined
.
Here’s an example. Let’s introduce a function isUnit
that takes string arguments and gives true
as a result just in case the argument is a unit.
isUnit =def {
λ ""! false;
λ α ⁀ β if α ≠ "" and β ≠ ""! false;
λ α. true
}
We want the third clause to apply only when the first two fail, so we give the other clauses higher precedence. If the argument to this function can be dissected into an α ⁀ β
where both parts are non-empty, then it is not a unit. Also, if the argument is itself empty, it’s not a unit. In all other cases it is a unit.
Now let’s introduce a function that returns the first unit of whatever string argument is provided to it. If the argument is the empty string, then we say the result is undefined:
firstUnit =def {
λ α ⁀ β if isUnit(α). α;
λ "". undefined
}
If you think through this definition, you should be able to determine that at least one clause will apply to every string argument, and that it’s never the case that both clauses apply. Also that when the first clause applies, there will never be multiple ways to dissect the argument resulting in α
being assigned different values. So this definition is not stinky.
Going back to the assuming {...}
rules we saw earlier but have been suppressing until now, another way to write the definition of firstUnit
would be like this:
firstUnit =def {
λ α ⁀ β if isUnit(α). α
} assuming { λ γ if γ ≠ "" }
When the pattern (and any guard) in the assuming {...}
rule isn’t satisfied, such a definition is understood to be undefined
for that argument. Another equivalent way to define this would be:
firstUnit =def {
λ γ if γ ≠ ""! dissect γ {
λ α ⁀ β if isUnit(α). α
};
λ γ. undefined
}
And the assuming {...}
notation is just shorthand for that more complex definition. Inside the inner {...}
(what comes before the assuming {...}
in the previous example), this definition only has a single clause, but you can have more there too. The last clause of the outer {...}
has to have lowest priority, and will catch all the arguments where the assuming {...}
rule is not satisfied.
Let’s define the length
function, that we’ve until now been understanding informally.
length =def {
λ "". 0;
λ α ⁀ β if isUnit(α). 1 + length(β)
}
We could just as well have said:
length =def {
λ "". 0;
λ α ⁀ β if isUnit(β). length(α) + 1
}
Note that we don’t have to give the clauses different precedences because no argument could ever match the patterns and guards of both clauses.
Using the definition length("")
will directly give us 0
. length("t")
will match the second clause, and the result will be 1 + length("")
, which is 1 + 0 = 1
. If the argument is a string like "the"
, its length will depend on how that string is understood. Here I’ll understand it as the concatenation of three units, that is as "t" ⁀ "h" ⁀ "e"
. In that case length("the")
will match the second clause, and its result will be 1 + length("he")
, which if we keep following the definition to the end will turn out to be 3
.
Let’s define a predicate that determines whether one string is a prefix of another. (As we said before, we count the empty string as a prefix of every string, and strings also count as prefixes of themselves.) This function needs to take two arguments, so we will write it out longhand, without elliding the dissect
:
startsWith (γ, δ) =def dissect γ {
λ α ⁀ β if α = δ! true;
λ β. false
}
This will return true
just in case argument δ
is a prefix of argument γ
.
A few things to note here. We need to give the first clause higher precedence than the second, else the second will match all the cases that the first does (and more too, but that’s not the problem).
Second, in the guard to the first clause, we use the variables α
and δ
. The first of these gets assigned some value based on how the argument γ
is being dissected to match the pattern α ⁀ β
. The variable δ
on the other hand gets its value from what arguments are passed to the startsWith
function, same as with γ
. So if we apply startsWith
to the arguments ("abc", "og")
, then δ
will be bound to the value "og"
.
Third, we can’t express the first clause like this:
λ δ ⁀ β! true
because the variable δ
appearing there would be understood as a new pattern variable, that has to get assigned a new value based on how γ
is dissected, rather than being the value "og"
that was supplied to startsWith
when we asked whether startsWith("abc", "og")
.