Come scrivere utili programmi Java senza usare variabili mutabili


12

Stavo leggendo un articolo sulla programmazione funzionale in cui afferma lo scrittore

(take 25 (squares-of (integers)))

Si noti che non ha variabili. In effetti, non ha nient'altro che tre funzioni e una costante. Prova a scrivere i quadrati di numeri interi in Java senza usare una variabile. Oh, probabilmente c'è un modo per farlo, ma certamente non è naturale e non leggerebbe bene come il mio programma sopra.

È possibile raggiungere questo obiettivo in Java? Supponendo che ti venga richiesto di stampare i quadrati dei primi 15 numeri interi, potresti scrivere un ciclo for o while senza usare le variabili?

Avviso Mod

Questa domanda non è una gara di golf in codice. Siamo alla ricerca di risposte che spieghino i concetti coinvolti (idealmente senza ripetere le risposte precedenti), e non solo per l'ennesimo pezzo di codice.


19
Il tuo esempio funzionale usa le variabili all'interno, ma il linguaggio fa tutto dietro le quinte. Hai delegato in modo efficace le parti spiacevoli a qualcuno che ritieni abbia fatto correttamente.
Blrfl,

12
@Blrfl: l'argomento "dietro le quinte" uccide tutti i dibattiti basati sul linguaggio, poiché ogni pezzo di codice viene infine tradotto in codice macchina x86. Il codice x86 non è orientato agli oggetti, né procedurale, non funzionale, niente, ma queste categorie sono tag utili per i linguaggi di programmazione. Guarda la lingua, non l'implementazione.
thiton,

10
@thiton In disaccordo. Ciò che Blrfl sta dicendo è che quelle funzioni probabilmente usano variabili scritte nello stesso linguaggio di programmazione . Non c'è bisogno di andare a basso livello qui. Lo snippet sta semplicemente usando le funzioni di libreria. Puoi facilmente scrivere lo stesso codice in Java, vedi: squaresOf(integers()).take(25)(scrivere quelle funzioni è lasciato come un esercizio per il lettore; la difficoltà sta nell'infinito set per integers(), ma questo è un problema per Java a causa della sua valutazione entusiasta, niente a che fare con variabili)
Andres F.

6
Quella citazione è confusa e fuorviante, non c'è magia lì, solo zucchero sintattico .
yannis,

2
@thiton Ti suggerisco di saperne di più sui linguaggi FP, ma tuttavia lo snippet di codice non supporta (o rifiuta) l'affermazione che non sono necessarie "variabili" (per cui presumo che tu intenda "variabili mutabili", perché l'altro tipo è comune in FP). Lo snippet mostra solo le funzioni di libreria che avrebbero potuto essere implementate anche in Java, escludendo i problemi pigri / desiderosi che sono offtopici qui.
Andres F.

Risposte:


31

È possibile implementare un esempio del genere in Java senza utilizzare aggiornamenti distruttivi? Sì. Tuttavia, come menzionato da @Thiton e l'articolo stesso, sarà brutto (a seconda dei gusti). Un modo è usare la ricorsione; ecco un esempio di Haskell che fa qualcosa di simile:

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) la mancanza di mutazione, 2) l'uso della ricorsione e 3) la mancanza di anelli. L'ultimo punto è molto importante: i linguaggi funzionali non hanno bisogno di costrutti di ciclo integrati nel linguaggio, poiché la ricorsione può essere utilizzata per la maggior parte (tutti?) I casi in cui i loop vengono utilizzati in Java. Ecco una serie ben nota di articoli che mostrano quanto possano essere incredibilmente espressive le chiamate di funzione.


Ho trovato l'articolo insoddisfacente e vorrei fare un paio di punti aggiuntivi:

Quell'articolo è una spiegazione molto scarsa e confusa della programmazione funzionale e dei suoi benefici. Consiglio vivamente altre fonti per conoscere la programmazione funzionale.

La parte più confusa dell'articolo è che non menziona il fatto che ci sono due usi per le istruzioni di assegnazione in Java (e nella maggior parte degli altri linguaggi tradizionali):

  1. associare un valore a un nome: final int MAX_SIZE = 100;

  2. aggiornamento distruttivo: int a = 3; a += 1; a++;

La programmazione funzionale evita il secondo, ma abbraccia il primo (esempi: let-espressioni, parametri di funzione, itioni di alto livello define) . Questo è un punto molto importante da cogliere, perché altrimenti l'articolo sembra solo stupido e potrebbe lasciare Vi state chiedendo, quali sono take, squares-ofe integersse non le variabili?

Inoltre, l'esempio non ha senso. Non mostra le implementazioni di take, squares-ofo integers. Per quanto ne sappiamo, sono implementati usando variabili mutabili. Come ha detto @Martin, puoi scrivere banalmente questo esempio in Java.

Ancora una volta, consiglierei di evitare questo articolo e altri simili se vuoi davvero conoscere la programmazione funzionale. Sembra essere scritto più con l'obiettivo di scioccare e offendere piuttosto che insegnare concetti e fondamenti. Invece, perché non dare un'occhiata a uno dei miei documenti preferiti di tutti i tempi , di John Hughes. Hughes cerca di affrontare alcuni degli stessi problemi trattati dall'articolo (sebbene Hughes non parli di concorrenza / parallelizzazione); ecco un teaser:

Questo documento è un tentativo di dimostrare alla più ampia comunità di programmatori (non funzionali) il significato della programmazione funzionale e anche di aiutare i programmatori funzionali a sfruttare appieno i suoi vantaggi, chiarendo quali sono questi vantaggi.

[...]

Discuteremo nel resto di questo documento che i linguaggi funzionali forniscono due nuovi, importantissimi tipi di colla. Daremo alcuni esempi di programmi che possono essere modularizzati in nuovi modi e che possono quindi essere semplificati. Questa è la chiave della potenza della programmazione funzionale: consente una migliore modularizzazione. È anche l'obiettivo per il quale i programmatori funzionali devono impegnarsi: moduli più piccoli, più semplici e più generali, incollati insieme alle nuove colle che descriveremo.


10
+1 per "Vorrei raccomandare di evitare questo articolo e altri simili se vuoi davvero conoscere la programmazione funzionale. Sembra essere scritto più con l'obiettivo di scioccare e offendere piuttosto che insegnare concetti e fondamenti".

3
La metà del motivo per cui le persone non fanno FP è perché non ne sentono / imparano nulla in uni, e l'altra metà è perché quando lo guardano trovano articoli che li lasciano entrambi disinformati e pensano che sia tutto per alcuni fantasiosi giocare piuttosto che essere un approccio ragionato ponderato con benefici. +1 per fornire migliori fonti di informazioni
Jimmy Hoffa,

3
Metti la tua risposta alla domanda in cima assoluta se lo desideri, quindi è più diretta alla domanda e forse questa domanda rimarrà aperta allora (con una risposta diretta focalizzata sulla domanda)
Jimmy Hoffa,

2
Mi dispiace nitpick, ma non capisco perché hai scelto questo codice haskell. Ho letto LYAH e il tuo esempio per me è difficile da capire. Inoltre non vedo la relazione con la domanda originale. Perché non hai usato solo take 25 (map (^2) [1..])come esempio?
Daniel Kaplan,

2
@tieTYT bella domanda - grazie per averlo sottolineato. Il motivo per cui ho usato questo esempio è perché mostra come generare un elenco di numeri usando la ricorsione ed evitando variabili mutabili. Il mio intento era che l'OP vedesse quel codice e pensasse a come fare qualcosa di simile in Java. Per indirizzare lo snippet di codice, che cos'è [1..]? Questa è una funzionalità interessante incorporata in Haskell, ma non illustra i concetti alla base della generazione di tale elenco. Sono sicuro che anche gli esempi della Enumclasse (che richiede quella sintassi) sono utili, ma era troppo pigro per trovarli. Così, unfoldr. :)

27

Non lo faresti. Le variabili sono al centro della programmazione imperativa e se provi a programmare in modo imperativo senza usare le variabili, stai solo causando a tutti un dolore nel culo. In diversi paradigmi di programmazione, gli stili sono diversi e concetti diversi formano la tua base. Una variabile in Java, se usata bene con un ambito limitato, non è male. Chiedere un programma Java senza variabili è come chiedere un programma Haskell senza funzioni, quindi non lo chiedi e non ti lasci ingannare nel vedere la programmazione imperativa come inferiore perché usa le variabili.

Quindi, il modo Java sarebbe:

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

e non lasciarti ingannare per scriverlo in un modo più complesso a causa di un odio per le variabili.


5
"Odio delle variabili"? Ooookay ... Che cosa hai letto sulla programmazione funzionale? Quali lingue hai provato? Quali tutorial?
Andres F.

8
@AndresF .: Più di due anni di corso a Haskell. Non dico che FP è un male. Tuttavia, vi è una tendenza in molte discussioni FP-vs-IP (come l'articolo collegato) a condannare l'uso di entità nominate riassegnabili (variabili AKA) e a condannare senza una buona ragione o dati. La condanna irragionevole è l'odio nel mio libro. E l'odio rende davvero pessimo il codice.
thiton,

10
"L'odio delle variabili" è causale semplificazione eccessiva en.wikipedia.org/wiki/Fallacy_of_the_single_cause ci sono molti vantaggi per la programmazione senza stato che potrebbe anche essere dovuto in Java, anche se sono d'accordo con la risposta che in Java il costo sarebbe troppo elevato in complessità alla il programma ed essere non idiomatico. Non vorrei ancora andare in giro sventolando via l'idea che la programmazione apolide sia buona e che lo stato sia cattivo poiché una risposta emotiva piuttosto che una posizione ragionata e ben ponderata hanno raggiunto le persone a causa dell'esperienza.
Jimmy Hoffa,

2
In linea con quanto afferma @JimmyHoffa, ti rimanderò a John Carmack sull'argomento della programmazione in stile funzionale nei linguaggi imperativi (C ++ nel suo caso) ( altdevblogaday.com/2012/04/26/functional-programming-in-c ).
Steven Evers,

5
La condanna irragionevole non è odio, ed evitare lo stato mutevole non è irragionevole.
Michael Shaw,

21

Il più semplice che posso fare con la ricorsione è una funzione con un parametro. Non è molto simile a Java, ma funziona:

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 per il tentativo di rispondere effettivamente alla domanda con un esempio Java.
KChaloux,

Voterei questo per la presentazione in stile golf del codice (vedi avviso Mod ) ma non posso forzare se stesso a premere la freccia verso il basso perché questo codice corrisponde perfettamente alle dichiarazioni fatte nella mia risposta preferita : "1) la mancanza di mutazione, 2) l'uso di ricorsione, e 3) la mancanza di anelli "
moscerino del

3
@gnat: questa risposta è stata pubblicata prima dell'avviso Mod. Non stavo andando per il grande stile, stavo andando per la semplicità e soddisfacendo la domanda originale del PO; per illustrare che è possibile fare tali cose in Java.
FrustratedWithFormsDesigner,

@FrustratedWithFormsDesigner sicuro; questo non mi impedirebbe di DVing (dal momento che dovresti essere in grado di modificarlo per renderlo conforme) - è la partita sorprendentemente perfetta che ha fatto la magia. Ben fatto, davvero ben fatto, abbastanza educativo - grazie
moscerino

16

Nel tuo esempio funzionale non vediamo come sono implementate le funzioni squares-ofe take. Non sono un esperto di Java, ma sono abbastanza sicuro che potremmo scrivere quelle funzioni per abilitare un'istruzione come questa ...

squares_of(integers).take(25);

che non è molto diverso.


6
Nitpick: squares-ofnon è un nome valido in Java ( squares_ofè però). Ma per il resto, un buon punto mostra che l'esempio dell'articolo è scadente.

Sospetto che l'articolo integergeneri pigramente numeri interi e la takefunzione preleva 25 squared-ofnumeri integer. In breve, dovresti avere una integerfunzione per produrre pigramente numeri interi all'infinito.
Onesimus Nessun impegno (

È un po 'folle chiamare qualcosa come (integer)una funzione - una funzione è ancora qualcosa che associa un argomento a un valore. Si scopre che (integer)non è una funzione, ma semplicemente un valore. Si potrebbe persino spingersi così lontano per dire che integerè una variabile legata a uno strem infinito di numeri.
Ingo

6

In Java potresti farlo (specialmente la parte dell'elenco infinito) con gli iteratori. Nel seguente esempio di codice, il numero fornito al Takecostruttore può essere 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());
    }
}

O con metodi di fabbrica concatenabili:

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

Dove SquaresOf, Takee Integersdi estendereNumbers

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
Ciò dimostra la superiorità del paradigma OO rispetto a quello funzionale; con un corretto design OO puoi imitare il paradigma funzionale ma non puoi imitare il paradigma OO in uno stile funzionale.
m3th0dman,

3
@ m3th0dman: con un corretto design OO puoi possibilmente imitare metà FP, proprio come qualsiasi lingua che ha stringhe, elenchi e / o dizionari potrebbe simulare metà OO. L'equivalenza di Turing delle lingue di uso generale significa che, dato uno sforzo sufficiente, qualsiasi lingua può simulare le caratteristiche di qualsiasi altra.
cHao,

Si noti che gli iteratori in stile Java come in while (test.hasNext()) System.out.println(test.next())sarebbero un no-no in FP; gli iteratori sono intrinsecamente mutabili.
cao

1
@cHao Non credo quasi che si possa imitare il vero incapsulamento o polimorfismo; anche Java (in questo esempio) non può davvero imitare un linguaggio funzionale a causa della rigorosa valutazione entusiasta. Credo anche che gli iteratori possano essere scritti in modo ricorsivo.
m3th0dman,

@ m3th0dman: il polimorfismo non sarebbe affatto difficile da imitare; anche C e il linguaggio assembly possono farlo. Basta rendere il metodo un campo nell'oggetto o un descrittore di classe / vtable. E l'incapsulamento in senso nascosto non è strettamente necessario; metà delle lingue là fuori non lo forniscono, quando il tuo oggetto è immutabile, non importa quanto le persone possano comunque vederne il fegato. tutto ciò che serve è il wrapping dei dati , che i suddetti campi del metodo potrebbero facilmente fornire.
cao

6

Versione breve:

Per far sì che lo stile a singola assegnazione funzioni in modo affidabile in Java, occorrerebbe (1) una sorta di infrastruttura intuitiva e (2) supporto a livello di compilatore o runtime per l'eliminazione delle chiamate in coda.

Possiamo scrivere gran parte dell'infrastruttura e possiamo organizzare le cose per cercare di evitare di riempire lo stack. Ma finché ogni chiamata prende uno stack frame, ci sarà un limite su quanta ricorsione si può fare. Mantieni i tuoi iterabili piccoli e / o pigri e non dovresti avere grossi problemi. Almeno la maggior parte dei problemi che incontrerai non richiede la restituzione di un milione di risultati contemporaneamente. :)

Inoltre, poiché il programma deve effettivamente apportare modifiche visibili per poter essere eseguito, non è possibile rendere tutto immutabile. Tuttavia, puoi mantenere immutabile la stragrande maggioranza delle tue cose, usando un piccolo sottoinsieme di mutabili essenziali (flussi, ad esempio) solo in determinati punti chiave in cui le alternative sarebbero troppo onerose.


Versione lunga:

In poche parole, un programma Java non può evitare totalmente le variabili se vuole fare qualcosa che valga la pena fare. Puoi contenerli , e quindi limitare enormemente la mutabilità, ma il design stesso della lingua e dell'API - insieme alla necessità di cambiare il sistema sottostante - rendono l'immutabilità totale impossibile.

Java è stato progettato fin dall'inizio come un imperativo , orientato agli oggetti linguaggio.

  • Le lingue imperative dipendono quasi sempre da variabili mutabili di qualche tipo. Tendono a favorire l'iterazione rispetto alla ricorsione, per esempio, e quasi tutti i costrutti iterativi - anche while (true)e for (;;)! - dipendono totalmente da una variabile che cambia da iterazione a iterazione.
  • I linguaggi orientati agli oggetti praticamente immaginano ogni programma come un grafico di oggetti che inviano messaggi l'un l'altro e, in quasi tutti i casi, rispondono a quei messaggi mutando qualcosa.

Il risultato finale di queste decisioni di progettazione è che senza variabili mutabili, Java non ha modo di cambiare lo stato di nulla, nemmeno qualcosa di semplice come stampare "Hello world!" allo schermo comporta un flusso di output, che comporta il blocco dei byte in un buffer mutabile .

Quindi, per tutti gli scopi pratici, siamo limitati a bandire le variabili dal nostro codice. OK, possiamo farlo. Quasi. Fondamentalmente ciò di cui avremmo bisogno è sostituire quasi tutte le iterazioni con la ricorsione e tutte le mutazioni con chiamate ricorsive che restituiscono il valore modificato. così...

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()));
}

Fondamentalmente, costruiamo un elenco collegato, in cui ogni nodo è un elenco in sé. Ogni lista ha una "testa" (il valore corrente) e una "coda" (la restante lista secondaria). La maggior parte dei linguaggi funzionali fa qualcosa di simile a questo, perché è molto suscettibile di efficienza immutabilità. Un'operazione "successiva" restituisce semplicemente la coda, che in genere viene passata al livello successivo in una pila di chiamate ricorsive.

Ora, questa è una versione estremamente semplificata di questa roba. Ma è abbastanza buono per dimostrare un grave problema con questo approccio in Java. Considera questo codice:

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

Sebbene per il risultato siano necessari solo 25 pollici, squares_ofnon lo sappiamo. Restituirà il quadrato di ogni numero in integers. La ricorsione a 20 milioni di livelli di profondità causa grossi problemi a Java.

Vedi, i linguaggi funzionali in cui in genere faresti la stranezza in questo modo, hanno una funzione chiamata "eliminazione della coda". Ciò significa che, quando il compilatore vede l'ultimo atto del codice da chiamare se stesso (e restituisce il risultato se la funzione non è nulla), utilizza il frame di stack della chiamata corrente invece di impostarne uno nuovo e fa invece un "salto" di una "chiamata" (quindi lo spazio di stack utilizzato rimane costante). In breve, si tratta di circa il 90% della strada per trasformare la ricorsione della coda in iterazione. Potrebbe gestire quei miliardi di ints senza traboccare lo stack. (Alla fine sarebbe ancora a corto di memoria, ma assemblare un elenco di un miliardo di ints ti rovinerà comunque in termini di memoria su un sistema a 32 bit.)

Java non lo fa, nella maggior parte dei casi. (Dipende dal compilatore e dal runtime, ma l'implementazione di Oracle non lo fa.) Ogni chiamata a una funzione ricorsiva consuma la memoria di un frame dello stack. Usalo troppo e otterrai uno stack overflow. Traboccando lo stack tutto ma garantisce la morte del programma. Quindi dobbiamo assicurarci di non farlo.

Una soluzione semi-alternativa ... valutazione pigra. Abbiamo ancora le limitazioni dello stack, ma possono essere legate a fattori su cui abbiamo più controllo. Non dobbiamo calcolare un milione di ints solo per restituire 25. :)

Quindi costruiamoci un'infrastruttura di valutazione pigra. (Questo codice è stato testato qualche tempo fa, ma da allora l'ho modificato un po '; leggi l'idea, non gli errori di sintassi. :))

// 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()); }
    };
}

(Tieni presente che se questo fosse effettivamente praticabile in Java, il codice almeno in qualche modo come sopra sarebbe già parte dell'API.)

Ora, con un'infrastruttura in atto, è piuttosto banale scrivere codice che non ha bisogno di variabili mutabili ed è almeno stabile per piccole quantità di input.

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);
}

Funziona principalmente, ma è ancora in qualche modo incline agli overflow dello stack. Prova a takeingaggiare 2 miliardi di ints e ad agire di conseguenza. : P Alla fine genererà un'eccezione, almeno fino a quando 64+ GB di RAM diventeranno standard. Il problema è che la quantità di memoria di un programma riservata per il suo stack non è così grande. In genere è compreso tra 1 e 8 MiB. (Si può chiedere per i più grandi, ma non importa più di tanto di quanto si chiede - si chiama take(1000000000, someInfiniteSequence), si avrà . Ottenere un'eccezione) Fortunatamente, con valutazione pigra, il punto debole è in una zona che possiamo migliorare il controllo . Dobbiamo solo stare attenti a quanto take().

Avrà ancora molti problemi a scalare, perché il nostro utilizzo dello stack aumenta in modo lineare. Ogni chiamata gestisce un elemento e passa il resto a un'altra chiamata. Ora che ci penso, però, c'è un trucco che possiamo ottenere che potrebbe farci guadagnare un po 'più di margine: trasformare la catena di chiamate in un albero di chiamate. Considera qualcosa di più simile a questo:

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);
}

workWithfondamentalmente suddivide il lavoro in due metà e assegna ciascuna metà a un'altra chiamata a se stessa. Poiché ogni chiamata riduce la dimensione della lista di lavoro della metà anziché di una, ciò dovrebbe ridimensionare logaritmicamente anziché linearmente.

Il problema è che questa funzione richiede un input e, con un elenco collegato, per ottenere la lunghezza è necessario attraversare l'intero elenco. Questo è facilmente risolto, però; semplicemente non importa quante voci ci sono. :) Il codice sopra funzionerebbe con qualcosa come Integer.MAX_VALUEil conteggio, poiché un null interrompe comunque l'elaborazione. Il conteggio è per lo più lì, quindi abbiamo un solido caso di base. Se prevedi di avere più di Integer.MAX_VALUEvoci in un elenco, puoi controllare workWithil valore di ritorno - alla fine dovrebbe essere nullo. Altrimenti, recluta.

Ricorda, questo tocca tutti gli elementi che dici. Non è pigro; fa immediatamente la sua cosa. Vuoi farlo solo per azioni , cioè cose il cui unico scopo è applicarsi a ogni elemento in un elenco. Mentre ci sto pensando proprio ora, mi sembra che le sequenze sarebbero molto meno complicate se mantenute lineari; non dovrebbe essere un problema, dal momento che le sequenze non si chiamano comunque: creano semplicemente oggetti che le richiamano di nuovo.


3

In precedenza ho provato a creare un interprete per un linguaggio simile a lisp in Java, (alcuni anni fa e tutto il codice è stato perso come in CVS su sourceforge), e gli iteratori di utilizzo Java sono un po 'prolissi per la programmazione funzionale sugli elenchi.

Ecco qualcosa basato su un'interfaccia di sequenza, che ha solo le due operazioni necessarie per ottenere il valore corrente e ottenere la sequenza a partire dall'elemento successivo. Questi sono chiamati testa e coda dopo le funzioni nello schema.

È importante usare qualcosa come l' interfaccia Seqo Iteratorperché significa che l'elenco è stato creato pigramente. L' Iteratorinterfaccia non può essere un oggetto immutabile, quindi è meno adatta alla programmazione funzionale: se non riesci a capire se il valore che passi in una funzione è stato modificato da essa, perdi uno dei vantaggi chiave della programmazione funzionale.

Ovviamente integersdovrebbe essere un elenco di tutti i numeri interi, quindi ho iniziato da zero e alternativamente ho restituito quelli positivi e negativi.

Esistono due versioni di quadrati: uno che crea una sequenza personalizzata, l'altro mapche utilizza una "funzione" - Java 7 non ha lambda quindi ho usato un'interfaccia - e la applica a sua volta a ciascun elemento della sequenza.

Il punto della square ( int x )funzione è solo quello di rimuovere la necessità di chiamare head()due volte - normalmente l'avrei fatto inserendo il valore in una variabile finale, ma aggiungendo questa funzione significa che non ci sono variabili nel programma, solo parametri di funzione.

La verbosità di Java per questo tipo di programmazione mi ha portato a scrivere la seconda versione del mio interprete in 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

Come scrivere utili programmi Java senza usare variabili mutabili.

In teoria puoi implementare praticamente qualsiasi cosa in Java usando solo la ricorsione e nessuna variabile mutabile.

In pratica:

  • Il linguaggio Java non è progettato per questo. Molti costrutti sono progettati per la mutazione e sono difficili da usare senza di essa. (Ad esempio, non è possibile inizializzare un array Java a lunghezza variabile senza mutazione.)

  • Idem per le biblioteche. E se ti limiti alle lezioni in biblioteca che non usano la mutazione sotto la copertina, è ancora più difficile. (Non puoi nemmeno usare String ... dai un'occhiata a come hashcodeviene implementato.)

  • Le implementazioni Java mainstream non supportano l'ottimizzazione delle chiamate di coda. Ciò significa che le versioni ricorsive degli algoritmi tendono ad essere "affamate" di spazio nello stack. E poiché gli stack di thread Java non crescono, è necessario preallocare stack di grandi dimensioni ... o rischiare StackOverflowError.

Combina queste tre cose e Java non è in realtà un'opzione praticabile per scrivere programmi utili (cioè non banali) senza variabili mutabili.

(Ma hey, va bene. Ci sono altri linguaggi di programmazione disponibili per JVM, alcuni dei quali supportano la programmazione funzionale.)


2

Mentre stiamo cercando un esempio dei concetti, direi che mettiamo da parte Java e cerchiamo un ambiente diverso ma familiare in cui trovare una versione familiare dei concetti. Le pipe UNIX sono piuttosto simili al concatenamento di funzioni pigre.

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

In Linux questo significa, dammi byte ciascuno dei quali è composto da bit falsi anziché veri, finché non perdo l'appetito; cambia ciascuno di quei byte in un carattere di nuova riga; numerare ogni riga così creata; e produce il quadrato di quel numero. Inoltre ho appetito per 25 righe e non di più.

Sostengo che un programmatore non sarebbe mal consigliato di scrivere una pipeline Linux in quel modo. Sono relativamente normali script di shell Linux.

Sostengo che un programmatore sarebbe sconsigliato di provare a scrivere la stessa cosa in modo simile in Java. Il motivo è la manutenzione del software come fattore principale nel costo della vita dei progetti software. Non vogliamo confondere il prossimo programmatore presentando quello che è apparentemente un programma Java, ma in realtà è scritto in effetti in un linguaggio unico personalizzato duplicando elaboratamente funzionalità già esistenti nella piattaforma Java.

D'altro canto, sostengo che il prossimo programmatore potrebbe accettare di più se alcuni dei nostri pacchetti "Java" sono in realtà pacchetti Java Virtual Machine scritti in uno dei linguaggi funzionali o oggetto / funzionale come Clojure e Scala. Questi sono progettati per essere codificati concatenando funzioni insieme e per essere chiamati da Java nel modo normale delle chiamate al metodo Java.

Inoltre, può essere comunque una buona idea per un programmatore Java prendere ispirazione dalla programmazione funzionale, in alcuni punti.

Recentemente la mia tecnica preferita [era] quella di utilizzare una variabile di ritorno immutabile, non inizializzata e una singola uscita in modo che, come fanno alcuni compilatori di linguaggi funzionali, Java verifica che, indipendentemente da ciò che accade nel corpo della funzione, fornisco sempre una sola valore di ritorno. Esempio:

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;
}


Sono sicuro che circa il 70% Java già fa già il controllo del valore di ritorno. Dovresti ricevere un errore su una "dichiarazione di ritorno mancante" se il controllo può cadere dalla fine di una funzione non nulla.
cHao,

Il mio punto: se lo codifichi mentre int result = -n; if (n < 1) { result = 0 } return result;si compila bene e il compilatore non ha idea se intendevi renderlo equivalente alla funzione nel mio esempio. Forse quell'esempio è troppo semplice per rendere utile la tecnica, ma in una funzione con molti rami mi sembra bello chiarire che il risultato viene assegnato esattamente una volta indipendentemente dal percorso seguito.
minopret,

Se dici if (n < 1) return 0; else return -n;, però, finisci senza problemi ... ed è inoltre più semplice. :) Mi sembra che in quel caso, la regola "un ritorno" aiuti effettivamente a causare il problema di non sapere quando è stato impostato il valore di ritorno; altrimenti, potresti semplicemente restituirlo e Java sarebbe più in grado di determinare quando altri percorsi potrebbero non restituire un valore, perché non stai più separando il calcolo del valore dall'effettivo ritorno di esso.
cHao,

Oppure, per esempio di vostra risposta, if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
cHao,

Ho appena deciso di non voler passare altri momenti della mia vita a difendere la regola OnlyOneReturn in Java. Fuori va. Quando e se penso a una pratica di codifica Java che mi sento di difendere influenzata da pratiche di programmazione funzionale, inserirò un sostituto per quell'esempio. Fino ad allora, nessun esempio.
minopret,

0

Il modo più semplice per scoprirlo sarebbe quello di fornire al compilatore Frege quanto segue e guardare il codice java generato:

module Main where

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

Dopo alcuni giorni ho trovato i miei pensieri tornando a questa risposta. Dopo tutto parte del mio suggerimento era di implementare le parti di programmazione funzionale in Scala. Se consideriamo l'applicazione di Scala in quei punti in cui avevamo in mente Haskell (e penso di non essere l'unico blog.zlemma.com/2013/02/20/… ), non dovremmo almeno considerare Frege?
minopret,

@minopret Questa è davvero la nicchia che Frege è tragica - persone che hanno imparato a conoscere e ad amare Haskell e che tuttavia hanno bisogno della JVM. Sono fiducioso che un giorno Frege sarà abbastanza maturo per prendere almeno una seria considerazione.
Ingo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.