Back-End

4 ago, 2017

Programação Funcional Pragmática

Publicidade

A mudança para a programação funcional começou, com seriedade, há uma década. Nós vimos linguagens como Scala, Clojure e F# começarem a chamar a atenção. Este movimento foi mais que apenas o entusiasmo normal “Ah, legal, uma nova linguagem!”. Havia algo real conduzindo aquilo – ou assim pensamos.

A lei de Moore nos disse que a velocidade dos computadores seria dobrada a cada 18 meses. Esta lei foi válida desde a década de 1960 até 2000. E então parou. Frio. As taxas de clock atingiram 3ghz e depois se estabilizaram. O limite da velocidade da luz foi alcançado. Os sinais não podiam se propagar na superfície do chip rápido o suficiente para permitir velocidades mais altas.

Assim, os designers de hardware mudaram sua estratégia. Para obter mais produtividade, eles adicionaram mais processadores (núcleos). A fim de abrir espaço para esses núcleos, eles removeram muito do hardware de cache e pipelining dos chips. Assim, os processadores ficaram um pouco mais lentos do que antes; mas havia mais deles. A produtividade aumentou.

Recebi minha primeira máquina dual core há 8 anos. Dois anos depois, eu consegui uma máquina de quatro núcleos. E a proliferação dos núcleos começou. E todos nós entendemos que isso impactaria o desenvolvimento de software de maneiras que não poderíamos imaginar.

Uma das nossas respostas foi aprender Programação Funcional (PF). PF desencoraja fortemente a alteração do estado de uma variável, uma vez inicializado. Isso tem um efeito profundo sobre a concorrência. Se você não pode alterar o estado de uma variável, não pode ter uma condição de corrida. Se você não pode atualizar o valor de uma variável, você não pode ter um problema de atualização simultânea.

Isso, é claro, foi pensado para ser a solução para o problema multi-core. À medida que os núcleos proliferavam, simultaneidade, NAY!, a simultaneidade se tornaria um problema significativo. A PF deve fornecer o estilo de programação que mitigue os problemas de lidar com 1024 núcleos em um único processador.

Então todos começaram a aprender Clojure, ou Scala, ou F#, ou Haskell; porque sabiam que o trem de carga estava nas pistas em direção a eles, e eles queriam estar preparados quando ele chegasse.

Mas o trem de carga nunca chegou. Há seis anos consegui um laptop de quatro núcleos. Eu já tive mais dois desde então. O próximo laptop que vou receber parece que também será um laptop de quatro núcleos. Estamos vendo outro platô?

Como um aparte, eu assisti a um filme de 2007 na noite passada. A heroína estava usando um laptop, visualizando páginas em um navegador elegante, usando o google e recebendo mensagens de texto em seu celular. Tudo era muito familiar. Ah, estava datado – eu podia ver que o laptop era um modelo antigo, que o navegador era uma versão mais antiga e que o celular estava muito longe dos telefones inteligentes de hoje. Ainda assim – a mudança não foi tão dramática quanto a mudança de 2000 a 2011 teria sido. E não tão dramática quanto a mudança de 1990 a 2000 teria sido. Estamos vendo um platô na taxa de tecnologia de computador e software?

 

Então, talvez, PF não é uma habilidade tão crítica como pensamos. Talvez não estejamos inundados com núcleos. Talvez não precisemos nos preocupar com chips com 32.768 núcleos neles. Talvez possamos todos relaxar e voltar a atualizar nossas variáveis novamente.

Eu acho que seria um erro. Um dos grandes. Eu acho que seria um erro tão grande quanto o uso desenfreado de goto. Eu acho que seria tão perigoso quanto abandonar o envio dinâmico.

Por quê? Podemos começar com a razão pela qual nos interessamos em primeiro lugar. PF torna a concorrência muito mais segura. Se você estiver construindo um sistema com muitos threads ou processos, o uso da PF reduzirá fortemente os problemas que você pode ter com condições de corrida e atualizações simultâneas.

Por que mais? Bem, PF é mais fácil de escrever, mais fácil de ler, mais fácil de testar e mais fácil de entender. Agora eu imagino que alguns de vocês estão agitando suas mãos e gritando para a tela. Você tentou PF e você achou tudo, menos que foi fácil. Todos esses mapas e reduções e toda a recursão – especialmente a recursão da cauda são tudo, menos fáceis. Claro. Entendi. Mas isso é apenas um problema de familiaridade. Uma vez que você está familiarizado com esses conceitos – e não demora muito para desenvolver essa familiaridade – a programação torna-se muito mais fácil.

Por que torna-se mais fácil? Porque você não precisa acompanhar o estado do sistema. O estado das variáveis ​​não pode mudar; então, o estado do sistema permanece inalterado. E não é apenas o sistema que você não precisa acompanhar. Você não precisa acompanhar o estado de uma lista, ou o estado de um conjunto, ou o estado de uma pilha ou uma fila; porque essas estruturas de dados não podem ser alteradas. Quando você empurra um elemento para uma pilha em uma linguagem de PF, você obtém uma nova pilha, você não altera a antiga. Isso significa que o programador deve fazer malabarismos com menos bolas no ar ao mesmo tempo. Há menos para lembrar. Menos para acompanhar. E, portanto, o código é muito mais simples de escrever, ler, entender e testar.

Então, qual linguagem de PF você deve usar? Meu favorito é Clojure. A razão é que Clojure é absurdamente simples. É um dialeto da Lisp, que é uma linguagem simples e maravilhosa. Aqui, deixe-me mostrar-lhe.

Aqui está uma função em Java: f (x);

Agora, para transformar isso em uma função em Lisp, você simplesmente move o primeiro parêntese para a esquerda: (f x).

Agora você conhece 95% de Lisp, e você conhece 90% de Clojure. Essa sintaxe de parênteses pequena e boba é realmente quase toda a sintaxe que existe nessas linguagens. Elas são absurdamente simples.

Agora eu sei, talvez já tenha visto programas Lisp e você não gosta de todos aqueles parênteses. E talvez você não goste de CAR e CDR e CADR, etc. Não se preocupe. Clojure tem um pouco mais de pontuação do que Lisp, então há menos parênteses. Clojure também substituiu CAR e CDR e CADR com firt e rest e second. Além disso, o Clojure é construído na JVM e permite o acesso completo à biblioteca Java e a qualquer outro framework Java ou biblioteca que você queira usar. A interoperabilidade é rápida e fácil. E, melhor ainda, o Clojure permite o acesso total aos recursos OO da JVM.

“Mas espere!” Eu ouço você dizer. “PF e OO são mutuamente incompatíveis!” Quem te disse isso? Isso não faz sentido! Ah, é verdade que na PF você não pode alterar o estado de um objeto; mas e daí? Assim como empurrar um número inteiro para uma pilha dá a você uma nova pilha, quando você chama um método que ajusta o valor de um objeto, você recebe um novo objeto em vez de mudar o antigo. Isso é muito fácil de lidar, uma vez que você se acostuma.

Mas de volta a OO. Um dos recursos da OO que eu acho mais útil, no nível da arquitetura de software, é o polimorfismo dinâmico. E Clojure fornece acesso completo ao polimorfismo dinâmico de Java. Talvez um exemplo explique melhor isso.

(defprotocol Gateway
  (get-internal-episodes [this])
  (get-public-episodes [this]))

O código acima define uma interface polimórfica para a JVM. Em Java, essa interface seria assim:

public interface Gateway {
	List<Episode> getInternalEpisodes();
	List<Episode> getPublicEpisodes();
}

No nível da JVM, o código de byte produzido é idêntico. Na verdade, um programa escrito em Java implementaria a interface, como se estivesse escrito em Java. Do mesmo jeito, um programa Clojure pode implementar uma interface Java. Em Clojure que se parece com isto:

(deftype Gateway-imp [db]
  Gateway
  (get-internal-episodes [this]
    (internal-episodes db))

  (get-public-episodes [this]
    (public-episodes db)))

Observe o argumento do construtor db e como todos os métodos podem acessá-lo. Neste caso, as implementações da interface simplesmente delegam em algumas funções locais, passando o db junto.

O melhor de tudo, talvez, seja o fato de que Lisp e, portanto, Clojure, seja (esperam por isso) o Homoicônico, o que significa que o código é um dado que o programa pode manipular. Isso é fácil de ver. O seguinte código: (1 2 3) representa uma lista de três números inteiros. Se o primeiro elemento de uma lista for uma função, como em: (f 2 3), torna-se uma chamada de função. Assim, todas as chamadas de função em Clojure são listas; e as listas podem ser manipuladas diretamente pelo código. Assim, um programa pode construir e executar outros programas.

A linha inferior é essa. A programação funcional é importante. Você deve aprender. E se você está se perguntando sobre qual linguagem usar para aprender, sugiro Clojure.

 

***

Uncle Bob faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://blog.cleancoder.com/uncle-bob/2017/07/11/PragmaticFunctionalProgramming.html