From 1f82fda4a0a1536637e08166b2bc90ecfd9bf5dd Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 23 Aug 2024 08:35:54 +0200 Subject: [PATCH 1/2] Add documentation for lambdas. --- .../{blocks.mdx => blocks-and-lambdas.mdx} | 192 +++++++++++++++--- docs/language/index.mdx | 52 ++++- docs/language/listsetmap.mdx | 12 +- docs/language/loops.mdx | 2 +- docs/menu.yaml | 4 +- docs/tutorials/hardware/leds.mdx | 2 +- 6 files changed, 223 insertions(+), 41 deletions(-) rename docs/language/{blocks.mdx => blocks-and-lambdas.mdx} (69%) diff --git a/docs/language/blocks.mdx b/docs/language/blocks-and-lambdas.mdx similarity index 69% rename from docs/language/blocks.mdx rename to docs/language/blocks-and-lambdas.mdx index 70e4840d..d93aa35b 100644 --- a/docs/language/blocks.mdx +++ b/docs/language/blocks-and-lambdas.mdx @@ -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 + +### 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: @@ -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 +main: + printer = :: print it + [1, 2, 3].do: printer.call it +``` + +## Key Differences + +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 + +### Efficiency of Blocks + +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. @@ -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 @@ -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: @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 -- Blocks cannot take blocks as arguments. +### Basic Use of Lambdas + +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 + +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." + +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 + +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 @@ -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 + +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: + 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 + ``` diff --git a/docs/language/index.mdx b/docs/language/index.mdx index c9401ccd..f60886e7 100644 --- a/docs/language/index.mdx +++ b/docs/language/index.mdx @@ -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. @@ -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: @@ -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: @@ -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: + 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 diff --git a/docs/language/listsetmap.mdx b/docs/language/listsetmap.mdx index 5eb7cdcf..3077a103 100644 --- a/docs/language/listsetmap.mdx +++ b/docs/language/listsetmap.mdx @@ -24,7 +24,7 @@ There are two concrete implementations of `Set` and `Map`: `Set`/`Map` use the overwritable `==` operator to determine whether two keys are equal, whereas `IdentitySet`/`IdentityMap` use `identical`. -See some examples of use of `List` and `Map` in the section [Toit blocks](../blocks). +See some examples of use of `List` and `Map` in the section [Toit blocks](../blocks-and-lambdas). See also the documentation for the [List](https://libs.toit.io/core/collections/class-List), @@ -176,7 +176,7 @@ Often there are more succinct ways to achieve the same with variations of the The full set of map methods is available in the [class documentation](https://libs.toit.io/core/collections/class-Map). See also the guide to [using named block -parameters](../blocks#calling-functions-with-named-block-parameters) +parameters](../blocks-and-lambdas#calling-functions-with-named-block-parameters) ## Functional methods @@ -210,7 +210,7 @@ introduces the block argument to `any` does not get mistaken for the colon of th if-statement. The `any` method is often called `some` in functional languages.) Both of these examples made use of the -[automatic block argument, `it`](../blocks#block-arguments). +[automatic block argument, `it`](../blocks-and-lambdas#block-arguments). Similarly, Toit collections have the [`every`]() @@ -224,8 +224,8 @@ method known from functional languages: The reduce method: (The example shows the use of a -[block](../blocks) with [two named -arguments](../blocks#block-arguments)). +[block](../blocks-and-lambdas) with [two named +arguments](../blocks-and-lambdas#block-arguments)). ``` sum := collection.reduce: | a b | a + b @@ -282,7 +282,7 @@ That example also illustrates that the value of the block (returned to the `map` method) is the result of the last statement. In this case one of the branches of the `if` will be the last statement, producing the return value. An alternative way to do this would be to use -[continue.map](../blocks#local-return) +[continue.map](../blocks-and-lambdas#local-return) We could also reduce it to an almost unreadable one-liner by putting an arbitrary expression in a string interpolation, surrounding it with `$(` and diff --git a/docs/language/loops.mdx b/docs/language/loops.mdx index 1288b6f5..fcef2658 100644 --- a/docs/language/loops.mdx +++ b/docs/language/loops.mdx @@ -89,7 +89,7 @@ will explain how that works in Toit. If you simply want to execute a block of code multiple times you can use [`repeat`]() which is a method on the [int class](https://libs.toit.io/core/numbers/class-int) -that takes a [Toit block](../blocks). +that takes a [Toit block](../blocks-and-lambdas). Example of a loop that runs a fixed number of times, given by the `end` integer variable. diff --git a/docs/menu.yaml b/docs/menu.yaml index 48f0b003..cac614bd 100644 --- a/docs/menu.yaml +++ b/docs/menu.yaml @@ -64,8 +64,8 @@ items: path: /language/objects-constructors-inheritance-interfaces - name: Strings path: /language/strings - - name: Blocks - path: /language/blocks + - name: Blocks and lambdas + path: /language/blocks-and-lambdas - name: Lists, byte arrays, sets and maps path: /language/listsetmap - name: Control flow diff --git a/docs/tutorials/hardware/leds.mdx b/docs/tutorials/hardware/leds.mdx index 150b0991..d05fb2d7 100644 --- a/docs/tutorials/hardware/leds.mdx +++ b/docs/tutorials/hardware/leds.mdx @@ -62,7 +62,7 @@ main: leds.do: it.close ``` -The `leds.do` is a method on `List` that takes a [block](../../../language/blocks) +The `leds.do` is a method on `List` that takes a [block](../../../language/blocks-and-lambdas) ("code") as argument and calls it for every element in the list. Here there are four elements in the list, and the block is thus called four times. Each time the code is executed with it being set to the corresponding element. From 72cd4f81a25671bacf449239efe491bf1704f07e Mon Sep 17 00:00:00 2001 From: Florian Loitsch Date: Fri, 23 Aug 2024 09:34:07 +0200 Subject: [PATCH 2/2] Feedback. --- docs/language/blocks-and-lambdas.mdx | 38 +++++++++++++++------------- docs/language/index.mdx | 2 +- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/language/blocks-and-lambdas.mdx b/docs/language/blocks-and-lambdas.mdx index d93aa35b..74cef33b 100644 --- a/docs/language/blocks-and-lambdas.mdx +++ b/docs/language/blocks-and-lambdas.mdx @@ -4,7 +4,7 @@ Toit provides two mechanisms for creating and passing around pieces of executabl code: blocks and lambdas. While they share some similarities, they have distinct characteristics, use cases, and performance implications. -## Introduction to Blocks and Lambdas +## Introduction to blocks and lambdas ### Blocks @@ -42,13 +42,15 @@ While not as efficient as blocks, lambdas offer greater versatility. Example of a lambda: ```toit -printer/Lambda? := null +create-printer: + return (:: print it) + main: - printer = :: print it + printer := create-printer [1, 2, 3].do: printer.call it ``` -## Key Differences +## Key differences 1. **Syntax**: Blocks use `:`, lambdas use `::`. 2. **Return behavior**: Blocks can have non-local returns, lambdas cannot use `return` at all. @@ -56,9 +58,9 @@ main: 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 +## Blocks in detail -### Efficiency of Blocks +### Efficiency of blocks 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. @@ -511,9 +513,9 @@ Blocks are stack allocated, which is what makes them so efficient, but it also l - Lambdas cannot capture blocks. - Blocks cannot take blocks as arguments. -## Lambdas in Detail +## Lambdas in detail -### Basic Use of Lambdas +### Basic use of lambdas 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: @@ -534,7 +536,7 @@ 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 +### Storing and reusing lambdas Unlike blocks, lambdas can be stored in variables, fields, and collections: @@ -552,19 +554,19 @@ class WatchedBox: callbacks_.do: it.call new-value add-printer box/WatchedBox: - box.watch:: print "Was changed to $it." + box.watch:: print "was changed to $it" main: box := WatchedBox add-printer box - box.value = 499 // Prints "was changed to 499." - box.value = 42 // Prints "was changed to 42." + 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 +### Lambda arguments Like blocks, lambdas can take arguments. If a lambda takes a single argument, you can use `it` as the default parameter name: @@ -617,7 +619,7 @@ 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 +### Lambdas versus blocks: choosing the right tool 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. @@ -630,14 +632,14 @@ When writing functions that accept callbacks, consider the following guidelines: - 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: - my-function: callback.call it - my-function [block]: block.call 42 + my-function callback: + my-function: callback.call it + main: // Both of these will print 42. - my-function:: print it my-function: print it + my-function:: print it ``` diff --git a/docs/language/index.mdx b/docs/language/index.mdx index f60886e7..5217f3a9 100644 --- a/docs/language/index.mdx +++ b/docs/language/index.mdx @@ -823,7 +823,7 @@ blocks. Instead, we can use [lambdas](./blocks-and-lambdas#lambdas): ``` // Returns a function that adds n to its argument. -add-n n: +add-n n -> Lambda: return (:: it + n) ```