Skip to content

Commit b5afae2

Browse files
committed
Add Blocks section to Methods
1 parent 23ceaab commit b5afae2

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed

docs/tutorials/basics/60_methods.md

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,277 @@ puts life_universe_and_everything + 1 # Error: method top-level life_universe_an
163163
```
164164

165165
Now the compiler can show us exactly where the problem is originated. As we can see, providing type information is really useful for finding errors at compile time.
166+
167+
## Blocks
168+
169+
Methods may receive a `block of code` (or simply a `block`) as an argument. And using the keyword `yield` indicates the place in the method's body where we want to "place" said `block`:
170+
171+
```crystal-play
172+
def with_42
173+
yield
174+
end
175+
176+
with_42 do
177+
puts 42
178+
end
179+
```
180+
181+
### Blocks with parameters
182+
183+
We can parameterize `blocks` like this:
184+
185+
```crystal
186+
some_method do |param1, param2|
187+
end
188+
```
189+
190+
And using *curly braces* notation would be:
191+
192+
``` crystal
193+
some_method { |param1, param2| }
194+
```
195+
196+
We can rewrite our previous example, so that the `block` receives the number `42` as an argument.
197+
198+
```crystal-play
199+
def with_42
200+
yield 42
201+
end
202+
203+
with_42 do |number|
204+
puts number
205+
end
206+
```
207+
208+
And we can go a little further parameterizing also the method `#with_42` to receive not only the `block` but also the `number`:
209+
210+
```crystal-play
211+
def with_number(n : Int32)
212+
yield n
213+
end
214+
215+
with_number(42) do |number|
216+
puts number
217+
end
218+
219+
with_number(24) do |number|
220+
puts number
221+
end
222+
```
223+
224+
### Blocks' returned value
225+
226+
A `block`, by default, returns the value of the last expression (the same as a method).
227+
228+
```crystal-play
229+
def with_number(n : Int32)
230+
result = yield n
231+
puts result
232+
end
233+
234+
with_number(41) do |number|
235+
number + 1
236+
end
237+
```
238+
239+
#### Returning keywords
240+
241+
We can use the `return` keyword ... but, let's see the following example:
242+
243+
```crystal-play
244+
245+
def with_number(n : Int32)
246+
result = yield n
247+
puts result
248+
end
249+
250+
def test_number(n)
251+
with_number(n) do |number|
252+
# this block returns if the number is negative
253+
return number if number.negative?
254+
number + 1
255+
end
256+
257+
puts "Inside `#test_number` method after `#with_number`"
258+
end
259+
260+
test_number(42)
261+
```
262+
263+
Outputs:
264+
265+
```console
266+
267+
43
268+
Inside `#test_number` method after `#with_number`
269+
```
270+
271+
And if we want to `test_number(-1)` we would expect:
272+
273+
```console
274+
275+
-1
276+
Inside `#test_number` method after `#with_number`
277+
```
278+
279+
Let's see ...
280+
281+
```crystal-play
282+
283+
def with_number(n : Int32)
284+
result = yield n
285+
puts result
286+
end
287+
288+
def test_number(n)
289+
290+
with_number(n) do |number|
291+
# this block returns if the number is negative
292+
return number if number.negative?
293+
number + 1
294+
end
295+
296+
puts "Inside `#test_number` method after `#with_number`"
297+
end
298+
299+
test_number(-1)
300+
```
301+
302+
The output is empty! This is because Crystal implements *full closures*, meaning that using `return` inside the block will return, not only from the `block` itself but, from the method where the `block` is defined (`#test_number` in the above example).
303+
304+
If we want to return only from the `block` then we can use the `next` keyword:
305+
306+
```crystal-play
307+
308+
def with_number(n : Int32)
309+
result = yield n
310+
puts result
311+
end
312+
313+
def test_number(n)
314+
with_number(n) do |number|
315+
# this block returns if the number is negative
316+
next number if number.negative?
317+
number + 1
318+
end
319+
320+
puts "Inside `#test_number` method after `#with_number`"
321+
end
322+
323+
test_number(-1)
324+
```
325+
326+
The last keyword for returning from a `block` is `break`. Let's see how it behaves:
327+
328+
```crystal-play
329+
330+
def with_number(n : Int32)
331+
result = yield n
332+
puts result
333+
end
334+
335+
def test_number(n)
336+
with_number(n) do |number|
337+
# this block returns if the number is negative
338+
break number if number.negative?
339+
number + 1
340+
end
341+
342+
puts "Inside `#test_number` method after `#with_number`"
343+
end
344+
345+
test_number(-1)
346+
```
347+
348+
The ouput is
349+
350+
```console
351+
352+
Inside `#test_number` method after `#with_number`
353+
```
354+
355+
As we can see the behaviour is something between using `return` and `next`. With `break` we return from the `block` and from the method yielding the `block` (`#with_number` in this example) but not from the method where the `block` is defined.
356+
357+
### Non-captured blocks
358+
359+
Up to here we've been using `non-captured blocks`, meaning that **the `non-captured block` is inlined** in the place where we use `yield`. So this code:
360+
361+
```crystal-play
362+
def with_number(n : Int32)
363+
result = yield n
364+
puts result
365+
end
366+
367+
with_number(41) do |number|
368+
number + 1
369+
end
370+
```
371+
372+
it's the same as:
373+
374+
```crystal-play
375+
def with_number(n : Int32)
376+
number = n
377+
result = number + 1
378+
puts result
379+
end
380+
381+
with_number(41)
382+
```
383+
384+
### Captured blocks
385+
386+
When a `block` is captured then a [Proc](https://crystal-lang.org/reference/latest/syntax_and_semantics/literals/proc.html) is created with its `context` (ie. its [closure](https://crystal-lang.org/reference/latest/syntax_and_semantics/closures.html)). This means that **the `captured block` is not inlined**.
387+
388+
To capture a `block` we need to add it as a parameter, specifying its name and type.
389+
390+
```crystal-play
391+
def with_number(n : Int32, &block : Int32 -> Int32)
392+
result = block.call n
393+
puts result
394+
395+
puts typeof(block) # => Proc(Int32, Int32)
396+
end
397+
398+
with_number(41) do |number|
399+
number + 1
400+
end
401+
```
402+
403+
Note that we don't use `yield`. Instead we invoke the `block`'s method `#call`.
404+
405+
#### Returning keywords with captured blocks
406+
407+
When using `captured blocks` we can only use `next` for returning from the `captured block`:
408+
409+
```crystal-play
410+
411+
def with_number(n : Int32, &block : Int32 -> Int32)
412+
result = block.call n
413+
puts result
414+
end
415+
416+
def test_number(n)
417+
with_number(n) do |number|
418+
# this block returns if the number is negative
419+
next number if number.negative?
420+
number + 1
421+
end
422+
423+
puts "Inside `#test_number` method after `#with_number`"
424+
end
425+
426+
test_number(-1)
427+
```
428+
429+
Trying to use `return` or `break` in a `captured block` will error:
430+
431+
```console
432+
433+
Error: can't return from captured block, use next
434+
```
435+
436+
```console
437+
438+
Error: can't break from captured block, try using `next`.
439+
```

0 commit comments

Comments
 (0)