Eu estava lendo um artigo sobre programação funcional em que o escritor afirma
(take 25 (squares-of (integers)))
Observe que ele não possui variáveis. De fato, ele tem nada mais que três funções e uma constante. Tente escrever os quadrados dos números inteiros em Java sem usar uma variável. Ah, provavelmente existe uma maneira de fazer isso, mas certamente não é natural e não seria tão bom quanto o meu programa acima.
É possível conseguir isso em Java? Supondo que você precise imprimir os quadrados dos primeiros 15 números inteiros, você poderia escrever um loop for ou while sem usar variáveis?
Aviso de modificação
Esta questão não é um concurso de código de golfe. Estamos procurando respostas que expliquem os conceitos envolvidos (idealmente sem repetir respostas anteriores), e não apenas para mais um trecho de código.
fonte
squaresOf(integers()).take(25)
(a gravação dessas funções é deixada como um exercício para o leitor; a dificuldade está no conjunto infinito deintegers()
, mas isso é um problema para o Java por causa de sua avaliação ansiosa, nada a ver com variáveis)Respostas:
É possível implementar esse exemplo em Java sem usar atualizações destrutivas? Sim. No entanto, como @Thiton e o próprio artigo mencionaram, será feio (dependendo do gosto de alguém). Uma maneira é usar a recursão; Aqui está um exemplo de Haskell que faz algo semelhante:
Nota 1) a falta de mutação, 2) o uso de recursão e 3) a falta de loops. O último ponto é muito importante - as linguagens funcionais não precisam de construções em loop embutidas na linguagem, pois a recursão pode ser usada para a maioria (todos?) Dos casos em que loops são usados em Java. Aqui está uma série bem conhecida de documentos mostrando como podem ser chamadas de funções incrivelmente expressivas.
Achei o artigo insatisfatório e gostaria de fazer alguns pontos adicionais:
Esse artigo é uma explicação muito pobre e confusa da programação funcional e de seus benefícios. Eu recomendaria fortemente outras fontes para aprender sobre programação funcional.
A parte mais confusa do artigo é que ele não menciona que há dois usos para instruções de atribuição em Java (e na maioria das outras linguagens principais):
vinculando um valor a um nome:
final int MAX_SIZE = 100;
atualização destrutiva:
int a = 3; a += 1; a++;
A programação funcional evita o segundo, mas abrange o primeiro (exemplos:
let
expressões, parâmetros de função,define
itens de nível superior ) . Este é um ponto muito importante a entender, porque, caso contrário, o artigo parece bobo e pode fazer você se perguntar, o que sãotake
esquares-of
,integers
se não, variáveis?Além disso, o exemplo não tem sentido. Ele não mostra as implementações
take
,squares-of
ouintegers
. Pelo que sabemos, eles são implementados usando variáveis mutáveis. Como o @Martin disse, você pode escrever este exemplo trivialmente em Java.Mais uma vez, eu recomendaria evitar este artigo e outros, se você realmente quiser aprender sobre programação funcional. Parece ter sido escrito mais com o objetivo de chocar e ofender, em vez de ensinar conceitos e fundamentos. Em vez disso, por que não conferir um dos meus papéis favoritos de todos os tempos , de John Hughes. Hughes tenta abordar alguns dos mesmos problemas que o artigo abordou (embora Hughes não fale sobre concorrência / paralelismo); aqui está um teaser:
fonte
take 25 (map (^2) [1..])
como exemplo?[1..]
? Esse é um recurso interessante incorporado ao Haskell, mas não ilustra os conceitos por trás da geração dessa lista. Tenho certeza de que instâncias daEnum
classe (que essa sintaxe exige) também são úteis, mas eram preguiçosas demais para encontrá-las. Assimunfoldr
,. :)Você não faria. As variáveis estão no cerne da programação imperativa, e se você tentar programar imperativamente sem usar variáveis, estará apenas causando a todos um problema. Em diferentes paradigmas de programação, os estilos são diferentes e diferentes conceitos formam sua base. Uma variável em Java, quando usada bem com um escopo pequeno, não é má. Pedir um programa Java sem variáveis é como pedir um programa Haskell sem funções, para que você não o solicite e não se deixe enganar por ver a programação imperativa como inferior porque ela usa variáveis.
Então, a maneira Java seria:
e não se deixe enganar por escrevê-lo de maneira mais complexa devido a um ódio por variáveis.
fonte
O mais simples que posso fazer com a recursão é uma função com um parâmetro. Não é muito Java-esque, mas funciona:
fonte
No seu exemplo funcional, não vemos como as funções
squares-of
etake
são implementadas. Não sou especialista em Java, mas tenho certeza de que poderíamos escrever essas funções para ativar uma declaração como esta ...o que não é tão diferente.
fonte
squares-of
não é um nome válido em Java (squares_of
embora). Mas, caso contrário, um bom argumento mostra que o exemplo do artigo é ruim.integer
gere preguiçosamente números inteiros e atake
função escolhe 25 dossquared-of
númerosinteger
. Em resumo, você deve ter umainteger
função para produzir preguiçosamente números inteiros até o infinito.(integer)
uma função - uma função ainda é algo que mapeia um argumento para um valor. Acontece que isso(integer)
não é uma função, mas apenas um valor. Pode-se até dizer queinteger
é uma variável vinculada a uma infinita quantidade de números.Em Java, você pode fazer isso (especialmente a parte da lista infinita) com iteradores. No exemplo de código a seguir, o número fornecido ao
Take
construtor pode ser arbitrariamente alto.Ou com métodos de fábrica encadeáveis:
Onde
SquaresOf
,Take
eIntegers
estenderNumbers
fonte
while (test.hasNext()) System.out.println(test.next())
seriam um não-não no FP; iteradores são inerentemente mutáveis.Versão curta:
Para fazer com que o estilo de atribuição única funcione de maneira confiável em Java, você precisa (1) de algum tipo de infraestrutura amigável para imutáveis e (2) suporte no nível do compilador ou do tempo de execução para eliminação da chamada de cauda.
Podemos escrever grande parte da infraestrutura e organizar coisas para evitar o preenchimento da pilha. Mas, desde que cada chamada ocupe um quadro de pilha, haverá um limite para quanta recursão você pode fazer. Mantenha seus iterables pequenos e / ou preguiçosos, e você não deve ter grandes problemas. Pelo menos a maioria dos problemas com que você se depara não exige o retorno de um milhão de resultados de uma só vez. :)
Observe também que, como o programa precisa efetivamente fazer alterações visíveis para valer a pena ser executado, você não pode tornar tudo imutável. Você pode, no entanto, manter imutável a grande maioria de suas próprias coisas, usando um pequeno subconjunto de mutáveis essenciais (fluxos, por exemplo) apenas em determinados pontos-chave em que as alternativas seriam muito onerosas.
Versão longa:
Simplificando, um programa Java não pode evitar totalmente variáveis se quiser fazer algo que valha a pena fazer. Você pode contê- los e, assim, restringir a mutabilidade em um grande grau, mas o próprio design da linguagem e da API - junto com a necessidade de eventualmente alterar o sistema subjacente - inviabiliza a total imutabilidade.
Java foi projetado desde o início como um imperativo , orientado a objeto linguagem.
while (true)
efor (;;)
! - são totalmente dependentes de uma variável em algum lugar que muda de iteração para iteração.O resultado final dessas decisões de design é que, sem variáveis mutáveis, o Java não tem como alterar o estado de qualquer coisa - mesmo algo tão simples quanto imprimir "Olá, mundo!" para a tela envolve um fluxo de saída, que envolve a aderência de bytes em um buffer mutável .
Portanto, para todos os fins práticos, estamos limitados a banir as variáveis do nosso próprio código. OK, podemos meio que fazer isso. Quase. Basicamente, o que precisamos é substituir quase toda iteração por recursão, e todas as mutações por chamadas recursivas retornando o valor alterado. igual a...
Basicamente, criamos uma lista vinculada, onde cada nó é uma lista em si. Cada lista possui uma "cabeça" (o valor atual) e uma "cauda" (a sub-lista restante). A maioria das linguagens funcionais faz algo semelhante a isso, porque é muito passível de imutabilidade eficiente. Uma operação "next" apenas retorna a cauda, que normalmente é passada para o próximo nível em uma pilha de chamadas recursivas.
Agora, esta é uma versão extremamente simplificada desse material. Mas é bom o suficiente para demonstrar um problema sério com essa abordagem em Java. Considere este código:
Embora precisemos apenas de 25 polegadas para o resultado,
squares_of
não sabemos disso. Ele retornará o quadrado de cada número emintegers
. A recursão de 20 milhões de níveis de profundidade causa grandes problemas em Java.Veja, os idiomas funcionais em que você costuma fazer maluquices como essa têm um recurso chamado "eliminação de chamada de cauda". O que isso significa é que, quando o compilador vê o último ato do código sendo chamado (e retorna o resultado se a função não for nula), ele usa o quadro de pilha da chamada atual em vez de configurar um novo e faz um "salto" de uma "chamada" (para que o espaço de pilha usado permaneça constante). Em resumo, ele percorre cerca de 90% do caminho para transformar a recursão da cauda em iteração. Ele poderia lidar com esses bilhões de ints sem sobrecarregar a pilha. (Ele ainda acabou ficando sem memória, mas montar uma lista de bilhões de ints vai atrapalhar sua memória de qualquer maneira em um sistema de 32 bits.)
Java não faz isso, na maioria dos casos. (Depende do compilador e do tempo de execução, mas a implementação da Oracle não faz isso.) Cada chamada para uma função recursiva consome a memória de um quadro de pilha. Use muito e você terá um estouro de pilha. Transbordar a pilha quase garante a morte do programa. Portanto, temos que ter certeza de não fazer isso.
Uma semi-solução alternativa ... avaliação preguiçosa. Ainda temos as limitações da pilha, mas elas podem estar vinculadas a fatores sobre os quais temos mais controle. Não precisamos calcular um milhão de ints apenas para retornar 25. :)
Então, vamos construir uma infraestrutura de avaliação lenta. (Este código foi testado há algum tempo, mas eu o modifiquei bastante desde então; leia a ideia, não os erros de sintaxe. :))
(Lembre-se de que, se isso fosse realmente viável em Java, o código pelo menos um pouco como o acima já faria parte da API.)
Agora, com uma infraestrutura instalada, é bastante trivial escrever código que não precise de variáveis mutáveis e seja pelo menos estável para quantidades menores de entrada.
Isso funciona principalmente, mas ainda é um pouco propenso a estouros de pilha. Tentando
take
2 bilhões de polegadas e realizando alguma ação neles. : P Eventualmente, lançará uma exceção, pelo menos até 64 GB de RAM se tornar padrão. O problema é que a quantidade de memória de um programa reservada para sua pilha não é tão grande. É tipicamente entre 1 e 8 MiB. (Você pode pedir por mais, mas não importa o quanto pedir - você telefonatake(1000000000, someInfiniteSequence)
, você terá uma exceção.) Felizmente, com uma avaliação preguiçosa, o ponto fraco está em uma área que podemos controlar melhor . Nós apenas temos que ter cuidado com o quanto nóstake()
.Ainda haverá muitos problemas de expansão, porque o uso de nossa pilha aumenta linearmente. Cada chamada lida com um elemento e passa o restante para outra chamada. Agora que penso nisso, porém, há um truque que podemos adotar que pode ganhar um pouco mais de espaço: transformar a cadeia de chamadas em uma árvore de chamadas. Considere algo mais parecido com isto:
workWith
basicamente divide o trabalho em duas partes e atribui cada metade a outra chamada para si mesma. Como cada chamada reduz o tamanho da lista de trabalho pela metade e não por uma, isso deve ser escalado logaritmicamente e não linearmente.O problema é que essa função deseja uma entrada - e com uma lista vinculada, obter o comprimento requer percorrer a lista inteira. Isso é facilmente resolvido; simplesmente não se importa com quantas entradas existem. :) O código acima funcionaria com algo como
Integer.MAX_VALUE
a contagem, pois um nulo interrompe o processamento de qualquer maneira. Como a contagem é maior, temos um caso básico sólido. Se você antecipar ter mais do queInteger.MAX_VALUE
entradas em uma lista, poderá verificarworkWith
o valor de retorno - ele deve ser nulo no final. Caso contrário, recorra.Lembre-se de que isso toca tantos elementos quanto você deseja. Não é preguiçoso; faz sua coisa imediatamente. Você deseja fazer isso apenas para ações - isto é, coisas cujo único objetivo é aplicar-se a todos os elementos de uma lista. Como estou pensando agora, parece-me que as seqüências seriam muito menos complicadas se mantidas lineares; não deve ser um problema, pois as seqüências não se chamam de qualquer maneira - elas apenas criam objetos que as chamam novamente.
fonte
Eu já tentei criar um intérprete para uma linguagem semelhante a lisp em Java (há alguns anos e todo o código foi perdido como no CVS no sourceforge), e os iteradores utilitários Java são um pouco detalhados para a programação funcional nas listas.
Aqui está algo baseado em uma interface de sequência, que possui apenas as duas operações necessárias para obter o valor atual e iniciar a sequência no próximo elemento. Estes são nomeados cabeça e cauda após as funções no esquema.
É importante usar algo como o
Seq
ouIterator
interfaces, pois significa que a lista é criada preguiçosamente. AIterator
interface não pode ser um objeto imutável, portanto, é menos adequada à programação funcional - se você não pode dizer se o valor que você passa para uma função foi alterado por ela, você perde uma das principais vantagens da programação funcional.Obviamente,
integers
deve haver uma lista de todos os números inteiros, então comecei do zero e retornei alternadamente os positivos e negativos.Há duas versões dos quadrados - uma criando uma sequência personalizada e a outra usando
map
uma 'função' - o Java 7 não tem lambdas, então usei uma interface - e a aplico a cada elemento da sequência.O objetivo da
square ( int x )
função é apenas remover a necessidade de chamarhead()
duas vezes - normalmente eu teria feito isso colocando o valor em uma variável final, mas adicionar essa função significa que não há variáveis no programa, apenas parâmetros de função.A verbosidade do Java para esse tipo de programação me levou a escrever a segunda versão do meu intérprete no C99.
fonte
Em teoria, você pode implementar praticamente qualquer coisa em Java usando apenas recursão e sem variáveis mutáveis.
Na prática:
A linguagem Java não foi projetada para isso. Muitas construções são projetadas para mutação e são difíceis de usar sem ela. (Por exemplo, você não pode inicializar uma matriz Java de comprimento variável sem mutação.)
O mesmo vale para as bibliotecas. E se você se limitar a classes de bibliotecas que não usam mutação oculta, é ainda mais difícil. (Você nem pode usar String ... veja como
hashcode
é implementado.)As principais implementações de Java não suportam otimização de chamada de cauda. Isso significa que as versões recursivas dos algoritmos tendem a ficar com espaço na pilha "faminto". E como as pilhas de encadeamentos Java não crescem, é necessário pré-alocar pilhas grandes ... ou arriscar
StackOverflowError
.Combine essas três coisas e o Java não é realmente uma opção viável para escrever programas úteis (ou seja, não triviais) sem variáveis mutáveis.
(Mas, ei, tudo bem. Existem outras linguagens de programação disponíveis para a JVM, algumas das quais suportam programação funcional.)
fonte
Como estamos procurando um exemplo dos conceitos, eu diria que vamos deixar de lado o Java e procurar uma configuração diferente, porém familiar, na qual encontrar uma versão familiar dos conceitos. Os tubos UNIX são bastante semelhantes ao encadeamento de funções preguiçosas.
No Linux, isso significa, me dê bytes, cada um dos quais composto por bits falsos, em vez de verdadeiros, até que eu perca o apetite; altere cada um desses bytes para um caractere de nova linha; numere cada linha assim criada; e produza o quadrado desse número. Além disso, tenho apetite por 25 linhas e nada mais.
Afirmo que um programador não seria mal aconselhado a escrever um pipeline do Linux dessa maneira. É um script de shell do Linux relativamente normal.
Eu afirmo que um programador seria mal aconselhado a tentar escrever a mesma coisa da mesma forma em Java. O motivo é a manutenção de software como um fator importante no custo de vida útil de projetos de software. Não queremos incomodar o próximo programador apresentando o que é aparentemente um programa Java, mas na verdade ele é escrito com efeito em uma linguagem única personalizada, duplicando elaboradamente a funcionalidade que já existe na plataforma Java.
Por outro lado, afirmo que o próximo programador poderia aceitar mais se alguns de nossos pacotes "Java" fossem realmente pacotes da Java Virtual Machine gravados em uma das linguagens funcionais ou objeto / funcionais, como Clojure e Scala. Elas são projetadas para serem codificadas pelo encadeamento de funções e para serem chamadas do Java da maneira normal das chamadas de método Java.
Por outro lado, ainda pode ser uma boa idéia para um programador Java se inspirar na programação funcional, em alguns lugares.
Recentemente, minha técnica favorita [era] usar uma variável de retorno imutável e não inicializada e uma saída única para que, como fazem alguns compiladores de linguagem funcional, Java verifique se não importa o que aconteça no corpo da função, eu sempre forneço uma e apenas uma valor de retorno. Exemplo:fonte
int result = -n; if (n < 1) { result = 0 } return result;
quero dizer : se você o codificar, ele compila muito bem e o compilador não tem idéia se pretende ou não torná-lo equivalente à função no meu exemplo. Talvez esse exemplo seja muito simples para fazer com que a técnica pareça útil, mas em uma função com muitos ramos, acho bom deixar claro que o resultado é atribuído exatamente uma vez, independentemente do caminho a ser seguido.if (n < 1) return 0; else return -n;
, porém, acaba sem problemas ... e é mais simples. :) Parece-me que, nesse caso, a regra "um retorno" realmente ajuda a causar o problema de não saber quando seu valor de retorno foi definido; caso contrário, você poderia devolvê-lo e o Java seria mais capaz de determinar quando outros caminhos podem não retornar um valor, porque você não está mais divorciando o cálculo do valor do retorno real dele.if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;
,.A maneira mais fácil de descobrir isso seria fornecer o seguinte ao compilador Frege e examinar o código java gerado:
fonte