世界を動かす技術を、日本語で。

OCamlを私の主要なプログラミング言語として

概要

  • 本記事は筆者の OCaml との出会いと、その 利点 についての意見を述べるもの
  • 言語自体・エコシステム・コミュニティの 三つの視点 から解説
  • インターネット上の 誤解や神話 の解消も試みる
  • 筆者は OCamlエコシステム で働いているが、以前から推進していた経緯を明記
  • 他の参考リソースも紹介し、OCamlの 多様な評価 を示す

OCamlとの出会いと推進理由

  • OCamlは 産業界研究分野 の両方で活用されるプログラミング言語
  • 研究に基づく 先進的な機能 (例:ユーザー定義エフェクト)と、実用性重視の 産業的機能 (例:アフィンセッション)を併せ持つ
  • Jane Street などの大手企業の導入実績があり、進化し続ける「生きた言語」としての側面
  • 理論専用Coq/Rocq実装専用 という誤解を払拭し、「表現力と安全性重視の産業向け関数型言語」として再評価
  • 筆者は学生時代から Site du Zéro のコミュニティでOCamlを知り、大学での体験はあくまで補足

他の参考リソース

  • 「Why OCaml?」: Real World OCaml の序章、OCamlの利点を事実ベースで解説
  • 「Better Programming Through OCaml」: OCaml Programming: Correct + Efficient + Beautiful の序章、OCaml習得が他言語スキル向上にも寄与
  • Yaron Minsky (Jane Street CTO)による講演「Why OCaml?」:産業導入の動機を深掘り
  • 「OCaml for Fun & Profit: An Experience Report」:生産現場での具体的活用事例
  • 「Replacing Python for 0Install」: 0Install 開発者による多言語比較とOCaml採用理由の詳細検証
  • 公式サイトには産業・学術両面の事例や、 OCamlへの不満 を示す記事も掲載

OCaml言語の特徴

  • MLファミリー の言語で、高レベル(ガベージコレクションあり)、静的型付け、型推論対応
  • 関数型命令型 の両スタイルが可能、 オブジェクト指向 や強力なモジュールシステムも提供
  • ocamlc (バイトコード)と ocamlopt (ネイティブコード)の二つのコンパイル方式
  • Js_of_ocamlMelange を用いたJavaScript・WebAssemblyへの変換で高い相互運用性
  • 多用途性 が高く、個人・業務プロジェクトの両方に最適なツール

静的型検査について

  • 個人プロジェクトでも 型安全性 の恩恵は大きく、ユニットテストのような「安全網」として機能
  • CやJava の型システムは制約が多いが、OCamlやHaskellのような型重視言語では「表現力豊かな設計ツール」として活用可能
  • 一度 高度な型システム に慣れると、単純な型や動的型への移行は困難
  • 歴史的にも多くの言語が 型検査の強化 を進めており、 TypeScriptMypy などの例がある
  • White House の公式レポートでもメモリ安全性の観点から Rust (OCamlが起源)を推奨し、型システムの重要性を強調
  • Tarides 社によるOCamlの安全性に関する主張も紹介

OCamlの長所と機能(抜粋)

  • 表現力の高い型システム による安全性と設計力
  • 関数型・命令型・オブジェクト指向 の柔軟な組み合わせ
  • 高速なコンパイラ と効率的な実行環境
  • 豊富なエコシステム と活発なコミュニティ
  • 産業界・研究分野両面 での導入実績

OCamlに関する誤解とその解消

  • 「理論専用」「学術向けのみ」といった誤解への反論
  • 産業界での 実用例 や、進化する言語仕様を根拠に説明
  • 一部の 不満点や課題 も認識し、適切な利用文脈を提案

まとめ

  • OCaml は多様な文脈で有効な選択肢
  • 静的型検査先進的な機能 を求めるなら特におすすめ
  • 他言語にも応用可能な設計思想
  • 完璧な技術は存在しない が、OCamlは十分に検討に値する選択肢

Hackerたちの意見

和集合型: 例えば、KotlinやJava(実質的にはC#)は、継承関係に関連する構造を使ってシーリングを行います。これにより、ケースを自分自身の型として参照できる利点があります。 > 和集合型の表現は冗長で、私の見解では考えにくいです。一度和集合型を宣言してしまえば、何度も使えます。少し冗長な和集合型の宣言でも、ケースの使い方がクリーンになるならそれだけの価値があります。

少し冗長な和集合型の宣言でも、ケースの使い方がクリーンになるならそれだけの価値がある。 その通り。ただ、JavaやKotlinの話をするときはそうじゃないよ。見た目が悪くて、JVM言語の典型的なボイラープレートが多いアプローチだね。

OCamlの具体的なケースでは、インデクシングやGADTs、ポリモーフィックバリアントを使うことでも可能だよ。でも一般的には、自分自身の型として参照することは異なる目的を持ってると思う。私の視点から見ると、和のブランチを区別することは、しばしば推論が難しく、バリアンスや型の等価性の喪失に関する懸念から一般化が難しいコードを生むことが多いね。

この利点は、ケースを独自の型として参照できるようになることだ。和集合型のケースは、もちろん型を持つ表現(いわゆる型コンストラクタの種類)だ。 datatype shape = Circle of real | Rectangle of real * real | Point Circle : real -> shape Rectangle : real * real -> shape Point : () -> shape ケース自体は型ではないけど、型は持っている。パターンマッチングのおかげで、和集合型のケースを扱うときに型コンストラクタのパラメータをすでに展開している。すべては宣言の局所性に関することだ。(real * real)はshapeの存在に依存しない。和集合型からケースを異なる型として取り出し始めると、網羅性を回避する能力が生まれ、和集合型は無効なプログラム状態を表現できなくなる。もう和集合型ではなくなる。名目上異なる型の和集合型がある場合、その和集合型はそれらの型の存在に依存している。クラス階層では、この関係が奇妙に逆転していて、影響が出る。 > 和集合型を一度宣言して、何度も使う。そして通常は多くの和集合型を書く。使い捨てだし、もっと言えば、自分が書いたコードを読む必要もある。ここでの冗長性のコストは過小評価されている。 > 少し冗長な和集合型の宣言は、ケースを使いやすくするために価値がある。C#/Javaには実際には和集合型がない。彼らの型システムとは互換性のない形式主義だ。とにかく、これらの例を見てみよう: C#: public abstract record Shape; public sealed record Circle(double Radius) : Shape; public sealed record Rectangle(double Width, double Height) : Shape; public sealed record Point() : Shape; double Area(Shape shape) => shape switch { Circle c => Math.PI * c.Radius * c.Radius, Rectangle r => r.Width * r.Height, Point => 0.0, _ => throw new ArgumentException("Unknown shape", nameof(shape)) }; ML: datatype shape = Circle of real | Rectangle of real * real | Point val result = case shape of Circle r => Math.pi * r * r | Rectangle (w, h) => w * h | Point => 0.0 ほとんど同じだよね、C#のOOPの奇妙さが邪魔してるけど。

OCamlでは働いたことはないけど、F#を少し触ったことがあって、なかなか楽しい経験だったよ。LLMの時代に、みんながもう一度関数型言語をじっくり見直すべきか考えてるんだ。私の考えでは、OCamlやHaskellなどのFP言語が多くの情報を少ないテキストに圧縮できるなら、コンテキストウィンドウにとっては良いことだと思う。もしかしたら、JavaやC#、Rubyなどの言語よりも、もっと密度の高いプログラムをモデルに入れて、一度に大きな変更を加えられるかもしれないね。

正直に言うと、今はLLMをドキュメントを書くのを手伝ったり(記事の翻訳も)、それだけに使ってるけど、他の人たちも注目してるみたいだね: https://anil.recoil.org/wiki?t=%23projects

これは本末転倒だと思う。プログラムは一般的に書くよりも読む方が難しいから、ツールのために簡潔な出力を最適化して人間にとってのコストを高めるのは、私個人としては選ばないトレードオフだね。確かに、これは特定のスタイルでコードを読む/書くのに慣れるための議論かもしれないけど、LLMの利点がなくても、関数型パラダイムやツールの採用は苦労してるよ。

C++とHaskellでシンプルなCLIゲームを書くっていう、完全に主観的な実験をしたんだけど、Haskellの方がコード行数は確かに少なかった。でも、単語数はほぼ同じで、つまりHaskellのコードは「高い」んじゃなくて「広い」って感じだった。しかも、この「実験」をJavaとか他のマネージドな命令型言語でやってないから、手動メモリ管理を気にしなくていい分、もっと軽くなったかもしれない。だから、どれくらい真実があるのかはわからないけど、プログラムによって違うと思う。命令型スタイルに向いてるものもあれば、関数型スタイルの方が合うものもあるよね。

LLMは、トレーニング例、静的型付け、LSPの実装から、簡潔さよりも恩恵を受けると思う。

大きなHaskellコードベースに取り組み始める前は、楽観的に考えてたんだけど、トレーニングコーパスにFPが足りないっていう明らかな問題を除けば、簡潔な言語はLLMとうまくいかないみたい。私の予想では、冗長さが生成を自己修正するのに役立つんじゃないかな。もし「悪い」トークンを予測したら、もっと柔軟に切り替えられて、ちゃんと動くコードを生み出せると思う。

私の考えでは、OCamlやHaskellのようなFP言語が多くの情報を少ないテキストに圧縮できるなら、それはコンテキストウィンドウにとって良いことだと思う。Claude CodeのHaskellスタイルは非常に冗長で、if-then-elseや多くのネストされたcase-of、複数の意図レベルでのdoブロックがあって、トップレベルでの命名がほとんどない。シンプルなAPIクライアントのサンプルを与えて、別のAPIでも同じことをするように頼んだら、すごくうまくいった。HaskellについてはJavaやRustよりも意見が多いと結論づけた。見た目が良くないなら、Haskellを使う意味がないよね。スタイルの例をコンテキストスペースをほとんど取らないように与えることができると思う。それに、.cabalにすでに入っている場合は、ファイルごとに言語プラグマを有効にしないようにリマインドするのもいいかも。

手続きは、関数型/ML構文ではもっと簡潔に書けるけど、そうじゃないことも多い。例えばC#の依存性注入は、素晴らしいDIライブラリと(より理にかなった)インスタンスコンストラクタ構文のおかげで、もっと冗長性が少なくできるんだ。

Scalaでは、cats-effectっていうエフェクトライブラリを使って開発をスピードアップするのに、LLMsがめっちゃ役立ってる。過去にcats-effectを使った経験から言うと、簡単なことでも意外と分からなかったりするんだよね。最近使ってなかったり、似たような問題を解決してなかったりすると、ドキュメントを見ながら型シグネチャを目を細めて探す羽目になっちゃう。振り返ってみれば、実はシンプルでエレガントな解決策だったりするんだけど。LLMsのおかげで、こういうストレスがかなり減ったよ。「cats-effectでどうやって…?」って聞くだけで、80%の確率で即座に解決策が見つかる。残りの20%の時は、もう少し詳しい文脈を提供したり、別のLLMに聞いたりしてる。まだメンテナンスコーディングを十分にやってないから、エフェクトを使った関数型プログラミングのコスト/ベネフィットが根本的に変わるかは分からないけど、すごくワクワクしてる。cats-effectのコードを書くのは、いつも満足感とフラストレーションが半々だったけど、今のところ、フラストレーションの一部で自信と正確さを得られてる。まだClaude Codeをcats-effectのコードに使ってないから、どれくらい上手くいくか楽しみだな。

LLMがコードを書くのがちょっと上手くなったら、すごく強力な型システムや効果システムを使って、彼らができることを制限したり、正しいことを保証したりしたいかもね。例えば、依存型を使えば「この関数はソートされたリストを返す」とか、「この関数は有効な数独の解を返す」と言えるし、これらはコンパイル時にチェックされるんだ。もう一度言うけど、コンパイル時にね。これを効果システムと組み合わせると、「この関数は有効な数独の解を返すし、ネットワークやファイルシステムにはアクセスしない」とか言えるようになる。そうすれば、LLMを自由に動かせるってわけ。もしLLMが出力したコードがコンパイルできれば、レビューする必要もないし、ちゃんと動くってわかるし、ネットワークやファイルシステムにアクセスしないってこともわかるからね。もちろん、LLMがもっと上手くなれば、たぶんPythonで全部できちゃうだろうけど、ちょっとだけ上手くなるだけなら、信頼性のないLLMの周りにもっと良い決定論的システムを作って、信頼できるようにしたいかも。

数年前、OCamlをメイン言語にしたいと思ったけど、すぐに問題にぶつかったんだ。インストールが難しかった(Linuxでは、非常に珍しいツールが必要で、その名前と機能を忘れちゃった)、コミュニティからの問題解決の反応がなかった、しっかりしたPostgreSQLドライバーがなかった、などなど…。関数型言語を使いたかったから、F#に切り替えたんだけど、これは私にとって予想外の選択だった。Linuxしか使ってないからね。でも、この選択には満足してるし、今ではお気に入りの言語になったよ。一番の問題は、F#コミュニティの管理や、DotNetエコシステムにおけるF#の二級市民的な立場、Microsoftの開発者コミュニティの善意を損なう行動(例えば、ホットリロードのエピソード)だった。これがF#コミュニティの成長を妨げたと感じてる。今はRustを使い始めてるけど、これらの点での対比は大きいね。編集: ダウンボートした人、理由を教えてくれない?私の経験をシェアすることは評価されると思ったんだけど。私が間違ってた理由を知りたいな。

インストールが難しいなら、opamを使ってみて: https://opam.ocaml.org または https://opam.ocaml.org/doc/Install.html。さらに、こちらも見てね: https://ocaml.org/install#linux_mac_bsd と https://ocaml.org/docs/set-up-editor。例えば、Emacsでのセットアップは簡単だよ。VSCodiumにもOCamlの拡張があるし。OCamlコンパイラに必要なのはopamだけで、パッケージやコンパイラを全部管理してくれるよ。プロジェクトにはduneを使ってね: https://dune.readthedocs.io/en/stable/quick-start.html。

OCamlをやめたのは、ステッピングデバッガが動かなかったから。正確な問題は覚えてないけど、VSCodeにインストールしようとしたけどダメだったし、Emacsには興味ないんだよね。

Googleの人がAndroidチームでRustを使った経験について話しているのを聞いたんだけど、二つのポイントが印象に残った。彼らは多くのプロジェクトをPythonから移行したから、パフォーマンスがそんなに問題だったわけじゃないし、調査では人々が最も好きだった機能はパターンマッチングやADTのような基本的なものだった。私の結論は、多くのタスクにおいてRustの利点は1990年頃のMLから来ているもので、ライフタイムとかはあまり関係ないんじゃないかってこと。もしOCamlが2010年頃にマルチコアや他のいくつかの面倒なことをうまくまとめていたら、Rustになっていたかもしれない。残念ながら、学界が取り組む価値を正当化できるものと、産業界がやりたがるものの間に隙間ができてしまった。[1]: 実際のところ、31ビットのIntはビット操作をしようとすると面倒だし、見た目の面でダブルセミコロンは本当に嫌い。

実際にはダブルセミコロンを使う理由は全くないよ。見るべき場所はREPLを使っている時だけだね。

Rustはまだガーベジコレクションがないっていう利点があるんじゃない?Rustを書くのはあまり好きじゃないけど、メモリ安全性を保証しつつパフォーマンスの高いコードが書けるっていうのは、ずっと心に残ってる。

Googleは、プロダクションで使える言語のリストを短く保とうとしていると思う。RustはC++の代わりになるし、補完もできるけど、OCamlは無理だよね(むしろGoの代わりになるかも…無理だろうけど!)。だから、チームがRustを選んだのは、ADTsを持つ唯一の言語だったからだと思う。コンパイル時間が速いものが嫌いなわけじゃないはず。OCamlがRustの人気を奪うなんてありえないよ。GoからHaskellまで、結構優れたGCベースの言語があるし、2010年にはC++しかなかったけど、それはひどいもんだった(今でもひどいけど、C++11やC++17の前はもっとひどかった)。

OCamlは、確かに短いけど明るいReasonMLの瞬間が必要だった。構文を追加・修正・改善したり、ユーザーフレンドリーなエラーメッセージに取り組んだり。でも、これはもっと早くに起こるべきだったよね。

同感だよ。Rustのいいところは、メモリ安全とは関係ない部分も多いと思う。プログラムの構造を整える方法だよね。

OCamlが2010年頃にマルチコアやいくつかの面倒な問題を解決してたら、Rustになってたかもしれないと思う。もしかしたらScalaになってたかもしれないし、一時はScalaになるかと思ったけど、結局そうならなかったよね。高プロファイルなScalaパッケージのプログラミングスタイルが、型システムや演算子のオーバーロードを必要以上に押し進めて、みんなを遠ざけたのが原因かも。

見た目としては、ダブルセミコロンは本当にひどいし、余計にイライラする。20年くらいオプションだったと思うけど、トップレベルのインタラクティブ環境では実行を強制するために使われてるよね。とはいえ、なんでみんながあんなに構文に不満を持ってるのか、未だに理解できない。OCamlのコードを書いて一週間もすれば、すぐに慣れるよ。

こんなに経験豊富な人がReasonMLの構文の利点や欠点を比較してくれたらいいのに。(記事では一度、さらっと触れられているだけ。)

Reasonの構文は好きだし、もっと一般的になってほしいけど、OCamlコミュニティに参加したいなら、標準の構文を使うのがいいと思う。ほとんどの人がそれを使ってるから、エコシステム内のコードやドキュメントを読むためには理解しておく必要があるよ。

経験はあまりないけど、少し触った時はLSPがうまく動かない問題があったよ。

一番恋しかったのはletバインディング(https://ocaml.org/manual/5.3/bindingops.html)だね。ReasonMLにはモナドをある程度まともに扱えるカスタム演算子があった(>>=演算子とか)。リスクリプト(ReasonMLの「フォーク」)は、前にチェックした時は続いてなかった。でも、非同期コードに役立つasync/await構文はあったよ。ReasonMLは前にチェックした時は続いてなかったから、生のプロミスを使わないといけなかった。この記事で少し触れてるMelangeは、Reason構文でletバインディングをサポートしてると思う。これがReactを使うときに全てを変えるかも。letバインディングでまともなJSXが書けるから。Melangeがなければできなかったことだよね。確かにOCaml構文でPPXを使って逃げることはできるけど、コードエディタでの構文ハイライトがうまく機能するかはわからない。前にチェックした時はうまくいかなかったし。だから、フロントエンドのコーディングではMelangeのReasonMLが素晴らしい。両方あって、letバインディングが読みやすいモナディックコードの上に非同期構文をかなりうまく近似できるから。バックエンドコードでは、Pythonistaとしては波括弧が嫌いなんだ。括弧なしの関数呼び出しや定義が好きだけど、初心者のOCamlユーザーとしては非変数の関数引数で「変な」括弧を使わないといけないから、まだまだ苦労してる。これが「役立つ」といいな!

ごめん、ReasonMLは使ったことないから、4年で2回も死ぬ時間があった以外の利点は見えないな :)

すごくいい記事だね。これで「なんでF#を使わないの?」っていう議論が解決したよ。ほとんどのOCamlスレッドで、誰かがOCamlのツールを避けるためにF#を提案するんだ。OCamlにはずっと興味があったけど、誰かが「型付きのGo」って呼ぶから、Rustを書くのが苦手な私にはちょっと魅力的。でも、OCaml全体にはまだ納得できてないんだ。ErlangやRuby、Rust、Zigの人たちのようには心を掴まれない。ビジョンが見えないんだよね。

面白いね、私はF#のツールを避けるためにOCamlに移ったんだ。最後にF#を使ったときは、遅いコンパイラ、ますますC#専用のエコシステム、弱くてドキュメントもないMSBuild(カスタムタスクを書くのはいいけど!)、Ionideがクラッシュする、Fantomasが不安定…って感じだった。でもOCamlは残念ながら、私のすべてのユースケースに対してF#の代わりにはなれない。F#はCLRがサポートする多くのパフォーマンス指向の機能にアクセスできるけど、OCamlはそれができない。OxCamlが長期的にそれを解決できるかもしれないけど、今はシンプルなツールチェーンを持つパフォーマンスの良いMLライクなものが欠けてる。

OCamlに触ったのは数年前だけど、エコシステムが自分の求めていたものじゃなかったんだ。でも、コア言語は今でもお気に入り。理由を説明すると、コードがだいたい重い関数がいくつかあって、やりすぎちゃうんだよね。それに気づいたら修正できるけど、そういう方向に進みがち。OCamlのコードでは大きな関数を探しても…見つからないことが多かった。たくさんのことをやる一つの仕事馬的な関数がいなかったから、なんか良いコードを書くのが楽だったんだ。今はサイドプロジェクトでRustを使ってるけど、型システムが好きだから。でも、OCamlの方が好きなんだよね。これらの理由でF#もチェックしようと思ってるんだけど。

用語についての質問なんだけど、この記事みたいに高階関数の型を「指数型」って呼ぶのは一般的なの?高階関数のことはわかるんだけど、どうしてその型が「指数」って呼ばれるのかがイマイチ理解できないんだよね。

普通は合計と積のことだけを話すよね(記事がADT、つまり代数データ型を指してるから)。関数はデータとは言えないから、含まれないんだ。でも、同じトリック(つまり、a -> bはb^aの引数を持つ)を使って潜在的な住人の数を計算することはできるよ。

一階関数型はすでに指数型なんだ。和型はそのケースの合計と同じだけの可能な値を持ってる。例えば、A of bool | B of boolは2+2=4の値を持つ。同様に、積型や指数型もそう。例えば、型bool -> boolは2^2=4の値(id、not、const true、const false)を持つけど、副作用を考えなければね。

同じ疑問を持ってた。私の無知な推測だけど、数学では、和と積の次に指数が来るからね :) だから、例の中でその三番目の項を類似の方法で使ったのかもしれない。

OCamlを好きになろうと数年間頑張ったけど、引っかかるのは、もっと「モダン」な言語で解決されてる些細なことが多い。最大の問題は、任意のオブジェクトを「印刷」できないこと。自動的に「to string」関数を生成するppxもあるけど、設定するのがちょっと面倒だし、Rustのように使いやすくはない。SetやMap型を扱うのも追加作業が必要だし、例えば[1](2021年の情報だから状況が変わってるかも)。golangと比べると、ほぼ何でもゼロの手間で印刷できる"%v"や関連フォーマット文字列が使えるから、全然楽だよ。

OCamlってデスクトップGUIアプリのプログラミングに適してるのかな?OPでこれを見たんだけど: >例えば、Tkライブラリでバインディングを作ることについて考えてたから、質問してみた。

超古い。HKTsもないし、型クラスもない(モジュールは代わりにはならない)、コールサイトの展開もない。

  • 超古い:ほぼ半年ごとに新しいリリースがあって、最近のリリースでは言語のランタイムが変更されて、ユーザー定義のエフェクトが導入された。 - あなたの言う意味でのHKTsはないけど: ocaml module type S = sig type 'a t end type 'a tは高階型(ただしモジュールレベルで)。 - 型クラスは今のところないけど、https://arxiv.org/pdf/1512.01895 の最初のステップがレビュー中だよ: https://github.com/ocaml/ocaml/pull/13275 - コールサイトの展開は? https://ocaml.org/manual/5.0/attributes.html の inline 属性を見てみて。