Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a tips page to the book #881

Draft
wants to merge 7 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions data/tableOfContents.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,7 @@ part:
chapter:
- include: src/plfa/backmatter/Acknowledgements.md
epub-type: acknowledgements
- include: src/plfa/backmatter/Tips.md
epub-type: appendix
- include: src/plfa/backmatter/Fonts.lagda.md
epub-type: appendix
197 changes: 197 additions & 0 deletions src/plfa/backmatter/Tips.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
---
title : "Tips"
permalink : /Tips/
---

# Interactive development

## Equational reasoning skeleton

When starting to work on an equational reasoning problem, you can begin like this:
```agda
begin
?
≡⟨ ? ⟩
?
≡⟨ ? ⟩
?
≡⟨ ? ⟩
?
```

This sets up a series of reasoning steps with holes for both the expressions at each step, and the step itself.
Since the reasoning steps are holes, Agda will always accept this, so you can make incremental progress while keeping your file compiling.

Now call "solve" (`C-c C-s` in Emacs).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I run C-c C-a (auto) instead, which has the added benefit of getting rid of the hole in a single step. (At least in vscode I need to C-c C-space to give after solving.)

This will solve the expression holes at the beginning and the end so you don't have to fill them in.
You can do this again if you fix a step, so if you fill in the reasoning you want to do for a step, you can use "solve" and Agda will fill in the next term.

## Goal information will show computed terms

Suppose you have a partial equational reasoning proof:
```agda
begin
2 + (x + y)
≡⟨ ?2 ⟩
wenkokke marked this conversation as resolved.
Show resolved Hide resolved
?
≡⟨ ? ⟩
?
≡⟨ ? ⟩
?
```

The first term can be computed more.
To see this, you can ask for the goal context for `?2` and it will show something like this:
```
?2 = suc (suc (x + y)) = y_5244
```
i.e. your LHS and an unknown RHS (because it's a hole), but it will show the LHS fully computed, which you can copy into your file directly.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may need to disambiguate LHS and RHS for the reader.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also just remove ?2 and surrounding spaces (or replace it with refl) and then auto on the next ? below.


## Quickly setting up `with` clauses

If you have a function like this:
```agda
foo a = ?
```
and you want to introduce a `with`-clause, there is no direct hotkey for this.
The fastest thing to do is to write
```agda
foo a with my-thing
... | x = ?
```
and then ask to case-split on `x`.

## Quickly setting up record skeletons

"Refine" (`C-c C-r` in Emacs) will set up a skeleton for a record constructor if Agda knows a hole must be a record type.
This is useful for e.g. isomorphisms, products, sigmas, or existentials.
wenkokke marked this conversation as resolved.
Show resolved Hide resolved

# General tips

## Try and make your definitions compute as much as possible

For example, compare:
```agda
2 * foo b
```
vs
```agda
foo b * 2
```

The definition of `*` does case analysis on its first argument.
This means that if the first argument to `*` is a constructor application, Agda can do some computation for you.
The first example computes to `foo b + (foo b + zero)`, but the second one does not compute at all: Agda can't pattern match on `foo b`, it could be anything!

Doing this lets Agda do more definitional simplification, which means you have to do less work.
Unfortunately it does require you to know _which_ arguments are the one that Agda can evaluate, which requires looking at the definitions of the functions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some general advice we can probably give here to help people guess which arguments are evaluated, e.g., left associative operators tend to evaluate their left argument.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps tell people about jump-to-definition?


## `x + x` is better than `2 * x`

An expression like `2 * x` will compute to `x + (x + zero)`.
You now need to get rid of the `x + zero` with a rewrite or similar.
You don't have this problem if you just write `x + x`.

This is probably less true for larger constants: writing out `8 * x` would be tedious.

## The three styles of proof all tend to use the same assumptions

Consider these three proofs of the same theorem:
```agda
*-distrib-+ : ∀ (m n p : ℕ) → (m + n) * p ≡ m * p + n * p
*-distrib-+ zero n p = refl
*-distrib-+ (suc m) n p =
begin
(suc m + n) * p
≡⟨⟩
suc (m + n) * p
≡⟨⟩
p + (m + n) * p
≡⟨ cong (p +_) (*-distrib-+ m n p) ⟩
p + (m * p + n * p)
≡⟨ sym (+-assoc p (m * p) (n * p)) ⟩
(p + m * p) + n * p
≡⟨⟩
(suc m * p) + n * p

*-distrib-+‵ : ∀ (m n p : ℕ) → (m + n) * p ≡ m * p + n * p
*-distrib-+‵ zero n p = refl
*-distrib-+‵ (suc m) n p = trans ( cong (p +_) (*-distrib-+‵ m n p)) ( sym ( +-assoc p (m * p) (n * p)))

*-distrib-+‶ : ∀ (m n p : ℕ) → (m + n) * p ≡ m * p + n * p
*-distrib-+‶ zero n p = refl
*-distrib-+‶ (suc m) n p rewrite +-assoc p (m * p) (n * p) | *-distrib-+‶ m n p = refl
```

They use three approaches for doing "multiple steps":
1. Equational reasoning
2. `trans`
3. `rewrite`

However, notably they all use the same supporting proofs!
That means you can often write the overall proof however you find easiest and then rewrite it into another form if you want.
For example, do the proof slowly with equational reasoning, and then turn it into a compact proof with `rewrite`.
Copy link
Collaborator

@wenkokke wenkokke Jun 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd hesitate to advise folks to rewrite their readable equational reasoning proofs into less readable rewrite ones.

I'd ask that you remove that particular tip.

(It's also not obvious that such a rewrite always works.)


## Avoid mutual recursion in proofs by recursing outside the lemma
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand what this advice is trying to say, but it's a bit unclear. The title just adds to the confusion.

I think what you are trying to say here is that we should try to break up a proof into small lemmas, that do one reasoning step, which we then compose to get the overall goal?


Look at the second part of the proof of commutativity of `+`:
```agda
+-comm m (suc n) =
begin
m + suc n
≡⟨ +-suc m n ⟩
suc (m + n)
≡⟨ cong suc (+-comm m n) ⟩
suc (n + m)
≡⟨⟩
suc n + m
```
We use two equalities: the `+-suc` lemma, and a recursive use of `+-comm`.

If you were doing this for the first time, you might be tempted to make _one_ lemma for both those steps.
It wouldn't look that different, it would just have `n` and `m` swapped.
But then you would find that you needed to call `+-comm` from the lemma, which needs mutual recursion.

Instead, you can do what's done here and use the recursive call before/after your lemma.

## When to use implicit parameters

Roughly: if it can be inferred easily from the result, e.g.
```
≤-refl : ∀ { n : ℕ } → n ≤ n
```
Agda will be able to infer the `n` from the `n ≤ n` in the result.

Note that the situation is different for constructors and functions.
Generally, Agda can infer `x` from `f x` if `f` is a constructor (like `≤`) but _not_ if it is a function (like `_+_`).

## Avoiding "green slime"

"Green slime" is when you use a function in the index of a returned data type value.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe some explanation why "green slime"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further reading here: https://proofassistants.stackexchange.com/a/1615 (There is also a link to Connor's paper that coined this term.)

For example:
```agda
data Tree (A : Set) : ℕ → Set where
leaf : A → Tree A 1
node : ∀ {s : ℕ} → Tree A s → Tree A s → Tree A (s + s)
```
Here we end up with a `Tree (s + s)` as the result of `node`.
This means that if we pattern match on a value of type `Tree A n`, Agda may get confused about making `n` and `s + s` the same.

There are a few ways to avoid green slime, but one way that often works is to use a variable and also include a proof that it has the value that you want. So:

```agda
data Tree (A : Set) : ℕ → Set where
leaf : A → Tree A 1
node :
∀ {s : ℕ} {s‵ : ℕ}
→ Tree A s
→ Tree A s
→ s‵ ≡ s + s
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a regular quote or a prime, but this seems to be a backtick?

→ Tree A s‵
```

Now you can still get the information about the sum by matching on the proof, but Agda has an easier time.