5. Writing Tactics

5.1. A First Look at the Tactic Monad

The canonical way to invoke a tactic in Lean is to use the by keyword within a Lean expression. Suppose we write the following:

variables a b : Prop

example : a → b → a ∧ b :=
by _

Lean expects something of type tactic unit to fill the underscore, where tactic refers to the tactic monad. When the elaborator processes this definition, it elaborates everything outside the by invocation first (in this case, just the statement of the theorem), and then calls on the Lean virtual machine to execute the tactic. When doing so, the virtual machine interprets any axiomatically declared meta constant as references to internal Lean functions that implement the functionality of the tactic monad.

The tactic monad can be thought of as a combination of a state monad (where the internal “state” as accessed and acted on by the meta constant primitives) and the option monad. Because it is a monad, we have the usual do notation. So, if r, s, and t are tactics, you should think of

do a ← r,
   b ← s,
   t

as meaning “apply tactic r to the state, and store the return result in a; apply tactic s to the state, and store the return result in b; then, finally, apply tactic t to the state.” Moreover, any tactic can fail, which is analogous to a return value of none in the option monad. In the example above, if any of r, s, or t fail, then the compound expression has failed.

There is an additional, really interesting feature of the tactic monad: it is an alternative monad, in the sense described at the end of the last chapter. This is used to implement backtracking. If s and t are monads, the expression s <|> t can be understood as follows: “do s, and if that succeeds, return the corresponding return value; otherwise, undo any changes to the state that s may have produced, and do t instead.” This allows us to try s and, if it fails, go on to try t as though the first attempt never happened.

When the tactic expression after a by is invoked, the tactic wakes up and says “Whoa! I’m in a monad!” This is just a colorful way of saying that the virtual machine expects a function that acts on the tactic state in a certain way, and interprets primitive operations on the tactic state in terms of functions that are implemented internal to Lean. At any rate, when it wakes up, it can start to look around and assess its current state. The goal of this section is to give you a first look at of some of the things it can do there. Don’t worry if some of the expressions seem mysterious; they will be explained in the sections that follow.

One thing the tactic can do is print a message to the outside world:

open tactic

example (a b : Prop) : a  b  a  b :=
by do trace "Hi, Mom!",
   admit

When the file is executed, Lean issues an error message to the effect that the tactic has failed to fill the relevant placeholder, which is what it is supposed to do. But the trace message is printed during the execution, providing us with a glimpse of its inner workings. We can actually trace value of any type that Lean can coerce to a string output, and we will see that this includes a number of useful types. The admit tactic is a tactic version of sorry. Note that we use open tactic to open the tactic namespace and access the core tactic library. We will hide this line in the code snippets that follow.

Another thing we can do is trace the current tactic state:

example (a b : Prop) : a  b  a  b :=
by do trace "Hi, Mom!",
      trace_state,
      admit

Now the output includes the list of goals that are active in the tactic state, each with a local context that includes the local variables and hypotheses. In this case there is only one:

Hi, Mom!
a b : Prop
⊢ a → b → a ∧ b

This points to an important fact: the internal, and somewhat mysterious, tactic state includes at least a list of goals. In fact, it includes much more: every tactic is invoked in a rich environment that includes all the objects and declarations that are present when the tactic is invoked, as well as notations, option declarations, and so on. In most cases, however, the list of goals is most directly relevant to the task at hand.

Let us dispense with the trace messages now, and start to prove the theorem by introducing the first two hypotheses.

example (a b : Prop) : a → b → a ∧ b :=
by do eh1 ← intro `h1,
      eh2 ← intro `h2,
      skip

The backticks indicate that h1 and h2 are names; we will discuss these below. The tactic skip is a do-nothing tactic, simply included to ensure that the resulting expression has type tactic unit.

We can now do some looking around. The meta_constant called target has type tactic expr, and returns the type of the goal. The type expr, like name, will be discussed below; it is designed to reflect the internal representation of Lean expressions, so, roughly, via meta-programming glue, the expr type allows us to manipulate Lean expressions in Lean itself. In particular, we can ask the tactic to print the current goal:

example (a b : Prop) : a  b  a  b :=
by do eh1  intro `h1,
      eh2  intro `h2,
      target >>= trace,
      admit

In this case, the output is a b, as we would expect. We can also ask the tactic to print the elements of the local context.

example (a b : Prop) : a  b  a  b :=
by do eh1  intro `h1,
      eh2  intro `h2,
      local_context >>= trace,
      admit

This yields the list [a, b, h1, h2]. We already happen to have representations of h1 and h2, because they were returned by the intro tactic. But we can extract the other expressions in the local context given their names:

example (a b : Prop) : a  b  a  b :=
by do intro `h1,
      intro `h2,
      ea  get_local `a,
      eb  get_local `b,
      trace (to_string ea ++ ", " ++ to_string eb),
      admit

Notice that ea and eb are different from a and b; they have type expr rather than Prop. They are the internal representations of the latter expressions. At present, there is not much for us to do with these expressions other than print them out, so we will drop them for now.

In any case, to prove the goal, we can proceed to invoke any of the Lean’s standard tactics. For example, this will work:

example (a b : Prop) : a  b  a  b :=
by do intro `h1,
      intro `h2,
      split,
      repeat assumption

We can also do it in a more hands-on way:

example (a b : Prop) : a  b  a  b :=
by do eh1  intro `h1,
      eh2  intro `h2,
      mk_const ``and.intro >>= apply,
      exact eh1,
      exact eh2

The double-backticks will also be explained below, but the general idea is that the third line of the tactic builds an expr that reflects the and.intro declaration in the Lean environment, and applies it. The applyc tactic combines these two steps:

example (a b : Prop) : a  b  a  b :=
by do eh1  intro `h1,
      eh2  intro `h2,
      applyc ``and.intro,
      exact eh1,
      exact eh2

We can also finish the proof as follows:

example (a b : Prop) : a  b  a  b :=
by do eh1  intro `h1,
      eh2  intro `h2,
      e  to_expr ```(and.intro h1 h2),
      exact e

Here, the construct ```(...) is used to build a pre-expression, the tactic to_expr elaborates it and converts it to an expression, and the exact tactic applies it. In the next section, we will see even more variations on constructions like these, including tactics that would enable us to construct the expression and.intro h1 h2 more explicitly.

The do block in this example has type tactic unit, and can be broken out as an independent tactic.

meta def my_tactic : tactic unit :=
do eh1  intro `h1,
   eh2  intro `h2,
   e  to_expr ``(and.intro %%eh1 %%eh2),
   exact e

example (a b : Prop) : a  b  a  b :=
by my_tactic

Of course, my_tactic is not a very exciting tactic; we designed it to prove one particular theorem, and it will only work on examples that have the very same shape. But we can write more intelligent tactics that inspect the goal, the local hypotheses, and the environment, and then do more useful things. The mechanism is exactly the same: we construct an expression of type tactic unit, and ask the virtual machine to execute it at elaboration time to solve the goal at hand.

5.2. Names and Expressions

Suppose we write an ordinary tactic proof in Lean:

example (a b : Prop) (h : a  b) : b  a :=
begin
  split,
  exact and.right h,
  exact and.left h
end

This way of writing the tactic proof suggests that the h in the tactic block refers to the expression h : a b in the list of hypotheses. But this is an illusion; what h really refers to is the first hypothesis named h that is in the local context of the goal in the state when the tactic is executed. This is made clear, for example, by the fact that earlier lines in the proof can change the name of the hypothesis:

example (a b : Prop) (h : a  b) : b  a :=
begin
  revert h,
  intro h',
  split,
  exact and.right h',
  exact and.left h'
end

Now writing exact and.right h would make no sense. We could, alternatively, contrive to make h denote something different from the original hypothesis. This often happens with the cases and induction tactics, which revert hypotheses, peform an action, and then reintroduce new hypotheses with the same names.

Metaprogramming in Lean requires us to be mindful of and explicit about the distinction between expressions in the current environment, like h : a b in the hypothesis of the example, and the Lean objects that we use to act on the tactic state, such as the name “h” or an object of type expr. Without using the begin...end front end, we can construct the proof as follows:

example (a b : Prop) (h : a  b) : b  a :=
by do split,
   to_expr ```(and.right h) >>= exact,
   to_expr ```(and.left h) >>= exact

This tells Lean to elaborate the expressions and.right h and and.left h in the context of the current goal, and then apply them. The begin...end construct is essentially a front end that interprets the proof above in these terms.

To understand what is going on in situations like this, it is important to know that Lean’s metaprogramming framework provides three distinct Lean types that are relevant to constructing syntactic expressions:

  • the type name, representing hierarchical names
  • the type expr, representing expressions
  • the type pexpr, representing pre-expressions

Let us consider each one of them, in turn.

Hierarchical names are denoted in ordinary .lean files with expressions like foo.bar.baz or nat.mul_comm. They are used as identifiers that reference defined constants in Lean, but also for local variables, attributes, and other objects. Their Lean representations are defined in init/meta/name.lean, together with some operations that can be performed on them. But for many purposes we can be oblivious to the details. Whenever we type an expression that begins with a backtick that is not followed by an open parenthesis, Lean’s parser translates this to the construction of the associated name. In other words, `nat.mul_comm is simply notation for the compound name with components nat and mul_comm.

When metaprogramming, we often use names to refer to definitions and theorems in the Lean environment. In situations like that, it is easy to make mistakes. In the example below, the tactic definition is accepted, but its application fails:

open tactic

namespace foo

theorem bar : true := trivial

meta def my_tac : tactic unit :=
mk_const `bar >>= exact

-- example : true := by my_tac -- fails

end foo

The problem is that the proper name for the theorem is foo.bar rather than bar; if we replace `bar by `foo.bar, the example is accepted. The mk_const tactic takes an arbitrary name and attempts to resolve it when the tactic is invoked, so there is no error in the definition of the tactic. The error is rather that when we wrote `bar we had in mind a particular theorem in the environment at the time, but we did not identify it correctly.

For situations like these, Lean provides double-backtick notation. The following example succeeds:

open tactic

namespace foo

theorem bar : true := trivial

meta def my_tac : tactic unit :=
mk_const ``bar >>= exact

example : true := by my_tac -- fails

end foo

It also succeeds if we replace ``bar by ``foo.bar. The double-backtick asks the parser to resolve the expression with the name of an object in the environment at parse time, and insert the relevant name. This has two advantages:

  • if there is no such object in the environment at the time, the parser raises an error; and
  • assuming it does find the relevant object in the environment, it inserts the full name of the object, meaning we can use abbreviations that make sense in the context where we are writing the tactic.

As a result, it is a good idea to use double-backticks whenever you want to refer to an existing definition or theorem.

When writing tactics, it is often necessary to generate a fresh name. You can use mk_fresh_name for that:

example (a : Prop) : a  a :=
by do n  mk_fresh_name,
      intro n,
      hyp  get_local n,
      exact hyp

A variant, get_unused_name, lets you suggest a name. If the name is in use, Lean will append a numeral to avoid duplication.

example (a : Prop) : a  a :=
by do n  get_unused_name "h",
      intro n,
      hyp  get_local n,
      exact hyp

The type expr reflects the internal representation of Lean expressions. It is defined inductively in the file expr.lean, but when evaluating expressions that involve terms of type expr, the virtual machine uses the internal C++ representations, so each constructor and the eliminator for the type are translated to the corresponding C++ functions. Expressions include the sorts Prop, Type₁, Type₂, …, constants of each type, applications, lambdas, Pi types, and let definitions. The also include de Bruijn indices (with constructor var), metavariables, local constants, and macros.

The whole purpose of tactic mode is to construct expressions, and so this data type is fundamental. We have already seen that the target tactic returns the current goal, which is an expression, and that local_context returns the list of hypotheses that can be used to solve the current goal, that is, a list of expressions.

Returning to the example at the start of this section, let us consider ways of constructing the expressions and.left h and and.right h more explicitly. The following example uses the mk_mapp tactic.

example (a b : Prop) (h : a  b) : b  a :=
by do split,
   eh  get_local `h,
   mk_mapp ``and.right [none, none, some eh] >>= exact,
   mk_mapp ``and.left [none, none, some eh] >>= exact

In this example, the invocations of mk_mapp retrieve the definition of and.right and and.left, respectively. It makes no difference whether the arguments to those theorems have been marked implicit or explicit; mk_mapp ignores those annotations, and simply applies that theorem to all the arguments in the subsequent list. Thus the first argument to mk_mapp is a name, while the second argument has type list (option expr). Each none entry in the list tells mk_mapp to treat that argument as implicit and infer it using type inference. In contrast, an entry of the form some t specifies t as the corresponding argument.

The tactic mk_app is an even more rudimentary application builder. It takes the name of the operator, followed by a complete list of its arguments.

example (a b : Prop) (h : a  b) : b  a :=
by do split,
      ea  get_local `a,
      eb  get_local `b,
      eh  get_local `h,
      mk_app ``and.right [ea, eb, eh] >>= exact,
      mk_app ``and.left [ea, eb, eh] >>= exact

You can send less than the full list of arguments to mk_app, but the arguments you send are assumed to be the final arguments, with the earlier ones made implicit. Thus, in the example above, we could send instead [eb, eh] or simply [eh], because the earlier arguments can be inferred from these.

example (a b : Prop) (h : a  b) : b  a :=
by do split,
      eh  get_local `h,
      mk_app ``and.right [eh] >>= exact,
      mk_app ``and.left [eh] >>= exact

Finally, as indicated in the last section, you can also use mk_const to construct a constant expression from the corresponding name:

example (a b : Prop) (h : a  b) : b  a :=
by do split,
      eh  get_local `h,
      mk_const ``and.right >>= apply,
      exact eh,
      mk_const ``and.left >>= apply,
      exact eh

We have also seen above that it is possible to use to_expr to elaborate expressions at executation time, in the context of the current goal.

open tactic

-- BEGIN
example (a b : Prop) (h : a ∧ b) : b ∧ a :=
by do split,
   to_expr ```(and.right h) >>= exact,
   to_expr ```(and.left h) >>= exact
-- END

Here, the expressions ```(and.right h) and ```(and.left h) are pre-expressions, that is, objects of type pexpr. The interface to pexpr can be found in the file pexpr.lean, but the type is largely opaque from within Lean. The canonical use is given by the example above: when Lean’s parser encounters an expression of the form ```(...), it constructs the corresponding pexpr, which is simply an internal representation of the unelaborated term. The to_expr tactic then sends that object to the elaborator when the tactic is executed.

Note that the backtick is used in two distinct ways: an expression of the form `n, without the parentheses, denotes a name, whereas an expression of the form `(...), with parentheses, denotes a pexpr. Though this may be confusing at first, it is easy to get used to the distinction, and the notation is quite convenient.

Lean’s pre-expression mechanism also supports the use of anti-quotation, which allows a tactic to tell the elaborator to insert an expression into a pre-expression at runtime. Returning to the example above, suppose we are in a situation where instead of the name h, we have the corresponding expression, eh, and want to use that to construct the term. We can insert it into the pre-expression by preceding it with a double-percent sign:

example (a b : Prop) (h : a  b) : b  a :=
by do split,
   eh  get_local `h,
   to_expr ``(and.right %%eh) >>= exact,
   to_expr ``(and.left %%eh) >>= exact

When the tactic is executed, Lean elaborates the pre-expressions given by ``(...), with the expression eh inserted in the right place. The difference between ``(...) and ```(...) is that the first resolves the names contained in the expression when the tactic is defined, whereas the second resolves them when the tactic is executed. Since the only name occurring in and.left %%eh is and.left, it is better to resolve it right away. However, in the expression and.right h above, h only comes into existence when the tactic is executed, and so we need to use the triple backtick.

Finally, Lean can handle pattern matching on pre-expressions. To do so, use a single backtick, and use antiquotations to introduce variables in the patterns. The following tactic retrieves the goal, and takes action depending on its form.

meta def do_something : tactic unit :=
do t  target,
   match t with
   | `(%%a  %%b) := split >> skip
   | `(%%a  %%b) := do h  get_unused_name "h",
                        intro h,
                        skip
   | _            := try assumption
   end

example (a b c : Prop) : a  b  a  a  b :=
begin
do_something, do_something, do_something, do_something, do_something, do_something
end

example (a b c : Prop) : a  b  a  a  b :=
begin
do_something, do_something, do_something, do_something, assumption, assumption
end

example (a b c : Prop) : a  b  a  a  b :=
by repeat { do_something }

5.3. Examples

When it comes to writing tactics, you have all the computable entities of Lean’s standard library at your disposal, including lists, natural numbers, strings, product types, and so on. This makes the tactic monad a powerful mechanism for writing metaprograms. Some of Lean’s most basic tactics are implemented internally in C++, but many of them are defined from these in Lean itself.

The entry point for the tactic library is the file init/meta/tactic.lean, where you can find the details of the interface, and see a number of basic tactics implemented in Lean. For example, here is the definition of the assumption tactic:

meta def find_same_type : expr  list expr  tactic expr
| e []         := failed
| e (h :: hs) :=
  do t  infer_type h,
     (unify e t >> return h) <|> find_same_type e hs

meta def assumption : tactic unit :=
do ctx  local_context,
   t    target,
   h    find_same_type t ctx,
   exact h
<|> fail "assumption tactic failed"

The expression find_same_type t es tries to find in es an expression with type definitionally equal to t in the list of expressions es, by a straightforward recursion on the list. The infer_type tactic calls Lean’s internal type inference mechanism to infer to the type of an expression, and the unify tactic tries to unify two expressions, instantiating metavariables if necessary. Note the use of the orelse notation: if the unification fails, the procedure backtracks and continues to try the remaining elements on the list. The fail tactic announces failure with a given string. The failed tactic simply fails with a generic message, “tactic failed.”

One can even manipulate data structures that include tactics themselves. For example, the first tactic takes a list of tactics, and applies the first one that succeeds:

meta def first {α : Type} : list (tactic α)  tactic α
| []      := fail "first tactic failed, no more alternatives"
| (t::ts) := t <|> first ts

It fails if none of the tactics on the list succeeds. Consider the following example:

meta def destruct_conjunctions : tactic unit :=
repeat (do
  l  local_context,
  first $ l.map (λ h, do
    ht  infer_type h >>= whnf,
    match ht with
    | `(and %%a %%b) := do
      n  get_unused_name `h none,
      mk_mapp ``and.left [none, none, some h] >>= assertv n a,
      n  get_unused_name `h none,
      mk_mapp ``and.right [none, none, some h] >>= assertv n b,
      clear h
    | _ := failed
    end))

The repeat tactic simply repeats the inner block until it fails. The inner block starts by getting the local context. The expression l.map ... is just shorthand for list.map ... l; it applies the function in ... to each element of l and returns the resulting list, in this case a list of tactics. The first function then calls each one sequentially until one of them succeeds. Note the use of the dollar-sign for function application. In general, an expression f $ a denotes nothing more than f a, but the binding strength is such that you do not need to use extra parentheses when a is a long expression. This provides a convenient idiom in situations exactly like the one in the example.

Some of the elements of the body of the main loop will now be familiar. For each element h of the context, we infer the type of h, and reduce it to weak head normal form. (We will discuss weak head normal form in the next section.) Assuming the type is an and, we construct the terms and.left h and and.right h and add them to the context with a fresh name. The clear tactic then deletes h itself.

Remember that when writing meta defs you can carry out arbitrary recursive calls, without any guarantee of termination. You should use this with caution when writing tactics; if there is any chance that some unforseen circumstance will result in an infinite loop, it is wiser to use a large cutoff to prevent the tactic from hanging. Even the repeat tactic is implemented as a finite iteration:

meta def repeat_at_most : nat  tactic unit  tactic unit
| 0        t := skip
| (succ n) t := (do t, repeat_at_most n t) <|> skip

meta def repeat : tactic unit  tactic unit :=
repeat_at_most 100000

But 100,000 iterations is still enough to get you into trouble if you are not careful.

5.4. Reduction

[This section still under construction. It will discuss the various types of reduction, the notion of weak head normal form, and the various transparency settings. It will use some of the examples that follow.]

open tactic

set_option pp.beta false

section
  variables {α : Type} (a b : α)

  example : (λ x : α, a) b = a :=
  by do goal  target,
        match expr.is_eq goal with
        | (some (e₁, e₂)) := do trace e₁,
                                whnf e₁ >>= trace,
                                reflexivity
        | none            := failed
        end

  example : (λ x : α, a) b = a :=
  by do goal  target,
        match expr.is_eq goal with
        | (some (e₁, e₂)) := do trace e₁,
                                whnf e₁ transparency.none >>= trace,
                                reflexivity
        | none            := failed
        end

  attribute [reducible]
  definition foo (a b : α) : α := a

  example : foo a b = a :=
  by do goal  target,
        match expr.is_eq goal with
        | (some (e₁, e₂)) := do trace e₁,
                                whnf e₁ transparency.none >>= trace,
                                reflexivity
        | none            := failed
        end

  example : foo a b = a :=
  by do goal  target,
        match expr.is_eq goal with
        | (some (e₁, e₂)) := do trace e₁,
                                whnf e₁ transparency.reducible >>= trace,
                                reflexivity
        | none            := failed
        end
end

5.5. Metavariables and Unification

[This section is still under construction. It will discuss the notion of a metavariable and its local context, with the interesting bit of information that goals in the tactic state are nothing more than metavariables. So the goal list is really just a list of metavariables, which can help us make sense of the get_goals and set_goals tactics. It will also discuss the unify tactic.]