Cómo escribir programas Java útiles sin usar variables mutables

12

Estaba leyendo un artículo sobre programación funcional donde el escritor afirma

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

Tenga en cuenta que no tiene variables. De hecho, no tiene más que tres funciones y una constante. Intenta escribir los cuadrados de los enteros en Java sin usar una variable. Oh, probablemente haya una manera de hacerlo, pero ciertamente no es natural, y no se leería tan bien como mi programa anterior.

¿Es posible lograr esto en Java? Suponiendo que deba imprimir los cuadrados de los primeros 15 enteros, ¿podría escribir un bucle for o while sin usar variables?

Aviso de modificación

Esta pregunta no es un concurso de golf de código. Estamos buscando respuestas que expliquen los conceptos involucrados (idealmente sin repetir las respuestas anteriores), y no solo para otra pieza de código.

menosSeven
fuente
19
Su ejemplo funcional usa variables en el interior, pero el lenguaje lo hace todo detrás de escena. Has delegado efectivamente las partes desagradables a alguien que crees que lo ha hecho correctamente.
Blrfl
12
@Blrfl: El argumento "detrás de escena" mata todos los debates basados ​​en el lenguaje, ya que cada pieza de código se traduce finalmente en código máquina x86. El código x86 no está orientado a objetos, no es de procedimiento, no es funcional, no es nada, pero estas categorías son etiquetas valiosas para los lenguajes de programación. Mire el lenguaje, no la implementación.
thiton
10
@thiton no estuvo de acuerdo. Lo que dice Blrfl es que esas funciones probablemente usan variables escritas en el mismo lenguaje de programación . No hay necesidad de ir de bajo nivel aquí. El fragmento simplemente utiliza funciones de biblioteca. Puede escribir fácilmente el mismo código en Java, vea: squaresOf(integers()).take(25)(escribir esas funciones se deja como un ejercicio para el lector; la dificultad radica en el conjunto infinito integers(), pero eso es un problema para Java debido a su evaluación entusiasta, nada que ver con variables)
Andres F.
66
Esa cita es confusa y engañosa, no hay magia allí, solo azúcar sintáctico .
yannis
2
@thiton Le sugiero que aprenda más sobre los lenguajes FP, pero sin embargo, el fragmento de código no admite (o rechaza) la afirmación de que no se necesitan "variables" (por lo que supongo que se refiere a "variables mutables", porque el otro kind es común en FP). El fragmento solo muestra las funciones de la biblioteca que también podrían haberse implementado en Java, salvo los problemas vagos / ansiosos que son poco comunes aquí.
Andres F.

Respuestas:

31

¿Es posible implementar tal ejemplo en Java sin usar actualizaciones destructivas? Si. Sin embargo, como mencionó @Thiton y el artículo en sí, será feo (dependiendo del gusto de cada uno). Una forma es usar la recursividad; Aquí hay un ejemplo de Haskell que hace algo similar:

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 falta de mutación, 2) el uso de recursión, y 3) la falta de bucles. El último punto es muy importante: los lenguajes funcionales no necesitan construcciones de bucle incorporadas en el lenguaje, ya que la recursión se puede usar para la mayoría de los casos (¿todos?) En los que se usan bucles en Java. Aquí hay una serie de documentos bien conocidos que muestran cuán increíblemente expresivas pueden ser las llamadas a funciones.


El artículo me pareció insatisfactorio y me gustaría hacer un par de puntos adicionales:

Ese artículo es una explicación muy pobre y confusa de la programación funcional y sus beneficios. Recomiendo encarecidamente otras fuentes para aprender sobre programación funcional.

La parte más confusa sobre el artículo es que no menciona que hay dos usos para las declaraciones de asignación en Java (y la mayoría de los otros lenguajes principales):

  1. enlazar un valor a un nombre: final int MAX_SIZE = 100;

  2. actualización destructiva: int a = 3; a += 1; a++;

La programación funcional evita el segundo, pero abarca el primero (ejemplos: let-expresiones, parámetros de función, defineitiones de nivel superior ) . Este es un punto muy importante de entender, porque de lo contrario el artículo sólo parece tonto y podría dejarle con la duda, lo son take, squares-ofy integerssi no las variables?

Además, el ejemplo no tiene sentido. No muestra las implementaciones de take, squares-ofo integers. Por lo que sabemos, se implementan utilizando variables mutables. Como dijo @Martin, puedes escribir trivialmente este ejemplo en Java.

Una vez más, recomendaría evitar este artículo y otros similares si realmente desea aprender sobre programación funcional. Parece estar escrito más con el objetivo de sorprender y ofender en lugar de enseñar conceptos y fundamentos. En cambio, ¿por qué no lees uno de mis papeles favoritos de todos los tiempos , de John Hughes? Hughes intenta abordar algunos de los mismos problemas que cubrió el artículo (aunque Hughes no habla sobre concurrencia / paralelización); Aquí hay un adelanto:

Este documento es un intento de demostrar a la comunidad más grande de programadores (no funcionales) la importancia de la programación funcional, y también para ayudar a los programadores funcionales a aprovechar al máximo sus ventajas al dejar en claro cuáles son esas ventajas.

[...]

Argumentaremos en el resto de este documento que los lenguajes funcionales proporcionan dos nuevos tipos de pegamento muy importantes. Daremos algunos ejemplos de programas que se pueden modularizar de nuevas maneras y, por lo tanto, se pueden simplificar. Esta es la clave de la potencia de la programación funcional: permite una mejor modularización. También es el objetivo por el que deben esforzarse los programadores funcionales: módulos más pequeños, más simples y más generales, unidos con los nuevos pegamentos que describiremos.


fuente
10
+1 para "Recomendaría evitar este artículo y otros similares si realmente quieres aprender sobre programación funcional. Parece que está escrito más con el objetivo de sorprender y ofender en lugar de enseñar conceptos y fundamentos".
3
La mitad de la razón por la que las personas no hacen FP es porque no escuchan / aprenden nada al respecto en uni, y la otra mitad es porque cuando lo examinan encuentran artículos que los dejan a ambos desinformados y piensan que todo es para algunos fantasiosos jugando en lugar de ser un enfoque razonado pensado con beneficios. +1 por dar mejores fuentes de información
Jimmy Hoffa
3
Ponga su respuesta a la pregunta en la parte superior absoluta si quisiera, de modo que sea más directa a la pregunta y tal vez esta pregunta permanezca abierta entonces (con una respuesta directa centrada en la pregunta)
Jimmy Hoffa
2
Perdón por molestar, pero no entiendo por qué elegiste este código haskell. He leído LYAH y tu ejemplo es difícil de entender. Tampoco veo la relación con la pregunta original. ¿Por qué no lo usaste solo take 25 (map (^2) [1..])como ejemplo?
Daniel Kaplan
2
@tieTYT buena pregunta: gracias por señalar esto. La razón por la que usé ese ejemplo es porque muestra cómo generar una lista de números usando recursión y evitando variables mutables. Mi intención era que el OP viera ese código y pensara en cómo hacer algo similar en Java. Para abordar su fragmento de código, ¿qué es [1..]? Esa es una característica genial integrada en Haskell, pero no ilustra los conceptos detrás de la generación de dicha lista. Estoy seguro de que las instancias de la Enumclase (que requiere esa sintaxis) también son útiles, pero fue demasiado vago para encontrarlas. Por lo tanto, unfoldr. :)
27

No lo harías Las variables están en el centro de la programación imperativa, y si intentas programar imperativamente sin usar variables, simplemente estás causando molestias a todos. En diferentes paradigmas de programación, los estilos son diferentes, y diferentes conceptos forman su base. Una variable en Java, cuando se usa bien con un alcance pequeño, no es mala. Pedir un programa Java sin variables es como pedir un programa Haskell sin funciones, por lo que no lo solicita y no se deja engañar al ver la programación imperativa como inferior porque usa variables.

Entonces, la forma Java sería:

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

y no te dejes engañar para escribirlo de una manera más compleja debido al odio a las variables.

thiton
fuente
55
¿"Odio a las variables"? Ooookay ... ¿Qué has leído sobre la programación funcional? ¿Qué idiomas has probado? ¿Qué tutoriales?
Andres F.
8
@AndresF .: Más de dos años de cursos en Haskell. No digo que FP sea malo. Sin embargo, existe una tendencia en muchas discusiones FP-vs-IP (como el artículo vinculado) para condenar el uso de entidades nombradas reasignables (variables AKA), y para condenar sin una buena razón o datos. La condena irrazonable es odio en mi libro. Y el odio hace que el código sea realmente malo.
thiton
10
"El odio de variables" es causal simplificación en.wikipedia.org/wiki/Fallacy_of_the_single_cause hay muchos beneficios para la programación sin estado que incluso podría ser tenido en Java, aunque estoy de acuerdo con su respuesta que en Java el costo sería demasiado alto en la complejidad de el programa y no ser idiomático. Todavía no iría con la mano descartando la idea de que la programación sin estado es buena y con estado es mala, ya que una respuesta emocional en lugar de una postura razonada y bien pensada que la gente ha llegado debido a la experiencia.
Jimmy Hoffa
2
En línea con lo que dice @JimmyHoffa, lo referiré a John Carmack sobre el tema de la programación de estilo funcional en lenguajes imperativos (C ++ en su caso) ( altdevblogaday.com/2012/04/26/functional-programming-in-c )
Steven Evers
55
La condena irrazonable no es odio, y evitar el estado mutable no es irrazonable.
Michael Shaw
21

Lo más simple que puedo hacer con la recursión es una función con un parámetro. No es muy Java-esque, pero 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);
        }
    }
}
FrustratedWithFormsDesigner
fuente
3
+1 por intentar responder la pregunta con un ejemplo de Java.
KChaloux
Lo rechazaría para la presentación de código de estilo de golf (ver el aviso de modificación ) pero no puedo obligarme a presionar la flecha hacia abajo porque este código coincide perfectamente con las declaraciones hechas en mi respuesta favorita : "1) la falta de mutación, 2) el uso de recurrencia, y 3) la falta de bucles "
mosquito
3
@gnat: esta respuesta se publicó antes del aviso de modificación. No buscaba un gran estilo, buscaba simplicidad y satisfacía la pregunta original del OP; para ilustrar que es posible hacer tales cosas en Java.
FrustratedWithFormsDesigner
@FrustratedWithFormsDesigner seguro; esto no me detendría de DVing (ya que se supone que eres capaz de editar para cumplir): es la combinación sorprendentemente perfecta que hizo la magia. Bien hecho, muy bien hecho, bastante educativo - gracias
mosquito
16

En su ejemplo funcional, no vemos cómo se implementan las funciones squares-ofy take. No soy un experto en Java, pero estoy bastante seguro de que podríamos escribir esas funciones para habilitar una declaración como esta ...

squares_of(integers).take(25);

que no es muy diferente

Martín
fuente
66
Nitpick: squares-ofno es un nombre válido en Java ( squares_ofaunque sí lo es). Pero por lo demás, buen punto que muestra que el ejemplo del artículo es pobre.
Sospecho que el artículo integergenera perezosamente enteros, y la takefunción selecciona 25 de los squared-ofnúmeros integer. En resumen, debe tener una integerfunción para producir perezosamente enteros hasta el infinito.
Onésimo Sin
Es un poco loco llamar a algo así como (integer)una función: una función sigue siendo algo que asigna un argumento a un valor. Resulta que (integer)no es una función, sino simplemente un valor. Incluso se podría ir tan lejos como para decir que integeres una variable que está vinculada a un número infinito de números.
Ingo
6

En Java podría hacer esto (especialmente la parte de la lista infinita) con iteradores. En el siguiente ejemplo de código, el número suministrado al Takeconstructor puede 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());
    }
}

O con métodos de fábrica encadenables:

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

Dónde SquaresOf, Takey IntegersextenderNumbers

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() {}
}
artistoex
fuente
1
Esto muestra la superioridad del paradigma OO sobre el funcional; con un diseño OO adecuado, puede imitar el paradigma funcional pero no puede imitar el paradigma OO en un estilo funcional.
m3th0dman
3
@ m3th0dman: con un diseño OO adecuado, posiblemente pueda imitar a medias a FP, al igual que cualquier lenguaje que tenga cadenas, listas y / o diccionarios podría imitar a medias a OO. La equivalencia de Turing de los lenguajes de uso general significa que, con un esfuerzo suficiente, cualquier lenguaje puede simular las características de cualquier otro.
cHao
Tenga en cuenta que los iteradores de estilo Java como in while (test.hasNext()) System.out.println(test.next())serían un no-no en FP; Los iteradores son inherentemente mutables.
cHao
1
@ cHao Apenas creo que se pueda imitar la verdadera encapsulación o polimorfismo; También Java (en este ejemplo) no puede realmente imitar un lenguaje funcional debido a la estricta evaluación entusiasta. También creo que los iteradores se pueden escribir de forma recursiva.
m3th0dman
@ m3th0dman: el polimorfismo no sería difícil de imitar; incluso C y lenguaje ensamblador pueden hacerlo. Simplemente haga que el método sea un campo en el objeto o un descriptor de clase / vtable. Y la encapsulación en el sentido de ocultación de datos no es estrictamente necesaria; la mitad de los idiomas no lo proporcionan, cuando su objeto es inmutable, no importa tanto si la gente puede ver sus entrañas de todos modos. todo lo que se necesita es el ajuste de datos , que los campos de método mencionados anteriormente podrían proporcionar fácilmente.
cHao
6

Version corta:

Para que el estilo de asignación única funcione de manera confiable en Java, necesitaría (1) algún tipo de infraestructura amigable inmutable, y (2) soporte a nivel de tiempo de ejecución o compilador para la eliminación de llamadas de cola.

Podemos escribir gran parte de la infraestructura y podemos organizar cosas para tratar de evitar llenar la pila. Pero siempre que cada llamada tome un marco de pila, habrá un límite en la cantidad de recursión que puede hacer. Mantenga sus iterables pequeños y / o flojos, y no debería tener problemas importantes. Al menos la mayoría de los problemas con los que se encontrará no requieren devolver un millón de resultados a la vez. :)

También tenga en cuenta que, dado que el programa debe efectuar cambios visibles para que valga la pena ejecutarlo, no puede hacer que todo sea inmutable. Sin embargo, puede mantener la gran mayoría de sus propias cosas inmutables, utilizando un pequeño subconjunto de mutables esenciales (secuencias, por ejemplo) solo en ciertos puntos clave donde las alternativas serían demasiado onerosas.


Versión larga:

En pocas palabras, un programa Java no puede evitar totalmente las variables si quiere hacer algo que valga la pena. Puede contenerlos y, por lo tanto, restringir la mutabilidad en gran medida, pero el diseño mismo del lenguaje y la API, junto con la necesidad de cambiar eventualmente el sistema subyacente, hace que la inmutabilidad total sea inviable.

Java fue diseñado desde el principio como un imperativo , orientado a objetos lenguaje.

  • Los lenguajes imperativos casi siempre dependen de variables mutables de algún tipo. Tienden a favorecer la iteración sobre la recursividad, por ejemplo, y casi todas las construcciones iterativas, incluso while (true)y for (;;)! - dependen por completo de una variable en algún lugar que cambia de iteración a iteración.
  • Los lenguajes orientados a objetos visualizan prácticamente cada programa como un gráfico de objetos que se envían mensajes entre sí y, en casi todos los casos, responden a esos mensajes mutando algo.

El resultado final de esas decisiones de diseño es que sin variables mutables, Java no tiene forma de cambiar el estado de nada, incluso algo tan simple como imprimir "¡Hola, mundo!" a la pantalla implica una secuencia de salida, que implica pegar bytes en un búfer mutable .

Entonces, a todos los efectos prácticos, estamos limitados a desterrar las variables de nuestro propio código. OK, podemos hacer eso. Casi. Básicamente, lo que necesitaríamos es reemplazar casi todas las iteraciones por recursividad, y todas las mutaciones con llamadas recursivas que devuelven el valor cambiado. al igual que...

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

Básicamente, construimos una lista vinculada, donde cada nodo es una lista en sí misma. Cada lista tiene una "cabeza" (el valor actual) y una "cola" (la sublista restante). La mayoría de los lenguajes funcionales hacen algo similar a esto, porque es muy susceptible a la inmutabilidad eficiente. Una operación "siguiente" simplemente devuelve la cola, que generalmente se pasa al siguiente nivel en una pila de llamadas recursivas.

Ahora, esta es una versión extremadamente simplificada de estas cosas. Pero es lo suficientemente bueno como para demostrar un problema grave con este enfoque en Java. Considera este código:

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

Aunque solo necesitamos 25 pulgadas para el resultado, squares_ofno lo sabemos. Va a devolver el cuadrado de cada número integers. La recursión de 20 millones de niveles de profundidad causa problemas bastante grandes en Java.

Mire, los lenguajes funcionales en los que normalmente se comportaría de esta manera tienen una función llamada "eliminación de llamadas de cola". Lo que eso significa es que, cuando el compilador ve que el último acto del código es llamarse a sí mismo (y devolver el resultado si la función no es nula), usa el marco de la pila de la llamada actual en lugar de configurar uno nuevo y en su lugar hace un "salto". de una "llamada" (por lo que el espacio de pila utilizado permanece constante). En resumen, representa aproximadamente el 90% del camino para convertir la recursión de cola en iteración. Podría lidiar con esos mil millones de pulgadas sin desbordar la pila. (Con el tiempo, se quedaría sin memoria, pero reunir una lista de mil millones de ints te va a estropear la memoria de todos modos en un sistema de 32 bits).

Java no hace eso, en la mayoría de los casos. (Depende del compilador y del tiempo de ejecución, pero la implementación de Oracle no lo hace). Cada llamada a una función recursiva consume la memoria de un marco de pila. Usa demasiado y obtienes un desbordamiento de pila. Desborda la pila, pero garantiza la muerte del programa. Así que tenemos que asegurarnos de no hacer eso.

Una semi-solución ... evaluación perezosa. Todavía tenemos las limitaciones de la pila, pero pueden estar vinculadas a factores sobre los que tenemos más control. No tenemos que calcular un millón de pulgadas solo para devolver 25. :)

Así que construyamos una infraestructura de evaluación perezosa. (Este código se probó hace un tiempo, pero lo he modificado bastante desde entonces; lea la idea, no los errores de sintaxis. :))

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

(Tenga en cuenta que si esto fuera realmente viable en Java, un código al menos similar al anterior ya sería parte de la API).

Ahora, con una infraestructura en su lugar, es bastante trivial escribir código que no necesita variables mutables y que al menos sea estable para cantidades de entrada más pequeñas.

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

Esto funciona principalmente, pero todavía es algo propenso a desbordamientos de pila. Trate takeing 2 mil millones de enteros y haciendo algunas medidas al respecto. : P Eventualmente arrojará una excepción, al menos hasta que más de 64 GB de RAM se conviertan en estándar. El problema es que la cantidad de memoria de un programa que está reservada para su pila no es tan grande. Es típicamente entre 1 y 8 MiB. (Puede solicitar más grande, pero eso no importa tanto la cantidad que pide - se llama take(1000000000, someInfiniteSequence), que va . Obtener una excepción) Afortunadamente, la evaluación perezosa, el punto débil se encuentra en una zona podemos controlar mejor . Solo tenemos que tener cuidado con la cantidad que tenemos take().

Todavía tendrá muchos problemas para ampliar, porque nuestro uso de pila aumenta linealmente. Cada llamada maneja un elemento y pasa el resto a otra llamada. Ahora que lo pienso, sin embargo, hay un truco que podemos hacer que podría ganarnos un poco más de margen: convertir la cadena de llamadas en un árbol de llamadas. Considere algo más como esto:

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

workWithbásicamente divide el trabajo en dos mitades y asigna cada mitad a otra llamada a sí mismo. Como cada llamada reduce el tamaño de la lista de trabajo a la mitad en lugar de uno, esto debería escalar logarítmicamente en lugar de linealmente.

El problema es que esta función quiere una entrada, y con una lista vinculada, obtener la longitud requiere recorrer toda la lista. Sin embargo, eso se resuelve fácilmente; simplemente no me importa cuántas entradas hay. :) El código anterior funcionaría con algo Integer.MAX_VALUEcomo el recuento, ya que un valor nulo detiene el procesamiento de todos modos. El conteo está principalmente allí, así que tenemos un caso base sólido. Si prevé tener más de Integer.MAX_VALUEentradas en una lista, puede verificar workWithel valor de retorno, que debe ser nulo al final. De lo contrario, recurse.

Tenga en cuenta que esto toca tantos elementos como usted le indique. No es vago; hace lo suyo de inmediato. Solo desea hacerlo para acciones , es decir, cosas cuyo único propósito es aplicarse a cada elemento de una lista. Como lo estoy pensando ahora, me parece que las secuencias serían mucho menos complicadas si se mantuvieran lineales; no debería ser un problema, ya que las secuencias no se llaman a sí mismas de todos modos, solo crean objetos que las llaman de nuevo.

cHao
fuente
3

Anteriormente intenté crear un intérprete para un lenguaje similar a lisp en Java (hace unos años y todo el código se perdió como estaba en CVS en sourceforge), y los iteradores de utilidades de Java son un poco detallados para la programación funcional en las listas

Aquí hay algo basado en una interfaz de secuencia, que solo tiene las dos operaciones que necesita para obtener el valor actual y comenzar la secuencia en el siguiente elemento. Estos se denominan cabeza y cola después de las funciones en el esquema.

Es importante usar algo como las interfaces Seqo Iteratorya que significa que la lista se crea perezosamente. La Iteratorinterfaz no puede ser un objeto inmutable, por lo que es menos adecuada para la programación funcional: si no puede saber si el valor que pasa a una función ha cambiado, pierde una de las principales ventajas de la programación funcional.

Obviamente integersdebería ser una lista de todos los enteros, así que comencé en cero y alternativamente devolví los positivos y negativos.

Hay dos versiones de cuadrados: uno crea una secuencia personalizada y el otro usa mapuna 'función': Java 7 no tiene lambdas, por lo que utilicé una interfaz, y la aplica a cada elemento de la secuencia.

El objetivo de la square ( int x )función es solo eliminar la necesidad de llamar head()dos veces; normalmente lo hubiera hecho al poner el valor en una variable final, pero agregar esta función significa que no hay variables en el programa, solo parámetros de la función.

La verbosidad de Java para este tipo de programación me llevó a escribir la segunda versión de mi intérprete en 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() );
        }
    }
}
Pete Kirkham
fuente
3

Cómo escribir programas Java útiles sin usar variables mutables.

En teoría, puede implementar casi cualquier cosa en Java utilizando solo recursividad y sin variables mutables.

En la práctica:

  • El lenguaje Java no está diseñado para esto. Muchas construcciones están diseñadas para la mutación y son difíciles de usar sin ella. (Por ejemplo, no puede inicializar una matriz Java de longitud variable sin mutación).

  • Lo mismo para las bibliotecas. Y si te limitas a clases de biblioteca que no usan mutación debajo de la tapa, es aún más difícil. (Ni siquiera puedes usar String ... mira cómo hashcodese implementa).

  • Las implementaciones principales de Java no admiten la optimización de llamadas de cola. Eso significa que las versiones recursivas de algoritmos tienden a ser espacio de pila "hambriento". Y como las pilas de subprocesos de Java no crecen, debe preasignar grandes pilas ... o arriesgarse StackOverflowError.

Combine estas tres cosas, y Java no es realmente una opción viable para escribir programas útiles (es decir, no triviales) sin variables mutables.

(Pero bueno, está bien. Hay otros lenguajes de programación disponibles para la JVM, algunos de los cuales admiten la programación funcional).

Stephen C
fuente
2

Como estamos buscando un ejemplo de los conceptos, diría que dejemos de lado Java y busquemos una configuración diferente pero familiar en la que encontrar una versión familiar de los conceptos. Las tuberías UNIX son bastante similares a encadenar funciones perezosas.

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

En Linux, esto significa, dame bytes, cada uno de los cuales está compuesto de bits falsos en lugar de verdaderos, hasta que pierda el apetito; cambie cada uno de esos bytes a un carácter de nueva línea; numerar cada línea así creada; y produce el cuadrado de ese número. Además tengo apetito por 25 líneas y no más.

Afirmo que un programador no estaría mal aconsejado para escribir una tubería de Linux de esa manera. Es una secuencia de comandos de shell de Linux relativamente normal.

Afirmo que un programador no debería intentar escribir lo mismo de manera similar en Java. La razón es el mantenimiento del software como un factor importante en el costo de por vida de los proyectos de software. No queremos confundir al próximo programador presentando lo que aparentemente es un programa Java pero que en realidad está escrito en un lenguaje único personalizado al duplicar elaboradamente la funcionalidad que ya existe en la plataforma Java.

Por otro lado, afirmo que el próximo programador podría aceptar más si algunos de nuestros paquetes "Java" en realidad son paquetes de Java Virtual Machine escritos en uno de los lenguajes funcionales u objetos / funcionales como Clojure y Scala. Estos están diseñados para codificarse mediante el encadenamiento de funciones y para ser llamados desde Java de la manera normal de las llamadas a métodos Java.

Por otra parte, aún puede ser una buena idea que un programador de Java se inspire en la programación funcional, en algunos lugares.

Recientemente, mi técnica favorita [era] usar una variable de retorno inmutable, no inicializada y una única salida para que, como hacen algunos compiladores de lenguaje funcional, Java compruebe que no importa lo que ocurra en el cuerpo de la función, siempre proporciono una y solo una valor de retorno Ejemplo:

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

minopret
fuente
Estoy aproximadamente 70% seguro de que Java ya realiza la comprobación del valor de retorno de todos modos. Debería recibir un error sobre una "declaración de retorno faltante" si el control puede caerse al final de una función no nula.
cHao
Mi punto: si lo codifica, ya que int result = -n; if (n < 1) { result = 0 } return result;se compila muy bien y el compilador no tiene idea de si tenía la intención de hacerlo equivalente a la función en mi ejemplo. Tal vez ese ejemplo es demasiado simple para hacer que la técnica parezca útil, pero en una función con muchas ramas, creo que es bueno dejar en claro que el resultado se asigna exactamente una vez, independientemente de la ruta que se siga.
minopret
Sin if (n < 1) return 0; else return -n;embargo, si dices , terminas sin ningún problema ... y además es más simple. :) Me parece que en ese caso, la regla de "un retorno" en realidad ayuda a causar el problema de no saber cuándo se estableció el valor de retorno; de lo contrario, podría devolverlo y Java sería más capaz de determinar cuándo otras rutas podrían no devolver un valor, porque ya no se está divorciando del cálculo del valor de la devolución real del mismo.
cHao
O, por ejemplo, de su respuesta, if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
cHao
Simplemente decidí que no quiero pasar más momentos de mi vida defendiendo la regla OnlyOneReturn en Java. Fuera se va. Cuando y si pienso en una práctica de codificación de Java que tengo ganas de defender y que está influenciada por prácticas de programación funcional, pondré un reemplazo para ese ejemplo. Hasta entonces, no hay ejemplo.
minopret
0

La forma más fácil de averiguarlo sería enviar lo siguiente al compilador de Frege y observar el código Java generado:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x
Ingo
fuente
Después de unos días descubrí que mis pensamientos volvían a esta respuesta. Después de todo, mi sugerencia fue implementar las partes de programación funcional en Scala. Si consideramos aplicar Scala en esos lugares donde realmente teníamos a Haskell en mente (y creo que no soy el único blog.zlemma.com/2013/02/20/… ), ¿no deberíamos al menos considerar a Frege?
minopret
@minopret Este es de hecho el nicho que Frege está rastreando: personas que han llegado a conocer y amar a Haskell y aún así necesitan la JVM. Estoy seguro de que algún día Frege será lo suficientemente maduro como para tener una consideración seria al menos.
Ingo