Entendendo Programação Funcional com Stranger Things
Olá pessoa!
Que tal explorarmos um pouco sobre as teorias de programação funcional com Stranger Things? -E claro, sem spoilers da nova temporada.
Vamos falar sobre toda a teoria por trás de programação funcional? -Não, não vamos.
Vamos focar em alguns funções de alta ordem (return
, apply
, map
) e entender o conceito por trás delas. Todas essas funções tem uma coisa comum: elas lidam com values containers.
O que são values containers? -De forma bastante resumida, um value container é qualquer classe/struct/tipo que “envelopa” outro valor.
Alguns exemplos: listas, arrays, filas, pilhas e por aí vai, mas calma que não são só as coleções que fazem isso. Em C#, por exemplo, temos o Task<T>
e o Nullable<T>
, que não são coleções mas entrariam nesse mesmo conceito. Assim como o option
do F#.
O que isso tem a ver com Stranger Things? -Mais do que você imagina.
O Conceito por trás da Série
Na série Stranger Things existe um conceito chamado Upside down ou Mundo invertido, esse mundo funciona como uma versão paralela ao mundo real. No entanto, várias criaturas sinistras vivem por lá.
Na imagem abaixo podemos ver o conceito de forma mais explícita. Na parte de cima estão três dos personagens principais e podemos ver o último menino do grupo no Upside down (esse é o plot da primeira temporada).
Parte da trama da série se passa em entender como as criaturas e as pessoas transitam entre o mundo normal e o Upside down. E é aí que entram nossas funções de alta ordem!
Implementando o Upside Down
Se você precisasse modelar o Upside Down em uma classe ou tipo, como você faria?
Lembre-se que teoricamente, toda criatura do Upside Down pode vir para o mundo normal e qualquer humano do mundo normal pode ir para o Upside Down. Então ele precisa funcionar como um value container!
No mundo computacional precisamos lembrar que qualquer valor ou função do mundo normal, podem possuir um correspondente no Upside Down e vice-versa.
Normalmente este tipo de implementação requer um Computation Express em F#, mas para fins de simplificação, não vamos entrar nesse mérito (talvez no futuro).
Vamos começar modelando a classe em C# e o tipo em F#:
Agora que já temos o nosso tipo, precisamos de alguma maneira interagir com ele. Isso significa que precisamos de formas para sairmos do mundo normal e entrarmos no Upside down. E claro, mais importante ainda, formas de sairmos do Upside Down e voltarmos para o mundo normal, afinal, ninguém quer ficar preso por lá, certo?
Precisamos de um portal para cruzar os dois mundos.
Conceitos de Funções - Return
O primeiro conceito que vamos tratar aqui é chamado de return
. Não, não é o return
que você coloca no fim do método. Na verdade essa função pode receber outros nomes como: yield
e pure
, ou até, não ter um nome de função.
Em C#, por exemplo, você pode retirar um valor de uma lista acessando-o via Indexer: lista[indice]
. Mesmo nesses casos o conceito da função return
é aplicado.
Para nosso caso, vamos chamar a função return
de Portal. Precisamos de um portal para o mundo invertido e um portal para voltarmos ao mundo normal:
Para o código em C#, podemos fazer com que o próprio construtor seja um portal para o Upside Down, ou caso preferirmos, podemos manter o construtor privado e termos um método estático chamado Portal
para criar o objeto:
Para melhorar um pouco a usabilidade do código podemos criar métodos de extensão para interagir com o Upside Down, vamos começar com o método PortalToUpsideDown
:
Com essa implementação pronta, vale lembrar que o return
funciona tanto para valores simples quanto para funções, isso mesmo, podemos ter funções no Upside Down.
Mesmo em C# que não é tão focado em programação funcional podemos usar isso, inclusive com o método de extensão, veja:
Conceitos de Funções - Apply
A próxima função que precisamos é o apply
, diferente da função return
ela não funciona para valores simples, seu único foco são funções.
O que o apply
faz é basicamente quebrar uma função encapsulada no Upside down (UpsideDown< Func<X,Y> >
) em uma função do mundo normal onde os parâmetros estão encapsulados (Func< UpsideDown<X>,UpsideDown<Y> >
).
Nesse caso não temos nenhuma travessia entre os mundos, tudo se mantém no Upside Down, mas o que isso significa?
Na primeira temporada, um dos personagens fica preso no Upside Down consegue se comunicar com sua mãe através de luzes, nesse caso, simplesmente passar a função para o Upside Down não é suficiente, precisamos que ela afete o Upside Down e que produza resultados lá (ao invés de produzir resultados do mundo normal).
Essas são as luzes que a mãe do Will fez para realizar o apply
com seu filho perdido:
Agora vamos tentar implementar isso. Assim como fizemos antes, vamos enviar a função de mensagem através de nosso portal e tentar executá-la no Upside Down.
A função de mensagem através da luz pode simplesmente receber um parâmetro, escrevê-lo no console e retorná-lo. Mas mesmo em uma função assim simples, encontraremos um problema:
Uma vez que enviamos a função para o Upside Down não somos mais capazes de executá-la, isso porque não temos acesso direto a função. Além disso, como passamos a função para o Upside Down, não podemos esperar que ela receba por parâmetro um valor do mundo real, portanto precisamos que ela receba UpsideDown<"A">
ao invés de "A"
.
Por último, temos que lembrar que a função precisa afetar o Upside Down e não o mundo normal, logo, seu retorno também deve ser um valor no Upside Down.
O jeito mais simples de resolvermos isso, é remover o contexto dos valores e da função, dessa forma, podemos executar a função normalmente. Depois de executarmos a função podemos gerar o resultado no Upside Down enviando-o novamente através do portal.
Nesse caso específico a função em C# fica um pouco mais verbosa, pela necessidade de declaração dos tipos, veja o código abaixo:
Nesse caso é possível notar como em geral, C# e linguagens orientadas a objeto são bastante declarativas, enquanto linguagens funcionais tendem a ter um fluxo de continuídade maior. Isso não significa que um é melhor do que o outro, são apenas formas diferentes de fazer a mesma coisa.
Mas precisamos notar que, linguagens com currying automático e aplicação parcial acabam ganhando uma função apply
mais poderosa. Vamos fazer a implementação do método de mensagem de luz e depois veremos uma implementação que funcionará em F#, mas infelizmente não funcionará em C#.
Primeiro vamos à implementação da mensagem de luz com um parâmetro:
Em C# podemos fazer com que a função apply
além de estático também seja um método de instância do nosso objeto, podemos fazer da mesma forma da biblioteca Linq, utilizando os métodos de extensão:
Não temos métodos de extensão em F#, mas também há um ponto que pode soar mais prático (ou mais confuso). Trata-se da utilização de operadores para realizar o apply
. O operador mais comum para essa função é o <*>
, para definirmos e utilizarmos é bastante simples, veja:
Dessa forma basta inserir o operador entre a função e o parâmetro que tudo funcionará corretamente. Você pode argumentar que esse operador prejudica a visibilidade do código e eu tendo a concordar com isso. Mas uma vantagem inegável é o que comentei antes sobre funções com múltiplos parâmetros.
Imagine que ao invés da função de passar uma letra na mensagem de luzes precisamos enviar duas… Parece um problema super simples de resolver, mas vamos tentar solucionar isso usando C#.
Com o código que fizemos até agora é simplesmente impossível… O que podemos tentar fazer é utilizar expressões lambda para reduzir o parâmetro da função, criarmos novas sobrecargas para funções com múltiplos parâmetros e etc. Existem saídas, elas só precisariam ser implementadas.
No caso do F#, a linguagem já faz com que uma função com múltiplos parâmetros seja automaticamente reduzida múltiplas funções de um parâmetro, onde cada uma delas retorna uma nova função com menos parâmetros, se isso soa confuso para você acesse este post sobre Currying e Aplicação Parcial).
A vantagem de combinarmos isso com o operador é que podemos com o código que criamos anteriormente, transformar qualquer função com qualquer quantidade de parâmetro em uma função do Upside Down, veja:
Veja que tanto result
quanto result2
terão o mesmo valor, com a diferença de que um utiliza valores armazenados previamente e o último gera os valores no Upside Down inline.
Conceitos de Funções - Map
Vamos para a última e mais famosa função apresentada aqui. Se você está acostumado com qualquer linguagem de programação que suporte funções de alta ordem, provavelmente vai haver uma função map
. Com exceção do C#, que possui essa função, mas ela se chama Select
.
A função map
é responsável por fazer com que uma função do mundo normal, possa ser aplicada à um valor de um mundo diferente, como uma lista ou um valor do Upside Down.
Qual a diferença entre o map
e o apply
?
É bastante comum essas duas funções confundirem um pouco e parecerem a mesma, mas não são. Lembre-se do nosso exemplo, no caso do apply
estamos tentando reproduzir a função equivalente no Upside Down.
No caso das mensagens através de luzes, era preciso que as luzes existissem tanto mundo normal (string->string)
quanto no Upside Down UpsideDown<string -> string>
. No caso do map
não temos uma função equivalente no Upside Down, simplesmente queremos que uma função do mundo normal consiga afetar os objetos do Upside Down.
Isso seria como os Mike tentando se comunicar com o Will (enquanto ele estava preso no Upside Down através do walk talk. O que ele estava fazendo aqui é basicamente o funcionamento do map
. Utilizar-se de uma função do mundo normal e mapeá-la ao Upside Down.
Talvez você já tenha percebido, mas existem duas formas diferentes de implementarmos o map
. Na primeira implementação podemos retirar o parâmetro do Upside Down, aplicar a função normalmente e transformar o resultado em seu equivalente no Upside Down, veja:
Essa implementação funciona normalmente, mas além dela, também podemos reaproveitar as implementações de return
e apply
, afinal, eles combinados podem formar um map
.
Para fazer isso, basta transformarmos nossa função em seu equivalente no Upside Down através do return
e depois utilizarmos o apply
para resolvê-la:
No fim das contas não faz muita diferença sua implementação, o que vale ser lembrado aqui é que isso mostra que a combinação: return
+ apply
é mais poderosa do que o map
. Afinal, ela pode fazer coisas diferentes e emular um map
enquanto o contrário não é verdadeiro.
Uma coisa interessante para validar se a implementação do map
está correta é que podemos tirar a prova real. Uma função aplicada com um map
em um valor do Upside Down
deve produzir o mesmo valor que o resultado do return
da mesma função aplicada ao valor no mundo normal.
Assim como fizemos com o apply
, podemos ter a função map
nas formas de métodos de extensão (C#) e operador (F#):
Para finalizar, vamos implementar uma função que aumente o poder da Eleven!
Na verdade, vamos só incrementar um valor inteiro, mas podemos usar a imaginação aqui né?
Com o perdão do trocadilho, mas programação funcional as vezes pode soar um tanto quanto estranho, não acha?
Bom, o post de hoje era isso!
Espero que tenham gostado, qualquer dúvida, correção ou sugestão, deixem nos comentários!
E até mais.
Sempre vale lembrar que as informações e textos aqui no blog representam minha opinião pessoal, o que pode não ser igual à sua ou de qualquer outra pessoa, incluindo a empresa para qual eu trabalho. Portanto as publicações inseridas aqui estão relacionadas somente a mim.