type t (* void *)

ソフトウエアのこととか

プログラミング一般:プログラム書いてて感動した事

友人がOCamlを触ってくれているようで少し嬉しくなったので記事を書いてみます。SML触っていてプログラミング言語楽しいなぁと最初に思った時の話。

最初高階関数や関数を引数に渡せて何が嬉しいのだ、と思ってた頃の話です。ものすごいアタリマエのことをクドクド書いてみます。くどいくらいにMLに関する補足を入れています。

まずはじめに、関数型言語の紹介みたいなもののなかで、

let double f x = f ( f x )

というような、処理の枠組みが様々な型に対して汎用的に処理できる、と最初多相型や関数を引数に渡せることのメリット、と最初紹介されたのかな(どこの情報なのか覚えていませんが)

このケースの場合、

f : 'a -> 'a

x : 'a

double : ('a -> 'a ) -> 'a -> 'a

という型がつきます。一応MLを触ったことがない人のために説明しておくと、'aというのは任意の型という意味です。なのでintでもstringでもcharでも自分で宣言した型がfの引数に来てもいいわけです。

また->というのは関数を表しておりまして、'a -> 'aは'a型の値を受け取って'a型の値を返すような関数である、ということを表しています。'aという部分には同一の型の値が来なければなりません。

えーこんなことする機会あるのーよくわからんーと思った記憶があります。

最初に引数に関数を渡せることの嬉しさを感じたのは、半年間SML#を書いた時にmap関数の存在を知った時です。(そのときまで知らなかったのです

ほんとはSMLで書きたいところですがOCamlと少し(ではなくいっぱい)表記方法が混同しそうなくらい記法が違うのでOCamlで書いておきます。

ご存知の通り、ListというのはMLでは(様々な分野で?)[1;2;3;4;5]のような形で書かれる順序のある要素の羅列を表します。上の例にはint listという型がつきます。

このListの要素それぞれに何か変換をして、それを処理するための関数というのはその頃勉強が足りなかったのでmap関数の存在を知らず、

let rec hoge  list =

    match list with

    |    (x::xs) ->

            let target = (* complex and difficult calculation *) in

            target :: hoge f xs

    |     [ ] ->  [ ]

という書き方で行っていました。再帰関数で書いてますが勿論ループで書いてもいいと思います。

これって結局なにをやってるのかなーと考えた時、各要素に対して操作して、新しいListを作る操作を指定ます。これをもっとエレガントに書き直せます。標準ライブラリに提供されているList.mapを使ってみましょう。

# List.map;;

- : ('a -> 'b) -> 'a list -> 'b list = <fun>

# List.map (fun x -> x + 1) [1;2;3;4;5];;

- : int list = [2; 3; 4; 5; 6] 

 今回は非常に簡単な処理を例に出していますが各要素に対して処理する、という部分がList.mapというワードで簡単にかけました。勿論標準ライブラリにあった関数を使ったら簡単にかけた、というだけの話であるのですが。

続きが少しあります。実際この処理を書いていたとき、Listの中身に入っているのはcのヘッダーファイルの一行一行を表す新たに定義したある程度複雑な構造だったのです。で、その中から必要になる情報だけを取り出す、という事をやっていました。

分かる方向けにSML/NJに入っているckitを用いていました。

なので、実際はもっともっと面倒な処理を長々書いていたのですね。

それを書いている時、先生にmapを使うように指示されてから

  • まず各々の構造に対する処理を関数fに書く
  • それをList.map f listという形で全ての構造に対して処理をする

という枠組みで処理するようになって、非常に見通しがよくなったんですね。

見通しが良くなった、というのはソースが

  1. 読みやすくなった、構造を変換する関数が短くなって複数の関数に分解できた
  2. List.mapと書いてあると、これからListの各々の要素に対して処理します、というプログラマの汲み取りやすくなった、コードを読みやすくなった
  3. ソースが書きやすくなった、各々に対する処理を書いてから全体を処理する関数を書く、というような自分がまず何をしなくてはいけないのか明確になった

このことからList.mapのような、受け取ったリストに対して共通の処理を各々行いその結果をまたつなげてListを作る、というような「処理の枠組み」はやっぱり作れたほうが嬉しいわ、ということです。

何よりもList.mapのような共通の枠組みが提供されると、それに合わせて思考する方法が変えます。どういう風にコードを書くのか、それが変ります。それが良いなぁとその時感じました。

思えばいつ版最初の処理というのは、cでいうとMain関数の中で補助関数を定義せず全て処理をベタ書きしているようなものだったのかなと。それをMapを使って書けば処理を分けて自然に書くことになるし、mapを利用することによりそれをどういう風に分ければよいのか自明に分かるのです。

それを補助するための枠組みとして、多相関数や高階関数が非常に有用なんだ、という事がよくわかりました。

高階関数が便利であることはまだ示していませんが、例えば上の(fun x -> x + 1)というのはもっと上手く書けます。

OCamlでは+も関数で、実は中置演算子というのはシンタックスシュガーです。

(+) 1 2 とかけば3と評価されます。

# (+);;

- : int -> int -> int = <fun> 

# (+) 1 2;;

- : int = 3

 ああ、なるほど。+というのは2引数関数なんですね。これを用いて(fun x -> x + 1)は

(+) 1 とかけます。確認しておくと高階関数というものは関数を返り値として返す関数で、(+) 1 2とするとまず(+) 1 が評価され+1するための関数が返ってきます。それに2を渡しています。

ちなみにアローは右結合的です。前の例はint -> (int -> int)と括弧が省略されていてもこのように読みます。

このようにいっぺんに引数を渡さなければならないのではなく、例えば2引数関数に対して1つしか引数を与えず関数を受け取れるようにした形のことをカーリー化されていると読んだりします。

# List.map ((+) 1) list;;

- : int list = [2; 3; 4; 5; 6] 

 今回のケースだと簡単にかけた!嬉しい!というものですが、簡潔に書けるというのは大切な事だと思います。

上の引数に関数を与えられるという話で、引数に関数を与えるというのはc言語でも一応出来ます。関数ポインタを用いれば良いわけです。

ですが、その方法って簡単なのかな、分かりやすいのかなぁと考えた時に、僕にはこっちのほうが圧倒的に簡単に思えたのです。分かりやすさはその言語でコードを書ける人の数や、コードの読みやすさに直結すると思います。

余談ですがCの関数ポインタは色々怖いです。

また簡易的な処理を、ぱっと匿名関数で書けることはこの時初めて嬉しいことだと思いました。

例えばList.map (fun (x,y) -> x) listのようにタプル(組み、複数の値をもつラベルのない順序を保持する構造体みたいなもの)から左側の要素だけを取り出す、みたいな処理はしばしば書かねばなりません。が、こういう風にぱっと簡単な処理を書けることは非常に嬉しいことだと思います。

 

上の例とは少し関係あるかな?あまり関係ないのですが、とある先生の仰っていた標準ライブラリに書かれている関数を覚えることは重要、という話をこれを書いていて思い出しました。

東北大学の情報系の先生で、「知的処理の本質は名前をつけることにある」と主張されている先生がいらっしゃいます。

ああ、一連の作業にList.mapという名前をつけることで、その詳細を語ること無くその概念を伝えることが出来るのだなと。だから標準ライブラリの関数を覚えることは言語学習でいうところの単語を覚える作業に当たるのだ、と。