🤟

REPL駆動プログラミングについて

2022/08/29に公開

本記事は、Mikel Evins氏のOn repl-driven programmingの翻訳を、本人の御了承を得て公開するものです。なお、関連記事であるProgramming as teaching翻訳も公開していますので、どうぞご覧ください。


REPL駆動プログラミングについて

2020-12-18 by Mikel Evins

その昔、「entha_saava」というハンドルネームの人が、Hacker Newsにこんな質問を投稿していました。

誰か詳しい方に LispのREPL と PythonやRubyのREPLがどう違うのかを説明してもらえないでしょうか? LispのREPL駆動開発の差別化ポイントは何ですか?

その答えは、プログラムを実行しながら対話することでプログラムを構築する特有のプログラミング手法があり、そのような手法をサポートするために一から設計された言語やランタイムが存在するということです。

PythonやRubyはそのような言語の例ではありません。

なぜ違うのか? これがentha_saava氏の質問の核心ですね。 REPL駆動 のプログラミングシステムとは何なのか? PythonやRubyやその他のREPLを提供するあらゆる言語と何が違うのか?

それよりもまず、REPLって何ですか?

REPL とは Read-Eval-Print Loop の頭文字を取ったものです。この用語はLispの歴史に由来しています。60年前の当初から、Lispの標準的な操作方法は、言語処理系を起動し、プロンプトに式をタイプし、その式を評価して結果を表示するのを待ってから、次の式を入力する、というものでした。式を読み(Read)、評価(Eval)して、印刷(Print)する ループ(Loop)というわけです。

今、REPLが大流行しています。あらゆる言語とその兄弟言語がREPLを提供しています。repl.itというウェブサイトがあり、その目的はすべての言語のREPLを提供することです。

もちろん、本当にすべてのREPLを提供するわけではありません。特に皮肉なのは、典型的なREPL駆動開発環境 (Common LispとSmalltalkです) のどちらも提供されていないことです。

ここでentha_saava氏の質問に戻ります: Common LispとSmalltalkがREPL駆動環境で、PythonとRubyがそうでないとしたら、何が違うのでしょう? LispとSmalltalkにはあって、PythonとRubyにはないものは何でしょうか?

LispやSmalltalkにあるものは、言語エンジンと対話し、対話的にあなたのプログラムがいかにあるべきかを教え、実行中にプログラムを変更することでプログラムを開発することを前提に、一から設計された言語とランタイムシステムです。

もう反論が聞こえてきそうです。以前にも見たことがあります。そう、REPLを持つすべての言語は、REPLの中でいくつかのことができるのです。それはそうでしょう。そうでなければ、そのREPLは全く役に立たないわけですから。

REPLの中でいくつかのことができるようになっても、言語エンジンをREPL駆動のプログラミング環境にすることはできません。昔ながらのLispやSmalltalkの環境が異っているのは、REPLで何でもできることです。できることに余計な制限はありません。言語とランタイムができることはREPLでもできるのです。

たとえば、現在のバージョンの Clozure Common Lisp (CCL) をゼロから再構築するには、 REPLプロンプトで次の式を評価します。

  (rebuild-ccl :full t)

するとCCLは、自身を完全にソースから作り直すことで対応します。

ここでのポイントは、CCLを常にこの方法でリビルドしたいということではありません。重要なのは、REPLができることに人為的な制限がないということです。開発システムの全能力に、REPLからアクセスできるのです。

そのことが、新しい(機能の劣った)REPLを使って最初に気づくことの一つです。REPLからではできないことにいつも遭遇するのです。

単に制限から解放されるということだけではありません。対話的なプログラミングを適切にサポートするということは、言語とそのランタイムが実行中にプログラムを変更することをサポートする積極的な機能を備えていることを意味します。

以下のことをお好みのREPLで試してみてください:

まだ定義されていない他の関数 bar を呼び出す関数 foo を定義してください。では、 foo を呼び出してみましょう。どうなりますか?

明らかに、 bar が定義されていないため、 foo の呼び出しは中断(break)することになります。しかし、中断するとどうなるのでしょう? 次は何が起こるんでしょうか?

もし、あなたがPythonやRuby、その他の数十種類の新しいREPLを愛用しているなら、その答えは「エラーメッセージを表示して、プロンプトに戻る」というものでしょう。場合によっては、クラッシュしてしまうかもしれません。

で、何が言いたいんだということですね。他に何ができるんでしょう?

その答えこそがREPL駆動プログラミングの「差別化ポイント」なのです。昔ながらのLispやSmalltalkの環境では foo が中断したところであなたを breakloop に誘います。

breakloop は、メインのREPLが持つすべての機能を備えたREPLです。ただし、中断した関数の動的環境の中で動作しています。breakloopにおいて中断されたコールスタックを上下に移動し、各スタックフレームからレキシカルに見えるすべての変数を調べることができます。実際、実行中のプログラムのすべてのデータを調べられます。

さらに、プログラム内のすべての実データを編集できます。もし、ある特定の変数やフィールドの値が間違っていたために中断が発生したと考えられる場合、その値を対話的に変更してから中断していた関数を再開させることができます。これで正しく動作すれば、おめでとうございます; あなたは問題を発見できました!

そのうえ REPLの中で言語と開発システムのすべてを無制限に利用できるため、足りない関数 bar を定義してから foo を再開すれば、理にかなった結果を得ることができるのです。

実際、LispやSmalltalkの世界ではよく知られたプログラミングスタイルとして、トップレベルの関数を定義して、まだ存在しない他の関数を呼び出し、その結果生じるbreakloopの中でそれらの関数を定義するというものがあります。これは、プロシージャがどのように動作すべきか既に分かっている場合に、それを素早く実装する方法です。

昔ながらのLispやSmalltalkのユーザーであれば、こんなことは当然だと思われるかもしれませんが、そのような反応は一般的ではありません。むしろ驚きの方が多く、「何か裏があるのでは?」と疑われることさえあります。

言語システムの設計者は、その仕掛けを計画段階で考え抜かなければなりません。後付けでは まともな実装はできません。計算とそのコールスタックをbreakloopの環境にサスペンドした状態で、対話的に開発システム全体にフルアクセスできる必要があります。

対話的なプログラミングをサポートするために設計された仕掛けの別の例を見てみましょう。もう一度、以下のことをお好みのREPLで試してみてください:

データ型を定義してください。ここではクラス、構造体、レコード型などを意図していますが、あなたの好きな言語がサポートするユーザー定義型なら何でも構いません。そして、そのインスタンスをいくつか作成してください。そのインスタンスを操作するための関数(あるいはメソッド、プロシージャなど)を書いてください。

ここで 型の定義を変更します。どうなりますか?

言語のランタイムは型の定義が変更されたことに気づきますか? 既存のインスタンスに新しい定義があることを認識していますか? あるいはその方法がわからない場合、breakloopを開始してどうしたらいいかあなたに尋ねますか?

もし答えが「イエス」なら、あなたはおそらくLispかSmalltalkのシステムを使っているのでしょう。もし答えが「ノー」なら、あなたはREPL駆動開発の重要な要素を見逃していることになります。

重要なのはプログラミングを 対話的に サポートすることです。定義を変更したからといって、プログラムを停止して一からビルドし直すようなことは望まないでしょう。ばかげています。定義を追加したり変更したりすることは、あなたが行うことの大部分なのですから!
もし開発環境が対話的な開発をサポートするものであるなら、定義を変更したときにプログラムを実行し続ける方法を知っている必要があります。

古風なLispやSmalltalkのシステムはその方法を知っています。他にもいくつかの種類のシステムがあり、ほとんどが古いものですが、その方法を知っています。

これらは、決して突拍子もない新しいアイデアではありません。半世紀ほど前からあるものなのです。対話的な開発の生産性に大きく貢献するものです。

これが、単なるREPLを使った開発とは異なる REPL駆動開発 の特徴です。

さて、すべてのプログラマがそのような開発方法を好むわけではありません。プログラマの中には、設計し、計画を立て、設計図を作成して、作業台の上で部品を組み立てるようなプロセスを開発と考えることを好む人もいます。それはそれで悪いことではありません。実際、数十億ドル規模の国際的な産業が、その上に築かれているのですから。

しかし、もしあなたが対話的な開発を好み、それがあなたにとってより自然なことであるなら、生産性は非常に高くなり、言うまでもなく仕事も楽しくなります。

適切なREPL駆動環境による対話的開発は変わり種です。ほとんどのプログラミングは他の方法で行われています。

その結果、多くのプログラマが対話的プログラミングの存在すら知らず、聞いたこともない、というのが現状です。私の直感では、そのようなプログラマの何割かはよくサポートされた対話的プログラミングを好み、それが何であるかを知りさえすれば、その恩恵にあずかるだろうと思います。

そのようなプログラミングのスタイルに触れるプログラマが増えれば、それを取り入れた新しいツールも出てくるかもしれませんね。


訳者後記:

  • ANSI Common Lisp では、エラー等による実行中断時にユーザに関数を修正する機会を与えたり、修正後にその近傍から実行を再開したりする機能の提供に、通例 common lisp condition system を用いることになります(Smalltalkにも同様の機構があります)。それで、condition system の知識を得ることで、本記事の内容をより具体的に理解できます。

  • ただし、そういったことに関連する注解や、本記事の掘り下げに必要十分なcondition systemの内容について、本記事自体には注記しないことにしました。余分な注記が増えると原著のテイストが損なわれると考えたためです。

  • 本稿の下訳作成には DeepL(free)を用いました。

Discussion