Entendendo Funções e Blocos
A idéia é poder criar funções "customizáveis", ou seja, um pedaço de código que espera outro pedaço de código. Existem duas formas de se fazer isso. No mundo C podemos passar diretamente uma função como parâmetro para ser executada dentro de outra função. Isso não é uma closure, é o que chamamos de "callback". Em Objective-C e Swift, podemos passar uma função como parâmetro ou mesmo fazer uma função retornar uma função.
1 2 3 4 5 6 7 8 9 10 11 |
func soma(x: Int, y: Int) -> Int { return x + y } func calculadora(calculo: (Int, Int) -> (Int), a: Int, b: Int) { let resultado = calculo(a, b) println(resultado) } calculadora(soma, 10, 20) // "30" |
Veja o código acima, definimos uma função de soma, que recebe dois inteiros como parâmetro e retorna um inteiro. Depois definimos uma função genérica chamada "calculadora" que recebe como parâmetro uma função com a assinatura (Int, Int) -> Int que significa "uma função que receba dois inteiros e retorne um inteiro" e depois dois parâmetros inteiros.
Ao executar calculadora(soma, 10, 20), passamos a função soma, os números 10 e 20 e internamente atribuímos a função soma a uma variável chamada "calculo" e executamos passando os dois inteiros, que, obviamente, serão somados. E a resposta no final será 30.
1 2 3 4 5 6 |
func multiplicacao(x: Int, y: Int) -> Int { return x * y } calculadora(multiplicacao, 3, 5) // "15" |
Podemos agora criar quaisquer funções com a mesma assinatura e depois mandar para a calculadora. Em Ruby não temos a mesma funcionalidade:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def soma(x, y) x + y end def calculadora(calculo, a, b) puts calculo(a, b) end calculadora(soma, 10, 20) # ArgumentError: wrong number of arguments (0 for 2) # from (irb):1:in `soma' # from (irb):9 # from /usr/bin/irb:12:in `<main>' |
Em Ruby, parênteses são opcionais e ao tentar passar o método "soma" como parâmetro, na verdade ele está já tentando executar o método. Existe uma forma, não ortodoxa, que podemos ter um efeito similar, mas não é a mesma coisa, seria assim:
1 2 3 4 5 |
def calculadora(calculo, a, b) puts send(calculo, a, b) end calculadora(:soma, 10, 20) |
O método send é uma das formas de se enviar mensagens a objetos (Objective-C também tem isso, na forma de seletores e do método performSelector que expliquei neste outro post). Então, em vez de passar diretamente o método, passamos apenas o nome dele como um symbol e internamente executamos o método passando os parâmetros. Isso é só "similar" porque na prática o método em si nunca foi passado como parâmetro.
O que podemos fazer em Ruby é não usar métodos, mas blocos:
1 2 3 4 5 6 7 8 9 |
soma = lambda do |x, y| x + y end def calculadora(calculo, a, b) puts calculo.(a, b) end calculadora(soma, 10, 20) |
Aqui a semântica é diferente. Primeiro criamos um bloco, literalmente o que seria o "corpo de um método" usando lambda. Depois passamos o bloco com parâmetro ao método calculadora. E dentro dela executamos o bloco com um "ponto" antes dos parênteses, que é a forma curta de se fazer soma.call(a, b)
Um bloco, em Ruby, é diferente de uma método ou função. Isso porque ele também é um fechamento do estado ao redor do bloco. Blocos não são métodos. Em Ruby, o método está associado ("binding") à classe que a define (mesmo sem definir um class, estamos dentro sempre dentro de um objeto, diferente de Swift ou Objective-C ou mesmo outra linguagem). Um bloco está associado à uma variável e por isso podemos mais facilmente repassá-la para outros métodos.
Em Swift também podemos devolver funções ou ter "Nested Functions", por exemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func calculo(tipo: String) -> (Int, Int) -> Int { func soma(x: Int, y: Int) -> Int { return x + y } func multiplicacao(x: Int, y: Int) -> Int { return x * y } if tipo == "soma" { return soma } else { return multiplicacao } } func calculadora(calculo: (Int, Int) -> Int, a: Int, b: Int) { println(calculo(a, b)) } calculadora(calculo("soma"), 10, 20) // 30 |
Em Ruby, o mais próximo, usando blocos, seria:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def calculo(tipo) soma = lambda { |x, y| x + y } multiplicacao = lambda { |x, y| x * y } if tipo == :soma soma else multiplicacao end end def calculadora(c, a, b) puts c.(a, b) end calculadora(calculo(:soma), 10, 20) # 30 |
Entendendo Blocos em Swift
Sabendo dessa base podemos prosseguir para o próximo passo, blocos em Swift.
Primeiro, vejamos o uso mais comum de blocos em Ruby:
1 2 3 4 5 6 7 8 |
def numero(bla) yield(bla) if block_given? end numero 20 do |x| x * 10 end # 200 |
Definimos um método chamado numero que recebe um parâmetro "bla". Internamente chamamos yield que pega o bloco passado como último parâmetro do método e repassa o parâmetro "bla" a ele. Fora, executamos o método frase, passando 20 como parâmetro e um bloco (delimitado por "do..end") que recebe uma variável x e apenas multiplica ela por 10.
Podemos reescrever o mesmo código da seguinte forma:
1 2 3 4 5 6 |
def numero(bla, &bloco) bloco.(bla) if bloco end numero(20) { |x| x * 10 } # 200 |
É exatamente o mesmo código mas agora o bloco está definido como parâmetro mais explicitamente. O "&" diz que vamos passar o bloco fora dos parênteses do método. Executamos o bloco dentro com o "ponto" (no lugar de "call", como explicamos antes). E ao executar o método, desta vez deixei os parênteses opcionais e no lugar de "do..end" usei "{}", que é a mesma coisa. Por convenção, em Ruby, usamos "{}" quando um bloco tem somente uma linha de implementação e usamos "do..end" quando tem múltiplas linhas.
Obs, o @josevalim me explicou que há outra sintaxe que podemos usar e são equivalentes (embora pareça que só funcione em one-lines):
1 2 |
numero.map { (var x: Int) -> Int in return x * 10 } numero.map { $0 * 10 } // equivalente ao de cima |
Confinuando, podemos fazer a mesma coisa em Swift, assim:
1 2 3 4 5 |
func numero(bla: Int, bloco: (Int) -> Int) { println(bloco(bla)) } numero(20, { (x: Int) -> Int in return x * 10 } ) |
Por causa da necessidade de definir o seletor/assinatura, com parâmetros e tipo de retorno, a execução da closure em Swift é bem mais verbosa do que em Ruby. A sintaxe é semelhante, usando chaves "{}" para delimitar o bloco, a assinatura para delimitar a função anônima e o corpo do bloco depois de "in". Na prática é quase a mesma coisa.
Do livro oficial da Apple temos o seguinte exemplo que pode demonstrar um pouco melhor (eu mudei o exemplo pois no livro ele usa um Dictionary para "digitNames" mas as chaves são exatamente a posição num Array, então achei melhor usar diretamente um Array):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let digitNames = [ "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" ] let numbers = [16, 58, 510] let strings = numbers.map { (var number) -> String in var output = "" while number > 0 { output = digitNames[number % 10] + output number /= 10 } return output } // strings is inferred to be of type String[] // its value is ["OneSix", "FiveEight", "FiveOneZero"] |
A mesma coisa em Ruby ficaria assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
digit_names = [ "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" ] numbers = [16, 58, 510] strings = numbers.map do |number| output = "" while number > 0 output = digit_names[number % 10] + output number = number / 10 end output end # ["OneSix", "FiveEight", "FiveOneZero"] |
Veja como a lógica em si é bastante semelhante, se ignorar a definição mais exata de tipos do Swift, os dois códigos são praticamente idênticos.
No meu post de 2010 sobre como implementar o equivalente a "method_missing" em Objective-C eu parti deste exemplo comum de DSL do mundo Ruby:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
require 'builder' x = Builder::XmlMarkup.new(:target => $stdout, :indent => 1) x.html do |h| h.body do |b| b.h1 "Hello World" b.p "This is a paragraph." b.table do |t| t.tr do |tr| tr.td "column" end end end end |
E cheguei neste equivalente em Objective-C:
1 2 3 4 5 6 7 8 9 10 11 12 |
XmlBuilder* xml = [[XmlBuilder alloc] init]; [xml htmlBlock:^(XmlBuilder* h) { [h bodyBlock:^(XmlBuilder* b) { [b h1:@"Hello World"]; [b p:@"This is a paragraph."]; [b tableBlock:^(XmlBuilder* t) { [t trBlock:^(XmlBuilder* tr) { [tr td:@"column"]; }]; }]; }]; }]; |
Absolutamente verborrágico! Não era divertido usar blocos em Objective-C pela quantidade de delimitadores com chaves, parênteses, colchetes. Em Ruby é bem mais simples porque parênteses são todos opcionais e blocos são delimitados quase como métodos.
Ainda não reimplementei esse experimento que fiz em Objective-C para Swift (fica como lição de casa). Farei isso num próximo artigo sobre metaprogramação e seletores em Swift. Mas se tivéssemos reescrito, provavelmente o código ficaria mais ou menos assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Swift: // Ruby: xml = XmlBuilder() // x = Builder::XmlMarkup.new xml.html({ (var h) -> Void in // x.html do |h| h.body({ (var b) -> Void in // h.body do |b| b.h1("Hello World") // b.h1 "Hello World" b.p("This is a paragraph") // b.p "This is a paragraph." b.table({ (var t) -> Void in // b.table do |t| t.tr({ (var tr) -> Void // t.tr do |tr| tr.td("column") // tr.td "column" }) // end }) // end }) // end }) // end |
Veja que comparado à versão em Objective-C é "muito" melhor. Mesmo assim, se comparado ao que fazemos em Ruby, continua sendo mais verboso do que gostaríamos por causa dos parênteses obrigatórios e declaração de tipos das funções, mas agora sim fica muito mais prático ver que podemos fazer DSLs em Swift também.
Isso deve dar uma luz sobre como a nova sintaxe do Swift é de fato um real ganho de legibilidade e produtividade para programadores acostumados a Objective-C e como nós, de Ruby, podemos rapidamente nos adaptar a essa nova linguagem para produzir bibliotecas e frameworks. Uma vantagem do Swift é que ele é imediatamente compatível com toda a API escrita em Objective-C, portanto onde antes era chato escrever as closures, agora fica imediatamente mais simples.