Como escrever programas Java úteis sem usar variáveis ​​mutáveis


12

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.


19
Seu exemplo funcional usa variáveis ​​por dentro, mas a linguagem faz tudo isso nos bastidores. Você efetivamente delegou as partes desagradáveis ​​a alguém que acredita ter feito corretamente.
Blrfl

12
@Blrfl: O argumento "nos bastidores" mata todos os debates baseados em idiomas, pois cada pedaço de código é finalmente traduzido para o código de máquina x86. O código x86 não é orientado a objetos, nem processual, nem funcional, nem nada, mas essas categorias são marcas valiosas para linguagens de programação. Veja o idioma, não a implementação.
thiton

10
@thiton Discordou. O que Blrfl está dizendo é que essas funções provavelmente usam variáveis escritas na mesma linguagem de programação . Não há necessidade de diminuir o nível aqui. O trecho está apenas usando funções de biblioteca. Você pode escrever facilmente o mesmo código em Java, veja: squaresOf(integers()).take(25)(a gravação dessas funções é deixada como um exercício para o leitor; a dificuldade está no conjunto infinito de integers(), mas isso é um problema para o Java por causa de sua avaliação ansiosa, nada a ver com variáveis)
Andres F.

6
Essa citação é confusa e enganosa, não há mágica lá, apenas açúcar sintático .
21413 y an

2
@thiton Eu sugiro que você aprenda mais sobre linguagens FP, mas, no entanto, o trecho de código não suporta (ou rejeita) a afirmação de que "variáveis" não são necessárias (pelo que eu suponho que você queira dizer "variáveis ​​mutáveis", porque as outras tipo é comum no PF). O trecho mostra apenas as funções da biblioteca que também poderiam ter sido implementadas em Java, exceto problemas preguiçosos / ansiosos que são offtopic aqui.
Andres F.

Respostas:


31

É 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:

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []  

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):

  1. vinculando um valor a um nome: final int MAX_SIZE = 100;

  2. atualização destrutiva: int a = 3; a += 1; a++;

A programação funcional evita o segundo, mas abrange o primeiro (exemplos: letexpressões, parâmetros de função, defineitens 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ão takee squares-of, integersse não, variáveis?

Além disso, o exemplo não tem sentido. Ele não mostra as implementações take, squares-ofou integers. 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:

Este artigo é uma tentativa de demonstrar à comunidade maior de programadores (não funcionais) a importância da programação funcional e também de ajudar os programadores a explorar suas vantagens ao máximo, deixando claro quais são essas vantagens.

[...]

Argumentaremos no restante deste artigo que as linguagens funcionais fornecem dois tipos novos e muito importantes de cola. Daremos alguns exemplos de programas que podem ser modularizados de novas maneiras e, assim, podem ser simplificados. Essa é a chave para o poder da programação funcional - permite uma modularização aprimorada. É também o objetivo pelo qual os programadores funcionais devem se esforçar - módulos menores, mais simples e mais gerais, colados com as novas colas que descreveremos.


10
+1 em "Eu recomendaria evitar este artigo e outros, se você realmente quiser aprender sobre programação funcional. Parece ser escrito mais com o objetivo de chocar e ofender, em vez de ensinar conceitos e fundamentos".

3
Metade da razão pela qual as pessoas não praticam FP é porque não ouvem / aprendem nada sobre isso na universidade, e a outra metade é porque, quando examinam, encontram artigos que os deixam desinformados e acham que é tudo por uma fantasia. brincando sobre, em vez de ser uma abordagem racional pensada, com benefícios. +1 para dar melhores fontes de informação
Jimmy Hoffa

3
Coloque a sua resposta à pergunta no topo absoluta, se você iria por isso é mais direta com a questão e talvez essa pergunta vai ficar aberto, em seguida, (com uma resposta direta centrou-pergunta)
Jimmy Hoffa

2
Desculpe nitpick, mas não entendo por que você escolheu esse código haskell. Eu li LYAH e seu exemplo é difícil para mim. Também não vejo a relação com a pergunta original. Por que você não usou apenas take 25 (map (^2) [1..])como exemplo?
Daniel Kaplan

2
@tieTYT boa pergunta - obrigado por apontar isso. O motivo pelo qual usei esse exemplo é porque mostra como gerar uma lista de números usando recursão e evitando variáveis ​​mutáveis. Minha intenção era que o OP visse esse código e pensasse em como fazer algo semelhante em Java. Para endereçar seu snippet de código, o que é [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 da Enumclasse (que essa sintaxe exige) também são úteis, mas eram preguiçosas demais para encontrá-las. Assim unfoldr,. :)

27

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:

for (int i = 1; i <= 25; ++i) {
    System.out.println(i*i);
}

e não se deixe enganar por escrevê-lo de maneira mais complexa devido a um ódio por variáveis.


5
"Ódio de variáveis"? Ooookay ... O que você leu sobre Programação Funcional? Quais idiomas você já tentou? Quais tutoriais?
Andres F.

8
@AndresF .: Mais de dois anos de curso em Haskell. Eu não digo FP é ruim. No entanto, há uma tendência em muitas discussões sobre FP-vs-IP (como o artigo vinculado) de condenar o uso de entidades nomeadas redesignáveis ​​(variáveis ​​AKA) e de condenar sem uma boa razão ou dados. Condenação irracional é ódio no meu livro. E o ódio cria um código realmente ruim.
thiton

10
"Ódio de variáveis" é uma simplificação causal excessiva en.wikipedia.org/wiki/Fallacy_of_the_single_cause, existem muitos benefícios para a programação sem estado que podem até ser encontrados em Java, embora eu concorde com sua resposta de que em Java o custo seria muito alto em complexidade para o programa e sendo não-idiomático. Eu ainda não deixaria de lado a idéia de que a programação sem estado é boa e com estado é ruim, já que algumas respostas emocionais, em vez de uma postura fundamentada e bem pensada, às quais as pessoas chegaram devido à experiência.
Jimmy Hoffa

2
De acordo com o que @JimmyHoffa diz, encaminhá-lo-emos a John Carmack sobre o tópico de programação em estilo funcional em linguagens imperativas (C ++ no caso dele) ( altdevblogaday.com/2012/04/26/functional-programming-in-c )
Steven Evers

5
Condenação irracional não é ódio, e evitar estado mutável não é irracional.
Michael Shaw

21

O mais simples que posso fazer com a recursão é uma função com um parâmetro. Não é muito Java-esque, mas funciona:

public class squares
{
    public static void main(String[] args)
    {
        squares(15);
    }

    private static void squares(int x)
    {
        if (x>0)
        {
            System.out.println(x*x);
            squares(x-1);
        }
    }
}

3
+1 por tentar realmente responder à pergunta com um exemplo de Java.
KChaloux

Eu diminuiria isso para apresentação de estilo de código de golfe (consulte o aviso de modificação ), mas não posso me forçar a pressionar a seta para baixo, porque esse código corresponde perfeitamente às declarações feitas na minha resposta favorita : "1) a falta de mutação, 2) o uso de recursão e 3) a falta de loops "
gnat

3
@gnat: Esta resposta foi publicada antes do aviso de modificação. Eu não estava gostando muito do estilo, estava procurando a simplicidade e satisfazendo a pergunta original do OP; para ilustrar que é possível fazer essas coisas em Java.
FrustratedWithFormsDesigner

@FrustratedWithFormsDesigner sure; isso não me impedia de gravar em vídeo (já que você deve ser capaz de editar para cumprir) - é a combinação surpreendentemente perfeita que fez a mágica. Muito bem, muito bem feito, bastante educativo - obrigado
mosquito

16

No seu exemplo funcional, não vemos como as funções squares-ofe takesã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 ...

squares_of(integers).take(25);

o que não é tão diferente.


6
Nitpick: squares-ofnão é um nome válido em Java ( squares_ofembora). Mas, caso contrário, um bom argumento mostra que o exemplo do artigo é ruim.

Suspeito que o artigo integergere preguiçosamente números inteiros e a takefunção escolhe 25 dos squared-ofnúmeros integer. Em resumo, você deve ter uma integerfunção para produzir preguiçosamente números inteiros até o infinito.
criar o seu

É um pouco insano chamar algo como (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 que integeré uma variável vinculada a uma infinita quantidade de números.
Ingo

6

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 Takeconstrutor pode ser arbitrariamente alto.

class Example {
    public static void main(String[] a) {
        Numbers test = new Take(25, new SquaresOf(new Integers()));
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Ou com métodos de fábrica encadeáveis:

class Example {
    public static void main(String[] a) {
        Numbers test = Numbers.integers().squares().take(23);
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Onde SquaresOf, Takee IntegersestenderNumbers

abstract class Numbers implements Iterator<Integer> {
    public static Numbers integers() {
        return new Integers();
    }

    public Numbers squares() {
        return new SquaresOf(this);
    }

    public Numbers take(int c) {
        return new Take(c, this);
    }
    public void remove() {}
}

1
Isso mostra a superioridade do paradigma OO sobre o funcional; com design adequado de OO, você pode imitar o paradigma funcional, mas não pode imitar o paradigma de OO em um estilo funcional.
M3th0dman

3
@ m3th0dman: Com o design adequado do OO, você pode imitar o FP de maneira meio assertiva , assim como qualquer idioma que possua seqüências de caracteres, listas e / ou dicionários poderia imitar o OO de forma meio assediante. A equivalência de Turing das linguagens de uso geral significa que, com esforço suficiente, qualquer linguagem pode simular os recursos de qualquer outra.
cHao

Observe que os iteradores no estilo Java, como no, while (test.hasNext()) System.out.println(test.next())seriam um não-não no FP; iteradores são inerentemente mutáveis.
cHao 19/02

1
@cHao Mal acredito que o verdadeiro encapsulamento ou polimorfismo possa ser imitado; também Java (neste exemplo) não pode realmente imitar uma linguagem funcional por causa da avaliação rigorosa e ansiosa. Eu também acredito que os iteradores podem ser escritos de maneira recursiva.
M3th0dman

@ m3th0dman: Polimorfismo não seria difícil de imitar; até C e linguagem assembly podem fazer isso. Apenas torne o método um campo no objeto ou em um descritor de classe / vtable. E o encapsulamento no sentido de ocultar dados não é estritamente necessário; metade das línguas por aí não a fornece; quando seu objeto é imutável, não importa tanto se as pessoas conseguem ver suas entranhas de qualquer maneira. tudo o que é necessário é quebra de dados , que os campos de método acima mencionados poderiam fornecer facilmente.
Chao

6

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.

  • Linguagens imperativas quase sempre dependem de variáveis ​​mutáveis ​​de algum tipo. Eles tendem a favorecer a iteração sobre a recursão, por exemplo, e quase todas as construções iterativas - even while (true)e for (;;)! - são totalmente dependentes de uma variável em algum lugar que muda de iteração para iteração.
  • As linguagens orientadas a objetos encaram praticamente todos os programas como um gráfico de objetos enviando mensagens uns aos outros e, em quase todos os casos, respondendo a essas mensagens mudando alguma coisa.

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...

class Ints {
     final int value;
     final Ints tail;

     public Ints(int value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints next() { return this.tail; }
     public int value() { return this.value; }
}

public Ints take(int count, Ints input) {
    if (count == 0 || input == null) return null;
    return new Ints(input.value(), take(count - 1, input.next()));
}    

public Ints squares_of(Ints input) {
    if (input == null) return null;
    int i = input.value();
    return new Ints(i * i, squares_of(input.next()));
}

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:

public function doStuff() {
    final Ints integers = ...somehow assemble list of 20 million ints...;
    final Ints result = take(25, squares_of(integers));
    ...
}

Embora precisemos apenas de 25 polegadas para o resultado, squares_ofnão sabemos disso. Ele retornará o quadrado de cada número em integers. 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. :))

// Represents something that can give us instances of OutType.
// We can basically treat this class like a list.
interface Source<OutType> {
     public Source<OutType> next();
     public OutType value();
}

// Represents an operation that turns an InType into an OutType.
// Note, these can be the same type.  We're just flexible like that.
interface Transform<InType, OutType> {
    public OutType appliedTo(InType input);
}

// Represents an action (as opposed to a function) that can run on
// every element of a sequence.
abstract class Action<InType> {
    abstract void doWith(final InType input);
    public void doWithEach(final Source<InType> input) {
        if (input == null) return;
        doWith(input.value());
        doWithEach(input.next());
    }
}

// A list of Integers.
class Ints implements Source<Integer> {
     final Integer value;
     final Ints tail;
     public Ints(Integer value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints(Source<Integer> input) {
         this.value = input.value();
         this.tail = new Ints(input.next());
     }
     public Source<Integer> next() { return this.tail; }
     public Integer value() { return this.value; }
     public static Ints fromArray(Integer[] input) {
         return fromArray(input, 0, input.length);
     }
     public static Ints fromArray(Integer[] input, int start, int end) {
         if (end == start || input == null) return null;
         return new Ints(input[start], fromArray(input, start + 1, end));
     }
}

// An example of the spiff we get by splitting the "iterator" interface
// off.  These ints are effectively generated on the fly, as opposed to
// us having to build a huge list.  This saves huge amounts of memory
// and CPU time, for the rather common case where the whole sequence
// isn't needed.
class Range implements Source<Integer> {
    final int start, end;
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public Integer value() { return start; }
    public Source<Integer> next() {
        if (start >= end) return null;
        return new Range(start + 1, end);
    }
}

// This takes each InType of a sequence and turns it into an OutType.
// This *takes* a Transform, rather than just *implementing* Transform,
// because the transforms applied are likely to be specified inline.
// If we just let people override `value()`, we wouldn't easily know what type
// to return, and returning our own type would lose the transform method.
static class Mapper<InType, OutType> implements Source<OutType> {
    private final Source<InType> input;
    private final Transform<InType, OutType> transform;

    public Mapper(Transform<InType, OutType> transform, Source<InType> input) {
        this.transform = transform;
        this.input = input;
    }

    public Source<OutType> next() {
         return new Mapper<InType, OutType>(transform, input.next());
    }
    public OutType value() {
         return transform.appliedTo(input.value());
    }
}

// ...

public <T> Source<T> take(int count, Source<T> input) {
    if (count <= 0 || input == null) return null;
    return new Source<T>() {
        public T value() { return input.value(); }
        public Source<T> next() { return take(count - 1, input.next()); }
    };
}

(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.

public Source<Integer> squares_of(Source<Integer> input) {
     final Transform<Integer, Integer> square = new Transform<Integer, Integer>() {
         public Integer appliedTo(final Integer i) { return i * i; }
     };
     return new Mapper<>(square, input);
}


public void example() {
    final Source<Integer> integers = new Range(0, 1000000000);

    // and, as for the author's "bet you can't do this"...
    final Source<Integer> squares = take(25, squares_of(integers));

    // Just to make sure we got it right :P
    final Action<Integer> printAction = new Action<Integer>() {
        public void doWith(Integer input) { System.out.println(input); }
    };
    printAction.doWithEach(squares);
}

Isso funciona principalmente, mas ainda é um pouco propenso a estouros de pilha. Tentando take2 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ê telefona take(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ós take().

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:

public <T> void doSomethingWith(T input) { /* magic happens here */ }
public <T> Source<T> workWith(Source<T> input, int count) {
    if (count < 0 || input == null) return null;
    if (count == 0) return input;
    if (count == 1) {
        doSomethingWith(input.value());
        return input.next();
    }
    return (workWith(workWith(input, count/2), count - count/2);
}

workWithbasicamente 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_VALUEa 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 que Integer.MAX_VALUEentradas em uma lista, poderá verificar workWitho 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.


3

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 Seqou Iteratorinterfaces, pois significa que a lista é criada preguiçosamente. A Iteratorinterface 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, integersdeve 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 mapuma '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 chamar head()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.

public class Squares {
    interface Seq<T> {
        T head();
        Seq<T> tail();
    }

    public static void main (String...args) {
        print ( take (25, integers ) );
        print ( take (25, squaresOf ( integers ) ) );
        print ( take (25, squaresOfUsingMap ( integers ) ) );
    }

    static Seq<Integer> CreateIntSeq ( final int n) {
        return new Seq<Integer> () {
            public Integer head () {
                return n;
            }
            public Seq<Integer> tail () {
                return n > 0 ? CreateIntSeq ( -n ) : CreateIntSeq ( 1 - n );
            }
        };
    }

    public static final Seq<Integer> integers = CreateIntSeq(0);

    public static Seq<Integer> squaresOf ( final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return square ( source.head() );
            }
            public Seq<Integer> tail () {
                return squaresOf ( source.tail() );
            }
        };
    }

    // mapping a function over a list rather than implementing squaring of each element
    interface Fun<T> {
        T apply ( T value );
    }

    public static Seq<Integer> squaresOfUsingMap ( final Seq<Integer> source ) {
        return map ( new Fun<Integer> () {
            public Integer apply ( final Integer value ) {
                return square ( value );
            }
        }, source );
    }

    public static <T> Seq<T> map ( final Fun<T> fun, final Seq<T> source ) {
        return new Seq<T> () {
            public T head () {
                return fun.apply ( source.head() );
            }
            public Seq<T> tail () {
                return map ( fun, source.tail() );
            }
        };
    }

    public static Seq<Integer> take ( final int count,  final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return source.head();
            }
            public Seq<Integer> tail () {
                return count > 0 ? take ( count - 1, source.tail() ) : nil;
            }
        };
    }

    public static int square ( final int x ) {
        return x * x;
    }

    public static final Seq<Integer> nil = new Seq<Integer> () {
        public Integer head () {
            throw new RuntimeException();
        }
        public Seq<Integer> tail () {
            return this;
        }
    };

    public static <T> void print ( final Seq<T> seq ) {
        printPartSeq ( "[", seq.head(), seq.tail() );
    }

    private static <T> void printPartSeq ( final String prefix, final T value, final Seq<T> seq ) {
        if ( seq == nil) {
            System.out.println("]");
        } else {
            System.out.print(prefix);
            System.out.print(value);
            printPartSeq ( ",", seq.head(), seq.tail() );
        }
    }
}

3

Como escrever programas Java úteis sem usar variáveis ​​mutáveis.

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.)


2

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.

cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25

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:

int f(final int n) {
    final int result; // not initialized here!
    if (n < 0) {
        result = -n;
    } else if (n < 1) {
        result = 0;
    } else {
        result = n - 1;
    }
    // If I would leave off the "else" clause,
    // Java would fail to compile complaining that
    // "result" is possibly uninitialized.
    return result;
}


Estou com cerca de 70% de certeza de que o Java já faz a verificação do valor de retorno de qualquer maneira. Você deve receber um erro sobre uma "declaração de retorno ausente" se o controle puder cair no final de uma função não nula.
cHao 20/02

O que 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.
Minopret

Se você diz 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.
Chao

Ou, para o exemplo da sua resposta if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;,.
cHao

Acabei de decidir que não quero passar mais momentos da minha vida defendendo a regra OnlyOneReturn em Java. Fora isso vai. Quando e se eu pensar em uma prática de codificação Java que pretendo defender e influenciada por práticas de programação funcional, substituirei esse exemplo. Até então, não há exemplo.
minopret 20/02

0

A maneira mais fácil de descobrir isso seria fornecer o seguinte ao compilador Frege e examinar o código java gerado:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x

Depois de alguns dias, encontrei meus pensamentos voltando a esta resposta. Afinal, parte da minha sugestão foi implementar as partes de programação funcional no Scala. Se considerarmos aplicar o Scala naqueles lugares em que realmente pensávamos em Haskell (e acho que não sou o único blog.zlemma.com/2013/02/20/… ), não deveríamos ao menos considerar Frege?
minopret 21/02

@minopret Este é realmente o nicho que Frege é fascinante - pessoas que conheceram e amam Haskell e ainda precisam da JVM. Estou confiante de que um dia Frege estará maduro o suficiente para obter uma consideração séria, pelo menos.
Ingo
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.