Um bloco é um pedaço de código que pode ser passado a um método. Noutras linguagens de programação, isto é conhecido como função anónima ou lambda. Um método pode converter um bloco num Proc, que representa um bloco de código.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
Os blocos e os Procs são muito usados em Crystal, por exemplo para iteração e callbacks. Mas também para criar DSLs (linguagens específicas de domínio); alguns exemplos de DSLs são o framework web Kemal ou o framework de testes Spec (que é o que se usa para testar as tuas soluções em Crystal).
Para criar um método que aceite um bloco, tens de acrescentar um e comercial (&) antes do último argumento.
Isto converte o bloco num Proc e atribui-o ao argumento.
Depois, usar yield executa o bloco que foi dado ao método.
def my_method(&block)
1 + yield
end
Há duas formas de passar um bloco: com as palavras-chave do ... end ou com chavetas ({}).
A principal diferença entre as duas é a precedência: o do ... end suporta blocos com várias linhas, enquanto as chavetas ({}) não.
my_method do
2
end
# => 3
my_method { 2 }
# => 3
O yield é uma palavra-chave que executa o bloco dado ao método.
Isto significa que o bloco nunca é executado se o yield não for chamado.
Também é possível dar-lhe argumentos, que serão passados ao bloco.
Não há limite para o número de argumentos que se podem passar ao bloco.
Para usares os argumentos no bloco, usas a sintaxe |x, y, ...|.
Aqui, x, y, ... são os nomes dos argumentos.
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
No exemplo acima, o bloco recebe o argumento 2, multiplica-o por 3 e devolve 6.
Depois, o método soma 1 ao resultado e devolve 7.
Se o número de argumentos dados ao yield for maior do que o número de argumentos que o bloco espera, é lançado um erro.
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
Os blocos podem ser escritos de forma explícita ou implícita. Os blocos implícitos são os mais comuns e são os que aparecem nos exemplos acima. Podes até dar ao bloco argumentos de tipos diferentes, e o compilador vai inferir os tipos e criar um tipo união.
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
Quando escreves blocos explícitos, podes especificar os tipos dos argumentos e o tipo devolvido.
Isto faz-se com a sintaxe Arg1Type, Arg2Type, ... -> ResultType.
Aqui, os argumentos são separados por vírgulas (,) e o tipo do resultado é separado por ->.
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
Tal como acontece com outros tipos explícitos, o compilador garante que os tipos são os esperados. Se o bloco não corresponder ao tipo dado ao método, é lançado um erro.
Existe uma sintaxe abreviada para blocos que recebem apenas um argumento e que só lhe chamam um método.
Em vez de usares |x|, podes usar & antes do nome do argumento.
def my_method(&block)
yield "a"
end
# This is:
my_method { |x| x.upcase }
# => "A"
# Is the same as:
my_method &.upcase
# => "A"
O Proc tem uma ligação muito forte com os blocos.
A diferença essencial é que um Proc é um tipo e um bloco não é.
Um Proc representa um ponteiro para função e pode ser passado como uma variável.
Para definires um Proc, usas a sintaxe literal ->(args) { ... }.
Os argumentos são separados por vírgulas (,) e o bloco é delimitado por { ... }.
Para invocares um Proc, usas o método call e passas-lhe os argumentos.
Os tipos dos parâmetros são obrigatórios.
O tipo devolvido é inferido a partir do corpo do proc, mas também pode ser indicado de forma explícita.
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
Capturar blocos é uma forma de criar um Proc a partir de um bloco.
Isto faz-se usando o operador & antes do último argumento de um método.
Isto converte o bloco num Proc e atribui-o ao argumento.
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Os Procs podem ser passados a métodos que esperam um bloco. Isto dá jeito quando queres passar blocos diferentes consoante uma condição.
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
Quando declaras uma variável, podes usar a sintaxe : Type para declarar o tipo da variável.
Para os procs, usas a sintaxe Proc((ArgType, ...), ResultType).
O último tipo é sempre o tipo devolvido e os restantes são os tipos dos argumentos.
Ou seja, se houver apenas um tipo, esse será o tipo devolvido e o proc não recebe argumentos.
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
Também podes declarar um proc a partir de um método.
def my_method(a, b)
a + b
end
my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)