Skip to content

Commit ee92e43

Browse files
committed
Blocks section rewrite
1 parent 4686a3c commit ee92e43

File tree

3 files changed

+297
-0
lines changed

3 files changed

+297
-0
lines changed

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
* [Strings](tutorials/basics/40_strings.md)
144144
* [Control Flow](tutorials/basics/50_control_flow.md)
145145
* [Methods](tutorials/basics/60_methods.md)
146+
* [Blocks](tutorials/basics/70_blocks.md)
146147
* [Manuals](man/README.md)
147148
* [Using the Compiler](man/crystal/README.md)
148149
* [The Shards Command](man/shards/README.md)

docs/tutorials/basics/70_blocks.md

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# Blocks
2+
3+
As we already mentioned in the [methods](./60_methods.html#blocks) section, _blocks_ is an interesting and important topic. So let's start defining them:
4+
5+
A _block of code_ or _block_ for short, is a way to abstract a sequence of interactions between objects. They are commonly used as a method parameter, as a way to parameterize (forgive the redundancy) part of its behavior.
6+
7+
8+
# Blocks and Methods
9+
10+
Methods may receive 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`:
11+
12+
```crystal-play
13+
def with_42
14+
yield
15+
end
16+
17+
with_42 do
18+
puts 42
19+
end
20+
```
21+
22+
Or the **places** where we want to execute the _block_:
23+
24+
```crystal-play
25+
def three_times
26+
yield
27+
yield
28+
yield
29+
end
30+
31+
three_times do
32+
puts 42
33+
end
34+
35+
Let's see another example with a common use scenario for _blocks_: collections.
36+
37+
Here are two examples:
38+
39+
```crystal-play
40+
# Example 1
41+
arr = [1, 2, 3]
42+
index = 0
43+
44+
while index < arr.size
45+
elem = arr[index]
46+
47+
puts elem + 42
48+
49+
index = index + 1
50+
end
51+
```
52+
53+
```crystal-play
54+
# Example 2
55+
arr = ["John", "Paul", "George","Ringo"]
56+
index = 0
57+
58+
while index < arr.size
59+
elem = arr[index]
60+
61+
puts "Hello #{elem}"
62+
63+
index = index + 1
64+
end
65+
```
66+
67+
As we can see in the above examples, the structures are the same: we want to traverse the array and do _something_ with the current element at each step. And here is where _blocks_ come handy: we can parameterize that _something_ we want to execute.
68+
69+
So let's rewrite the examples. We are going to declare a new method that will abstract the structure duplicated in both examples: It will receive the array and what we want to do at each step using a _block_:
70+
71+
```crystal-play
72+
def with_array(arr)
73+
index = 0
74+
75+
while index < arr.size
76+
elem = arr[index]
77+
78+
yield elem # yes! we can pass an argument to a block.
79+
80+
index = index + 1
81+
end
82+
end
83+
84+
# Example 1
85+
with_array [1, 2, 3] do |current_elem|
86+
puts elem + 42
87+
end
88+
89+
# Example 2
90+
with_array ["John", "Paul", "George","Ringo"] do |current_elem|
91+
puts "Hello #{current_elem}"
92+
end
93+
```
94+
95+
The abstracted structure declared in the `with_array` method is very common and that's why it's already implemented in [Indexable#each](https://crystal-lang.org/api/latest/Indexable.html#each%28%26%3AT-%3E%29-instance-method).
96+
97+
**Note:** both blocks declare a parameter (the current element while traversing the array) which we will see next.
98+
99+
## Blocks with parameters
100+
101+
Like methods, _blocks_ may receive parameters:
102+
103+
```crystal
104+
some_method do |param1, param2, param3|
105+
end
106+
```
107+
108+
Let's see an example:
109+
110+
```crystal-play
111+
def other_method
112+
yield 42, "Hello", [1, "Crystal", 3]
113+
end
114+
115+
other_method do |n, s, arr|
116+
puts "#{s} #{arr[1]}"
117+
puts n
118+
end
119+
```
120+
121+
### Underscore
122+
123+
What happens if the _block_ we are providing does not use all the passed arguments? In that case we can use an underscore just to indicate that an expected parameter it's not used by the block (so no name needed):
124+
125+
```crystal-play
126+
def other_method
127+
yield 42, "Hello", [1, "Crystal", 3]
128+
end
129+
130+
other_method do |_, _, arr|
131+
puts arr[1]
132+
end
133+
```
134+
135+
### Unpacking parameters
136+
137+
Let's suppose we have an array of arrays and our _block_ prints each element in each array. One way to implement this would be:
138+
139+
```crystal-play
140+
arr = [["one", 42], ["two", 24]]
141+
arr.each do |arg|
142+
puts "#{arg[0]} - #{arg[1]}"
143+
end
144+
```
145+
146+
This works, but there is a concise way of writing the same but using parameter unpacking:
147+
148+
```crystal-play
149+
arr = [["one", 42], ["two", 24]]
150+
arr.each do |(word, number)|
151+
puts "#{word} - #{number}"
152+
end
153+
```
154+
155+
**Note:** we use parentheses to unpack the argument in the different block parameters.
156+
157+
Unpacking arguments into parameters works only if the argument's type responds to `[i]` (with `i` an `integer`). In our example `Array` inherits [Indexable#[ ]](https://crystal-lang.org/api/latest/Indexable.html#%5B%5D%28index%3AInt%29-instance-method)
158+
159+
### Splats
160+
161+
When the expected _block_ argument is a [Tuple](../syntax_and_semantics/literals/tuple.html) we can use auto-splatting (see [Splats](../syntax_and_semantics/operators.html#splats)) as a way of destructuring the `tuple` in block parameters (and without the need of parentheses).
162+
163+
164+
```crystal-play
165+
arr = [{"one", 42}, {"two", 24}]
166+
arr.each do |word, number|
167+
puts "#{word} - #{number}"
168+
end
169+
```
170+
171+
**Note:** `Tuples` also implements [Tuple#[ ]](https://crystal-lang.org/api/latest/Tuple.html#%5B%5D%28index%3AInt%29-instance-method) meaning that we also can use _unpacking_.
172+
173+
### Block's returned value
174+
175+
A _block_, by default, returns the value of the last expression (the same as a method).
176+
177+
```crystal-play
178+
def with_number(n : Int32)
179+
block_result = yield n
180+
puts block_result
181+
end
182+
183+
with_number(41) do |number|
184+
number + 1
185+
end
186+
```
187+
188+
#### Returning keywords
189+
190+
We can use the `return` keyword ... but, let's see the following example:
191+
192+
```crystal-play
193+
def with_number(n : Int32)
194+
block_result = yield n
195+
puts block_result
196+
end
197+
198+
def test_number(n)
199+
with_number(n) do |number|
200+
return number if number.negative?
201+
number + 1
202+
end
203+
204+
puts "Inside `#test_number` method after `#with_number`"
205+
end
206+
207+
test_number(42)
208+
```
209+
210+
Outputs:
211+
212+
```console
213+
214+
43
215+
Inside `#test_number` method after `#with_number`
216+
```
217+
218+
And if we want to `test_number(-1)` we would expect:
219+
220+
```console
221+
222+
-1
223+
Inside `#test_number` method after `#with_number`
224+
```
225+
226+
Let's see ...
227+
228+
```crystal-play
229+
def with_number(n : Int32)
230+
block_result = yield n
231+
puts block_result
232+
end
233+
234+
def test_number(n)
235+
with_number(n) do |number|
236+
return number if number.negative?
237+
number + 1
238+
end
239+
240+
puts "Inside `#test_number` method after `#with_number`"
241+
end
242+
243+
test_number(-1)
244+
```
245+
246+
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 (i.e. `#test_number` in the above example).
247+
248+
If we want to return only from the _block_ then we need to use the keyword `next`:
249+
250+
```crystal-play
251+
def with_number(n : Int32)
252+
block_result = yield n
253+
puts block_result
254+
end
255+
256+
def test_number(n)
257+
with_number(n) do |number|
258+
next number if number.negative?
259+
number + 1
260+
end
261+
262+
puts "Inside `#test_number` method after `#with_number`"
263+
end
264+
265+
test_number(-1)
266+
```
267+
268+
The last keyword for returning from a _block_ is `break`. Let's see how it behaves:
269+
270+
```crystal-play
271+
def with_number(n : Int32)
272+
block_result = yield n
273+
puts block_result
274+
end
275+
276+
def test_number(n)
277+
with_number(n) do |number|
278+
break number if number.negative?
279+
number + 1
280+
end
281+
282+
puts "Inside `#test_number` method after `#with_number`"
283+
end
284+
285+
test_number(-1)
286+
```
287+
288+
The ouput is
289+
290+
```console
291+
292+
Inside `#test_number` method after `#with_number`
293+
```
294+
295+
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.

docs/tutorials/basics/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ Contents:
1313
* [Strings](40_strings.md) – Examples on how to modify strings
1414
* [Control Flow](50_control_flow.md) – How to structure a program with conditionals and loops
1515
* [Methods](60_methods.md) – Introduction to methods, arguments and type restrictions
16+
* [Blocks](70_blocks.md) – Introduction to blocks

0 commit comments

Comments
 (0)