如何在Java中实现Haskell的IO类型?

5

这是我的第一次尝试:

import java.util.function.*;
import java.util.ArrayList;
public class IO<A> {
    private Function<World,Tuple<World,A>> transform;
    private class World {
        private ArrayList<String> stdin;
        private ArrayList<String> stdout;
        public World() {
            this.stdin  = new ArrayList<String>();
            this.stdout = new ArrayList<String>();
        }
    }
    private class Tuple<F,S> {
        public F fst;
        public S snd;
        public Tuple(F fst, S snd) {
            this.fst = fst;
            this.snd = snd;
        }
    }
    public IO(Function<World,Tuple<World,A>> transform) {
        this.transform = transform;
    }
    public IO<A> pure(A a) {
        return new IO<A>(r -> new Tuple<World,A>(r,a));
    }
    public <B> IO<B> bind(IO<A> io, Function<A,IO<B>> f) {
        return new IO<B>(r -> {
            Tuple<World,A> result = io.transform.apply(r);
            IO<B> ioB = f.apply(result.snd);
            return ioB.transform.apply(result.fst);
        });
    }
}

但是当我尝试编译这个时,我会得到以下错误:

IO.java:29: error: incompatible types: IO<B>.World cannot be converted to IO<A>.World
            Tuple<World,A> result = io.transform.apply(r);
                                                        ^
  where B,A are type-variables:
    B extends Object declared in method <B>bind(IO<A>,Function<A,IO<B>>)
    A extends Object declared in class IO

我不理解的是,世界级别与类型变量没有关系,但javac认为它们有关系。我做错了什么?

我不太确定您在做什么,但我认为您误解了“World”的目的,这不是考虑“IO”单子最好的方式。无论如何,您对“World”的定义意味着“变换”可以添加到输入并从输出中删除,这可能是不希望发生的行为。 - Derek Elkins left SE
目标是向Java开发者介绍Haskell。我认为纯IO是Haskell最酷的概念,因此我只是制作了一个简单的IO模型。不过我同意stdin列表应该是只读的。 - Kyle McKean
我不是Java开发人员,但我不认为这样复杂的东西是介绍Haskell的IO类型的好方法。对此,我建议采用“操作单子”方法,其中IO a被视为由原始命令和延续构成的数据结构。 - dfeuer
你认为这个为什么很棘手? - Kyle McKean
最大的问题是它模仿了 GHC 的概念上错误的 I/O 内部模型。世界并不是一个你可以随意传递的值。至少,操作式单子没有谎言——它保留解释的可能性。 - dfeuer
相关:https://dev59.com/2Ww15IYBdhLWcg3wYasx。 - atravers
2个回答

4
略过你在Java中复制Haskell的IO类型时的忠实性:
编译器认为您在bind方法签名中的A与类定义中的A相同。 您已经告诉我们这些不同。 为了将此传达给编译器,您需要使事物静态化并引入一些方法级别的类型参数:
import java.util.function.*;
import java.util.ArrayList;
public class IO<A> {
    private Function<World,Tuple<World,A>> transform;
    private static class World {
        private ArrayList<String> stdin;
        private ArrayList<String> stdout;
        public World() {
            this.stdin  = new ArrayList<String>();
            this.stdout = new ArrayList<String>();
        }
    }
    private static class Tuple<F,S> {
        public F fst;
        public S snd;
        public Tuple(F fst, S snd) {
            this.fst = fst;
            this.snd = snd;
        }
    }
    private IO(Function<World,Tuple<World,A>> transform) {
        this.transform = transform;
    }
    public static <A> IO<A> pure(A a) {
        return new IO<A>(r -> new Tuple<World,A>(r,a));
    }
    public static <A,B> IO<B> bind(IO<A> io, Function<A,IO<B>> f) {
        return new IO<B>(r -> {
            Tuple<World,A> result = io.transform.apply(r);
            IO<B> ioB = f.apply(result.snd);
            return ioB.transform.apply(result.fst);
        });
    }
}

啊,我试过了,但是我没有将方法设为静态的。谢谢! - Kyle McKean
bind保留为非静态方法,仅删除第一个参数并将其替换为this可能更好,也更合理。 - Derek Elkins left SE
@DerekElkins 您可能是对的,但现在我只是添加了一个免责声明。我认为问题不止这一个… - Matt McHenry

2

我认为采用“操作单子”方法更好地解释Haskell I/O的本质。Haskell版本可以非常简单:

data PrimOp a where
  PutStr :: String -> PrimOp ()
  GetLine :: PrimOp String
  -- Whatever other primitives you want

data MyIO a where
  Pure :: a -> MyIO a
  Bind :: !(MyIO a) -> (a -> MyIO b) -> MyIO b
  LiftPrim :: !(PrimOp a) -> MyIO a

instance Functor MyIO where
  fmap = liftM

instance Applicative MyIO where
  pure = Pure
  (<*>) = ap

instance Monad MyIO where
  (>>=) = Bind

MyIO 的值并不是一些神奇的可传递函数;它们只是普通的数据。如果我们愿意,我们可以解释这些数据以实际执行所表示的操作:

runPrimOp :: PrimOp a -> IO a
runPrimOp (PutStr s) = putStr s
runPrimOp GetLine = getLine

runMyIO :: MyIO a -> IO a
runMyIO (Pure a) = pure a
runMyIO (Bind m f) = runMyIO m >>= runMyIO . f
runMyIO (LiftPrim prim) = runPrimOp prim

实际上可以编写一个Haskell编译器,其IO类型看起来很像MyIO,并且运行时系统直接解释该类型的值。

我尝试以下面的Java代码进行翻译。我从未真正成为过Java程序员,而且已经很长时间没有使用过它了,所以这可能非常不符合惯例,甚至是错误的。我想您可能希望使用某个版本的“访问者模式”来表示BindPure的解释(将事物带入类似于Control.Monad.Operational 之类的通用领域),同时使用PrimOp子类的run方法来处理IO。由于我不知道正确的Java方式,因此我试图保持简单。
public interface IO <A> {
  public A run ();
}

public final class Pure <A> implements IO <A> {
  private final A val;
  Pure (A x) { val = x; }
  public A run () {
      return val;
      }
}

难点在于Bind,它需要存在量化。我不知道在Java中惯用的做法,所以我做了一些笨拙的东西,但似乎可以工作。也就是说,我编写了一个辅助类OpenBind,它公开了两个类型变量,然后编写了一个类Bind来包装OpenBind,使其中一个变量保持原样。

import java.util.function.Function;

public final class Bind <A> implements IO <A> {
    private final OpenBind <?,A> ob;

    public <B> Bind (IO <B> m, Function <B,IO <A>> f) {
        ob = new OpenBind <B,A> (m, f);
    }

    public A run() {
        return (ob.run());
    }

    private final static class OpenBind <Fst,Snd> {
        private final IO <Fst> start;
        private final Function <Fst, IO <Snd>> cont;
        public OpenBind (IO <Fst> m, Function <Fst, IO <Snd>> f) {
            start = m;
            cont = f;
        }
        public final Snd run () {
            Fst x = start.run();
            IO <Snd> c = cont.apply(x);
            return (c.run());
        }
    }
}

这些原语本身非常简单(我找不到 Java 相当于 () 的东西,所以我写了自己的 Unit):

public class PutStr implements IO <Unit> {
  private String str;
  public PutStr (String s) {
      str = s;
  }
  public Unit run () {
      System.out.print(str);
      return Unit.unit;
  }
}

public final class Unit {
  private Unit () {}
  public static final Unit unit = new Unit ();
}

public class GetLine implements IO <String> {
    private GetLine () {}
    public static final GetLine getLine = new GetLine ();
    public String run () {
      // Replace the next line with whatever you actually use to
      // read a string.
      return "";
  }
}

我不清楚如何将GADTs转换为Java数据类型,但这是一个更好的解决方案。 - Kyle McKean
@KyleMcKean,我已经添加了一些注释来翻译Java代码,但是有一些注意事项。 - dfeuer
另一位非Java开发者的一些注释:有一个重复导入语句。OpenBind可以是一个静态嵌套类,以限制其可见性。请提供一个如何使用它的示例。 - Stefan Hanke
Java中的通配符类型是否是类型安全的?它们与存在类型(existential)相同吗? - Kyle McKean
@KyleMcKean,它们是类型安全的。我不是Java专家,也不是类型系统专家,所以我不能说它们是否与存在性完全相同,但似乎一个简单的?(没有任何超类/子类约束)意味着任意类。请注意,我们仅在OpenBind类内部使用startcont具有匹配类型的事实,这正是我们可以看到它们的地方。如果startcont是公共的,并且我们从Bind中访问它们,我们将得到一个IO <?>和一个Function <?, IO <A>>,两者都没有反射就无法使用。 - dfeuer
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接