Haskell 始めました,以上です.

以降余談となります.

こんにちは,えだまめです.
去年のアドベントカレンダーではJavaScriptを勉強し始めたという内容の話をしていたように思いますが,今年はまた別の新しい言語を勉強しています.
それは Haskell です.

Haskell とは関数型プログラミング言語と呼ばれるものに分類される言語です.
特にHaskellはあらゆる副作用を排除した純粋関数型言語というものです.今回はその「副作用を排除」ということについてHaskellを勉強してきた中で考えたことを述べます.
個人の解釈を大いに含んでいると思われるので決して正しいことを言っていると思わないでください.

副作用とは

まず副作用とは何かということについてです.
皆さんはプログラミングをする中でいろいろな関数を使用すると思います.そして関数を実行するとき,一般的に引数を入力として与えそれに応じた結果の出力を受け取ります.
副作用とはこの関数を実行するときに得られる結果以外の出力のことを言います.

例えば,以下のようなC言語の関数があったとします.

int func( int x ){
    global = global + 1;
    return global + x;
}

globalというのはグローバル変数です.
この関数を複数回実行すると

// global = 1
func( 3 );  // 結果: 5
func( 3 );  // 結果: 6
func( 3 );  // 結果: 7

このように,同じ引数を与えても実行するたびに異なる結果を返します.
これは,関数funcがグローバルな状態(globalの値)の変更を行っているからです.
このグローバルな状態の変更が副作用です.
また printf 関数は,フォーマット文字列とそれに合った出力したい変数を引数として標準出力の状態を変更し,書き込んだ文字数を結果として返します.
この標準出力の状態を変えるのも副作用です.

副作用を取り除く

さて,先ほど述べたようにHaskellでは関数に副作用がありません.
すべての関数が副作用を持たず,結果は引数によってのみ決まります.
そのような性質を持つ関数を純粋関数といいます.
それではHaskellでは状態を扱った計算はできないのでしょうか.
もちろんそんなことはありません.
状態を扱う計算を純粋に行うことは可能です.

上で挙げた状態を扱う関数funcから副作用を取り除いてみます.

struct Result {
    int state;
    int result;
}

struct Result func( int x, int s ){
    struct Result r;
    r.state = s + 1;
    r.result = r.state + x;
    return r;
}

C言語で複数の値を関数の結果として返すために構造体Resultを定義します.
そして,関数funcの引数には x だけではなく状態を表す変数 s も一緒に与えます.
この関数は以下のようにして使用します.

struct Result res[4];

res[0].state = 1;  // 初期の状態を設定

res[1] = func( 3 , res[0].state );  // res[1].result == 4
res[2] = func( 3 , res[1].state );  // res[2].result == 5
res[3] = func( 3 , res[2].state );  // res[3].result == 6

このように,状態を関数の引数として与え,関数を実行した後の状態を計算結果とともに返します.
そのあとは得られた状態を次の関数の引数として与え,また得られる新しい状態を次の関数の引数にというように実行していくことで状態を扱った計算をすることができます.
しかも,この関数は結果が完全に引数によってのみ決定しているので純粋関数です.

以上のように関数から副作用を取り除くことができました.
次に,コンソールに文字列を出力する関数も同じように副作用を取り除いてみます.
この場合は標準出力の状態を関数の引数として与えます.

struct Result {
    char out[64];
    int result;
}

struct Result print( const char *str, const char *out ){
    struct Result r;
    strcpy( r.out , out );
    strcat( r.out , str );
    r.result = strlen( str );
    return r;
}

ここでは仮想的な標準出力 out を定義し,そこに出力という形で文字列を連結していきます.
以下のように使用します.

struct Result res[3];

strcpy( res[0].out , "abc" );  // 初期状態

res[1] = print( "hello" , res[0].out );  // res[1].out == "abchello"
res[2] = print( "world" , res[1].out );  // res[2].out == "abchelloworld"

作成した print 関数は出力したい文字列と現在の標準出力の状態を受け取って標準出力に文字列を出力(連結)します.
というわけでこの関数にも副作用はありません.

Haskellにおいて状態を扱う計算は大体このようになっているはずです.
入出力に関しても,GHCというHaskellコンパイラにおいて入出力を表す型 IO a は

newtype IO a = IO ( State# RealWorld -> ( State# RealWorld , a ) )

というようになっています.
これは
「IO a とは世界の状態をとって世界の状態と計算結果(型がaの値)を出力する関数」
という感じの意味です.
例えば,標準入力から一行分文字列(String型)を取得する getLine という関数は IO String という型です.
この場合の世界の状態とは標準入出力やメモリなどコンピュータの状態を指しています.

Haskellでは最終的に main という IO () 型の関数を作成して実行します.( () は何もないことを表す型)
つまり,Haskellのプログラムというのは
「実行したときのコンピュータの状態を引数として実行される関数」
という見方ができると考えています.
そして結果としてプログラム実行後の状態を得られるというわけです.
よってHaskellには副作用がないといえます.

終わりに

今回の話はこれで終わりにしたいと思います.
ここまでお読みくださりありがとうございます.
Haskellはとても楽しい言語なのでぜひ皆さんも触れてみてください.


次回の記事は「かすぴーがIt's a Sony展に行ったお話」です。お楽しみに!

Twitterでフォローしよう

おすすめの記事