uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

プログラミング言語Frege(フレーゲ)を紹介します

これはマイナー言語 Advent Calendar 2013の21日目の記事です。
Frege(フレーゲ*1 )を紹介します。

Fregeは、Java VM上で動作するHaskell風の言語です。以下のような特徴を持っています。

これらの特徴は、Haskellと共通するものであり、構文も基本的なところについてはHaskellとだいたい同じか似ているかもしくはサブセットです。標準関数やデータ型やモジュールについても、Haskell 2010からたくさん引っぱってきているそうです。

しかしながら、Fregeはその目標において、Haskellとの完全な互換性を達成しようとはしていません。実際かなり違っています。特にJava VM上で有用であることに重点が置かれており、プリミティブ型はJavaのものと等価なものを使用しており、Javaで定義されたメソッドを呼び出すためのインターフェースが工夫されています。

また、Haskellですっきりしてないところなどを直したり、改良しようとしているところもあります(例: MonadはApplicative型クラスのインスタンスとかデータ型がスコープを持つとか。)。またHakell標準ではない機能(GHC拡張)を標準的に使えるようにしているところもあります(例えばランクn多相,RankNTypes)。

Fregeの面白いところの一つは、fregeのコンパイラは中間コードとしてJavaソースコードを吐くところです。コンパイル結果としてJavaソースも静的ファイルとして残るので、遅延評価っていったいどういうことだろう?など、Javaコードを良く読むとわかるかもしれません。REPLではオンメンモリでjavacを走らせてるみたいです。

fregeの使い方紹介

オンラインREPL

お気軽に試すにはオンラインのREPLがありますのでどうぞ。
「:java」で生成されたJavaソースを見ることができます。

REPLをインストールして動かす

frege-replのページからたどれるダウンロードページからfrege-repl-x.x.xの一番新しいのをダウンロードして展開、

$ mkdir /tool/frege
$ cd /tool/frege
$ unzip ~/Downloads/frege-repl-1.0.1.zip
Archive:  ~/Downloads/frege-repl-1.0.1.zip
  inflating: jline-2.10.jar
  inflating: frege-interpreter-1.0.0.jar
  inflating: frege-maven-plugin-1.0.5-frege-3.21.232-g7b05453.jar
  inflating: ecj-4.2.2.jar
  inflating: memory-javac-1.0.0.jar
  inflating: frege-repl-1.0.1.jar

REPLを起動します。

$ java -jar frege-repl-1.0.1.jar
Welcome to Frege 3.21.232-g7b05453 (Oracle Corporation Java HotSpot(TM) 64-Bit Server VM, 1.7.0**)

frege> 1+1
2
frege> quicksort [] = []

frege> quicksort (x:xs)   =  quicksort [y | y <- xs, y<x ] ++ [x] ++ quicksort [y | y <- xs, y>=x]

frege> quicksort [3,0,1,3,4,5]
[0, 1, 3, 3, 4, 5]

ghciでは関数とかを定義する際にlet文にすることが必要ですがfregeのreplではいりません。

コンパイルして実行する

前述のfrege-replのzip中にあったfrege-maven-plugin*.jarにはfrege本体のjarも含まれているので、これを使ってFregeソースコードJavaクラスファイルにコンパイルすることができます。とはいえ、frege-maven-plugin*.jarに入っているfregeは最新版ではない可能性があるので、最新版を使いたい場合こちらから直接ダウンロードする必要があります*2

まずソースを確認。

$ cat test.fr
module sample.Test where
quicksort [] = []
quicksort (x:xs)   =  quicksort [y | y <- xs, y<x ] ++ [x] ++ quicksort [y | y <- xs, y>=x]
main _ = print $ quicksort [1,3,2,4,5,0,7]

コンパイラを実行。

$ java  -cp /tool/frege/frege-maven-plugin-1.0.5-frege-3.21.232-g7b05453.jar frege.compiler.Main test.fr
runtime 4.304 wallclock seconds.

生成結果を確認。

$ ls -la sample
total 120
drwxr-xr-x  12 uehaj  wheel    408 12 21 05:59 ./
drwxr-xr-x  10 uehaj  wheel    340 12 21 05:59 ../
-rw-r--r--   1 uehaj  wheel   2445 12 21 05:59 Test$1Flc$1_3259.class
-rw-r--r--   1 uehaj  wheel   2448 12 21 05:59 Test$1Flc$4_3263.class
-rw-r--r--   1 uehaj  wheel    939 12 21 05:59 Test$IJ$1.class
-rw-r--r--   1 uehaj  wheel   1104 12 21 05:59 Test$IJ$2.class
-rw-r--r--   1 uehaj  wheel   1091 12 21 05:59 Test$IJ$3.class
-rw-r--r--   1 uehaj  wheel   1004 12 21 05:59 Test$IJ$4.class
-rw-r--r--   1 uehaj  wheel    674 12 21 05:59 Test$IJ$5.class
-rw-r--r--   1 uehaj  wheel   1593 12 21 05:59 Test$IJ.class
-rw-r--r--   1 uehaj  wheel   8570 12 21 05:59 Test.class
-rw-r--r--   1 uehaj  wheel  14999 12 21 05:59 Test.java

sample/Test.javaが生成されていますね。実行はこうです。

$ java -cp /tool/frege/frege-maven-plugin-1.0.5-frege-3.21.232-g7b05453.jar:. sample.Test
[0, 1, 2, 3, 4, 5, 7]
runtime 0.248 wallclock seconds.

これでわかるように、「module sample.Test where」というモジュール宣言をしたFregeソースは、FQCNが「sample.Test」であるJavaクラスにコンパイルされます。モジュール名のパッケージを除いた部分(Javaではクラス名)である「Test」は大文字で始まる必要があります。このモジュールで定義された関数は、sample.Testクラスのstaticメソッド定義にコンパイルされます。例えば上のquicksort関数は

final public static PreludeBase.TList quicksort(
  final PreludeBase.COrd ctx$1, final PreludeBase.TList arg$1
) {

こんなメソッドシグネチャメソッドコンパイルされるようですね。

Gradleでコンパイル

こちらにGradleプラグインがあります。とはいえこのプラグイン自身がどこかのリポジトリに上がってるわけではないので、このプラグインソースを自分のプロジェクトのbuildSrc配下に展開しておく必要があります。んで以下のようにして、開発するfregeソースはsrc/main/fregeに置きます。

apply plugin: "java"
apply plugin: org.gradle.frege.FregePlugin

repositories {
  flatDir name:"frege-lib", dirs:"lib"
}

dependencies {
  compile ':frege:3.21.232-g7b05453'
}

compileFrege {
  outputDir = project.file("$buildDir/frege")
  verbose = true
}

サンプルコード

ほぼHaskellなので例を挙げて紹介するのは省略します。自分がオフラインどう書くの問題をいくつかFregeで解いたものはこちら。ほとんどHakellで書いてからFregeに書きなおしてます。本体配布物に含まれている例はこちら
Real World Haskellの例をFregeで書き直そうというプロジェクトReal World Fregeというのもあります。

特徴を散発的に紹介

  • lambdaに複数引数は使えない。ただし、\x -> \y -> expは\x \y -> expと書ける(\x y -> expが駄目)。
  • 関数合成演算子の「.」はクラスのメンバー指定という意味に転用されている。関数合成には代りに •(BULLET,U+2022)を用いる。なお、互換性のために、「.」の前後に空白が置くもしくは「(.)」と書くと合成の意味になるようにしてあるそうです。
  • データ型は名前空間になるので、レコードフィールド名がトップレベルを汚染しない。runSTとかrunStateとかバリエーション作らなくてもrunで良いわけです。
  • 正規表現リテラルが使えて、パターンマッチに使える。
     frege> test ´^[ABC]´ = "start with A or B or C"
     frege> test "CDEF"
     start with A or B or C
     frege> test "GHI"
     frege.runtime.NoMatch: test at line 3 no match for value GHI
  • ランクN多相GHC拡張に相当するものを標準で使える。STの定義に使われている。
  • 型クラスの機能はHaskellにくらべ不完全とのこと。
  • モナドとかの型階層は違っている(MonadはApplicativeとか。結果としてApplicativeのpureはreturnにリネームされている)
  • 「パッケージから外部に公開するときにexport」するんじゃなくて「デフォルトは公開で、公開したくないときprivateを付ける」。abstractを付けるとすべてのConstructorがprivateになる。
  • 演算子は任意に定義できる(:で始まらなくても良い)。
  • 数値型はJavaのを使う(Int,Double,Float)
  • 文字列定数はjava.lang.Stringでありリストではない。charのリストと相互変換するにはunpack/packを使う。
  • Bool型はjavaのbooleanのAliasであり、代数データ型ではない。データ構築子True,Falseも無い。trueとfalseが予約語(リテラル)。
  • レイアウトルールは何か違うらしい。
  • read(s) は s.atoi
  • トレースするには以下をガードに入れる(便利)
   | traceLn (":"++show x) = undefined
  • QuickCheckの移植版もあるが使い方がよくわからない
  • javadocみたいなドキュメントコメントシステムがある
  • Eclipse Pluginもあるけど試してない

Javaとのインターフェース

時間不足により、今回は詳しい紹介を断念しますが、非常にエレガントです。Javaメソッドを何もせずに直接呼び出せる、というわけではなく、入出力の有無と状態変更をするかどうか(評価順を定めるかどうか)によって、IO aとST s aでラップした宣言をして呼び出します。副作用がなければ、pureと宣言します。たとえば以下はHashSetをFregeから使うための宣言で、native-genというnative宣言のジェネレータで生成しました。Javaメソッドがpureかどうかは今のところ人間が判定するしかないので、この自動生成ジェネレータはすべて安全サイドに倒して評価順の定まる処理の中で評価されるようにSTとして宣言されます。Imutableなクラスとそれに対するメソッドであれば、pureと宣言すれば純粋関数からも直接呼び値を得たり処理したりすることができます。
STモナドについてはこちら

data HashSet e = native java.util.HashSet where

  native new :: Int -> STMutable s (HashSet e)
              | Int -> Float -> STMutable s (HashSet e)
              | Mutable s (Collection e) -> STMutable s (HashSet e)
              | () -> STMutable s (HashSet e)

  native add :: Mutable s (HashSet e) -> e -> ST s Bool

  native clear :: Mutable s (HashSet e) -> ST s ()

  native clone :: Mutable s (HashSet e) -> ST s Object

  native contains :: Mutable s (HashSet e) -> Object -> ST s Bool

  native isEmpty :: Mutable s (HashSet e) -> ST s Bool

--  native iterator :: Mutable s (HashSet e) -> STMutable s (Iterator e)

  native remove :: Mutable s (HashSet e) -> Object -> ST s Bool

  native size :: Mutable s (HashSet e) -> ST s Int

instance Serializable (HashSet e)

まとめ

Fregeの最大の利点は、Haskellに似ている、ということです。そして、最大の障壁(?)の一つは、おそらく、Haskellに似ている、ということです。今のところ、Fregeを理解するにはHaskellの知識が色々な意味で間違いなく必須です。純粋関数型言語に興味があるというJavaプログラマが興味を持つきっかけとしては良いと思います。
今後の発展を期待します。

*1:命題論理と述語論理の公理化を最初に行なったドイツの先駆的論理学者ゴットロープ・フレーゲからの名前とのことです。

*2:REPLを本体側に同梱で配布して欲しいわ!

モナドはなぜHaskellでしか積極的に使われていないのか?

(Haskellな日々になってるな…。)

モナドというものがあり、Haskellで有名ですが、実際には、Java8のOptional、ScalaのOptionやfor内包表記などでは使用されています。ScalazというScalaのライブラリや、monadlogieというGroovyのライブラリでも使われています。

とはいえ、一般に、Haskellでのように積極的には使われていないというのが公平な見かたでしょう*1Haskellでは本当にいろんなものがモナド化されています。入出力(IO)、状態、失敗するかもしれない計算(Maybe、Either)、非決定計算、継続、パーサ(モナディックパーサ)、リーダ、ライタ、etc.etc……。

なぜこのような差が生じるのでしょうか?

その前に、まず押さえておくべきことは、モナドは非常に汎用的な機能だということです。数学的定義はともかく、機能的に言うと、一連の処理(計算)を実行するにあたって、ぞれぞれのステップでその入力(引数)と出力(返り値)をキャプチャして、各段階で多様な処理を挟み込み、表面上の計算に対する追加的な別の意味(文脈)を外付けで付与することがモナドの機能です。つまり「ステップを踏む一連計算」なら何でもモナドにできます。

このような「文脈の付与」は、Groovyでは例えば「ビルダー」や「DSL」で見ることができます。これらでは、プログラムコードの表面上書かれていることと、実行していることが別ですよね。

GroovyでビルダーやDSLを実装するにあたり、モナドが不要である理由は、おそらく、文脈に状態を持たせる方法がとれるからです。HogeBuilderが{..}中の、メソッド呼び出しやプロパティ評価の結果を受けてなんらかの構造を構築する場合、ビルダー内の状態を更新することで実装するのが自然です。しかし、Haskellは参照透過なので、1つのメソッドの呼び出しと結果から新規に「モナド値」を作り、それを受け渡す*2ことでしか、文脈に必要な情報を維持管理できないのです。

おそらく他の言語では、モナドに相当することは

  • リフレクション
  • バイトコードエンジニアリング
  • AST変換
  • メタプログラミング
  • AOP
  • 構文規則の自己拡張

などで実現するのが普通だと思うのですが、上記で達成できることを(全てではないにせよ)、参照透過という厳しい制約の元で、一つの型定義と2個かそこらの関数を書くだけで実現できる、ある意味単純なモナドなる1概念がこなしてのけるのは、非常なおどろきです*3

逆に言うと、副作用を持てる言語ではモナドを使う必要がないところを、純粋なHaskellではモナドを使うしかない。それが良いかどうかは別として、純粋さをとことん追求した必然の孤高の帰結がモナドというわけです。

*1:私が知らないだけかもしれませんが。

*2:正確に言うとラムダ式のスコープも一役買ってます

*3:本当に見やすいか?とかは疑問だし、理解しやすいかと言うと端的に言って非常にわかりにくいと思いますが。

Groovyでリストモナド(orリストアプリカティブ)を書いてみる

Groovyでモナドを書くシリーズその3、「Groovyでリストモナド(orリストアプリカティブ)を書いてみる」です。(参考: GroovyでMaybeモナドを書いてみたGroovyでStateモナドを書いてみる)。

Haskellではリストは、「要素が非決定計算のそれぞれの可能性を表す」モナドとして見做すことができます(そういう風にみなすためのbindが定義されている)。

なのでおもしろいことができるのですが、それをGroovyで真似て見ようというわけです。

List.metaClass.rightShiftUnsigned = { Closure c -> // Haskell's >>=
    delegate.collect(c).inject([], {acc,elem->acc+elem}) // concatMap
}
// instance Applicative [] where
//   fs <*> xs = [f x | f <- fs, x <- xs]
List.metaClass.multiply = { List xs -> // Haskell's <*>
    List fs = delegate
    fs >>> {f->
    xs >>> {x->
        assert f instanceof Closure
        if (f.parameterTypes.size() > 1) {
            // Haskellの様に引数が足り無ければ自動的に
            // 部分適用になるということは無いので、明示的に部分適用。
            return [f.curry(x)]
        }
        else {
            return [f(x)]
        }
    }}
}

// リストモナドは非決定計算(可能性の計算)を表す。
// つまり「1 or 2 or 3」と「4 or 5 or 6」を掛け算した結果は、
// 「4 or 5 or 6 or 8 or 10 or 12 or 12 or 15 or 18」となる。
listManip1 = [1,2,3] >>> {x->      // listManip1 = do x <- [1,2,3]
             [4,5,6] >>> {y->      //                 y <- [4,5,6]
             [x*y]                 //                 pure x*y
             }}
assert listManip1 == [1*4, 1*5, 1*6, 2*4, 2*5, 2*6, 3*4, 3*5, 3*6]
println listManip1

// アプリカティブは、箱の中に対する透過的な演算を可能とする。
// モナドはアプリカティブでもある。
// リストモナド(非決定計算)に対して通常の関数(1引数関数(x+1))を適用する。
listManip2 = [{it+1}] * [1,2,3]   // listManip2 = pure (+1) <*> [1,2,3]
assert listManip2 == [2,3,4]
println listManip2

// アプリカティブは、箱の中に対する透過的な演算を可能とする。
// モナドはアプリカティブでもある。
// リストモナド(非決定計算)に対して通常の関数(2引数関数x+y)を適用する。
listManip3 = [{x,y->x+y}] * [1,2,3] * [4,5,6]
// listManip3 = pure (+) <*> [1,2,3] <*> [4,5,6]
assert listManip3 == [1+4, 1+5, 1+6, 2+4, 2+5, 2+6, 3+4, 3+5, 3+6]
// 「1 or 2 or 3」と「4 or 5 or 6」を足し算した結果は、
// 「5 or 6 or 7 or 6 or 7 or 8 or 7 or 8 or 9」となる。
println listManip3

ちなみに上では、Haskellでは「型クラス」であるApplicativeやMonadを、Groovyのクラスとして表現できていません。「データ.操作()」というパターンしか継承の対象にできない、GroovyやJavaなどのクラスベースの型システムの限界があるのですが、リストにメタクラスでオペレーションを付け足しして強引に突破してます。ある意味わかりやすい。

ちなみに上のコード中に出てくる

listManip1 = [1,2,3] >>> {x->      // listManip1 = do x <- [1,2,3]
             [4,5,6] >>> {y->      //                 y <- [4,5,6]
             [x*y]                 //                 pure x*y
             }}

は、[ x*y | x<- [1,2,3], y<-[4,5,6] ]というリスト内包表記というのがHaskellにあって、それはこのようなリストモナドに対する操作のシンタックスシュガーとして実現されています。コメントにあるのが、もう一つ別のdo記法というやつでこれもモナド操作のシンタックスシュガーです。Scalaだとfor{…}というモナド内包表記になるのかな(良く知らない)。


お勧めの本「すごいHaskellたのしく学ぼう!」Kindle版

すごいHaskellたのしく学ぼう!
オーム社 (2012-09-21)
売り上げランキング: 2,943

お勧めの本「すごいHaskellたのしく学ぼう!」Kindleじゃない版。

すごいHaskellたのしく学ぼう!
Miran Lipovača
オーム社
売り上げランキング: 15,643

GroovyでStateモナドを書いてみる

4年前に、GroovyでMaybe Monadを書いてみた。という記事を書きましたが、続編としてStateモナドをGroovyで書いてみます。
いかに当時わかってなかったかが判りました。

abstract class Monad {
    abstract Monad bind(Closure c);
    Monad rightShiftUnsigned(Closure c) { // Haskell's >>=
        bind(c)
    }
    abstract Monad bind0(Monad m);
    Monad rightShift(Monad m) { // Haskell's >>
        bind0(m)
    }
}

class State extends Monad {
    Closure runState // runState :: s -> [a,s]
    State(Closure runState) {
        this.runState = runState
    }
    @Override
    State bind(Closure funcReturnsState) { // bind :: State s a -> (a -> State s b) -> State s b
        return new State({ oldState ->
            def (returnValue, newState) = runState(oldState)
            funcReturnsState(returnValue).runState(newState)} )
    }
    @Override
    State bind0(Monad/*State*/ state) { // bind0 :: State s a -> State s b -> State s b
        return new State({ oldState ->
            def (_, newState) = runState(oldState)
            state.runState(newState)})
    }
    static State Return(x) { // Return :: a -> State s a
        new State({ s -> [x, s] })
    }
}

def push(n) { new State({stack-> stack.push(n); [null, stack]})}
pop = new State({stack-> [stack.pop(), stack]})

stackManip = push(3) >>
             push(4) >>
             pop >>> { x ->
             pop >>> { y ->
             push(x+y) >>
             pop
             }}

(value, stack) = stackManip.runState([])
assert value == 7
assert stack == []

モナドは別言語で作ってみないと結局理解できませんね〜。少なくとも私はそうでした。
次は「IOモナドをGroovyで書いてみる」行きます。

c.f.
uehaj.hatenablog.com


(追記)id:kmizushimaさんがScalaでStateモナドを書いているのを発見(笑

Haksellでwhileを定義する

Haskell WIkiの「IO Inside」に、

Haskell's ability to work with IO actions as with any other (functional and non-functional) values allows us to define control structures of arbitrary complexity. Try, for example, to define a control structure that repeats an action until it returns the 'False' result:

while :: IO Bool -> IO ()
while action = ???

Most programming languages don't allow you to define control structures at all, and those that do often require you to use a macro-expansion system. In Haskell, control structures are just trivial functions anyone can write.

(私訳)

HaskellのIOアクションは、(関数型か否かに関わらず、)他のどんな値とも同様に扱うことができ、任意の複雑な制御構造を定義することができます。試しに、値がFalseを返すまで繰り返す制御構造を定義しみてごらんなさい。

while :: IO Bool -> IO ()
while action = ???

多くのプログラミング言語では、制御構造を定義することが全くできないか、マクロ拡張システムを必要とします。Haskellでは、制御構造は誰もが書けるただの関数にすぎません。

とあるのでやってみた。conditionとactionを分けてみました。

import Data.IORef

while :: IO Bool -> IO a -> IO ()
while cond action = do
  c <- cond
  if (c) then do action
                 while cond action
  else return()

main :: IO ()
main = do
  i <- newIORef 0
  while (do { n<-readIORef i; return (n<10) }) (do
              n<-readIORef i
              modifyIORef i (+1)
              print $ "hello"++show n)


「制御構造」に見えるかな。
whileに与えるところでdoを使うのが違和感があるので、使わないようにするとこうなる。

import Data.IORef

while :: IO Bool -> IO a -> IO ()
while cond action = do
  c <- cond
  if (c) then do action
                 while cond action
  else return()

main :: IO ()
main = do
  i <- newIORef 0
  while (readIORef i >>= (\n -> return (n<10) )) (do
              n <- readIORef i
              print $ "hello"++show n
              modifyIORef i (+1))

Groovyならクロージャを渡すようなことで実現できますが、上ではクロージャを全く使ってないところが興味深いです。(lambda式は>>=に渡すために使っているだけ)。

whileに渡したりwhileから返されたりするIOの値に含まれるRealWorld値(モナドによって隠蔽されている)は、それを導くのに使った計算に紐付いて来ていて、そのRealWorld値がmainのdoの地でも受け渡されていることで、mainの結果の計算に関与することになり、mainから始まっている命令的実行に組み込まれるようになるのだな…というイメージがないと黒魔術としか思えないです。


RealWorldによる値の制御は、プログラム上の表記位置とは全く独立なGotoみたいなもの。
グレッグ・イーガンの「順列都市」に出てきた「塵理論」を思い出させる、、って誰か指摘してるかな。

IOはいかにして命令書(アクション)であるのか:(たぶん)解決編

えー、先日「IOはいかにして命令書(アクション)であるのか」という記事を書きましたが、その疑問は「IO inside」を読んだところすべて氷解したのでご報告します*1

要するに、

type IO a  =  RealWorld -> (a, RealWorld)

です。おしまい*2id:nobsunさんやid:kazu-yamamotoさんが指摘していてくれたことは今おそえばおそらくこれでした。IO aは値ではなく関数なわけです。代数型データ構造(Hoge Int)みたいな表記に惑わされてコンテナだと見誤ったのが、誤解の元。

関数型がファーストクラスな言語では任意の計算が任意に遅延評価できるのだから「遅延評価仮説」は成り立たないし、この意味での副作用の除去*3の理由として持ち出す必要はありません。すみません。

純粋だから何なのさ!

ではいかなる意味で、副作用除去(というより無いと言い張るためにトリック)なのかというと、引数を追加した上でその引数を隠蔽するという一休さんレベルのウルトラトリックなのですがInside IOを読んでもらうのが良いでしょう。

あとは余談です。もう引っこんでろ!という気もしますが。自分の感想メモです。

Inside IOの説明から受ける印象は、「Haskellに副作用が無い*4こと」というのは、「特別・特殊な工夫によって初めて実現された、すばらしい機能」ではないということです。むしろ「命令的に実行する機能の欠落」であり、もっと正確に言うと「命令的に実行しようとすると整合性が破壊される」という欠陥のようなものです。このことが所与で、そのような整合性破壊を防ぐ工夫が、IOアクション=命令書であると。安定性を意図的に低減した上で、コンピュータ制御で安定させて飛ぶ「運動能力向上機」みたいな。

「...(命令書か何か)…によって副作用の除去が実現された」のではなく、もともと無いのが前提で所与。コンパイラをそう作っちゃったんだけど、困ったねえ、というところから始まる*5。マイナスから始まって普通の命令型言語程度までなんとか並ぶとこまで戻すのが命令書。

以下、例え話です。

架空言語Hの話

某H言語のコンパイラは、オプション「-OsuperAgressive」を付けると、すべての関数呼び出しをmemoize(キャッシュ)する。それだけでなく「-OsuperAgressive」の凄いところは、値の使用を精密に検出して、不要な呼び出しを徹底的に削除してしまうところだ。最終結果の算出に「関数」の字義通りの意味で寄与しない関数の呼び出しを軒並削除してしまう、というより、計算をリオーダーして本当に必要になるときまで計算を引き延ばすことによって(実行時の大域的依存性解析に相当)、本当に必要な計算しかしない。本当にしないのだ。不要な計算がなされることは絶対にない。

しかし、副作用を伴う処理をしようとしたとき、問題が生じる。副作用を伴う関数を呼び出すことは何なくできるとはいえ、その制御が全くできないのだ。同じ引数だと二度目の呼び出しは無視される(値は同じはずだ、と仮定されてmemoizeされたキャッシュ値で代替されてしまう)わ、値を返さなかったり、結果の値を使わないなら不要コードとして呼び出し自体が削除されてしまう。リオーダーも激しく、この結果、副作用を任意の順序で発生させることができないし、そもそも確実に発生させるということができない。

問題は、オプション「-OsuperAgressive」は、とある事情で外すことができないということだ。(外した状態のソースコードがHDDのクラッシュで失なわれてしまったか、外すためにはものすごい金額の有償版へのアップグレードが必要!とか。なんかそんな理由だ)。また、「aの次にbを呼ぶ」という単純構文がエラーとなるというバグもある。ヤレヤレ。

このクソな状態を「純粋だ」とか言う人もいるらしいいってね。ものは言いようだ。Hahaha.

さて、あなたのミッションだが、この某H言語のコンパイラを使って、副作用を持つ関数を呼び出してなんとか動作するプログラムを書く必要があるということだ。なにしろその基本性能はすばらしいのだから…。

ということで編み出しましたのがこの方法です。

〜スクロールしながら現われる: Haskell IO 〜


Inside IOを読んでの印象は「おまえは何と戦っているんだ」です。コンパイラの制限との戦いで何を得ているんだと。「unpureという予約語を導入する」とか「副作用あり指定プラグマ」じゃ駄目なんですか??!!!

「参照透過性」という名目のため?でも、RealWorldを導入することで死守できた参照透過性というのは絵に書いた餅でしかない。お前は今まで呼んだIO関数のRealWorld引数の値を覚えてるのか?ってことです(覚えてませんしそもそも知りません)*6

おそらく、命令型という性質を外付け・客体化・操作対象としていることが意義なのでしょう。

Hskellの純粋性は、むしろ原始的なもの。命令型言語処理系では不可分な総体だと思われていたものを「純粋部」と「命令部」に分解し、命令部をライブラリレベルで実装しオープン化している。Cで入出力がそうされたように。

結局Haskellに副作用はあるのかないのか?

ないという前提のコンパイラ上に、あるように見せかける機構が作り込まれている。「本当はないのだがあるかのように作り込まれている」。その作り込みがあまりにも巧妙で、デメリットも含めて「副作用の有り状態」が再現されているので「本当は副作用が無い」と言うことの意義が良くわからなくなるぐらいである。

*1:この文書はHaskellの初歩的な理解のために必読だと思われます。

*2:GHCなどの実装で本当に文字通り上記かの通り定義されているかというと、たしか違う。ST s aが介してるんだっけかな。

*3:命令書と実行系を分けて考えるという意味での副作用の除去の意味で。

*4:引数によってのみ関数結果が定まる、という意味で。

*5:もちろん本当はそうではないでしょうけど。

*6:もしそれを覚えていて引数とmemoizeされた返り値も併せて利用できるなら、「バックステップ実行」とかができて楽しいかも。スタックトレースなんかよりはるかにデバッグには有効でしょう。

IOはいかにして命令書(アクション)であるのか

2013.10.14追記、IOはいかにして命令書(アクション)であるのか:(たぶん)解決編を書きました。
2013.10.9 AM6:21「追記その2」として「おぼろに全容として思いついた理解」を追記しました。

Haskellおもろいで

この1ヶ月ぐらいHaskellをかじってます。Haskellはこれ以上ないってぐらいエキサイティングな言語で楽しいものですね。まだ初心者ですが40半ばの手習いです。どうでもいいですが、この年齢だと新パラダイム言語に挑戦するのはもう最後のチャンスぐらいかなと思ってますが、そんなことはどうでもいい。

この果てしなく遠いモナド

ところで、Haskellには「IOモナド」という概念があるのですが、なかなか判るまで判りにくいです。

IOモナドは入出力というごく基本的な機能にかかわるものであり、初心者でも否応なく直面せざるを得ない機能であるにもかかわらず、「モナド」なる概念操作をベースに作られているものなので「モナドの壁」にまずぶちあたることになります。IOモナドモナドの一種です。モナドっていうのはご存知の人も多いと思うのですが、いろんな人がいろんなことをブログに書いているので初学者にとって混乱しやすい概念です。このブログ記事もその一つです。

でも、本記事はモナドが何かを説明することを目的とはしていません。ご安心ください。

基本的にはモナドは明確な概念ですが、高階操作と多相型操作に強く基づいており、そのような操作に頭だけではなく手を動かして体得した上ではないと理解できないような、そういう脳トレを経た上で「体で(脳細胞の連接を作る)」覚える部分もある概念だと思います。ちなみにわたしはまだまだその域には達することができてはいません。

さてそのような学習者にとって、id:kazu-yamamoto さんが書かれた
QAで学ぶMonad」という記事はモナドをたいへんわかりやすく整理されており、珠玉の良記事です。

この記事の趣旨は

Monad とは、単なる型クラスの一つです。難しいという風評もありますが、それ以上でもそれ以下でもありません。

につきると思っています。名記事です。モナドなんて怖くないぞ!

「命令書」に例えられるIO

しかし、この記事を読んで行くと

しかし、実際 getChar が返すのは「一文字読み込め」という命令書です。どんな状況においても、同じ命令書を返します。ですから、副作用はないのです。副作用を発生させるのは、命令書を実行するランタイムです。

IO は命令書であり、命令ではありません。>>= を使えば、小さな命令書を合成して、大きな命令書を作成できます。最終的な命令書には main という名前が付きます。これを実行するのはランタイムです。

ただ、IO が命令書であることを活かせるようになるには、相当修行を積む必要があります。そこで最初のうちは、「IO は副作用を起こす関数を表している」と理解しても問題はありません。IO が付いていなければ純粋で、IO が付いていれば副作用があって、それらを明確に区別できるから Haskell は素晴らしと理解しても結構です。

ただ、Haskell を使っていくどこかの時点で、正しい理解をして下さい。

モナドがわかったような気持になったのが、この時点で見事に打ち砕かれます。モナドは単なる「単なる型クラスの一つ」だったはずが、重要なモナドであるIOに限って(?)は「命令書」なる未知の概念の体現であり、その活用には相当修行が必要であり、「まあ、せいぜい初心者は便宜的な理解をしておいてください。それは結局はくつがえされるであろう理解なのですが、せいぜいHaskell は素晴らしいと理解しておいてください。いつか正しい理解をしたときにまた御会いしましょう、でわ!!」なのです。

ずこーっ、と思いますよね。じゃあその命令書って何だ? って思いますよね。
先の記事を揶揄するつもりはありません。「IOは命令書」の意味がわからないと、モナドとしてのIOに限っては、元記事の趣旨をくつがえしてしまうかのように見える、ということです。

なお、IOの結果は「命令書」だから、Haskellそのものは純粋だ、と言うことの核心的根拠としても使われる場合があるようです。

例えば「Haskellには副作用がないのか?」という記事では、

評価器までがHaskell
この立場の説明では、Haskell には副作用はない。なぜなら、Haskell が作るのは命令書のみで、それが実行されるのは Haskell の外での話だからだ。
たとえば、getChar :: IO Char は、実行されるごとに(同じこともあるが)別の文字を返す。それでも Haskell には副作用はない。Haskell が作り出すのは、「標準入力から文字を読み込め」という命令書だけであって、実行はしないからだ。

などです。

しかしながら、上の立場を検討するための前提条件として、「なんでIOの結果は命令書なのか」がやはり問われる必要があります。

なぜ実際のIO処理の起動(評価)が、評価器ではなく実行器側でなされるのか? それができるという前提だからこそ、上の主張が可能なわけです。「IOの結果がいかなる意味で命令書であるのか、いかにしてそれは命令書なのか、この場合の命令書とはどういう振舞いを差して命令書と言っているのか」を明確にしないなら、なんでもありの議論になりかねません。なんとなくな比喩としてなら判るのですが、それは理解ではありません。

命令書遅延評価仮説

「命令書」とは何かも、その機構も明確ではないので、とりあえず自分が推測した限りでつじつまの合う、もしくは比喩として説明のつきやすい機構を考えてみましょう。

まあ初心者の言うことなので、あてにはならんのですが、あがきです。仮説は以下のようなものです:

命令書であることの必要条件は、何の変哲もない多相型(IO a)を実体型(IO Charなど)にしたコンテナであるはずのIOを返す関数の結果である純粋データ=値を、実行器(ランタイム)が解釈実行するにあたっては、実際に副作用を持った操作を含む操作列として正しい順序で実行でき、元のHaskellプログラムの必要性に従い値と副作用を得られることです。また、mainに設定できる値はmainへの代入の右辺を評価器が評価した結果であるところのIO aを実体化した型のデータのみですから、命令書はその型のデータのみで表現できる必要があります*1

上から想像できるのは、これを実現できそうなのは、遅延評価の枠組みそのものだ、ってことです。

元の「命令書」という表現をした人が、「命令書」という言葉で何を想定していたかは置いておいたとしても、上記は上記で命令書として機能しそうな仮説ですよね。なお、ややこしくなりそうなのでこの仮説「遅延評価で実現される命令書」を「命令書X(エックス)」と以降呼ぶことにします。

この仮説が正しいとするなら、「命令書X」としての動作を実現しているのは遅延評価の仕組みであり、IOともモナドとも関係がありません。Haskellの評価機の純粋性を担保しているのも、遅延評価ということになります。評価機の中でIOを返す関数が何度呼び出しても同じ結果を返すのは、異なる結果が発生するのは実行時なので、それが実行器(ランタイム)に先伸ばしにされているからに他なりません。IOはうっかり屋さんのために、副作用コードと純粋コードを分けて、その混合をコンパイルエラーとして排除するためだけの機構で、便利なのでモナドを使って実装しているだけ、ということになります。

またもしそうならば、先の「評価器までがHaskellだ」という主張について、同感するかどうかはともかく全く疑問がありません。「命令書」というわけのわからない未定義語が説明に含まれなくなり、意味がわかるようになるからです。

先の仮説が、「命令書の定義」とは別に機構的には成立しそうに感じられる話が「本物のプログラマはHaskellを使うITpro第7回 入出力と遅延評価の間を取り持つIOモナド (2/3)」という記事にありました。

モナドによって複数のI/Oアクションがきちんと順序づけられるとして、I/Oアクションの呼び出し自体はどうなっているのでしょうか? Haskellでの値が遅延評価されるのに対し、Cの標準ライブラリやシステムコール(system call)などのAPIは、遅延評価を行わない一般的な言語から呼び出すことを前提に提供されています。そのままではI/OアクションをHaskellから利用することはできません。
実はHaskellの値は、遅延評価を可能にするために、マシン表現(ビット・パターン(bit-pattern)、非ボックス化型)を直接扱うのではなく、「未評価の中断(suspension)状態、あるいはデータ構成子に格納された値のマシン表現のいずれか」を表現するヒープ上のオブジェクト(heap-allocated object)へのポインタ(ボックス化(boxed)された型)として扱われるようになっています。だとすれば、I/Oアクションの呼び出しを行う前にHaskellの値を評価し、APIにマシン表現を渡してI/Oアクションを実行し、返値のビット・パターンを改めて(IO型というコンテナに包んだ)Haskellの値として格納すればよいのではないでしょうか?
これができれば、あとは実行環境で外のAPIを呼び出すようにすることで、I/Oアクションを実現できます。

残る問題1

上の仮説が正しいかどうかは正直今の自分では判りませんが、もし正しいとしても疑問なのは、世の中で、なぜかIOだけが命令書として説明されているように感じられることです。上の記事にも、すごいHaskellたのしく学ぼう!、とかこちらの記事にも「IOは命令書(アクション)」と書いてあります。遅延評価仮説が正しければ、IOだけではなく、遅延評価される式はすべてが命令書Xの性質を満しているはずなのに…。

もちろん「IOが命令書Xは正しい」のですが、言及がIOに関してだけだと「それ以外はどうなの?命令書Xじゃないの?書いてないってことは違いそうだな。ならIOだけが命令書になる合理的な理由がわからないし、うーんじゃあ結局わからない」という人がいそうです←私。

それはそうと、「命令書概念の成立の根本には遅延評価が寄与している」とどこを見てもひとことも書いてないし。ていうことはやっぱり違うんだろうか?? それともあたりまえすぎて、言うまでもないからだろうか?

IO限定なら、「命令書」はHaskellにだけ通用する説明になってしまいます。そうでないなら、言語を越えて、「命令書概念」が共通に成立するための条件を知りたいのです。

残る問題2

「QAで学ぶMonad」 の記事には、再掲ですが

IO は命令書であり、命令ではありません。>>= を使えば、小さな命令書を合成して、大きな命令書を作成できます。最終的な命令書には main という名前が付きます。これを実行するのはランタイムです。

と書いてあります。上の仮説によれば、>>=を使う使わないに関わらず、命令書Xは合成されるので、ここで言う命令書と命令書Xとは、やっぱり異なるものなのかもしれません。

まとめ

おしまいです。初学者としての疑問と見解をまとめてみました。ご存知の方がいらっしゃいましたら、ご教示頂けますと幸いです。

追記その1

適切に定義された>>=により、IOが実行順序を制御する存在となっているというのは理解しているつもりです。「QAで学ぶMonad」にはまさに

IO が記述順に実行されるのも、同じ理屈です。たとえば、IO の実現方法として、Worldという正格データ、すなわち実行環境を渡していく方法があります。
(略)
Monad が実行順を決めるのではなく、Monad のあるインスタンスの >>= が、実行順を守るように定義されているだけです。

とありますしね。でも、このことをもってして「命令書」と言っているということなのでしょうか。例えば、

f(g(h))

h,g,fの順に評価されることを期待できると思いますが、これは命令書なんでしょうか。
あるいはこれを巧妙に例えば

do
h
g
f

のように表記できるシンタックスシュガーがあるから、命令書になるんでしょうか?
ここまでならIOやモナドだけに閉じた話ではないですしね。

もっと複雑な操作、非モナド値との相互作用を含めて記述できるから、命令書なんでしょうか。
ならば、その境界がわからないな…。
そしてそれが純粋性を生じさせることに寄与しているとしたら、そう思おうとすることの動機が理解できません。

追記その2

おぼろに全容として思いついた理解は、純粋関数型言語Haskellの上に、命令型言語のエミュレーション層を作ったよ(ドヤッ!)、ということかなと。その命令型言語のコード列を「命令書」と呼んでいると。

その命令型言語のエミュレーション層のフロントエンド(構文解析側)はモナドを駆使した内部DSLとして実装されておりそれをIOモナドと呼ぶ。コンパイラが生成する中間コードは遅延評価により評価途中の値を含む、Haskellのサンクから構成される評価値の木構造。その言語のバックエンド側(ランタイム、インタプリタ?)の実行は遅延評価が担保していると*2

利点はおそらく、Java VM上で、C言語からトランスレートされたコードを実行するようなもんで、底が抜けない、と。

「命令書」という名称は、目的的、用途的につけられた名称であるため、機構的な意味での言語的な差別化要因はない*3。機構的には、IOを返す関数の結果と他のモナドやリストを返す関数と違いはない。遅延評価はいずれに対しても本質的な実装機構。

今はこれが精一杯。



すごいHaskellたのしく学ぼう!
Miran Lipovača
オーム社
売り上げランキング: 16,143

*1:特別扱いはされていないという仮定はそんなに不自然ではないでしょう。

*2:もっとも、バックエンドはhaskellのそれそのものです。

*3:あえていえば、haskellの処理系の実装者は入出力処理を行う組み込み関数は細心の注意を払ってIOを返すようにしているであろう。