Llega nuestra cuarta parte sobre programacion funcional. Ya vimos en una primera entrega los funtores, en la segunda hablamos de funtores aplicativos y en la tercera vimos las monadas. Hoy vamos a ver como abstraer nuestros programas gracias a todos estos conceptos
Una monada, aunque la definimos como un funtor aplicativo, tambien la podemos definir, como dice Juan Manuel Serrano, como una API para escribir programas imperativos
Veamoslo con un ejemplo sencillo, un programa que hace eco, es decir que recibe una entrada y repite esa entrada en la salida devolviendonos la entrada. Lo primero que hacemos cuando queremos abstraer la logica de una aplicacion es usar una interfaz
public interface IO { String read(); void write(String msg); }
Con esa interfaz podemos crear nuestra logica de aplicacion
public class Program { private final IO io; public Program(IO io) { this.io = io; } public String echo() { String read = io.read(); io.write(read); return read; } }
Ahora toca la implementacion de IO
que lee de consola y escribe en consola
import java.util.Scanner; public class ConsoleIO implements IO { @Override public String read() { Scanner scanner = new Scanner(System.in); return scanner.nextLine(); } @Override public void write(String msg) { System.out.println(msg); } }
Hasta aqui todo esta perfecto pero cuando queremos que nuestra implementacion sea asincrona usando por ejemplo Future
empezamos a tener problemas
public class FutureIO implements IO { @Override public Future<String> read() { } @Override public Future<Void> write(String msg) { } }
La opcion que tenemos es usar el metodo get
de la clase Future
lo que hace que el codigo sea bloqueante y deje de ser asincrono. Si no queremos hacer esto lo unico que nos queda es cambiar la interfaz IO
para que devuelva Future
y cambiar toda la logica de Program
Nuestra logica depende de nuestro interprete y eso hace que cada vez que se cambie el interprete haya que reescribir la logica, algo que no queremos
Veamos como resolver este problema usando lo aprendido en los ultimos posts
Reescribimos la interfaz IO
de la siguiente forma
public interface IO<f> { __<f, String> read(); __<f, Void> write(String msg); }
Ahora nuestra interfaz recibe clases parametrizadas. Y podemos reescribir nuestra clase Program
public class Program<f> { private final IO<f> io; public Program(IO<f> io) { this.io = io; } public String echo() { String read = io.read(); io.write(read); return read; } }
Por desgracia este codigo no compila ya que io.read()
no devuelve String
. Podemos cambiar la clase asi
public class Program<f> { private final IO<f> io; public Program(IO<f> io) { this.io = io; } public String echo() { __<f, String> read = io.read(); io.write(read); return read; } }
Pero ahora tendremos errores de compilacion en la linea io.write(read)
y return read
. La unica solucion que tenemos es usar las monadas cambiando nuestra clase Program
public class Program<f> { private final IO<f> io; private final Monad<f> monad; public Program(IO<f> io, Monad<f> monad) { this.io = io; this.monad = monad; } public __<f, String> echo() { return monad.bind(io.read(), msg -> monad.bind(io.write(msg), a -> monad.point(msg))); } }
Ya podemos usar nuestro codigo para cualquier tipo de clases siempre y cuando hayamos definido una monada para ese tipo. Como ejemplo dejo una implementacion de la monada para Future
. Primero hacemos el wrapper para nuestro tipo
@HktConfig(withVisibility = Visibility.Same) public class FutureT<t> implements __<FutureT, t> { public final Future<t> future; public FutureT(Future<t> future) { this.future = future; } }
Y luego definimos la monada
public class MonadFutureP implements Monad<FutureT> { @Override public <A, B> __<FutureT, B> bind(__<FutureT, A> fa, Function<A, __<FutureT, B>> f) { try { Future<A> future = Hkt.asFutureT(fa).future; return f.apply(future == null ? null : future.get()); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } @Override public <A> __<FutureT, A> point(A a) { return new FutureT<>(new FutureTask<>(() -> a)); } }
Esta no es la mejor definicion ya que usamos el metodo get
de Future
en vez de comprobar si ya ha sido ejecutado, aun asi nos vale para ver como implementar la monada