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 documentation for lambdas. #498

Merged
merged 2 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
192 changes: 169 additions & 23 deletions docs/language/blocks.mdx → docs/language/blocks-and-lambdas.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
# Blocks
# Blocks and Lambdas

Blocks are a mechanism to conveniently and efficiently provide callbacks to
functions. This mechanism is, for example, used for the methods `int.repeat`
and `List.do`.
Toit provides two mechanisms for creating and passing around pieces of executable
code: blocks and lambdas. While they share some similarities, they have distinct
characteristics, use cases, and performance implications.

```
## Introduction to Blocks and Lambdas
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
## Introduction to Blocks and Lambdas
## Introduction to blocks and lambdas

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


### Blocks

Blocks are a highly efficient mechanism to provide callbacks to functions. They are
introduced with a colon `:` and are commonly used in the core libraries, for example
with `int.repeat` and `List.do`. Blocks are very efficient and should be used
liberally.

Example of a block:

```toit
main:
// Print the numbers from 1 to 10, one per line.
10.repeat:
Expand All @@ -16,13 +27,44 @@ main:
```

Here we used the automatic block argument, `it`, [see
below](#block-arguments)
below](#block-arguments).

Syntactically `repeat` and `do` look like they are built-in to the language
like `for` and `if` are, but they are actually normal methods that use the
block feature.

## Basic use of blocks
### Lambdas

Lambdas are more flexible function-like objects introduced with a double colon `::`.
They can be stored in variables, passed as arguments, and returned from functions.
While not as efficient as blocks, lambdas offer greater versatility.

Example of a lambda:

```toit
printer/Lambda? := null
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this a top-level variable?

Copy link
Member Author

Choose a reason for hiding this comment

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

I was trying to show that it can be stored in places where a block can't be.
Changed to printer = create-printer.

main:
printer = :: print it
[1, 2, 3].do: printer.call it
```

## Key Differences
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
## Key Differences
## Key differences

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


1. **Syntax**: Blocks use `:`, lambdas use `::`.
2. **Return behavior**: Blocks can have non-local returns, lambdas cannot use `return` at all.
3. **Lifetime**: Blocks are tied to the current context and cannot outlive it. Lambdas can be stored and used later.
4. **Flexibility**: Lambdas can be stored in variables, fields, and collections, and returned from functions. Blocks cannot.
5. **Efficiency**: Blocks are significantly more efficient than lambdas.

## Blocks in Detail
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
## Blocks in Detail
## Blocks in detail

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


### Efficiency of Blocks
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
### Efficiency of Blocks
### Efficiency of blocks

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


Blocks are designed to be highly efficient in Toit. They are stack-allocated,
which means they have minimal overhead in terms of memory usage and execution time.
This efficiency makes blocks the preferred choice for most callback scenarios.

### Basic use of blocks

Blocks are used extensively in the Toit standard library, so you need to know
the basics of calling functions that accept blocks.
Expand Down Expand Up @@ -53,7 +95,7 @@ main:
4.repeat: print "again"
```

## Block arguments
### Block arguments

Blocks often receive arguments when they are called as callbacks. For instance,
`List.do` iterates over a list and calls the block with every element. When the
Expand Down Expand Up @@ -120,13 +162,13 @@ If a block accepts multiple arguments, but you only need to refer to the first,
then you can omit the naming and just use `it`. If there are parameters that
you don't need, then you can use a wildcard `_` instead of giving them a name.

## Returning from blocks and accessing variables
### Returning from blocks and accessing variables

Blocks look like they are built-in which makes it important that they also act
like they are built-in. This means that returning from a block with `return`,
or accessing local variables must just work.

### Non-local return
#### Non-local return

The non-local return is the most common return from blocks. It is best explained based on an example:

Expand Down Expand Up @@ -157,7 +199,7 @@ doesn't just return from the block or `do`, it returns from `first-negative`.
This means that we can short circuit the search when we find the first negative
number.

### Local return
#### Local return

In some situations, it is useful to do a local return. That is, to just return
from the block. To do this, you use the `continue` keyword followed by a dot
Expand Down Expand Up @@ -190,7 +232,7 @@ Here the block given to `map` uses `continue` to do a local return of an odd
value when it sees an even value. The snippet `continue.map` can be read as
"continue in the `map` function".

### Default return
#### Default return

The default return of a block is a local return. The block returns the value of
the last statement in the block. For example
Expand Down Expand Up @@ -362,7 +404,7 @@ main:

Now the intend is clear: you are `do`'ing something to the list which corresponds to visiting each element of the list.

## Calling functions with named block parameters
### Calling functions with named block parameters

Block parameters can also be named. This is indicated in the function signature
by enclosing the named parameter in brackets `[ ]` (the same as for other block
Expand Down Expand Up @@ -431,7 +473,7 @@ Unable to find 4

Here we have formatted the arguments differently, so each of the block arguments are easily identified.

## Blocks as values
### Blocks as values

Blocks are values in Toit which is why functions accept them as arguments. A
block value is defined as a colon `:` followed by the body of the block which
Expand Down Expand Up @@ -459,24 +501,89 @@ Syntactically, it looks a bit cryptic when blocks are stored in local
variables. However, the main purpose of blocks is to pass snippets of code to
functions.

Blocks aren't just "normal" functions as in other programming languages. They
are much more efficient, but also come with some restrictions.

### Restrictions
#### Restrictions

Blocks are stack allocated, which is what makes them so efficient, but it also limits where they can be used:

- Blocks cannot be stored in instance fields, static class fields, and globals.

- Blocks cannot be stored in any collection.

- Blocks cannot be returned from functions and methods.
- Lambdas cannot capture blocks.
- Blocks cannot take blocks as arguments.

- [Lambdas](../tasks) cannot capture blocks.
## Lambdas in Detail
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
## Lambdas in Detail
## Lambdas in detail

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


- Blocks cannot take blocks as arguments.
### Basic Use of Lambdas
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
### Basic Use of Lambdas
### Basic use of lambdas

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


Lambdas are more versatile than blocks and can be used in a wider variety of
situations, though at a cost of reduced efficiency compared to blocks:

```toit
start-task:
task::
3.repeat:
print "still running"
sleep --ms=300

main:
start-task
print "task started"
```

In this example, we define a lambda that runs as a separate task, continuously
printing a message. While less efficient than a block, a lambda is necessary here
due to its ability to be stored and executed later.

### Storing and Reusing Lambdas
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
### Storing and Reusing Lambdas
### Storing and reusing lambdas

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


Unlike blocks, lambdas can be stored in variables, fields, and collections:

## Defining functions that take block arguments
```toit
class WatchedBox:
value_/any := null
callbacks_/List ::= []

watch callback/Lambda:
callbacks_.add callback

value -> any: return value_
value= new-value:
value_ = new-value
callbacks_.do: it.call new-value

add-printer box/WatchedBox:
box.watch:: print "Was changed to $it."
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
box.watch:: print "Was changed to $it."
box.watch:: print "was changed to $it."

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 would drop the trailing period here (and in the comments). It just seems misplaced and we don't typically have that in debug prints.

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


main:
box := WatchedBox
add-printer box
box.value = 499 // Prints "was changed to 499."
box.value = 42 // Prints "was changed to 42."
```

This example demonstrates how lambdas can be stored in a list
(`callbacks_`) and called later.

### Lambda Arguments
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
### Lambda Arguments
### Lambda arguments

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


Like blocks, lambdas can take arguments. If a lambda takes a single argument,
you can use `it` as the default parameter name:

```toit
main:
multiplier := :: it * 2
print (multiplier.call 21) // Prints 42.
```

For multiple arguments, you need to declare them explicitly:

```toit
main:
adder := :: | a b | a + b
print (adder.call 20 22) // Prints 42.
```

## Defining functions that take callbacks

You can define your own functions that accept block arguments. You have already
seen what the function signature should look like: block arguments must be
Expand All @@ -495,3 +602,42 @@ class int:
```

Note: non-named block parameters have to be the last in the function signature.

Similarly, you can define a function that takes a lambda as an argument:

```
call-lambda callback/Lambda:
callback.call 42

main:
call-lambda:: print it
```

A lambda is an object of type `Lambda`. You can store it in a variable, pass it
as an argument, and call it later. The `call` method is used to execute the
lambda.

### Lambdas vs. blocks: choosing the right tool
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
### Lambdas vs. blocks: choosing the right tool
### Lambdas versus blocks: choosing the right tool

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


Only the receiver of a callback can determine whether to use a block or a lambda. As
a user, one has to follow the type declared in the method signature.

When writing functions that accept callbacks, consider the following guidelines:
- Use blocks for short-lived, context-dependent callbacks that don't need to be
stored or returned. Blocks are significantly more efficient and should be the
default choice when possible.
- Use lambdas when you need to store the function.
- Consider providing both block and lambda versions if you need to support both
use cases. The lambda version can always redirect to the block version:
```
my-function callback:
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe flip the order of these two functions?

Copy link
Member Author

Choose a reason for hiding this comment

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

done.

my-function: callback.call it

my-function [block]:
block.call 42

main:
// Both of these will print 42.
my-function:: print it
my-function: print it
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels more logical to start with the block-variant call.

Copy link
Member Author

Choose a reason for hiding this comment

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

done.

```
52 changes: 44 additions & 8 deletions docs/language/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ for each element using block structure:
Here the statement is on a single line, so there is no need to use indentation.
When using `names.do`, a method available on all
[collections](./listsetmap), the [special variable
`it`](./blocks#block-arguments) contains the individual elements from the list in
`it`](./blocks-and-lambdas#block-arguments) contains the individual elements from the list in
turn. If there are 5 elements in the `names` list, we will call `print` 5 times
producing 5 separate lines of output.

Expand Down Expand Up @@ -778,7 +778,7 @@ have used:
print "Hello, $names[i]"
```

## Blocks
## Blocks and lambdas

We already saw the `repeat` method on integers and the `do` method on lists:

Expand All @@ -794,8 +794,7 @@ main:
```

Syntactically they look like they are built in to the language like `if` and
`for`, but they are actually perfectly normal methods on the List and Integer
classes:
`for`, but they are actually normal methods on the List and int classes:

<!-- SKIP CODE -->

Expand All @@ -805,23 +804,60 @@ class List:
do [block]:
size.repeat: block.call this[it]

class Integer:
class int:
// ...
repeat [block]:
for i := 0; i < this; i++:
block.call i
```

They are making use of a feature called [blocks](./blocks). These are
They are making use of a feature called [blocks](./blocks-and-lambdas). These are
snippets of code that can be passed down the stack as arguments to methods and
functions. At the call site we precede the block with a colon, '`:`', and at
the function definition we surround the parameter name with square brackets,
'`[]`'. Often, there is one block parameter, it is in the final position and
it is called `block`.

## Blocks that return a value
If a callback needs to survive the scope in which it is defined, we can't use
blocks. Instead, we can use [lambdas](./blocks-and-lambdas#lambdas):

A block can return a value each time it is run. This is used for example in the `filter` method on lists.
```
// Returns a function that adds n to its argument.
add-n n:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
add-n n:
add-n n -> Lambda:

Copy link
Member Author

Choose a reason for hiding this comment

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

done.

return (:: it + n)
```

The syntax for lambdas is the same as for blocks, except that the we use `::`
instead of `:`. The lambda above is a function that takes a single argument and
returns the sum of that argument and `n`. We can use it like this:

```
main:
add-5 := add-n 5
print (add-5.call 10)
```

This will print `15`.

## Blocks and lambdas with multiple arguments

Blocks and lambdas can have parameters, just like methods. The parameters are
listed after the `::` or `:`. Here is an example of a block with two parameters:

```
main:
map := { "Lars": 1, "Kasper": 2 }
map.do: | key value |
print "$key has value $value"
```

The block in the `do` method has two parameters, `key` and `value`. The `do`
method will call the block with each key-value pair in the map.

## Blocks and lambdas that return a value

A block or lambda can return a value each time it is run. This is used for example
in the `filter` method on lists.

```
// Takes a list of words, and returns a new list with only the
Expand Down
Loading