Skip to content

Commit 6524049

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

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed

docs/tutorials/basics/60_methods.md

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,272 @@ 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+
def with_number(n : Int32)
245+
result = yield n
246+
puts result
247+
end
248+
249+
def test_number(n)
250+
with_number(n) do |number|
251+
# this block returns if the number is negative
252+
return number if number.negative?
253+
number + 1
254+
end
255+
256+
puts "Inside `#test_number` method after `#with_number`"
257+
end
258+
259+
test_number(42)
260+
```
261+
262+
Outputs:
263+
264+
```console
265+
266+
43
267+
Inside `#test_number` method after `#with_number`
268+
```
269+
270+
And if we want to `test_number(-1)` we would expect:
271+
272+
```console
273+
274+
-1
275+
Inside `#test_number` method after `#with_number`
276+
```
277+
278+
Let's see ...
279+
280+
```crystal-play
281+
def with_number(n : Int32)
282+
result = yield n
283+
puts result
284+
end
285+
286+
def test_number(n)
287+
288+
with_number(n) do |number|
289+
# this block returns if the number is negative
290+
return number if number.negative?
291+
number + 1
292+
end
293+
294+
puts "Inside `#test_number` method after `#with_number`"
295+
end
296+
297+
test_number(-1)
298+
```
299+
300+
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).
301+
302+
If we want to return only from the `block` then we can use the `next` keyword:
303+
304+
```crystal-play
305+
def with_number(n : Int32)
306+
result = yield n
307+
puts result
308+
end
309+
310+
def test_number(n)
311+
with_number(n) do |number|
312+
# this block returns if the number is negative
313+
next number if number.negative?
314+
number + 1
315+
end
316+
317+
puts "Inside `#test_number` method after `#with_number`"
318+
end
319+
320+
test_number(-1)
321+
```
322+
323+
The last keyword for returning from a `block` is `break`. Let's see how it behaves:
324+
325+
```crystal-play
326+
def with_number(n : Int32)
327+
result = yield n
328+
puts result
329+
end
330+
331+
def test_number(n)
332+
with_number(n) do |number|
333+
# this block returns if the number is negative
334+
break number if number.negative?
335+
number + 1
336+
end
337+
338+
puts "Inside `#test_number` method after `#with_number`"
339+
end
340+
341+
test_number(-1)
342+
```
343+
344+
The ouput is
345+
346+
```console
347+
348+
Inside `#test_number` method after `#with_number`
349+
```
350+
351+
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.
352+
353+
### Non-captured blocks
354+
355+
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:
356+
357+
```crystal-play
358+
def with_number(n : Int32)
359+
result = yield n
360+
puts result
361+
end
362+
363+
with_number(41) do |number|
364+
number + 1
365+
end
366+
```
367+
368+
it's the same as:
369+
370+
```crystal-play
371+
def with_number(n : Int32)
372+
number = n
373+
result = number + 1
374+
puts result
375+
end
376+
377+
with_number(41)
378+
```
379+
380+
### Captured blocks
381+
382+
When a `block` is captured then a [Proc](../../syntax_and_semantics/literals/proc.md) is created with its `context` (ie. its [closure](../../syntax_and_semantics/closures.md)). This means that **the `captured block` is not inlined**.
383+
384+
To capture a `block` we need to add it as a parameter, specifying its name and type.
385+
386+
```crystal-play
387+
def with_number(n : Int32, &block : Int32 -> Int32)
388+
result = block.call n
389+
puts result
390+
391+
puts typeof(block) # => Proc(Int32, Int32)
392+
end
393+
394+
with_number(41) do |number|
395+
number + 1
396+
end
397+
```
398+
399+
Note that we don't use `yield`. Instead we invoke the `block`'s method `#call`.
400+
401+
#### Returning keywords with captured blocks
402+
403+
When using `captured blocks` we can only use `next` for returning from the `captured block`:
404+
405+
```crystal-play
406+
def with_number(n : Int32, &block : Int32 -> Int32)
407+
result = block.call n
408+
puts result
409+
end
410+
411+
def test_number(n)
412+
with_number(n) do |number|
413+
# this block returns if the number is negative
414+
next number if number.negative?
415+
number + 1
416+
end
417+
418+
puts "Inside `#test_number` method after `#with_number`"
419+
end
420+
421+
test_number(-1)
422+
```
423+
424+
Trying to use `return` or `break` in a `captured block` will error:
425+
426+
```console
427+
428+
Error: can't return from captured block, use next
429+
```
430+
431+
```console
432+
433+
Error: can't break from captured block, try using `next`.
434+
```

0 commit comments

Comments
 (0)