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

[ doc ] Improve docs for let and := #3159

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG_NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ This CHANGELOG describes the merged but unreleased changes. Please see [CHANGELO

* Existing `System.Console.GetOpt` was extended to support errors during options
parsing in a backward-compatible way.

### Other Changes

* Improved the docs for `let` and `:=` to (hopefully) avoid confusion.
98 changes: 78 additions & 20 deletions src/Idris/Doc/Keywords.idr
Original file line number Diff line number Diff line change
Expand Up @@ -505,31 +505,71 @@ withabstraction = vcat $
```
"""]

letbinding : Doc IdrisDocAnn
letbinding = vcat $
-- to keep docs consistent between `:doc let` and `:doc (:=)`
letBinding : String
letBinding =
"""
* Let bindings are defined using the `:=` syntax and can be used to bind the
result of intermediate computations. A type annotation can be added but is
not required. However, this results in different behaviours, for example:
```idris
power4 : Nat -> Nat
power4 n = let square := n * n
in square * square

power4T : Nat -> Nat
power4T n = let square : Nat := n * n
in square * square
```
In the first example, the type annotation is omitted and so `:=` gets
elaborated to the lambda expression `(\square => square * square) (n * n)`.

The second example does have a type annotation and as a result, `square`
gets computed rather than lambda-abstracted, meaning the result will be
reused on reference, instead of potentially being recomputed every time.
Comment on lines +524 to +529
Copy link
Member

Choose a reason for hiding this comment

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

I don't think that's true. The code for parsing let bindings is as follows:

    letBinder : Rule LetBinder
    letBinder = do s <- bounds (MkPair <$> multiplicity fname <*> expr plhs fname indents)
                   (rig, pat) <- pure s.val
                   ty <- option (PImplicit (virtualiseFC $ boundToFC fname s))
                                (decoratedSymbol fname ":" *> typeExpr (pnoeq pdef) fname indents)
                   (decoratedSymbol fname "=" <|> decoratedSymbol fname ":=")
                   val <- typeExpr pnowith fname indents
                   alts <- block (patAlt fname)
                   pure (MkLetBinder rig pat ty val alts)

As you can see if there is no type annotation, we replace it with a hole.

And so computationally the two expressions are the same.


Note that, unlike functions, let bindings do not reduce to their values so
may not be appropriate in all cases.
"""

letKeyword : Doc IdrisDocAnn
letKeyword = vcat $
header "Let binding" :: ""
:: map (indent 2) [
"""
The `let` keyword is used for both local definitions and let bindings.
Local definitions are just like top-level definitions except that they are
defined in whatever extended context is available at the definition site.
NOTE: We distinguish between "local let" (local definitions) and let
bindings, as well as between function definitions and values.

Let bindings can be used to bind the result of intermediate computations.
They do not necessitate but can have a type annotation. They will not unfold
in the type of subsequent terms so may not be appropriate in all cases.

For instance, in the following definition the let-bound value `square`
ensures that `n * n` is only computed once:
* Local definitions are effectively lambdas, that is the expression
```idris
power4 : Nat -> Nat
power4 n = let square := n * n in square * square
let a = foo in b
```
is equivalent to
```idris
(\a => b) foo
```

It is also possible to pattern-match on the result of the intermediate
computation. The main pattern is written in place of the variable and
an alternative list of clauses can be given using the `|` separator.
For instance, we can shortcut the `square * square` computation in case
the returned value is 0 like so:
\{letBinding}

* Local function definitions allow for more complex computational behaviour,
and are defined in a similar manner to local definitions, with the addition
of annotating the `let`-expression with a type. For example:
```idris
let bar : Nat -> Nat -> Nat
bar k n = k * n + k
in e
```
Local definitions are just like top-level definitions except that they are
defined in whatever extended context is available at the definition site.
Be careful that if `e` uses `bar` multiple times, and `bar` is an expensive
computation, this may lead to `bar` being evaluated every time. Consider
using a "proper" let binding (`:=`) instead, if possible.

* Finally, it is also possible to pattern-match on the result of the
intermediate computation. The main pattern is written in place of the
variable and an alternative list of clauses can be given using the `|`
separator. For instance, we can shortcut the `square * square` computation
in case the returned value is 0 like so:
```idris
power4 : Nat -> Nat
power4 n = let square@(S _) := n * n
Expand All @@ -543,7 +583,7 @@ keywordsDoc =
"data" ::= datatypes
:: "module" ::= "Keyword to start a module definition"
:: "where" ::= whereblock
:: "let" ::= letbinding
:: "let" ::= letKeyword
:: "in" ::= "Used by `let` and `rewrite`. See either of them for more details."
:: "do" ::= doblock
:: "record" ::= recordtypes
Expand Down Expand Up @@ -699,6 +739,24 @@ recordUpdate = vcat $ header "Record updates" :: ""
"""
]

letOrRecord : Doc IdrisDocAnn
letOrRecord = vcat $
header "Let binding or record update"
:: ""
:: map (indent 2) [
"""
\{letBinding}


* Record updates use `:=` to set a field's value.
For example, given a record `r` with a field `name` of type `String`, we can
reassign the value of the field using `:=` with the following syntax:
```idris
r = { name := "Olwyn Griffiths" } r
```
"""
]

symbolsDoc : All DocFor (Source.symbols ++ Source.reservedInfixSymbols)
symbolsDoc
= "," ::= ""
Expand All @@ -715,7 +773,7 @@ symbolsDoc
declares a new toplevel definition `id` of type `a -> a`.
"""
:: "=" ::= "Definition or equality type"
:: ":=" ::= "Let binding or record assignment"
:: ":=" ::= letOrRecord -- "Let binding or record assignment"
:: "$=" ::= recordUpdate
:: "|" ::= "Additional patterns showing up in a `with` clause"
:: "|||" ::= "Document string attached to the following definition"
Expand Down
Loading