見出し画像

Retro-gaming and so on

OCaml と言う言語

isamさんにF#を紹介した関係もあって、ちとここでOCamlと言うプログラミング言語を紹介しておこうと思う。

その前に。
F#と言うプログラミング言語に付いて。
Microsoftはハードウェアを直に叩くようなプログラミング言語をC/C++に任せ、Javaのような仮想マシン(.NETだな)前提のプログラミング言語をいくつか作ったわけだ。
3つ程作ってて、それらをVB.net、C#、そしてF#と言う。
実はこの3つ、全く基盤は同じだ。
VB.netは今までのVisual Basicユーザーを受け入れる基盤として作り、C#は今までC/C++を使ってプログラムを書いてたんだけど、もっとラクに、そして生産性を上げたい、と言う人たちあるいは会社の為のプログラミング言語として。
そして、Microsoftが関数型言語ブームを横目に見て、「これからのプログラミング言語」として、そのスタンダードを目指したのがF#となる。
この3つはそれぞれ、「全く同じライブラリを使える」ある意味、異体同心、だ。根幹は同じだが「表現が違う」と言う言語群になっている。
そしてこの3つの中で、一番人気がないのがF#だ。
ここで「人気がない」と言う事を聞いて不安に思うかもしれない。しかし、構造上、「C#で出来る事はF#でも全部出来る」と言うのがまず1つ。そして、F#は「今までのプログラミングパラダイム」を変えるようにデザインされている。つまり、このブログでは既にお馴染みだが、「命令形->構造化->オブジェクト指向」と流れてきたプログラミングのトレンドじゃない場所、つまり「関数型プログラミング」を布教するために作られてる言語と言う事だ。
このパラダイム変換についてこられる人はそうそういない。当然だ。だからユーザー数はまだまだ少ない。
ただし、Stack Overflow Developper Survey 2021での「もっとも愛されるプログラミング言語」でも、投票数は少なくとも、満足度は62%超えだ。しかも「収入」で見ると、F#ユーザーは稼ぎが二番手にまでなってる。つまり、「高収入なプログラミング言語」なんだ。
このテの話は「現時点」だけ見てもしょーがなく、むしろ今後、伸びていく、って考えた方が良いと思うし、VBやC#に「慣れない」人の為に、第三の選択としては大いに「アリ」だと言って良いだろう。なんせ3つとも「最終的に出来る事は全く同じ」なのだから。

さて、Microsoftは当然、プログラミング言語開発前に市場調査とプログラミング言語の調査をキチンとやる。
そして新規開発の関数型言語のベースとして、MicrosoftはLispでもHaskellでもなく、OCamlを選んだ。
その理由は色々とあるんだろう。
恐らく、まずは静的型付けの方が.Netと相性が良かったろう事が1つ。オブジェクト指向の有用性を切り捨てたくなかったろう事が1つ。そして入出力のような副作用を簡単に扱える事が1つ。
この3つの条件をクリアした言語のベースを提供してくれるのがOCamlだった、と言う事なんだろう。

なお、今、isamさんがやってるのはMicrosoftが提供している「F#のツアー」と言うページらしいんだが、この他のまとまったページや本などはなかなか無い、との事。
そういうわけで、F#を学んでみたい、って人は同時にOCamlを知れば色々と面倒事はないんじゃないか、と言うんで勢いでこの記事を書き始めたわけだ。

と言う事でOCamlの話に入る。OCamlと言う言語がMicrosoft F#の母体になっている。
上で見た通り、OCamlと言うプログラミング言語は関数型プログラミング言語で、なおかつ、

  • C言語、JavaやPascalのような静的型付け言語
  • C++、Javaのようなオブジェクト指向言語
  • 副作用がある不純な関数型プログラミング言語
と言う事になる。
特にここで特徴的なのは、OCamlは「関数型言語」の癖に「オブジェクト指向言語」だと言う事だ。
元々、関数型言語とオブジェクト指向言語は相性が悪い。両者を融合しよう、とする研究はあるにはあるが、殆ど唯一と言って良い性交成功例がOCamlだ。
そしてMicrosoftはHaskellのような純粋関数型プログラミング言語を選ばなかった。ここはLisp系語族のような「不純な」関数型言語の方が確かに扱いやすい。Haskellのような純粋型だと入出力をする事さえ大変になってしまうからだ。フツーの言語と違ってprintするのも一苦労、っつーのなら当然支持は得られないだろうからな。
と言うわけで、OCamlは静的型付けの関数型/オブジェクト指向言語、って事になる。

OCamlは実はフランス製のプログラミング言語だ。フランス国立情報学自動制御研究所(INRIA)と言うフランス国家機関が作ったプログラミング言語である。
つまり、OCamlを学べばオシャレなパリジェンヌとパリの空の下、セーヌ川の畔で愛を語り合う事も可能となる言語と言う事だ(謎 
ジュテーム言語である(謎
ちなみに、以前、フランス人のプログラマ志望の若者とネットで交流を持った事があったが、フランス人の癖にOCamlと言う言語の存在を全く知らんかった(爆)。
冗談はさておき、フランス人でさえ知らんフランス製プログラミング言語OCaml。意外な事に、日本では結構愛好家が多い。多分聞いた話だと、特に日本の中部(愛知県周り?)を中心に愛好家が多い模様だ。実は日本ではそこそこOCamlユーザーがいて、と同時に、日本人は実は外国人に比べると「関数型言語」に比較的抵抗がない国民だ、ってのが見て取れる。

なお、一時期、お茶の水女子大学でのプログラミング入門講座用のプログラミング言語として採用されていた事がある。そしてその講座の為に書かれた教科書も一般向けに販売されている。

 
実はこの本、マジで「今まで一回もプログラミングをやった事が無い人向け」って意味では、知ってる限り一番よく書けてる本だ。ホント、全員この本からプログラミングを学びはじめてもエエんちゃうの?って思うくらいのデキの本なんだわ(※1)。マジで。
まぁ、お茶の水女子大学の女子学生がはじめて授業で使う念頭で書かれているんで当然と言えば当然なのだが。
しかも、お茶の水女子大学卒の新人プログラマーがプログラミング関連会社に就職した際、会社の先輩らのコーディングを見て

破壊的に変更するなんて信じらんなーーい。キモッ♡

と言わせた、と言う伝説の書だ(笑)。こんなに正しく学生に影響を与えた本なんざ他にねぇだろ(笑)。
なお、お茶の水女子大学の著者のページでこの本を使った講義ビデオが無料で公開されている

さて、OCamlはザーッと見ると、大きな特徴が2つある。

1. 型が超厳格

PascalやC言語からはじまって、整数と浮動小数点数を「足せる」と言うのがフツーだが、OCamlはこれを許さない。
これがOCamlをはじめて触ると一番ビックリする事だろう。


そう、OCamlでは整数と整数、浮動小数点と浮動小数点と、両方の型が同じじゃないと四則演算さえ出来ないんだ。
これはかなり驚く(しかも整数用と浮動小数点数用の演算記号が違う)。
いや、ホントの事を言うと、フツーのプログラミング言語でも実はそうなんだ。ただし、フツーは暗黙で型変換してくれる。ところがOCamlはそれをしない。
これはホント、初めて触るとぶったまげるだろう。
「こんなんでマトモなプログラミングが出来るんか?」とまで思うだろう。
しかし、これはOCamlのかなりハッキリした特徴なんで覚えておく必要がある・・・ただし、Microsoft の F# は、「さすがにこれじゃアレだろ」ってぇんで、演算子オーバーロードと言う機能でこの辺の不都合を解消してるようだ。

2. パターンマッチングを多用する

これはHaskellなんかもそうなんだけど、とにかくパターンマッチングを多用する。
このパターンマッチングとはPythonで言うアンパックとC言語のswitch文を合わせたような機能だ。
例えばリストの長さを測るには、OCamlだと次のように書く。


lstと言う変数を受け取った際、lstが空リスト([])であるパターンと、firstrestconsされてる、と言うパターンに分かれている。これがパターンマッチングで、lstと言う変数が「どんなパターンを持つか」分離しつつ、なおかつif文なんかに頼ってないわけだ。
ちなみに、Racketで言うと、上のコードは次のようなコードを意図してる。


ここで分かるのは、

  1. OCamlはScheme/Racketと同様に、関数定義と変数定義は全く同じだ。Scheme/Racket での define がOCamlだと let になっている。
  2. OCamlで再帰関数を書く場合は、let 構文ではなく let rec 構文を用いる。これはScheme/Racketのローカル関数定義構文、letrecを彷彿とさせる。
Scheme脳で見ると、かなりOCamlは「分かりやすい」と思う(もっともSchemeにはパターンマッチング構文は存在しないが、Racketにはある)。
ただし、Lispと違うのはリストの取扱いだ。
Lispは「リスト万能」と言ってよく、どんな型のデータだろうと、構わずリストに追加する事が出来る。
一方、OCamlはそうじゃない。一旦、int型のリストを生成する、と決めたら別の型のデータ、例えばfloat型のデータをそのリストには追加出来ない。
言い換えるとint型のリストはint型のリスト、float型のリストはfloat型のリストなんだ。
Lisp観点で言うと「そんな不自由な!」と思うだろうけど、そもそもOCamlはリストが基盤のプログラミング言語じゃない。単にリストは「扱えるデータ型の1つ」でしかないんだ。
Lispみたいな「プログラムさえリストで表現する」と言うリスト偏重の言語とは違う、と言う事が1つ。
また、C言語なんかで連結リストを実装した経験がある人はこう思うだろう。「OCamlのリストの方がフツーじゃない?」と。
そう、「どんな型でも無作為に入れられる」Lispのリストの方が異常なのである。
Python、Ruby、Lispなんかが扱ってるリスト(あるいは配列)の方が自由度が高すぎて、むしろ異常だ、って事を知る事となるだろう。

さて、OCamlも関数型言語なんで、高階関数を扱うのもお手の物だ。これを基盤としたF#。C#やVBなんかでも辛うじて扱える高階関数だが、F#では「あまりにも自然に高階関数を扱える」んでビックリするだろう。それもベースとなったOCamlがそうだから、だ。
例えばisamさんがここでやってた2の累乗を求める計算はOCamlだとこう書く。



非常に短く仕上がってるだろう。
OCamlのラムダ式は、Lispみたいにlambdaとは打たない。形式的にはJavaScriptの無名関数とC++のテンプレート以降のラムダ式の間の子のようなカタチになっている。
上は累乗の例だが、どういうわけか、OCamlだと整数型の累乗は取れないようになってるらしい。従って、2.と浮動小数点数の累乗を取る、ってのが原則だ。
実行結果は以下のようになる。


そしてisamさんのように、100以上の数だけすり抜けさせたかったら、同様に、ラムダ式とfilterと言う高階関数を使う。


例によって、整数と浮動小数点数の比較もOCamlは出来ないので、型に気を使ってラムダ式を書かなければならないが、それでも相当短く書ける、って言うのが分かるだろう。

ちなみに、Racketで言うとこういう事だ。



当然、OCamlでも星田さんが挑戦してるような再帰関数や、高階関数(fold)を扱う事も出来る。
ちょっとここでも星田さんがネタにしてるようなOCamlでのコード例を見てみよう。
ただし、言っておくが、これがOCaml及びF#ででの「望ましい」コード例を保証してるわけではない。あくまでRacketユーザーがOCamlを使うとこんなカンジになり、かつ、OCamlの特徴とも言えるパターンマッチ構文を強調するとこんなカンジで書ける、と言う意味だ。

まず最初は階乗計算から見てみよう。



まずは軽くジャブ。
上のコードは再帰で書かれていて、下の方は高階関数foldで書いている。
見て分かると思うけど、一般には再帰関数で書くより高階関数で書く方がコードが短縮される、と言われている。
ただし、OCamlはちと逆気味だ。
それもこれも、コード圧縮としてパターンマッチ構文が物凄く効いてるのかもしんない。

なお、OCamlのfold関数(List.fold_left)はリスト引数は1つしか取れない。そしてScheme/Racketと違って、ラムダ式の引数の順番と、List.fold_leftの関数引数以降の引数の順番は同じだ、と言う事を言っておこう。



やっぱり、「そのまま書く」と言う意味だとOCamlでは、パターンマッチが極めて物事を短くしてしまう。
そしてLispだとプログラムを面白くしてた高階関数版フィボナッチ数列はOCamlだとイマイチ冴えない。
と言うのも、OCamlに於いてはリスト操作そのものがあまり映えないんだな。
Lispの関数とOCamlのリスト操作メソッド対応は以下のようになってる。


一見使ってる用語は短く見えるが、しかし、プログラムを組み立ててみると、OCamlでは「これらのリスト操作」があたかもペナルティのように、ソースコードを「汚して」行くように見える。
やはりOCamlではリストを分解するにせよ「まずはパターンマッチングを通して」と言うのがユーザーへのメッセージなんじゃ?とちと思ってしまう。
そしてここまで見てきた人も驚いてる筈だ。
「条件分岐を書かなくてエエんだ」と。
それくらいパターンマッチングは強力なのだ。

なお、Scheme/Racketと違い、OCamlではラムダ式で受けた引数は「全部使わなアカン」と言う前提だ。この前提を崩すためにignoreで「yを無視する」と宣言してる(この辺、むしろANSI Common Lispっぽい)。
また、OCamlにはrange関数がないんで、(List.init n (fun x -> x + 1)) と言う怪しい記述で(笑)、range代わりをしている。これがまた高階関数版の魅力を落としてるのは重々承知してはいる(※2)。



これは再帰版・高階関数版、共に短く済んでる例じゃないか。



これも再帰版・高階関数版ともにそれほど差が見られない例だ。



さって・・・・・・これは高階関数版がやたら長くなる例だ。Lispじゃこんなに苦労せんかったのにね。
まぁ、この辺、Lispの目で見ると、OCamlの「弱点」がよく見える例になってしまった。しかし、C言語なんかの目で見るとそうじゃないかもしれない。
ちと説明しよう。
まず上にも書いたけど、そもそもOCamlではLispに比べるとリスト操作が得意じゃない印象を受ける。もうそれだけで「書く量が増える」ってのは嬉しくない話だろう。
上の高階関数版のコードでもっとメンド臭くなるのはここだ。高階関数版はRacketだったらこんな風になっている。

(define (take1 lst n)
 (car (foldl (lambda (y x)
      `((,@(car x) ,(cadr x)) ,@(cddr x))) `(() ,@lst) (range n))))

かなりサッパリしてるだろう。OCaml版とは大違いだ。
もちろん、`とか,とか@とか、Racketだとそもそも「短く書くためのショートカットさえ」多用はしてる。それは認めよう。
ただし、もっと大きな違いがあるんだ。それはここだ。

`(() ,@lst)

コードで「初期値」を与えてる部分だ。ここがOCamlだとこうなってる。

([]::[lst]

よく見比べて欲しい。全く違う言語だが。実はこの2つは全く違う事をやってるんだ。
前者のRacket版は、例えば外部から引数を通じて与えられたリストが

'(1 2 3 4 5 6 7 8)

だった場合、生成する「初期値」リストは次のようになる。

'(() (1 2 3 4 5 6 7 8))

そう、空リストがcarのリストを生成する。もっと言うと、空リストと数値が混在してるリストを生成してるんだ。
一方、OCamlのコードをもう一回見てみよう。

([]::[lst]

これはcarが空リスト、そしてcdrlstなリストだ(consされてるから)。
つまり、例えば外部から

[1; 2; 3; 4; 5; 6; 7; 8]

と言うリストが引数lstを媒介に手渡されてきたとすると、ここで生成するリストはあくまで

[[]; [1; 2; 3; 4; 5; 6; 7; 8]]

と言うリストなんだ。何故ならOCamlはリストと数字をリストの中で混在出来ないから。しかしリストのリストなら作れるからこういう形式にしたんだよな。しかしそのせいで、書いてるコードが余計メンド臭い状態に陥ってしまったわけだ。
言い換えると、「何でも要素として突っ込める」リストを持つLispだったら、ちょっとしたアイディアでハックが可能だけど、OCamlは「ちょっとしたアイディア」を実現させるにはとてつもない労力を要する場合があるんだ、って事を上の例は見せてる気がする。
練習する分にはいいだろう。が、OCamlはどっちかっつーと、突飛な事をやるには向いてなくって、でもスタンダードに従う以上、物凄く簡単に書けるようにしてるプログラミング言語だ、って言う言い方が出来るんじゃないか。



この辺は割に素直。どっちにせよ、「スタンダード」の範疇の問題で、特にOCmal/F#でも高階関数foldの類を使う際の練習問題としてはオススメの問題になるだろう。
ひねりもクソもないから至極大事な問題なんだ。

これ以上のList.fold_leftの応用に関して言うと、色々試してみたが、実はそこまでList.fold_leftの自由度は高くない。代わりにList.iterと言うメソッドを多用したほうが良い、と言う結果になってしまった。
それに関して言うと、この記事の程度を超えてしまうので、ここで終了しようと思う。
しかしながら、Lispに比べると正直自由度は低いな、とは思うけど(※3)、充分関数型プログラミングを楽しめる言語だとは思っている。

なお、OCamlはコンパイラもインタプリタも備えている実装で、インタプリタで色々とテストしつつ「本番のコードを書いていける」ようになっている。
Microsoft F#もこの辺は受け継いでいて、Microsoft F#でインタプリタを使いたい場合は、Windowsの端末(DOS窓かあるいはPowerShell)で

dotnet fsi

と打つと起動する。
もう1つ、OCaml/F# のインタプリタの特徴としては、インタプリタ上で式を実行する際には、行末に2つセミコロン(;)を打ってからリターンしないと評価されない、と言う事を付け加えておこう。


ソースコード上では 1 + 2 だけで構わないが、インタプリタ上では 1 + 2 ;; と打ってからリターンしないと評価されない、と言う特徴がある。

※1: なぜ言わんのか?簡単だ。OCamlと言う処理系は、公式にはWindowsバージョンを出してないから、だ。
お茶の水女子大学がどうこの辺解決する為に頑張ったのかは知らんが、「とある言語処理系を入れる為だけに」OSを変える、なんつーのはさすがにバカバカしい話になる。
OCamlが「マイナー」だとすれば、それは単純にWindowsに移植されてないからだ、ということに他ならない。
極論、Windowsの為にOCamlの代わりにF#がある、って言って良いような状況になっている。
ちなみに、OCamlではMicrosoftもスポンサーとして名を連ねている、と言う事を付け加えておこう。

※2: Microsoft F# でrangeに価する関数があるかどうか、すぐ調べてみるべきだと思う。Pythonでは「多用すべき関数ではない」と言ったが、反面、関数型言語で高階関数を使う際には割にお世話になるのは間違いない。

※3: 例えば動的型付け言語のLispは、返り値がどんな型だろうと気にしないが、OCamlの場合はそこも気にせんとアカン。
ただし、そういう部分はC言語やPascalを始めとする性的静的型付け言語では割に当たり前で、言い換えるとVB.netやC#等の静的型付け言語に慣れてる人なら問題なく扱えるだろう。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事