Cómo aprender Haskell me ayudó a entender flatMap en Java

Sobre mí

Soy estudiante universitario, graduado de programador y desarrollador de software Java

Por qué quería aprender Haskell ayudando a entender flatMap en Java

Quería aplicar la Programación Funcional en mis proyectos del día a día.

Cómo me acerqué a aprender Haskell ayudando a comprender flatMap en Java

Empecé a aprender Haskell en la universidad, luego con un amigo, finalmente por mi cuenta y StackOverflow. Sabía que estos conceptos serían útiles algún día y podrían aplicarse con una calculadora simple en Haskell:

data Expr = Val Int | Div Expr Expr

eval :: Expr -> Int
eval (Val n) = n
eval (Div x y) = div (eval x) (eval y)

n1 = Val 8
n2 = Val 4
n3 = Val 0 
d1 = Div n1 n2
d2 = Div d1 d1
d3 = Div d2 n3

main = do
  putStrLn $ show (eval d2)

Se imprimirá como se puede imaginar, “1”. Pero este ejemplo obviamente me lleva al caso de “División cero”, y una forma simple de emularlo es con el tipo Quizás:

data Expr = Val Int | Div Expr Expr

eval :: Expr -> Maybe Int
eval (Val n) = Just n
eval (Div x y) = do
                  v1 <- eval x
                  v2 <- eval y
                  if v2 == 0 
                  then Nothing
                  else return (div v1 v2)
n1 = Val 8
n2 = Val 4
n3 = Val 0 
d1 = Div n1 n2
d2 = Div d1 d1
d3 = Div d2 n3

main = do
  putStrLn $ show (eval d2)
  putStrLn $ show (eval d3)

¡Aquí vamos! ahora tenemos la salida:

Just 1
Nothing

Desafíos que enfrenté

Ahora, ¿cómo traducir este ejemplo a código Java?

Primero: creamos nuestro método en una interfaz:

import java.util.Optional;
public interface Expr {

    public Optional<Integer> eval();

}

Luego necesitamos los casos para Val y Expr, vamos primero con el fácil, Val:

import java.util.Optional;

public class Val implements Expr{

    Optional<Integer> value;

    public Val(int value) {
        this.value = Optional.of(value);
    }

    @Override
    public Optional<Integer> eval() {
        return value;
    }
}

Ahora viene el pequeño giro, ¿cómo crear la parte recursiva en Java con Opcional? Vamos a ver:

import java.util.Optional;

public class Div implements Expr {

    Expr expr1;
    Expr expr2;

    public Div(Expr expr1, Expr expr2) {
        this.expr1 = expr1;
        this.expr2 = expr2;
    }

    @Override
    public Optional<Integer> eval() {
        return expr1.eval().flatMap(v1 ->
                expr2.eval().flatMap(v2 ->
                    (v2 == 0) ? Optional.empty() : Optional.of(v1 / v2)
                )
               );
    }

    public static void main(String[] args) {
        Expr iv1 = new Val(6);
        Expr iv2 = new Val(3);
        Expr iv3 = new Val(2);
        Expr iv4 = new Val(0);
        Expr div1 = new Div(iv1, iv2);
        Expr div2 = new Div(div1, iv3);
        Expr div3 = new Div(div2, iv4);

        System.out.println(div2.eval());
        System.out.println(div3.eval());

    }
}

imprimirá:

Optional[1]
Optional.empty

Aquí traducimos nuestra notación do en dos mapas planos, el equivalente al método “>>=”, en Haskell el tipo recursivo podría reescribirse con >>= como:

eval (Div x y)  = eval x >>= (\v1 -> eval y >>= \v2 -> if v2 == 0 then Nothing else return (div v1 v2))

esos dos >>= son nuestros flatMap() de Java.

Conclusiones clave

no es tan obvio cuándo o dónde usar flatMap, o cómo combinarlo con otro flatMap o con un mapa. La clave es entender si la función te devolverá otro nivel en la mónada o no.
Otro ejemplo rápido para ver esto es con este ejemplo simple:

public class MainTest {
    public static void main(String[] args) {

        Optional<Integer> o1 = Optional.of(1);
        Optional<Integer> o2 = Optional.of(0);

        Optional<Integer> integer1 = o1.map(i -> i + 2);

        Optional<Optional<Optional<Integer>>> integer = o1.map(i -> o2.map(i2 -> div(i, i2)));
        Optional<Optional<Integer>> integer2 = o1.flatMap(i -> o2.map(i2 -> div(i, i2)));
        Optional<Integer> integer3 = o1.flatMap(i -> o2.flatMap(i2 -> div(i, i2)));
        Optional<Integer> integer4 = o1.flatMap(i -> o2.map(i2 -> sum(i, i2)));
        Optional<Optional<Integer>> integer5 = o1.map(i -> o2.map(i2 -> sum(i, i2)));

        List<Integer> l1 = Arrays.asList(5,4,6);
        List<Integer> l2 = Arrays.asList(2,4,5);
        List<Integer> l3 = new ArrayList<>();

        l1.forEach(e -> {
            l2.forEach(e2 -> {
                if(e > e2){
                    l3.add(e + e2);
                }
            });
        });

        List<Integer> collect = l1.stream().flatMap(e ->
                l2.stream().flatMap(e2 -> e > e2 ? Stream.of(e + e2) : Stream.empty()))
                .collect(Collectors.toList());

        List<Integer> collect2 = l1.stream().flatMap(e -> l2.stream().flatMap(e2 -> Stream.of(e + e2))).collect(Collectors.toList());
        List<Integer> collect3 = l1.stream().flatMap(e -> l2.stream().map(e2 -> e + e2)).collect(Collectors.toList());

        System.out.println(collect);
        System.out.println(collect2);
        System.out.println(collect3);
        System.out.println(l3);
    }

    public static Optional<Integer> div(Integer i, Integer i2){
        if(i2 == 0){
            return Optional.empty();
        }else{
            return Optional.of(Math.floorDiv(i, i2));
        }
    }

    public static Integer sum(Integer i, Integer i2){
        return i+i2;
    }
}

Con ese ejemplo, uno puede jugar y ver cómo encaja el tipo y salen los resultados.

Sugerencias y consejos

La programación funcional es nuestra amiga, y podría aplicarse en nuestro trabajo diario si vemos cómo. Es realmente fantástico.

Reflexiones finales y próximos pasos

Mi próximo objetivo es dar otro paso hacia flatMap, pero con Java Streams, dejo un ejemplo rápido. Pero quiero ir más profundo.

Similar Posts

Leave a Reply

Your email address will not be published.