PageRenderTime 57ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/cap06-funcoes-alta-ordem/cap06-part05.md

https://github.com/taylorrf/learnhaskell
Markdown | 157 lines | 133 code | 24 blank | 0 comment | 0 complexity | f8f353dec21912e607b90a6bd6647990 MD5 | raw file
  1. Somente dobras e cavalos
  2. ========================
  3. Anteriormente quando estávamos lidando com recursividade, notamos um tema em comum em várias
  4. funções recursivas que trabalham em listas. Normalmente temos um caso extremo para a lista vazia.
  5. Introduziremos o padrão [code]x:xs[/code] e então faremos algo que envolverá um único elemento e o
  6. resto da lista. Este padrão é muito comum, por isso serão introduzidas algumas funções bastante úteis
  7. para o entendermos melhor. Estas funções são chamadas de
  8. <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)" title="_blank">"folds"</a>
  9. (livremente traduzidas aqui como <i>dobras</i>). Elas são como a função [code]map[/code], que reduzem
  10. a lista em um valor único.
  11. Uma "fold" recebe uma função binária, com um valor inicial (gosto de chamá-lo de acumulado) e uma
  12. lista que será dobrada em diversas etapas até se tornar uma dobra única. A função binária tem dois
  13. parâmetros. A função binária é chamada com o valor acumulado e o primeiro (ou último) elemento e produz
  14. um novo acumulador. Então a função binária é chamada novamente com o novo valor acumulado e agora com o
  15. novo primeiro (ou último) elemento, e assim por diante. Uma vez percorrida toda a lista apenas o valor
  16. acumulado permanece, que é o que sobra da lista que reduzimos.
  17. Primeiro vamos dar uma olhada na função [function]foldl[/function], também chamada de "left fold"
  18. (dobra esquerda). Isto dobra a lista a partir do lado esquerdo. A função binária é aplicada entre o
  19. valor inicial e a <i>cabeça</i> da lista. Isto produzirá um novo valor acumulado e a função binária
  20. será chamada com este valor e o próximo elemento, etc.
  21. Vamos implementar novamente o [code]sum[/code], dessa vez, utilizando o fold no lugar da recursão.
  22. Testando, um dois três:
  23. Vamos dar uma olhada melhor em como esta "dobra" funciona. [code]\acc x -&gt; acc + x[/code] é a
  24. função binária. [code]0[/code] é o valor inicial e [code]xs[/code] é a lista a ser dobrada.
  25. Primeiramente, [code]0[/code] é usado sendo o parâmetro [code]acc[/code] na função binária e
  26. [code]3[/code] é usado como sendo o parâmetro [code]x[/code] (ou o elemento atual).[code]0 + 3[/code]
  27. produzirá um [code]3[/code] e será o novo valor do acumulador, como foi dito. Logo depois,
  28. [code]3[/code] será usado como sendo o valor acumulado e [code]5[/code] como sendo o elemento atual e
  29. [code]8[/code] passa a ser o novo valor acumulado. Indo adiante, [code]8[/code] é o novo valor acumulado,
  30. [code]2[/code] é o elemento atual, então o valor do novo elemento acumulado será [code]10[/code].
  31. Finalmente, aquele [code]10[/code] é utilizado como o valor acumulado e [code]1[/code] é o elemento
  32. atual, produzindo um [code]11[/code]. Parabéns, você fez uma dobra!
  33. Este diagrama profissional a sua esquerda ilustra como a dobra acontece, passo a passo
  34. (a cada momento!). O número verde escuro é o valor acumulado. Você pode ver como a lista é ordenada e
  35. consumida de cima a partir da esquerda pelo acumulador. Aeee ae ae ae! Se levarmos em conta que aquela
  36. função é curried, poderemos escrever esta implementação mais sucintamente dessa maneira:
  37. A função lambda [code](\acc x -&gt; acc + x)[/code] é o mesmo que [code](+)[/code]. Nós podemos omitir
  38. que o [code]xs[/code] é um parâmetro porque chamar [code]foldl (+) 0[/code] irá retornar uma função
  39. com uma lista. Geralmente, se você tem uma função como [code]foo a = bar b a[/code], você poderá
  40. reescreve-la como [code]foo = bar b[/code], por causa do currying.
  41. Vamos implementar outra função com a dobra esquerda e depois com dobras a direita. Tenho certeza
  42. que todos vocês sabem que aquele [code]elem[/code] verifica qualquer valor que seja parte de
  43. uma lista, então não vou introduzir isto novamente (epa, falei!). Vamos implementar isto com a
  44. dobra esquerda.
  45. Bem, bem, bem, o que fizemos aqui? O valor inicial e o acumulado aqui são um valor booleano. O tipo
  46. do valor acumulado e o resultado final são sempre os mesmos quando lidamos com dobras. Lembre-se
  47. disso se você ainda não sacou como é usado o valor inicial, isto pode te dar uma idéia. Começamos
  48. com [code]False[/code]. Isto faz sentido se usarmos [code]False[/code] como valor inicial. Nós
  49. assumimos que ele não esta . Outra coisa, se chamarmos uma dobra em uma lista vazia, o resultado
  50. será somente o valor inicial. Então vamos verificar se o elemento inicial é o elemento que estamos
  51. procurando. Se for, setamos o acumulado como [code]True[/code]. Se não, nós simplesmente não mudamos
  52. o valor acumulado. Se ele for [code]False[/code] permanecerá assim porque o elemento atual não é isso.
  53. Se ele for [code]True[/code], saímos por .
  54. A dobra a direita [function]foldr[/function], funciona de forma similar a dobra esquerda, que o
  55. acumulador começa a consumir os valores a partir da direita. Além disso, funções binárias com dobra
  56. esquerda tem o seu acumulador como o primeiro parâmetro e o valor atual sendo o segundo
  57. (como [code]\acc x -&gt; ...[/code]), as funções binárias com dobra direita tem como valor atual o
  58. primeiro parâmetro e o acumulador o segundo (como [code]\x acc -&gt; ...[/code]). Isto fará sentido
  59. se essa dobra direita tiver o acumulador na direita, porque isso irá dobrar a partir do lado direito.
  60. O valor acumulado (e por isso, o resultado) da dobra pode ter qualquer tipo. Ele pode ser um número,
  61. um booleano ou também uma nova lista. Vamos implementar a função map com a dobra a direita. O acumulador
  62. deverá ser uma lista, que iremos acumulando e mapeando a lista elemento por elemento.
  63. Se nós mapearmos [code](+3)[/code] em [code][1,2,3][/code], acessaremos a lista a partir do lado
  64. direito. Pegamos o último elemento, que é [code]3[/code] e aplicamos a função nele, que no final
  65. será [code]6[/code]. Então, nós acrescentamos isto no valor acumulado, que será [code][][/code].
  66. [code]6:[][/code] é [code][6][/code] e é agora o valor acumulado. Nós aplicamos [code](+3)[/code] ao
  67. [code]2[/code], que será [code]5[/code] e acrescentamos ([code]:[/code]) isto ao acumulado, então o
  68. valor acumulado será agora [code][5,6][/code]. Aplicamos [code](+3)[/code] ao [code]1[/code] e
  69. acrescentamos ele ao acumulado e então o valor final será [code][4,5,6][/code].
  70. É claro, vamos querer implementar esta função com a dobra a esquerda também. Isto poderá ser
  71. [code]map' f xs = foldl (\acc x -&gt; acc ++ [f x]) [] xs[/code], porém a idéia é que aquela função
  72. [code]++[/code] seja muito mais custosa do que [code]:[/code], então normalmente utilizamos dobras a
  73. direita quando nós queremos construir novas listas a partir de uma lista.
  74. Se você reverter a lista, você poderá fazer a dobra direita do mesmo modo que fazemos a dobra esquerda
  75. e vice-versa. Algumas vezes você não tem como fazer isso. A função [code]sum[/code] pode ser
  76. implementada muito bem do mesmo jeito com uma dobra esquerda e direita. A grande diferença é que a
  77. dobra direita funciona em listas infinitas, ao contrario da esquerda que não! Para esclarecer melhor,
  78. se você pegar uma lista infinita a partir de um ponto e você dobrá-la a partir da direita, você irá
  79. eventualmente descobrir o início da lista. Entretanto, se você quiser pegar uma lista infinita a partir
  80. de um ponto e tentar dobrá-la a partir da esquerda, você nunca chegará no fim!
  81. <em>Dobras podem ser usadas para implementar qualquer função onde você percorre uma lista uma única
  82. vez, elemento por elemento, e então retorna algo baseado nisso. Sempre que você quiser percorrer uma
  83. lista para retornar alguma coisa, é provável que você queira uma dobra.</em> Por isso que dobras vem com mapas
  84. e filtros, um dos tipos mais úteis de funções na programação funcional.
  85. As funções [function]foldl1[/code] e [function]foldr1[/code] funcionam da mesma forma que
  86. [code]foldl[/code] e [code]foldr[/code], que você não precisa informar explicitamente qual o
  87. valor inicial. Ela assume que o primeiro (ou último) elemento da lista é o valor inicial da dobra
  88. e então inicia a dobra com ele. Com isto em mente, a função [code]sum[/code] pode implementar algo
  89. como: [code]sum = foldl1 (+)[/code]. Como eles dependem de pelo menos um elemento para dobrar a
  90. lista, isto causará um <i>runtime error</i> caso seja chamado com uma lista vazia. Por outro lado,
  91. [code]foldl[/code] e [code]foldr[/code] trabalham bem com listas vazias. Quando criar uma dobra,
  92. pense sobre como isso irá se comportar com uma lista vazia. Se a função não fizer sentido quando
  93. tiver uma lista vazia, você provavelmente usará [code]foldl1[/code] ou [code]foldr1[/code] para
  94. implementar isso.
  95. para te mostrar como dobras são poderosas, vamos implementar algumas funções da biblioteca
  96. padrão utilizando dobras:
  97. [code]head[/code] e [code]last[/code] são implementadas melhor pelo uso de pattern matching,
  98. mas para ver, você pode obte-los através do uso de dobras. Nossa definição de [code]reverse'[/code]
  99. é bastante clara, eu acho. Nós temos um valor inicial de uma lista vazia e então acessamos nossa
  100. lista a partir da esquerda e vamos adicionamos ao nosso valor acumulado. No final, nós construímos
  101. uma lista reversa. [code]\acc x -&gt; x : acc[/code] assemelhasse a função [code]:[/code], somente
  102. os parâmetros são movimentados. Este é o porque que poderíamos ter escrito nosso reverse como
  103. [code]foldl (flip (:)) [][/code].
  104. Outro jeito para esboçar a dobra direita e a esquerda é algo como: digamos que temos uma dobra
  105. direita e uma função binária [code]f[/code] com um valor inicial [code]z[/code]. Se formos dobrar
  106. a lista [code][3,4,5,6][/code], essencialmente faremos isso: [code] f 3 (f 4 (f 5 (f 6 z)))[/code].
  107. [code]f[/code] é chamado com o último elemento da lista e o valor acumulado, este valor é dado ao
  108. valor acumulado para o próximo último valor e assim por diante. Se nós fizermos o [code]f[/code]
  109. ser [code]+[/code] e o valor acumulado inicial ser [code]0[/code], aquilo ficará
  110. [code] 3 + ( 4 + ( 5 + (6 +0 ) ) ) [/code]. Ou se nós colocarmos o [code]+[/code] como uma função
  111. prefixo, aquilo ficaria então [code] (+) 3 ((+) 4 ( (+) 5 ( (+) 6 0) ) )[/code]. De forma similar,
  112. fazemos a dobra esquerda com uma lista [code]g[/code] com [code]flip (:)[/code] sendo a função binária
  113. e [code][][/code] o valor acumulado (então reverteríamos a lista), isto seria o equivalente a
  114. [code]flip (:) (flip (:) (flip (:) (flip (:) [] 3) 4) 5) 6[/code]. E certamente, se executarmos
  115. esta expressão, teríamos [code][6,5,4,3][/code].
  116. [function]scanl[/function] e [function]scanr[/function] são como [code]foldl[/code] e
  117. [code]foldr[/code], eles informam todos os estados intermediários do valor acumulado na forma de
  118. uma lista. também o [code]scanl1[/code] e [code]scanr1[/code], que são idênticos ao
  119. [code]foldl1[/code] e [code]foldr1[/code].
  120. Quando usamos o [code]scanl[/code], o resultado final estará no último elemento da lista resultante
  121. enquanto o [code]scanr[/code] irá colocar o resultado no primeiro.
  122. Scans são usadas para monitorar a progressão de uma função que pode ser implementada como uma dobra.
  123. Vamos responder essa nossa questão: <em>Quantos elementos precisamos ter para somar a raiz de todos
  124. os números naturais que excedem 1000?</em>. Para obter o quadrado de todos os números naturais, nós
  125. temos que fazer [code]map sqrt [1..][/code]. Agora, para ter a soma, nós temos que fazer uma dobra,
  126. mas como nós temos interesse em como a soma irá progredir, faremos usando o scan. Uma vez tendo
  127. feito o scan, podemos ver quantas somas estão sob 1000. A primeira soma no resultado do scanlist
  128. será normalmente 1. O segundo será 1 mais a raiz quadrada de 2. O terceiro será a soma da raiz
  129. quadrada de 3. Se estas X somas estiverem abaixo de 1000, então somamos X+1 dos que excederem 1000.
  130. Usamos aqui [code]takeWhile[/code] no lugar de [code]filter[/code] porque [code]filter[/code]
  131. não trabalha com listas infinitas. Apesar de sabermos que a lista é ascendente, [code]filter[/code]
  132. não faz assim, então usamos [code]takeWhile[/code] para terminar o scanlist na primeira ocorrência da
  133. soma maior do que 1000.