私は時折、コーディングに対する考え方を変えさせられるような、従来と非常に異なるプログラミング言語に出会います。本記事では、その中でも特に気に入っている発見をいくつかご紹介したいと思います。
これは、先賢による「関数型プログラミングは世界を変える!」的な投稿ではありません。本記事で挙げるのは、もっと「知る人ぞ知る」的なリストです。多くの読者の方にとって、以下の言語やパラダイムは聞いたことのないものが大半だと思いますので、私が経験したように、これらの新しい概念を学ぶ楽しさを感じていただければ幸いです。
注:私は以下の言語の多くに関して最低限の経験しかありません。その発想に引き込まれたのであって、専門的知識は持ち合わせていないため、訂正すべき点や誤りがあればどうぞご指摘ください。また、本記事で取り上げていない新しいパラダイムや概念に出会った方は、ぜひお知らせください。
最新情報:本記事がr/programmingとHacker Newsのトップページで紹介されました。素晴らしいフィードバックをありがとうございます。以下にいくつか訂正を加えました。
並行処理がデフォルト
仰天のものから始めましょう。世の中には、並行処理がデフォルトのプログラミング言語があります。つまり、コードのあらゆる行が並列で実行されるのです。
例えば、A、B、Cという3行のコードを書いたとしましょう。
A; B; C;
大抵のプログラミング言語では、最初にA、次にB、Cと実行されます。しかし、ANIのような言語では、A、B、Cが全て同時に実行されるのです。
ANIにおいてコードの行間の制御フローや順序付けは、行間の明示的依存性の副作用にすぎません。例えば、Aで定義されている変数への参照がBにある場合、AとCは同時に実行される一方、BはAの実行が済んでから初めて実行されます。
ANIの例を見てみましょう。チュートリアルで説明されているように、ANIプログラムは、ストリームとデータフローの操作に使われる「パイプ」と「ラッチ」から構成されます。構文が普通と異なるのでパースするのが難しく、言語の開発は停止しているようですが、かなり面白い概念です。
以下が、ANIで書いた「Hello World」の例です。
"Hello, World!" ->std.out
ANIの用語では、"Hello, World!"
オブジェクト(文字列)をstd.out
ストリームに送っていることになります。別の文字列をstd.out
に送るとどうなるでしょうか。
"Hello, World!" ->std.out "Goodbye, World!" ->std.out
コードのどちらの行も並列で実行されるので、コンソールには順不同で表示されます。今度は、ある変数を1行で導入し、あとでその変数を参照するとどうなるか見てみましょう。
s = [string\]; "Hello, World!" ->s; \s ->std.out;
1行目で、文字列を格納するs
という「ラッチ」(ラッチは変数に少し似ています)を宣言します。2行目で、テキスト"Hello, World!"
をs
に送ります。3行目では、s
を「アンラッチ」して、その内容をstd.out
に送ります。これでANIの暗黙的プログラムシーケンシングがお分かりいただけるでしょう。各行が前の行に依存するので、このコードは書かれた順序で実行されるのです。
Plaid言語もデフォルトの並行処理をサポートしているとのことですが、こちらの論文で説明されているように、制御フローのセットアップにパーミッションモデルを使っています。Plaidは他にも、型状態指向プログラミングのような興味深い概念を模索しています。型状態指向プログラミングでは、状態の変化がその言語の第一級オブジェクトとなります。つまりオブジェクトを、クラスとしてではなく、コンパイラのチェックを受ける一連の状態および遷移として定義するのです。これは、Rich Hickeyの講演「Are We There Yet?」(もう到達したか)で論じられているように、第一級言語構成概念としての時間のエクスポーズに対する興味深い見方のように思われます。
マルチコアは増えていますが、並行処理はまだ大部分の言語において実装が大変です。この問題に対してANIやPlaidの提供する新しい見方は、パフォーマンスを飛躍的に向上させる可能性があります。問題は、「並列処理がデフォルト」によって並行処理を扱うのがより簡単になるか、逆に難しくなるかということです。
最新情報:上記の説明では、ANIとPlaidの要点をご紹介しました。その際、「並行(concurrent)」と「並列(parallel)」を同じ意味で使いましたが、両者には異なる意味があります。詳しくは「Concurrency Is Not Parallelism」(並行処理は並列処理ではない)をご参照ください。
依存型
皆さんはおそらく、C、Javaといった言語の型システムには慣れているでしょう。ある変数が整数、リスト、または文字列であるかをコンパイラがチェックできるシステムです。しかし、ある変数が「正の整数」、「長さ2のリスト」、または「回文である文字列」であるかをコンパイラがチェックできるとしたらどうでしょうか。
それが、依存型をサポートする言語の背後にある考え方です。コンパイル時に変数の値のチェックが可能な型を指定できるというものです。Scalaのshapelessライブラリは、依存型の部分的、実験的なサポート(おそらくプライムタイムにはまだ使えないように読み取れます)をScalaに追加します。簡単な使用例をいくつか見てみましょう。
shapelessライブラリで値1、2、3を格納するVector
は、以下のように宣言します。
val l1 = 1 :#: 2 :#: 3 :#: VNil
これで変数l1
が作成されますが、その型シグネチャは、この変数がInts
を格納するVector
であるということだけでなく、長さ3のVector
であるということも指定します。コンパイラはこの情報を使って、エラーを見つけることができます。Vector
のvAdd
メソッドで、2つのVectors
のペアによる加算を実行してみましょう。
val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: 3 :#: VNil val l3 = l1 vAdd l2 // Result: l3 = 2 :#: 4 :#: 6 :#: VNil
上記の例がうまくいく理由は、どちらのVectors
も長さが3であることを型システムが把握しているからです。しかし、長さの異なる2つのVectors
をvAdd
しようとすると、コンパイル時にエラーが発生しますので、実行時まで待つ必要はありません。
val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: VNil val l3 = l1 vAdd l2 // Result: a *compile* error because you can't pairwise add vectors // of different lengths!
shapelessは素晴らしいライブラリです。しかし、まだちょっと粗削りで、依存型付けのサブセットしかサポートしておらず、コードや型シグネチャはかなり冗長になってしまうというのが個人的な印象です。一方、Idrisは、型をこの言語の第一級要素としているので、依存型システムがはるかに強力かつ簡潔なように思われます。両者の比較については、講演「Scala vs Idris: Dependent Types, Now and in the Future」(ScalaとIdris:依存型の現在と未来)をご参照ください。
形式的検証は、long型には活用されているものの、多くの場合、汎用プログラミングに使うには煩雑すぎるものでした。Idrisのような言語の依存型や、ひょっとすると未来のScalaの依存型が、その代替手段を提供できるかもしれません。より軽量で実用的でありながら、エラー発見における型システムの能力を劇的に高めるような手法です。もちろん、停止問題に起因する本来的な制限があるため、全てのエラーを発見できる依存型システムはありませんが、うまく開発されれば、依存型は静的型システムにとって次の大躍進となるかもしれません。
連結型言語
変数や関数適用を使わないプログラミングがどのようなものであるか、考えたことがあるでしょうか。ないですか? 私もありません。でもどうやら、中にはそんな考えを持った人もいたようで、彼らは連結型プログラミングというものを考案しました。考え方としては、言語内の全ての要素はスタックに対してデータをプッシュまたはポップする関数であるというもので、プログラムはほぼ関数合成だけで構成されます([連結は合成](http://concatenative.org/wiki/view/Concatenative language/Concatenation is composition))。
かなり抽象的ですので、Catで書いた単純な例を見てみましょう。
2 3 +
上記は、2つの数値をスタックにプッシュしてから、+
関数を呼び出します。この関数は両数値をスタックからポップして、それらを足し合わせた結果をスタックにプッシュして戻します。コードの出力は5となります。次は、もう少し面白い例です。
def foo { 10 < [ 0 ] [ 42 ] if } 20 foo
1行ずつ見ていきましょう。
- まず、関数
foo
を宣言します。Catにおける関数は入力パラメータを指定しないことに注意してください。パラメータは全て、暗黙的にスタックから読み取られます。 foo
は<
関数を呼び出します。この関数はスタックの最初の項目をポップし、それを10と比較して、True
またはFalse
をスタックにプッシュして戻します。- 次に、値0と42をスタックにプッシュします。それぞれブラケットでくくり、評価されないままスタックにプッシュするようにしています。これらは次行にある
if
関数の呼び出しで「then」分岐と「else」分岐としてそれぞれ使われるためです。 if
関数はスタックから、ブーリアン条件、「then」分岐、「else」分岐という3項目をポップし、ブーリアン条件の値に応じて、「then」または「else」分岐の結果をスタックにプッシュして戻します。- 最後に、スタックに20をプッシュして、
foo
関数を呼び出します。 - 結果は、数値42となります。
もっと詳しく知りたい方は、「The Joy of Concatenative Languages」(連結型言語の楽しさ)をご参照ください。
このスタイルのプログラミングには面白い特性があります。プログラムを無数のやり方で分割、連結して新しいプログラムを作れること、ごく最低限の構文(LISPよりも最低限)なのでプログラムが非常に簡潔なものとなること、そして強力なメタプログラミングサポートです。連結型プログラミングは目覚ましい思考実験だと思いますが、実用性については私は納得できていません。スタックの現在の状態をコードの変数名から読み取ることができず、記憶あるいは推測しなければならないという状況では、コードについて論理的に考えるのが難しくなりそうだからです。
宣言型プログラミング
宣言型プログラミングは長年使われていますが、概念としてはまだ知らないというプログラマが大部分です。要点を挙げましょう。大抵の主流言語では、特定の問題をどのように解決するかを記述します。一方、宣言型言語では得たい結果だけを記述し、その解決法は言語自体が導くのです。
例えば、ソートアルゴリズムをCで一から書いているなら、マージソートの命令を記述するかもしれません。再帰的にデータセットを半分に分け、ソートされた順序でマージして返すという方法を、段階を追って記述するのです。このようなコードです。対して、Prologなどの宣言型言語で数値をソートするなら、得たい出力を「同じ値リストが欲しいが、インデックスi
の各項目はインデックスi + 1
の項目以下となる」のように記述することになります。前述のCの解決法と以下のPrologコードを比べてみてください。
sort_list(Input, Output) :- permutation(Input, Output), check_order(Output). check_order([]). check_order([Head]). check_order([First, Second | Tail]) :- First =< Second, check_order([Second | Tail]).
SQLを使ったことのある方は、宣言型プログラミングの形式で書いたことがありながら、その認識がなかったかもしれません。select X from Y where Z
のようなクエリを発行する際には、あなたは返してほしいデータセットを記述しているのであり、クエリをどのように実行するかを実際に導くのはデータベースエンジンなのです。大抵のデータベースでは、explainコマンドを使って実行計画を表示し、データベースエンジンが実際にどのような処理を行ったかを確認できるようになっています。
宣言型言語の魅力は、はるかに上位の抽象化レベルで作業ができることです。あなたは、得たい出力の条件を記述するだけでいいのです。例えば、以下に挙げるPrologで書いたシンプルな数独ソルバのコードは、解かれた数独パズルの各横列、縦列、斜めの列がどのようになるかを単純にリストアウトします。
sudoku(Puzzle, Solution) :- Solution = Puzzle, Puzzle = [S11, S12, S13, S14, S21, S22, S23, S24, S31, S32, S33, S34, S41, S42, S43, S44], fd_domain(Solution, 1, 4), Row1 = [S11, S12, S13, S14], Row2 = [S21, S22, S23, S24], Row3 = [S31, S32, S33, S34], Row4 = [S41, S42, S43, S44], Col1 = [S11, S21, S31, S41], Col2 = [S12, S22, S32, S42], Col3 = [S13, S23, S33, S43], Col4 = [S14, S24, S34, S44], Square1 = [S11, S12, S21, S22], Square2 = [S13, S14, S23, S24], Square3 = [S31, S32, S41, S42], Square4 = [S33, S34, S43, S44], valid([Row1, Row2, Row3, Row4, Col1, Col2, Col3, Col4, Square1, Square2, Square3, Square4]). valid([]). valid([Head | Tail]) :- fd_all_different(Head), valid(Tail).
上記の数独ソルバを実行すると、以下のようになります。
| ?- sudoku([_, _, 2, 3, _, _, _, _, _, _, _, _, 3, 4, _, _], Solution). S = [4,1,2,3,2,3,4,1,1,2,3,4,3,4,1,2]
あいにく欠点もあります。宣言型プログラミング言語はパフォーマンスのボトルネックに達しやすいことです。上記の単純なソートアルゴリズムの計算量はO(n!)
となりそうです。この数独ソルバは力ずくの探索を実行するのです。よって、SQLクエリを実行する際には、コストが高く効率の悪い実行計画とならないよう、大抵は開発者がデータベースにヒントや追加のインデックスを与える必要があります。
記号プログラミング
言語例:Aurora
Aurora言語は、記号プログラミングの一例です。こうした言語で書く”コード”には、プレーンテキストだけでなく画像、数式、グラフ、チャートなどを使うことができます。このため、多様なデータを、全てテキストで書くのではなくデータのネイティブ形式で記述できるのです。また、Auroraは完全にインタラクティブであり、REPLの強化版のごとくコードの各行の結果を即座に表示してくれます。
Aurora言語の作者であるChris Grangerは、Light Table IDEを構築した人物でもあります。Chrisは記事「Toward a better programming」(より良いプログラミングに向けて)でAuroraを推進する動機を概説していますが、プログラミングをより観察しやすく直接的なものにし、副次的な複雑さを減らすことなどを目的として挙げています。さらに詳しく知りたい方は、Bret Victorの素晴らしい講演「Inventing on Principle」(主義としての発明)と「Media for Thinking the Unthinkable」(考えられないことを考えるための媒体)、そして「Learnable Programming」(学べるプログラミング)をぜひご覧ください。
最新情報:「記号プログラミング」はAuroraに対して使う正しい用語ではなさそうです。詳しくはWikipediaの記号プログラミングをご参照ください。
知識ベースプログラミング
言語例:Wolfram言語
前述のAurora言語とほぼ同様、Wolfram言語も記号プログラミングを基礎としています。しかし記号層は、Wolfram言語のコアに一貫したインターフェースを提供するための単なる手段にすぎません。そのコアこそ、知識ベースプログラミングです。言語に膨大な数のライブラリ、アルゴリズム、データが組み込まれているのです。このため、Facebookのつながりのグラフ化、画像の操作、天気のチェック、自然言語クエリの処理、地図への方向のプロッティング、数式の解答といった様々な作業がやりやすくなるというものです。
Wolfram言語には、現存のあらゆる言語の中で最大の「標準ライブラリ」とデータセットがあるのではないでしょうか。また、コードを書く環境にインターネット接続を組み込むというのもすごい発想だと思います。ほとんど、オートコンプリート機能がGoogle検索を行ってくれるIDEといった感じです。記号プログラミングモデルがWolframの主張するほど柔軟性に富み、その様々なデータを真に活用できるかは非常に興味深いところです。
最新情報:Wolframは、Wolfram言語が「記号プログラミング」と「知識プログラミング」をサポートしていると主張していますが、これらの用語には少し違った定義があります。詳しくはWikipediaの知識レベルと記号プログラミングをご参照ください。