プログラミング言語の未来はどうなるか
κeenです。最近JEITAのソフトウェアエンジニアリング技術ワークショップ2020に参加したんですが、そこで五十嵐先生、柴田さん、Matzとパネルティスカッションをしました。その議論が面白かったので個人的に話を広げようと思います。
年末年始休暇に書き始めたんですが体調を崩したりと色々あって執筆に時間がかかってしまいました。 時間を置いて文章を書き足していったので継ぎ接ぎ感のある文体になってるかもしれませんがご容赦下さい。 というのを踏まえて以下をお読み下さい。
いくつか議題があったのですが、ここで拾うのは一番最後の「プログラミング言語の未来はどうなるか」という話題です。 アーカイブが1月末まで残るようです。もうあと数日しかありませんが間に合うかたはご覧下さい。
そのとき各人の回答を要約すると以下でした。
- 五十嵐先生:DSLを簡単に作れる言語というのが重要。それとプログラム検証、プログラム合成などの仕組みが普及すると楽しい。
- κeen:五十嵐先生とほぼ同意見で、DSLを簡単に作れる言語。プログラム検証に興味があるのでIdrisなどの依存型のある言語とか流行るといいな。
- 柴田さん:これからはマルチコアをちゃんと扱えるかが重要になる。特に複雑なことをやっても破綻しない仕組み(特にメモリモデルとか)を言語側でどうにかしてほしい。
- Matz:いかに自分のやりたいことを簡潔にコンピュータに伝えられるかに興味がある。また、IDEとのインタラクションによってコーディング体験を向上させるような言語設計も重要。
一応文脈を説明しておくと、五十嵐先生はFeatherlight Javaや書籍プログラミング言語の基礎概念などのプログラミング言語の基礎理論で知られる方です。 κeenは一応Rustのことを知ってそうな人として呼ばれました。 柴田さんはEffective Javaやプログラミング言語Goの和訳などで有名で、特にGoに詳しい方として呼ばれてます。 Matzは説明不要かと思いますがRubyの作者です。
それぞれ特色が出ていて面白いなと思ったのですが、どれも納得できる内容です。 このディスカッションが面白かったのでここから発展して、私個人の考察を色々語ります。
プログラング言語が影響を受けるもの
新しく流行るプログラング言語が影響を受けるものをパッと思いつくも範囲で挙げてみました
- 新しいアイディア
- 実行環境
- 開発マシン
- 周辺環境(エコシステム)
- 開発する人
- 開発ツール
- 言語の開発体制
- コミュニティ
上記の発言を上に当て嵌めるとすれば五十嵐先生や私の発言は新しいアイディア、柴田さんは実行環境、Matzは開発ツールですかね。
ちょっと私見を並べてみます。 私はWebプログラマなので見てる世界が偏ってることに注意して下さい。
新しいアイディア
既存の言語にない設計で、かつそれが上手く既存の言語の問題を解決していれば使いたいですよね。 RustのライフタイムやGoのCSP(goroutineとチャネルのベースになった基礎理論)などは既存の主流の言語にはなく、かつメモリ管理や並行性といった難しかった問題にアプローチできているので使ってみようかなという気になる訳です。
実行環境
業務用マシンでなく普通のデスクトップやノートPCでもマルチコアは当たり前になりましたよね。 しかしスクリプト言語を筆頭にマルチコアを上手く扱えない言語は未だ多くあります。 そういった課題を解決する、マルチコアを簡単に扱える言語の需要は高いでしょう。
本来はここにスマートフォンも並べたいのですが、スマートフォンはプラットフォーマの制約が強くてあまり言語選択に自由度がないので挙げるだけ無駄ですかね。
もう1つ、コンテナやWebAssemblyなどのある程度制限された環境での実行もあります。 となると依存の少なさやランタイムの軽さやクロスコンパイルの容易さなども重要視されるようになるかもしれません。
開発マシン
現在は個人のデスクトップマシンやノートPCが主流ですがそのうちタブレットやスマートフォンからの開発が多くなってもおかしくありません。 するとタッチデバイスで開発しやすい言語がある程度普及するかもしれません。
特にプログラミング言語教育や「ちょっと興味があるからプログラミング齧ってみる」という人からの需要が多いでしょう。 あるいは、プログラマでなくともプログラミングっぽいことをする(分かりやすい例だとIFTTTとか)人にも需要があるかもしれません。
逆に、デスクトップマシンやノートPCを使い続けるならマルチコア環境が主流になります。 となるとコンパイラやLSPなどの開発サポートツールもマルチコアを使えるような設計が望まれます。
周辺環境(エコシステム)
新しい言語が既存のエコシステムを使えると流行の初速にブーストがかかりますよね。 分かりやすい話がJVM言語とかaltJSとか。FFIでCのライブラリを呼び出せるかとか。
あるいはWebAssemblyへのコンパイルが容易な言語としてRustが注目されていたり、Jupyter Notebookから使えるJuliaの熱が高まっていたりなんて例もあります。
開発する人
最近の大学入試では情報系の学科の人気が高いらしいですね。 噂によるとかなり成績がよくないと情報系には入れないんだとか。 現状、プログラマには私も含めて非情報系の人間が割と多くいますが、そういう優秀な後輩達に業界が底上げされるかもしれません。 すると難しめの言語機能なんかも割とすんなり受け入れられる可能性があります。
一方で既卒で一からプログラミングを勉強して始める人もいるでしょう。 もう少し話を広げると、プログラマでない人がちょっとだけプログラムを書くようなケースも考えられます。 そういう人にはとっつきやすい言語が人気になりそうです。
開発ツール
プログラマが当たり前に使うツールによって言語の需要が変化することもあります。 先述のディスカッションで気付いたのですが、現在はプログラミング言語側に多人数開発向けの機能とかは要求されませんよね。 GitなどのVCSを使って開発していれば困ることはそんなにないですし、そもそも色々な機能が絡まりあう実装はよくないものとされていますからね。
そのように周辺ツールが言語へ影響することもあるようです。 例えば自動補完が当たり前になると記述の短さよりも分かりやすさが重視されるようになったり、自動補完が効きやすい設計が好まれるようになったり。 最近だとLSPなんかもあるので、言語と直接は関係ないですが、LSPを作りやすい言語設計の言語が多く登場するなんてこともあるかもしれません。
言語の開発体制
これと次のコミュニティの項は新しい言語設計はどこからやってくるかではなく、新しい言語がどうやったら流行るかになるんですがついでなので書いておきますね。
新しい言語を学ぶのを投資と考えると、どうせなら将来に価値が上がるものに投資したいですよね。 言語の価値が上がるのに色々要因はありますが、1つは開発リソースが挙げられます。 というか、大抵の問題は開発リソースで殴れば解決するのでリソースがほぼ全てです。 となると開発体制、もうちょっと言うとどんな企業がサポートしているかが重要なファクターになります。
この話を広げると企業が開発をサポートしたくなる言語が次に流行る言語ということになるんですが、私は企業それぞれの事情には詳しくないので次にいきましょう。
コミュニティ
言語を使う人が増えるプロセスを考えましょう。 興味を持った人が処理系をインストールして、サンプルプログラムを動かしはじめ、ドキュメントや入門書を読み、小さなプログラムを書いてみて、手応えを感じたらもうちょっと使ってみて、次第に習熟してライブラリを公開したり、質問サイトやコミュニティで他の初心者を導いたりします。
それぞれの「興味をもつ」「処理系をインストールする」「サンプルプログラムを動かす」などのステップの間で一定数のが脱落します。 この離脱率を減らせると使う人が増えやすくなります。特に重要なのが最初、処理系のインストールやサンプルプログラムを動かすなどの部分の易しさじゃないでしょうか。 もう1つ重要なのが習熟して初心者のためになるリソースを残す層の存在です。 これは人依存なので言語開発者からしたらほぼ運ゲーですが、逆にいうと言語のユーザは自分の努力でコミュニティを大きくできるということでもあります。 コミュニティが大きくなればなるほど自身がコミュニティから受ける恩恵も大きくなるので先行投資としては悪くないんじゃないでしょうか。
個々の言語機能への所感とか
ここから前項に輪をかけて一般論というより私個人の意見が強くなるんですが「ここのあたりをこうしてほしい」とかを書き連ねていきます。
一応触れておくと、私の考え方の根底には「プログラムはアプリケーションを書くための巨大なDSLライブラリと、そのDSLを使った小さなプログラムからなる」の思想があります。
Lispでは,プログラムをただプログラミング言語に従って書くことはしない. プログラミング言語を自分の書くプログラムに向けて構築するのだ – Paul Graham (On Lispより)
どう書くと便利かはアプリケーションごとによって違うので、言語処理系はプリミティブだけど色々なことに使える機能を提供し、ユーザ側でそれを便利機能としてリメイクすべきという方針です。ドメイン駆動設計もドメインに集中するために外側に大きなDSLを作るので似たようなものですね。
言語を作るにはどうしてもプリミティブな機能が必要になります。逆に、高級な機能はどうせ自分で作ってしまうのであまり必要ありません。 なので紹介する機能もどちらかというとプリミティブなものが多いです。
並行性の扱い
上でも触れたようにマルチコアのマシンが普及するにつれ、プログラムでも並行性を扱うことが増えてきます。 並行なプログラムを書くために言語側では何が必要でしょうか。
個人的にはpthreadの(ような)APIはよくないと思ってます。 「よし、並行なプログラムを書くぞ」と思わないとプログラムが並行にならないからです。 もっと自然に設計に並行性が入ってきてほしいです。
つまり、並行性を提供する言語機能単体ではなく並行性を含んだ設計のフレームワークがまずあって、言語機能はそれを補助するようにできていてほしいです。 そして並行性の持ち込むバグや複雑性から守ってほしいです。
Goのgoroutineはチャネルがある分pthreadに比べるとまだマシですが、理想からはほど遠いです。 チャネルでのやりとりが手続き的すぎます。 手続的ということは相手の状態を考えながらプログラミングしないといけません。 これでは並行性による複雑性がそのまま表出してしまいます。 もうちょっと言うと、スレッド間のやりとりに「データを送る」しかないのが不便です。 何かの処理をお願いしたいのにデータしか送れないのではいちいちお願いをデータにエンコードしないといけません。 通信プロトコルを手書きしているようなものですね。 そういうプログラミングは手間ですし、コードの見通しが悪くなります。
そういった意味ではErlang/OTPのようなフレームワークは理想形に近いです。 機能単体ではなくフレームワークとして存在しているので自然と並行性を設計に組込めます。 しかしあれはBEAM VMだからこそ生きるフレームワークなので難しいですね。
Actorモデルを他の言語向けにアレンジするなら、インスタンスを作るときに「このインスタンスは並行に動く」と指定するとかですかね。 指定すると裏でインスタンスが別(軽量)スレッドに作られて、メソッド呼び出しはそのスレッドにメッセージが送られることになる。 これならActorモデルのメッセージパッシングとインスタンスのメソッドの呼び出しに自然に対応がとれますし、裏側の通信プロトコルを意識する必要もなくなります。 スレッドとプロセスの違いはありますがdRubyが近いかな? まあ、スレッド間でのデータ共有はどうするのかとかの問題は残りますが大枠としてはいいんじゃないでしょうか。
もう1つのトピックとしてデータ並列がありますが、これは簡単なものならFork Join Poolとイテレータインタフェースで割とどうにかなると思ってるのでそこまで興味はないです。 GPU使いたいとかまでくると考えものですが、それはデータ並列以前にヘテロプロセッサの扱いの問題があるのであんまり気にしなくてよいでしょう。
非同期処理
ぐだぐだ書きましたが、実際のところ私の領域であるWebプログラミングではそんなにスレッドを気にすることはないです。 なぜならリクエスト単位での並行性というものすごく粒度も丁度いい並行性があるからです。 そういうのはWebフレームワークが解決してくれるので、プログラマはあんまりスレッドとかを気にしなくてよいです。
どちかというとIOをどう捌くかの方が関心が高いです。 WebアプリケーションはDBや他のサービスとの通信などのネットワークを介した処理がよくあります。 そこでまともに通信が終わるのを待ってるとほとんどの時間が待ち時間で終わってしまうのでスレッドが完全に遊んでしまいます。 となるとIOでブロックしない仕組みとタスクを中断したり再開したりできる仕組みが必要になります。
タスクを中断したり再開したりする仕組みはぼ言語のサポートが必須といっていいでしょう。 CPS変換したりステートマシンを書いたりする方法がなくはないですが、とても面倒です。 なので言語によるグリーンスレッド、あるいは便利構文のサポートか、(限定)継続が必要になります。 async/awaitがよく採用されますが、高階関数との親和性がよくないので個人的には微妙だなって思ってます。 このあたりは話すと長くなるのでこのくらいでやめておきましょう。
IOでブロックしない仕組みは多くの場合は言語ではなくライブラリの問題です。 ただしブロックするIOとブロックしないIOは混ぜてはいけないので注意が必要です。 2つを混ぜないとなると、ブロックするIOとブロックしないIOの2系統のAPIを提供しないといけなくなります。 あるいは1系統のみに徹するか。 ここの方針の影響を受けるのが標準ライブラリの設計です。 なので広い意味での言語設計上、IOをどうしたいかは決めておく必要があります。
IOでブロックしない仕組みを言語(ランタイム)で解決する方法もあります。 IO処理を必ずランタイムで捕捉できるようにしておけばランタイムでいかようにもできますからね。 ただし、これをやるにはタスクを中断したり再開したりする仕組みもランタイムで実装する必要があります。
標準ライブラリを非同期のみにしたのがnode、ランタイムでIOがブロックしない仕組みを作ろうとしたのがErlangやGoですね(あとHaskellもかな?)。
あるいはAlgebraic Effects and HandlersがあればIOでブロックする/しないはアプリケーションで制御できるし、IOは2系統に分かれないし、タスクを中断したり再開したりする仕組みも自動でついてきます。
メモリ(リソース)管理
ここでいうGCはいわゆる古典的なMark and SweepだとかCopy GCだとかの実行時にゴミを発見して回収するシステムのことを指しています。
ガーベジコレクションはない方がいいですよね。 どんなにデータローカリティを最適化していようが、応答速度を気にしていようが、Copy on Writeを意識した作りをしていようが、GCが走ると全ての努力を無に帰します。 Hello Worldのプログラムにさえリンクされますし、アプリケーションがどんなに速くてもGCが遅ければ全体が遅くなります。 さらにはちゃんと作らないと言語に並行性を導入できませんし、並行性に耐えられるように作ってもどうしてもStop the Worldが発生します。 また、GCによるリソースの回収はタイミングが遅いのでファイルディスクリプタなんかは別途手で管理しないとならなくなります。
こういうと「Rustを使えば解決じゃん」と言われそうですが、Rustのライフタイムによるメモリ管理は銀の弾丸ではないです。 Rustはシステムプログラミング言語だから許されるのであって他の言語でライフタイムとかを意識しながら書くのはつらいです。 Rustよりももう少し緩い仕組みが必要です。
もう1つ、使ったメモリをそのまま再利用する仕組みがあると嬉しいです。 さっきまで使ってたメモリというのはキャッシュに載ったホットなデータなのでそのまま捨てるには惜しいです。 メモリを捨てずに別のデータを上書ける仕組み、要にはC++のplacement newのようなものをコンパイラが頑張ってやってくれたら便利ですよね。
最悪、GCはあってもいいのですがメモリを即座に開放できる仕組みもセットで欲しくなります。 個人的に有望だなと思ってるのは以下の3つです。
- ML Kit with Regionsみたいに処理系がデータの生存期間をある程度推論して開放する仕組み
- C# みたいにユーザが特定の書き方をしたらメモリを即座に開放できる仕組み
- C# にあまり詳しくないんですがローカル変数に
null
を代入したらその場で開放してくれるんですよね?
- C# にあまり詳しくないんですがローカル変数に
- Lobster みたいにいくつかの仕組みを組み合わせて参照カウントでも安心して使える仕組み
- Idris 2みたいに線形型と他の型をシームレスに連携できる仕組み
- 実装上メモリ管理まで手が回ってるかは不明だが、原理的にはできるはず
ちょっとIdris 2の型について触れますね。 Rustみたいに値を1人しか使えないシステムの、すぐに思い付く拡張として値を高々n人しか使えないシステムがあります。 さらにnを定数ではなくパラメータ化、つまり変数ごとに「この変数は1回使う、こっちの変数は5回使う」と利用数を変えられるシステムも思い付きます。 ここで参照カウントに思いを馳せると、所有者が1人のときのみカウントの情報量がゼロになるのでRustみたいにGCレスでメモリを管理できるのが分かると思います。 つまるところ、1とそれ以外だけ区別すればよいです。 そこでIdris 2は0回使える型(メタ情報向け)、1回のみ使える型、何回でも使える型を持っています。 そして何回でも使える型は1回使える型の部分型になるので両者が共存できるのです。 …と書いてたらIdris2は0.3で利用回数(Multiplicities)の部分型付けをやめてしまった。 まあ、そのうちLinear HaskellみたいなMultiplicity Polymorphismとか入るでしょ。
GCによって発生する問題は基本的にアロケーションの問題です。 ユーザの書き方の工夫でアロケーションを減らせたりGCが走る前にメモリを回収できるならGCによる問題はそこまで発生しません。 GC一本槍ではなくいくつかの仕組みを組み合わせたハイブリッドなシステムができるといいですね。
メモリ解析
これは比較的コンパイラ内部の話です。だけどユーザにもちょっと関係があります。
コンパイラってメモリが絡んだ最適化に弱いんですよね。 どのポインタとどのポイタが同じところを指しているのかとか、メモリ上の値がいつ書き変わってるのかなどを追うのが難しいからです。 とはいえメモリの絡んだ最適化ができないとどうしようもないので、コンパイラもそりなりに頑張ってはいますし、色々な手法が発明されています。 ですがCやC++のように自由にメモリを書き換えられるモデルでは原理的に全てのポインタエイリアスとかの解析を行うのはできなかったはず (もしかしたらただのNP完全だったかも)。
Rustのようにポインタの操作に制限のある言語であればかなりの部分が改善するんですが、どうしても最後の砦が残ります。 配列へのアクセスです。 実行時に計算した結果を添字に使えるのでどの要素にアクセスしているかはおろか境界外アクセスをしているかどうかさえ静的には完全には分かりません。 Rustもスライスへのインデックスアクセスは動的に境界チェックをしています(境界内であることが簡単に分かる場合には最適化で境界チェックのコードは消えます)。
メモリアクセスの解析、特に配列のインデックスアクセスを解析できる手法が出てきたらなと思ってます。解析できれば境界外アクセスをコパイラでエラーにしたり、境界チェックのコードを省けたりします。 方針として私が思い付くのはいくつかあります。
1つはRustのようにメモリアクセス系の操作に制限を加えた言語を作る方法です。 配列のインデックスまでケアしようとすると、今の私の知識では依存型か篩型が必要になるんですが、もしかしたらもうちょっと楽な仕組みが生れるかもしれません。 例えばシンボリック実行エンジンをユーザにも見せる設計とか(これって実質篩型と同じ?詳しい方教えて下さい)。
もう1つはJITのように動的解析で使える情報を増やして踏み込んだ解析をする方法です。 解析した結果、最適化したり未定義動作かもよという警告が出せたら面白いですね。 境界外アクセスは、複雑な仕組みがなくても範囲チェックすれば起きていることだけは分かるのですが、「この計算で範囲外の値が出てきてるよ」って言えたら夢がありますよね。
例外
既存の例外の仕組みってどれも便利じゃないですよね。
例外って扱いが面白くて、コード内で関心がある部分とない部分がくっきり分かれるんですよね。 例外は何か処理をしてるときにどうにもできないことが起きたら呼び出し元に報告する仕組みです。 ですがその呼び出し元もどうしたら良いか分からなかったらさらにその呼び出し元にカスケードして報告します。 例外をどう扱えばいいか知ってる人のところまできたらその人がどうにかします。
が、多くの言語ではそこで止まってしまいます。
本当は責任を持てる人が例外が起きた現場に指示を出して差し戻すのも必要なはずです。
例えばJSONをパースしていて、処理できない文字が出た場合に「その文字をスキップして進めなさい」「その文字をこの文字に置き換えて進めなさい」「今パースできてる分だけで処理を継続しなさい」とかの要求ができて然るべきです。
一応、 onError
のコールバックを持つAPIがあることもありますが、CPS変換を手でするのはつらいというのは非同期処理で人類が既に通った道です。
例外を送出したところからリスタートする機能があると嬉しいですね。
また、Javaを筆頭に例外が嫌われますが、個人的にはあれは構文が重いせいと、例外を特別扱いしすぎてるせいだと思ってます。 例外を気軽に値として取り出したり、「例外を返すかもしれない関数」に対するメソッド呼び出しなどができると随分楽になると思っています。
例外が大域脱出をするかしないかという設計の問題もあります。これは一長一短あるのでどういう言語にしたいかですね。
先に触れたように例外処理は普通複数の関数呼び出しを越えるものだとすると、例外に大域脱出させる設計をすることになります。 大域脱出は正常系の処理に集中できるという点では良いのですが、値として取り出したりするのが難しくなります。また、例外を特別扱いすることになるので言語が複雑になります。
言語のシンプルさを取れば大域脱出しないことになります。関数を呼び出した人がその関数で起きる例外に責任を持つというモデルですね。 大域脱出しない例外だとエラー値を取り出すのが楽になるのでエラーハンドリングはしやすくなりますが、 エラーをそのままカスケードするのは手間になります。特に無名関数などと相性が悪くなりがちです。
これらの問題を解決する例外システムがあればなと思ってます。 つまり、私は以下の全てを満たす例外システムがあるといいなと思っています。
- 例外を送出するのが簡単
- 例外を再開できる
- 例外を値に変換するのが簡単
- 特に、複数起きるかもしれない例外をちゃんと区別して扱える必要がある
- 「例外が起きるかもしれない関数呼び出しの結果」を第一級の値として扱える仕組み
再開できる例外はCommon Lispのコンディションシステムや、Schemeの raise-conituable
なんかにあります。
あるいはAlgebraic Effects and Handlersや(限定)継続でもできそうです。
例外を値として取り出したり「例外が起きるかもしれない関数呼び出しの結果」を第一級の値として扱える仕組みはいわゆるEitherモナドのある言語であれそのまま満たします。
ただ、エラー値をまとめたりがちょっと苦しいのでOpen Unionか多相ヴァリアントがほしいところです。
例外を送出するのはRustのように ?
を持つか、Eitherモナドは扱いが簡単と言い張るかするとよいでしょう。
Eitherモナド単体はそんなに大変でもないんですが他のモナドと組み合わせたときに不便になるのだけどうにかなりませんかね。
大域脱出する例外を値として取り出すのは結構難しいですが、Scalaの Try
なんかは実現していますね。
総合すると、2つの選択肢が見えてきます。 大域脱出しない例外を採用するならヴァリアント集合の合併などヴァリアントに対して操作ができる代数的データ型を用意した言語。 大域脱出する例外を採用するならAlgebraic Effects and HandlersとCall by nameを持つ言語か、Lisp。
例外に対する課題意識はおおむね以下で説明されています。
ただ、私は何のエラーが上がってくるか型で明示してほしいのと、例外はリスタート可能であってほしいと思っています。
ヴィジュアルプログラミング言語
テキスト形式のプログラミング言語が未来永劫のあらゆる場面ににおいて最適解とも限りませんし、そのうち新しい形式の言語も出てくるだろうと思ってます。 そのうち有望そうなのがヴィジュアルプログラミング言語です。
ここでちょっと慎重になりたいのですが、ヴィジュアルプログラミング言語は2つに分けた方が良いと思ってます。 1つがScratchのようにテキスト形式のプログラミング言語のヴィジュアルエディタになっているものです。 もう1つがViscuitのように、描いたものが動く、本当にヴィジュアルプログラミング言語になっているものです。 どっちも面白いと思ってます。
ヴィジュアルエディタはNo Code/Low Codeとかの文脈で話題ですよね。 そうでなくても目覚まし時計の設定もある意味ではプログラミングですし(某教授の受け売り)、IFTTTやBlenderのシェーダノードエディタなどの手軽にプログラミングできるエディタは多くあります。 もうちょっと本気出した例だとFF XVの開発ではプログラマがゲームを作るためのヴィジュアルエディタを開発して、ゲームデザイナがそれを使ってゲームを作ったという話などがあるでしょうか。 まさしく「プログラムはアプリケーションを書くための巨大なDSLライブラリと、そのDSLを使った小さなプログラムからなる」の好例です。 No Code/Low Codeがビジネス的に成功するかは怪しいと思ってますが、副産物としてアプリケーションを書くための巨大なDSLライブラリのヴィジュアルエディタを作るためのフレームワークが世に出てこないかなと期待しています。 DSLはまず背後にあるモデルをしっかり作り込むところがスタートラインで、それをどう見せたら書きやすいかを考えるのがDSLなんですが、どうせNo Codeとか叫ぶ人は背後のモデルとかあんまり考えずに場当たり的に言語を作って使いづらいものができあがってあんまり使われずに爆散して「プログラマ以外がコードを書くのは難しすぎた」みたいな雑な責任転嫁して終わるんでしょうと思ってます(偏見)。
ヴィジュアルエディタに期待しているもう1つの理由はスマホやタブレットの存在です。 今テキスト形式のプログラミング言語が最適とされている理由はみんなキーボードを使っているからだと思うんですよね。 ですがスマホでコードを書いてみると恐ろしく書きづらいのが分かると思います。 そうなるとタッチデバイスでの入力に最適化された言語が出てきてもいいんじゃないかと思ってます。 そして多分それはヴィジュアルエディタになるんじゃないかなーと踏んでます。 まあ、(不本意ながら)プラットフォーマ側で外部からプログラムを受け付けるアプリは禁止しているようなので簡単には普及しなそうですが、アイディアというかPoCだけでもでてきたら良いですね。
本物のヴィジュアルプログラミング言語についてはプログラミングの可能性を模索していってほしいと思ってます。 ループと分岐だけがプログラムを書く手段ではないはずですからね。 残念ながら私にはこうなるだろうとかのアイディアはまったくないのですが、天才的ひらめきの人がいつか全く新しいプログラミング体験を提供してくれると思ってます。 そういった意味ではViscuitはあれを発想して実装して教育用途で実用してるのは本当にすごいと思います。
メタプログラミング
メタプログラミングはほぼ必須の機能だと思ってます。 マクロだとかコードジェネレータだとかの類ですね。私は両方とも必要だと思います。 ただ、色々な問題を孕むので導入に弱腰になる言語があるのも理解できます。
コードジェネレータの場合、ビルドフローどうなるのとか(そもそもスクリプト言語ではビルドフローが存在しない場合もある)の問題があります。
マクロの場合、ルールベースのマクロと手続マクロどっちにするかとか、自動補完とかのサポートどうなるんだっけとか、マクロの入力と出力は何(文字列?トークン?AST?コア言語?型推論前と後どっち?)とか、手続マクロでIOを許したときに再コンパイル判定どうなるのとか、手続マクロっていつコンパイルされていつどうやって実行されるのとかの問題があります。
やっぱり外部情報(API定義やDBスキーマなど)からコードを生成するにはコードジェネレータは必要ですし、DSLを作るにはマクロが必要です。 マクロについてはLispに一日の長があるので、極論を言えば全員Lispを使えばいいんですが、Lisp相当の表現力を持ったマクロを他言語で実現するとどうなるのかには興味があります。
他には多段階計算だとかの仕組みがありますが、どこまで有用なんでしょうね。
形式手法
現在のソフトウェアって数個の値で動かしてみて望む結果が得られるなら正しいとされることが多いですが、それって全然正しくないですよね。 少なくとも数学の試験でそれをやったら×になります。
別にソフトウェアテストが完全に間違ってるとは思ってませんが、テスト以外にもソフトウェアの振舞いを検証する手段があってもいいじゃんと思ってます。 もう少しちゃんと正しさを保証できる手法、数学の試験でやっても○がもらえる手法として形式手法が普及して欲しいなと思ってます。 形式手法には形式仕様記述やモデル検査など色々ありますが、私が注目しているのは定理証明、特に依存型を使った定理証明です。
定理証明は昔からあるのにあんまり普及してないのはツールの問題があるんじゃないかと思ってます。 すなわち、プログラムを書いてるときに「あ、この部分証明できそう」と思ったら一旦そのプログラムを定理証明支援系に移植して証明してExtractするのが面倒すぎるのです。
これに対する反論として普段から定理証明支援系でプログラムを書けばいいじゃんというのがあります。 それで問題ないならいいんですが、定理証明支援系ってプログラムを書くのにそんなに便利じゃなくないですか? 私が想定している証明の使い方は、普通にプログラムを書いて、普通にテストして、特に重要そうな部分は証明つけよっかってなるフローです。
そこにきて依存型のあるプログラミング言語ならプログラムを書きつつ、気が向いたときに証明ができます。 まあ、依存型のあるプログラミング言語と定理証明支援系の本質的違いはないんですが構文や標準ライブラリの設計、利用者層の違いがあります。 特に、依存型はプログラムのパーツとしても便利なので何の気なしにプログラムを書いていたらうっかり足を滑らして定理証明をはじめてしまえるのもポイントですね。 既存の言語に依存型が入るのでも依存型のある言語が流行るのでもいいんですが、依存型を簡単に使える環境が整うと嬉しいです。
定理証明は昨今だとブロックチェーンやスマートコントラクトの文脈でも需要があるんじゃないでしょうか。
型
最も成功した形式手法こと型にも注目ですね。 型もある種の誤りを機械的に検出する仕組みの1つです。
動的型と静的型の組み合わせ
型に関する話題では動的型付き言語への型の導入がホットトピックです。 TypsScriptにPythonのType HintsやRuby 3のRBS/TypeProfなど、動的型付き言語に型が導入される流れがきてますね。
この中でも特に成功したのはTypeScriptでしょうか。 TypeScriptが広く使われているのは私からすると意外でした。 型システムは複雑なくせに健全性(型チェックに通ったら実行時に型エラーが出ないという保証)はありません。 ビルドの手間やコンパイル時間は必要な(しかも初期の実装はかなり遅かった)のに特に実行が速くなる訳でもありません。 私が発案者だったら5秒でボツにするような仕組みが世に出され、普及するとは思いませんでした。
TypeScriptが普及した理由が分かれば他の言語に型を導入するときのヒントになるかもしれません。 別にTypeScriptユーザじゃないですがTypeScriptが普及した理由を多少考察してみます。 語尾に「知らんけど」を補いながら読んで下さい。
- フロントエンドはJavaScriptしか選択肢がなかったので無理矢理にでも型をつけたかった
- altJSは色々あったが、TypeScriptは生のJSをコピペしても動くように作られていた点で優れていた
- 元々ビルドツールが必要だったのでワークフローへの導入が楽だった
- 型の健全性を気にしない人が多いか、部分的に型検査ができるだけでもありがたいような状況だった
- TypeScriptの型がJavaScriptを書く人のメンタルモデルによくマッチしていた
この中でも特に2番目と3番目が効いてるのかなーと思ってます。
既存のコードベースから手間なく小さく始められるというのは大事ですよね。
そう考えるとRuby(on Rails)も rake test
で自動でRBS/TypeProfの検査が走るようにすれば普及しそうな気がします。
ところでTypeScriptはJavaScriptへとコンパイルできる言語ですが、PythonのType Hintsは言語仕様の拡張、RubyのRBS/TypeProfは新しいソースコードの追加です。それぞれアプローチが違って面白いですね。
Pythonの型やその検査ツールがどれだけ使われてるかは分かりませんが(そもそも私のマシンの python
コマンドはType Hintsがまだ搭載されていないPython 2.7です)、Rubyの型はドキュメントやテストの一種として普及していくんじゃないかなと思ってます。
まあ、まだ初期実装の段階のようなので今後を占うには少し早いかもしれませんね(実装の完成度によって普及度が変わるため)。
…というのが既存の言語に後から型を導入する話でした。 しかし最初からオプショナルな型を持つ言語を設計するならまた状況は変わってきます。 新しい言語なら既存のコードとの互換性を気にしなくていいので理想的な機能が入れられます。
例えば言語仕様内に型宣言が入るでしょう。 言語仕様内に型宣言がある例だとJuliaがあります。Juliaはデフォルトで動的でありつつ型宣言も持っていますね。 オプショナルな型宣言自体は古くはCommon Lispの時代からある仕組みですが、それがちゃんと検査されることを保証している言語で市民権を得ているものは私はJuliaくらいしか知りません。
あとは漸進的型付けも採用されるんじゃないでしょうか。 それも上に挙げた言語の型拡張のようなものではなく、正しくキャストを実装した理想的な漸進的型です。 正しいキャストとはキャストを行なった時点で動的に型チェックが行なわれ、期待しない型だった場合のその場でエラーになる仕組みです。 これは既存のコードのセマンティクスを変えないという方針のTypeScript、Type Hints、RBS/TypeProfでは実現できませんね。 理想的な漸進的型けは以下の記事を参考にして下さい。
もしかしたらdenoのTypeScriptサポートでキャストをちゃんと実装するかもしれません。
あとは部分型を構造的にするか名前的にするかはどうなるんでしょうね。 というかそもそも部分型って実装されるんですかね。 クラスベースのオブジェクト指向が入れば名前的な部分型が入るでしょうし、プロトタイプベースのオブジェクト指向が入れば構造的な部分型が入るでしょう。 ただ、クラスやプロトタイプを21世紀に作られる言語が採用するかはちょっと怪しいです。
面白型システム
Rustとかで型システムで面白いことやったら面白い言語が作れるというのが知られたので、面白い型システムを搭載した言語がいくつか登場するんじゃないでしょうか。 個人的には線形型、依存型、セッション型あたりが来るんじゃないかなと思ってます。というか線形型はRustのことを考えると既に来てますね。 線形型と依存型はそれぞれメモリ管理と形式検証のところで触れたのでセッション型について触れます。
セッション型は通信に型をつける仕組みです。 普通のチャネルを使った通信だと1種類のデータしか送れません。 ですが、セッション型を使えば「数値を送って次に文字列を送れば文字列の配列が返ってくるチャネル」のような型も表現できます。 分岐や繰り返しもあるのでそこそこ複雑な通信でもちゃんと型をつけられます。
チャネルを使った通信が流行るならこういった仕組みも流行るんじゃないかなと思ってます。 ただ、並行性のところでも書いたように私はそもそもチャネルを使った手続き的な書き方は微妙だなと思っています。 じゃあなんでわざわざ言及したんだよっていうのは私の目指したい方向とは逆のものが流行ることの方が多いからです。
継続
継続欲しいですよね。 プログラムを開始状態から終了状態まで状態遷移していくものと考えると、値だけを使ったプログラムはスタートからゴールに向かう方向のみの縛りで状態遷移を書いている状態です。 ですがゴールから逆順に状態遷移を書く方法があってもいいと思うんですよ。 「正常に終了するプログラムは必ずこの処理に到達する」とか「異常終了するプログラムはこういう処理をして終わる」とか書きたいじゃないですか。 グラフ探索だって初期状態だけから探索するより初期状態と終了状態の両側から探索した方が高速ですよね。 この終了状態側からプログラムを書くのが継続です。
Schemeには継続を第一級市民として扱う機能がありますが、あまりにも貧弱です。 継続を値として扱えるのみです。継続は継続、値は値として扱えてほしいです。 $\bar{\lambda}\mu\tilde{\mu}$ 計算ベースの言語とか出てきませんかね。 まあ、お前が作れっよて話なんですが…。
それとは別に、限定継続に皮を被せてとっつきやすくしたAlgebraic Effects and Handlersにも注目です。 これは既に実装した言語がいくか出ているので試そうと思えば試せます。
私もそのうち試さないとですねー。
パターンマッチ
便利なことがある程度知られてきましたしそのうち色々な言語に取り入れられるんじゃないでしょうか。 最近Rubyに入ったパターンマッチを見て気付いたんですが、パターンマッチってデータを照合するのでその言語でのデータの定義の仕方によって色々変わるんですね。 お気に入りの言語にパターンマッチが入るとしたらどんな仕様になるか考えてみるのも面白いかもしれません。 もしかしたらEgisonみたいな複雑なパターンをサポートする言語も出てくるかもしれませんね。
プログラミング言語の未来はどうなるか
色々語ったところで、少し大胆ですがプログラミング言語の未来はどうなるかを予測してみましょう。
既存の言語の進化
既に使われてて開発に慣性の乗ってる言語はそうそうに消えないんじゃないですかね。 とはいえ古い時代の設計のままの言語がそのままずっと通用するという訳でもないので相応のアップデートを重ねていくことになります。 Rubyに3.0でJITや並列サポートが入ったのは記憶に新しいですね。 昔はMatzが速度の問題はCPUの性能向上で解決するって言ってましたが、ここのところCPUの性能向上は頭打ちになってるので処理系側の変化が必要になる訳です。 (一応補足しておくとRubyはずっと性能改善に向き合っていて、その取り組みの一環としてJITがあります。「JITが入った」が分かりやすいマイルストーンなので取り上げました。)
Rubyに限らず「マルチスレッド上手く扱いたい」「非同期IOしたい」「インタプリタやVMじゃ限界だしJITが見えてきた」「どうやら型はあると便利っぽい」あたりはどの言語も通っていくんじゃないでしょうか。
領域ごとの言語が増えるかも?
プログラミングをするようになった領域や、プログラムで実現したいことの幅が大きくなってきたので領域ごとに使われがちな言語が分かれるのは増えていくんじゃないですかね。 想像しやすいのは教育用言語とか、あとはスマートコントラクト用の契約言語だとか。 量子コンピュータ向け言語とか自動微分とかも夢があっていいなって思います。
もちろん、これらの領域向けの処理も既存の汎用言語のDSLとして実現しても書けないことはないのですが、やっぱり記述が煩雑になりがちです。 それにプログラマではなくその領域のエキスパートが必要に応じてプログラムを書くようなケースを想定すると、汎用言語とDSLの両方を覚えないといけないモデルはハードルが高いんじゃないかとも思ってます。
コンパイル時間に重点を置いた言語?
多くの言語が機能を増やす方向に向かっていて、その結果コンパイル時間も長くなりがちです。 これが言語によっては耐えられないくらいコンパイルが遅いこともあって、コンパイル時間を理由に忌避されるケースもあるほどです。 実際、コンパイルが遅いと開発のテンポが悪くて作業が進んでる気がしないというのはよく分かります。
そうなると、並列化などを頑張るか、機能を減らしてコンパイルが速くなるようにした言語の需要があるんじゃないかなと思います。 まあ、機能を減らした方はGoがあるんですが、機能はリッチなままコンパイラの高速化を頑張った言語はみてみたいですね。
コンパイラって書きましたがスクリプト言語でも一緒です。 ライブラリのロード時間とかの立ち上がりが遅い処理系だと開発の手返しが悪いので速くなってほしいですよね。 まあ、これについては1ファイルにまとめるだとかバイトコンパイルだとかの目に見えた解決策があるので需要があれば実装されるんじゃないでしょうか。
静的型つき言語の時代になるかも?
「型はどうやら便利っぽい」「全部型を書かなくても推論できる技術があるらしい」というのが知られてきたので今後出る言語は静的に型がつくものが多くなるんじゃないでしょうか。
型があると便利というのはバグを防ぐ仕組みというのの他に補完などのコーディング支援を受けやすいというのもあります。 どのオブジェクトのどのメソッドを呼び出してるのかが簡単に分かると嬉しいですよね。
頑張れば動的型付き言語でもできるんじゃと思うかもしれませんが、部分的には可能でも全部は無理です。 分かりやすい例だと伝家の宝刀evalを使われるとどう頑張っても静的に解析はできません。 やるとしたら実際に実行させながら処理系に問い合わせるような仕組みになるかと思いますが、裏でDBアクセスしてメソッドを生やすようなことをやられてると難しいです。 そういったのを排除して静的に解析できる範囲だけで書こうとするなら最初から静的な解析がついている言語で書いた方が手っ取り早いですよね?
汎用ランタイムとそれを使った処理系が出る?
早い話がジェネリック版Goのランタイム。 ランタイムに求められるものが割と見えてきたしどっかで汎用ランタイムが作られたらそれに乗っかる処理系もできるんじゃないですかね。 ここでいうランタイムはlibgcみたいなライブラリを想定していますが、JVMみたいなVM実装もあるかもしれませんね。 今見えている範囲だとWebAssemblyのシステムインタフェース(WASI)の拡張として非同期サポートが入るとそれっぽくなるんじゃないでしょうか。
まあこういう汎用部品は結局メインターゲットの言語が1つに絞られなくて誰が使っても微妙に便利じゃない感じになって結局誰も使わないまま終わることになりがちですが…。
オブジェクト指向は残るんじゃないかな
最近色々言われがちですがオブジェクト指向は残るんじゃないかなと思ってます。 オブジェクト指向というか obj.method() の記法です。 あれは . をタイプしただけでデータを操作するメソッドの補完候補が簡単に出せます。 そういうツールにやさしい構文は今後生き残るでしょうし、むしろ増えていくんじゃないでしょうか。
色々言われるというのはオブジェクト指向vs関数型の論争のことです。 個人的にはあれは論争というより話が噛み合ってないだけなんじゃないかなって思ってます。
オブジェクト指向側はオブジェクト指向に限らない設計の原則までオブジェクト指向ととらえていて、それを使わずにプログラムを書くなんて想定できないって言ってるように見えます。 象徴的なのがオブジェクト指向入門 第2版 原則・コンセプト です。 冷静に考えて1000ページ近くもある本で説明しないといけない原則は原則として破綻しています。 あれは色々な設計の原則をオブジェクト指向風に実現するとこうなるとかの内容がほとんどです。 それなのにあの本の内容がオブジェクト指向で、オブジェクト指向を捨てるということはそれを全てしないということだなんて思ってると話が噛み合いません。
一方、関数型側の主張はよく見るとオブジェクト指向に直接言及してることは少ないです。 「破壊的変更をすると分かりづらいよね」とか「高階関数って便利だよね」くらいのことしか言ってないことが多々あります。 まあ、たまに特定の言語というか特定の処理系のユーザが暴れ回ってることもありますけどね。 強いていうならクラスの継承とそれによる差分プログラミングはスパゲティコードになるので個人的にはよくないと思ってます。 ですがクラスと継承だけがオブジェクト指向という訳ではないですし、それさえやめてしまえば別に問題ないと思ってます。
ということでobj.method()の記法をする言語は生き残るんじゃないでしょうか。
まとめ
最近のプログラミング言語について思っていることを吐き出しました。 人それぞれ見ている世界が違ったり異なる意見があったりするかと思います。 私もここに書いたことが全て正しいとは思ってませんし、色々な人の期待、様々な将来予想があるでしょう。 気が向いた人は5年後、10年後、あるいはもっと未来のプログラミング言語がどうなってるかの未来予想図を認めてみて下さい。