Haskell と Scheme の共通点

Haskell の本を再度読み始めてみて、すっかり忘れていたいろいろを改めて認識した。同じ関数型言語だけあって Scheme と同じ考え方が多い。

リストの扱い

HaskellScheme もリストが超重要なデータ構造なんだけど、どちらも以下のような特徴がある

  • リンクリスト
    • 前から後ろに一方通行でたどれるリンクリストになっている点
  • 先頭要素、先頭以外を取得
    • Scheme の car、cdr は Haskell では head、tail となる(パターンマッチなら (x:xs))

前に Haskell を勉強したときは、特に head も tail も印象に残らなかったけど、Scheme を勉強した後だと、これがなくてどうやって再帰的にリストを操作するのよ、という風に感じてしまう。

共通点があるといっても、相違点も存在する。

Scheme ではリストには何でも入れられるけど、Haskell では入れられる型が決まっている。じゃあ Scheme のように何でも入れたい場合はどうするか。わかりません (><;/~

束縛

他の手続き型言語だと「x = 10」を「変数 x に値 10 を代入する」という風に表現するんだけど、Haskell だと「値 10 に変数 x を束縛する」という。なんかしっくりこなかったんだけど、Scheme でも同様の表現をする。

この言い回しの理由としてひとつわかったのは、あくまでも x に 10 を代入するのではなく 10 に x というラベルを付け、そのラベルを 10 の代わりに使用する、という考え方だとうことだ(これは Haskell の場合)。なので、10 に x というラベルをつけて、そのラベルがついているオブジェクトを 20 にする、ということはできない、つまり再代入という考え方が封殺される、と取れる。これが参照透明性を支える要素のひとつで、それがために「値 10 に変数 x を束縛する」なんて(手続き型言語プログラマにとっては)ちょっと混乱を招くような言い回しになるわけだ。

ループ構文が存在しない

HaskellScheme も、繰り返し処理を行いたい場合は再帰呼び出しを行うことで実現できる。

Scheme には末尾再帰という考え方があるけど、Haskell には仕様上(文法上)そうした制約は存在しない。ただし、ひたすら再帰するとスタックオーバーフローになりそう。そういう場合は継続モナドを使えばよくて、この継続モナドは超簡単に説明すると Scheme の末尾再帰と同じ(再帰呼出しの戻り値がその関数の戻り値)仕組み。

let とか

一時的に変数を用意したい場合に let を使うのは HaskellScheme も共通。

習得が困難

これが最大にして最強の共通項かと。

Haskell で「与えられた文字列を第一要素は行の内容、第二要素は改行コードを持つタプルのリスト」にするプログラムを脳内コーディングしているんだけど、一向に完成しない(というかどうすればよいか思いつかない*1)。

linesWithLineEnd :: String -> [(String, String)]

関数定義はこんな感じなんだけど、その先の処理を考えるに当たり、どうしても C や Java のような手続き思考をしてしまう。

慣れの問題だと思うんだけど、この「慣れ」というのがプログラマにとってはこれまで蓄積してきた経験値やレベルに相当するので、それを捨てて(正確には「利用せずに」)新しい領域のレベル上げをする、というのはかなりの苦痛だったりそうでなかったり。

人生、時間がたっぷりあればいいのにね、と思う常々。

*1:思いつかない、はちょっと言い過ぎで、与えられた文字列を tails 関数で全部なめつつ、isPrefixOf で改行コードを探して、splitAt で分割、という風にするんだと思うんだけど、どうやって splitAt に渡すインデックスを取得するのか、リストを分割してタプルに入れるにはどうするべきなのか、それをリストに組み直すにはどうするのか、などがさっぱりさっぱり