読者です 読者をやめる 読者になる 読者になる

yunomuのブログ

酒とゲームと上から目線

JavaコードをHaskellで書きなおすのに手こずった話

「続・アルゴリズムを学ぼう」が発売されて、校正をほんのちょっと手伝ったお礼かなんかで発売された本を頂きまして、ぼちぼち読んだりしています。
前作に比べて使いやすいというか、親しみやすいネタが多くて面白いですね。
「続・アルゴリズムを学ぼう」http://www.amazon.co.jp/dp/4048913948/

で、ちょっと暇つぶしに「続・アルゴリズムを学ぼう」に載ってるリバーシHaskellで実装してみました。
https://github.com/yunomu/exercises/tree/master/reversi

この本に載っているプログラム例はJavaで書かれているので、要はJavaHaskellで書きなおすという作業をしたような感じになりました。

前になんか勘違いしていた話

JavaHaskellといえば、前に社内で"Types and Programming Languages"の読書会をやろうって話になった時にこんな事を言っている。

ここで言う型クラスというのはHaskellの型クラスのことで、見た目は割と似ていますよね。似てるんじゃないかな。
例えばEqとか、

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool

ってなってて、いかにもインタフェースを定義している感じ。こういう振る舞いをするデータを作りたかったら、データ型を作ってこの型クラスのインスタンスにすればいいんじゃないかなと思う。
まあやってみると全然違うんですが。

Javaのinterfaceは型だけども、Haskellの型クラスは型ではないので、Javaのinterfaceみたいなノリで型クラスを使うと、徐々に無理が出てきて後悔することになる。

class ToInt a where
    toInt :: a -> Int

instance ToInt Int where
    toInt = id

instance ToInt Char where
    toInt = const (-1)

みたいな型クラスがあったとして

f :: ToInt a => [a] -> [Int]
f = map . toInt

a = f [2, 'a']

みたいなことはできない。リスト内の型が違うから。

b = [toInt 2, toInt 'a']

これはできる。まあ、そりゃできる。
こうしてみれば当たり前なんだけど、toInt関数の扱い方に気をつけなきゃいけないのでそれなりに面倒な場面が増えてくるというか、感覚的には「同じtoIntって関数が使えるんだから同じように扱えてもいいじゃん」って思うんだけど、シンボルというか名前が同じでもChar型のtoIntとInt型のtoIntは別物なので、別物として扱いましょう。
同じ型クラスならリストに入れられるようにしようみたいな話もどっかにあった気もしますが。

型クラスはやっぱりShowみたいに型によって処理を振り分けたい時に使うのが良さそうですね。
インタフェースが同じなら素直に同じ型にしましょう。

継承やコンストラクタやインタフェース定義

Haskellの型定義にはJavaの継承とかコンストラクタみたいなものはなくて、メソッドもなくて、どうするのこれってなります。
ならないなら幸せです。人やコードによってはならないんじゃないかな。
本ではプレイヤーをこんな風に定義してる。

public abstract class Player {
    protected final Turn turn;
    protected Player(Turn turn) {
        this.turn = turn;
    }
    public Turn getTurn() {
        return turn;
    }
    public abstract Position play(Board board);
}
public class HumanPlayer extends Player {
    ...
}

抽象クラスの`Player`を定義して、それを継承してHumanPlayerとかSimpleAIPlayerとかを作ってる。
これはそのままだとHaskellに変換できないんだけど、よく考えれば具象クラスが要らないだけだったりする。コンストラクタの代わりに初期化関数を作る。

data Player = Player
    { playerTurn :: Turn
    , playerPlay :: Board -> Player -> IO (Maybe Position, Player)
    }

initHumanPlayer :: Turn -> Player
initHumanPlayer t = Player t humanPlay

humanPlay :: Board -> Player -> IO (Maybe Position, Player)
humanPlay = ...

Java風に言うと、テンプレートメソッドパターンがストラテジーパターンになってる。というかJavaでもこう書けよという気がしてくる。抽象クラスがいいのかストラテジーがいいのかはここでは微妙なところかもしれないけど。

戻り値の型

Javaのplayメソッドのシグネチャはこうなってるけど

    public abstract Position play(Board board);

Haskellのplayの型はこうなってる。

playerPlay :: Board -> Player -> IO (Maybe Position, Player)

このインタフェースはちょっと悩んだんですけど、

  • Playerの内部状態を使うかもしれないから引数にPlayerが必要
  • Playerの内部状態を更新するかもしれないから戻り値にPlayerが必要
  • Positionが決まらない場合があるからMaybe Position(元プログラムではnullを返している)
  • ユーザの入力や乱数を利用して手を決めるかもしれないからIO

という感じで決めました。
SimpleAIPlayerだとIOが要らないし、HumanPlayerだとMaybeが要らないし、もっと後の方になるまで戻り値のPlayerが要らなかったりするので、ここは先見性が試されるというか、3回くらい書き直しました。

IOが必要かどうか、値が存在しない場合があるかどうか、このあたりを最初から慎重に設計してなきゃいけないけども、このあたりは結構難しいところだったと思います。機能追加の度に共通部分を書き直す羽目になりました。よく考えれば自明なんですけど、Javaのメソッドは基本的に副作用があるというのは忘れがちな気がします。

そんなこんなで、暇だからAI作って遊び相手になってもらおうと思って書き始めたものの、案外面倒くさくて、ここまで書き終わる頃には新幹線が目的地に着いてしまったという。それはそれで良い暇つぶしにはなりましたけど。