Pregunta Haskell: ¿Implementación de mónadas de IO real, en otro idioma?


¿Cómo se implementa realmente la IO mónada? En el sentido de, cuál sería la implementación real de la main ¿función?

¿Cómo llamaría a la función haskell (IO) de otro idioma y en ese caso necesito mantener yo mismo la función IO?

Hace main toma acciones IO (perezosamente) como referencias y luego las llama? ¿O es un trabajo de intérprete, cuando encuentra acciones a su manera, puede llamarlas? O tal vez algo más?

¿Hay una buena implementación de mónadas IO en diferentes idiomas que pueden ayudar a entender profundamente lo que sucede en la función principal?

Editar:

Tal hGetContents me confunde mucho y no estoy seguro de cómo se implementa realmente el IO.

Ok, digamos que tengo una intérprete de Haskell pura y simple que desafortunadamente no tiene soporte de IO y por curiosidad quiero agregarle estas acciones de IO (unsafeIO trucos también). Es difícil obtenerlo de GHC, Hugs u otros.


14
2017-07-11 09:00


origen


Respuestas:


Aquí hay un ejemplo de cómo se podría implementar la mónada IO en Java:

package so.io;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import static so.io.IOMonad.*;  
import static so.io.ConsoleIO.*;    

/**
 * This is a type containing no data -- corresponds to () in Haskell.
 */
class Unit {
    public final static Unit VALUE = new Unit(); 
}

/**
 * This type represents a function from A to R
 */
interface Function<A,R> {
    public R apply(A argument);
}

/**
 * This type represents an action, yielding type R
 */
interface IO<R> {
    /**
     * Warning! May have arbitrary side-effects!
     */
    R unsafePerformIO();
}

/**
 * This class, internally impure, provides pure interface for action sequencing (aka Monad)
 */
class IOMonad {
    static <T> IO<T> pure(final T value) {
        return new IO<T>() {
            @Override
            public T unsafePerformIO() {
                return value;
            }
        };
    }

    static <T> IO<T> join(final IO<IO<T>> action) {
        return new IO<T>(){
            @Override
            public T unsafePerformIO() {
                return action.unsafePerformIO().unsafePerformIO();
            }
        };
    }

    static <A,B> IO<B> fmap(final Function<A,B> func, final IO<A> action) {
        return new IO<B>(){
            @Override
            public B unsafePerformIO() {
                return func.apply(action.unsafePerformIO());
            }
        };
    }

    static <A,B> IO<B> bind(IO<A> action, Function<A, IO<B>> func) {
        return join(fmap(func, action));
    }
}

/**
 * This class, internally impure, provides pure interface for interaction with stdin and stdout
 */
class ConsoleIO {
    static IO<Unit> putStrLn(final String line) {
        return new IO<Unit>() {
            @Override
            public Unit unsafePerformIO() {
                System.out.println(line);
                return Unit.VALUE;
            }
        };
    };

    // Java does not have first-class functions, thus this:
    final static Function<String, IO<Unit>> putStrLn = new Function<String, IO<Unit>>() {
        @Override
        public IO<Unit> apply(String argument) {
            return putStrLn(argument);
        }
    };

    final static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    static IO<String> getLine = new IO<String>() {
            @Override
            public String unsafePerformIO() {
                try {
                    return in.readLine();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        };
}

/**
 * The program composed out of IO actions in a purely functional manner.
 */
class Main {

    /**
     * A variant of bind, which discards the bound value.
     */
    static IO<Unit> bind_(final IO<Unit> a, final IO<Unit> b) {
        return bind(a, new Function<Unit, IO<Unit>>(){
            @Override
            public IO<Unit> apply(Unit argument) {
                return b;
            }
        });
    }

    /**
     * The greeting action -- asks the user for his name and then prints a greeting
     */
    final static IO<Unit> greet = 
            bind_(putStrLn("Enter your name:"), 
            bind(getLine, new Function<String, IO<Unit>>(){
                @Override
                public IO<Unit> apply(String argument) {
                    return putStrLn("Hello, " + argument + "!");
                }
            }));

    /**
     * A simple echo action -- reads a line, prints it back
     */
    final static IO<Unit> echo = bind(getLine, putStrLn);

    /**
     * A function taking some action and producing the same action run repeatedly forever (modulo stack overflow :D)
     */
    static IO<Unit> loop(final IO<Unit> action) {
        return bind(action, new Function<Unit, IO<Unit>>(){
            @Override
            public IO<Unit> apply(Unit argument) {
                return loop(action);
            }
        });
    }

    /**
     * The action corresponding to the whole program
     */
    final static IO<Unit> main = bind_(greet, bind_(putStrLn("Entering the echo loop."),loop(echo)));
}

/**
 * The runtime system, doing impure stuff to actually run our program.
 */
public class RTS {
    public static void main(String[] args) {
        Main.main.unsafePerformIO();
    }
}

Este es un sistema de tiempo de ejecución que implementa la interfaz para la E / S de la consola junto con un pequeño programa puramente funcional que saluda al usuario y luego ejecuta un ciclo de eco.

No se puede implementar la parte insegura en Haskell porque Haskell es un lenguaje puramente funcional. Siempre se implementa con instalaciones de menor nivel.


23
2017-08-26 00:43



Si quieres entender la implementación de la mónada IO, está muy bien descrita en un galardonado papel de Phil Wadler y Simon Peyton Jones, quienes fueron los que descubrieron cómo usar las mónadas para hacer entradas / salidas en un lenguaje puro . El papel es Programación funcional imperativa y está en los sitios web de ambos autores.


7
2017-07-14 00:41



Con Java 8 Lambdas, puede tomar el código de la respuesta de Rotsor anterior, eliminar la clase de Función, ya que Java 8 proporciona una FunctionalInterface con lo mismo y eliminar la clase anónima cruft para lograr un código de aspecto más limpio como el siguiente:

package so.io;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.function.Function;

import static so.io.IOMonad.*;
import static so.io.ConsoleIO.*;

/**
 * This is a type containing no data -- corresponds to () in Haskell.
 */
class Unit {

   // -- Unit$

   public final static Unit VALUE = new Unit();

   private Unit() {
   }

}

/** This type represents an action, yielding type R */
@FunctionalInterface
interface IO<R> {

   /** Warning! May have arbitrary side-effects! */
   R unsafePerformIO();

}

/**
 * This, internally impure, provides pure interface for action sequencing (aka
 * Monad)
 */
interface IOMonad {

   // -- IOMonad$

   static <T> IO<T> pure(final T value) {
      return () -> value;
   }

   static <T> IO<T> join(final IO<IO<T>> action) {
      return () -> action.unsafePerformIO().unsafePerformIO();
   }

   static <A, B> IO<B> fmap(final Function<A, B> func, final IO<A> action) {
      return () -> func.apply(action.unsafePerformIO());
   }

   static <A, B> IO<B> bind(IO<A> action, Function<A, IO<B>> func) {
      return join(fmap(func, action));
   }

}

/**
 * This, internally impure, provides pure interface for interaction with stdin
 * and stdout
 */
interface ConsoleIO {

   // -- ConsoleIO$

   static IO<Unit> putStrLn(final String line) {
      return () -> {
         System.out.println(line);
         return Unit.VALUE;
      };
   };

   final static Function<String, IO<Unit>> putStrLn = arg -> putStrLn(arg);

   final static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

   static IO<String> getLine = () -> {
      try {
         return in.readLine();
      }

      catch (IOException e) {
         throw new RuntimeException(e);
      }
   };

}

/** The program composed out of IO actions in a purely functional manner. */
interface Main {

   // -- Main$

   /** A variant of bind, which discards the bound value. */
   static IO<Unit> bind_(final IO<Unit> a, final IO<Unit> b) {
      return bind(a, arg -> b);
   }

   /**
    * The greeting action -- asks the user for his name and then prints 
    * greeting
    */
   final static IO<Unit> greet = bind_(putStrLn("Enter your name:"),
         bind(getLine, arg -> putStrLn("Hello, " + arg + "!")));

   /** A simple echo action -- reads a line, prints it back */
   final static IO<Unit> echo = bind(getLine, putStrLn);

   /**
    * A function taking some action and producing the same action run repeatedly
    * forever (modulo stack overflow :D)
    */
   static IO<Unit> loop(final IO<Unit> action) {
      return bind(action, arg -> loop(action));
   }

    /** The action corresponding to the whole program */
    final static IO<Unit> main = bind_(greet, bind_(putStrLn("Entering the echo loop."), loop(echo)));

}

/** The runtime system, doing impure stuff to actually run our program. */
public interface RTS {

    // -- RTS$

    public static void main(String[] args) {
       Main.main.unsafePerformIO();
    }

 }

Tenga en cuenta que también cambié los métodos estáticos declarados por clase a métodos estáticos declarados por interfaz. ¿Por qué? No hay una razón en particular, solo que se puede en Java 8.


7
2017-08-08 12:00



los IO la mónada se implementa básicamente como un transformador de estado (similar a State), con una ficha especial RealWorld. Cada operación IO depende de este token y lo pasa cuando termina. unsafeInterleaveIO introduce un segundo token, para que pueda comenzar una nueva operación IO, mientras que el otro todavía está haciendo su trabajo.

Por lo general, no tiene que preocuparse por la implementación. Si desea llamar a IO-functions desde otros idiomas, GHC se preocupa por eliminar el contenedor IO. Considere este pequeño fragmento:

printInt :: Int -> IO ()
printInt int = do putStr "The argument is: "
                  print int

foreign export ccall printInt :: Int -> IO ()

Esto genera un símbolo para llamar printInt desde C. La función se convierte en:

extern void printInt(HsInt a1);

Dónde HsInt es solo una (dependiendo de su plataforma) typedefre int. Así que ya ves, la mónada IO ha sido eliminado por completo.


5
2017-07-11 15:26



Dejaré la cuestión de implementar IO a otras personas que saben un poco más. (Aunque señalaré, como estoy seguro también lo harán, que la verdadera pregunta no es "¿Cómo se implementa IO en Haskell?" Sino "¿Cómo se implementa IO en GHC?" O "¿Cómo se implementa IO?" en abrazos? ", etc. Imagino que las implementaciones varían enormemente). Sin embargo, esta pregunta:

cómo llamar a la función haskell (IO) desde otro idioma y, en ese caso, ¿necesito mantener yo mismo mi IO?

... se responde en profundidad en el Especificación FFI.


2
2017-07-11 21:48



A continuación se muestra la implementación real de IO en GHC 7.10.

los IO tipo es esencialmente una mónada de estado en el tipo State# RealWorld (definido en GHC.Types)

{- |
A value of type @'IO' a@ is a computation which, when performed,
does some I\/O before returning a value of type @a@.
There is really only one way to \"perform\" an I\/O action: bind it to
@Main.main@ in your program.  When your program is run, the I\/O will
be performed.  It isn't possible to perform I\/O from an arbitrary
function, unless that function is itself in the 'IO' monad and called
at some point, directly or indirectly, from @Main.main@.
'IO' is a monad, so 'IO' actions can be combined using either the do-notation
or the '>>' and '>>=' operations from the 'Monad' class.
-}
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))

los IO la mónada es estricto, porque bindIO es definido por case juego (definido en GHC.Base)

instance  Monad IO  where
    {-# INLINE return #-}
    {-# INLINE (>>)   #-}
    {-# INLINE (>>=)  #-}
    m >> k    = m >>= \ _ -> k
    return    = returnIO
    (>>=)     = bindIO
    fail s    = failIO s

returnIO :: a -> IO a
returnIO x = IO $ \ s -> (# s, x #)

bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO $ \ s -> case m s of (# new_s, a #) -> unIO (k a) new_s

Esta implementación se discute en una publicación de blog de Edward Yang.


2
2018-04-13 19:11



De hecho, "IO a" es simplemente "() -> a" en un lenguaje impuro (donde las funciones pueden tener un efecto secundario). Digamos que quieres implementar IO en SML:

structure Io : MONAD =
struct
  type 'a t = unit -> 'a
  return x = fn () => x
  fun (ma >>= g) () = let a = ma ()
                      in g a ()
  executeIo ma = ma ()
end

1
2017-07-15 11:37



Preguntas populares