プログラミング言語の表層構文の合理性/プログラミング言語の見た目を自然言語に寄せることが良いとは限らない

プログラミング言語の表層構文を見ていると、「自然言語(特に英語)に寄せたかったのかな」と思わされる語順を時々見かけます。例えば、Pythonの from somemod import a, b という語順は英語として自然に読めることを意識していると思われます。

こういう設計の背後には「自然言語のように読める構文が良い構文だ」という暗黙の仮定があるように思えます。しかし、それは正しいのでしょうか?

この記事では、いくつかの例を元に、プログラミング言語の構文の「合理性」はどのような基準で測られるべきなのかを考えます。筆者の主観(感覚)に基づく基準もあり、普遍的な基準を作成することを目指しているわけではありません。「それはお前がそう思っているだけなのでは?」と思われる部分もあるかもしれません。仮説、あるいは議論の材料の提示と思っていただくのが良いかもしれません。

いくつかの例

まず、いくつかの言語で見られる構文を取り上げ、「どういう性質があると良さそうか」を抽出します。

import文の語順

ファイル分割やモジュール機構を備えたプログラミング言語は、他のファイルに書かれた内容を使えるようにするため、import文と呼ばれるものを備えていることが多いです。C/C++の #include も、名前と機能は違いますが見た目上は似たようなものだと思えます。まずは、こういうimport文の語順について考えます。

Pythonのimport文は、大きく分けると

from <module> import item
import <module>

の二通りあります。前者は from というトークンから始まりますが、「import」や「#include」以外のトークンからimport文が始まる言語は少ないのではないかと思います。ChatGPTに聞いたらModula-2の影響が挙げられたので先例がないわけではなさそうですが、現代に普及している言語の中では珍しい特徴ではないでしょうか。

「import文が『import』(または同等のトークン)以外のトークンから始まると不便なのでは?」というのが最初の論点です。理由付けになりそうな仮説はこの後述べます。

別の言語も見てみます。Haskellのimport文の例を見てみます:

import ModA
import ModB (itemB)
import qualified ModC
import ModD as ModX
import qualified ModE as ModY (itemE, itemX)

ここで注目したいのは「qualified」というトークンの位置です。Haskellではqualifiedを使うことで、モジュールの中身をトップレベルの名前空間に引き込むのではなく、モジュール名(またはas以下に指定した名前)に基づいた名前空間でアクセスするようにできます。名前の衝突の回避手段ということですね。

とにかく、通常のHaskellでは「qualified」トークンはモジュール名の前に来ます。Haskellを書いていると、この語順は正直あまり嬉しくありません。ソースコード中でモジュール名のカラム位置を揃えようとすると

import           ModA
import qualified ModC

という風に大量の空白を入れる必要があります。この語順が嬉しくないのは筆者だけではないようで、最近のGHC(主要なHaskell処理系)では、ImportQualifiedPostという拡張が導入されてqualifiedを後に置けるようになりました:

import ModA
import ModC qualified

ここからあえて教訓を引き出すとすれば、「importキーワードとimport対象の名前の間にはなるべく他のキーワードが挟まらない方が良い」となるでしょうか。

ところで、import文はファイルの先頭にまとめて書かれて置かれることが多く、また、順序が重要ではないことが多いです。そうすると、import文の並んだブロックをアルファベット順でソートしたくなることがあるのではないでしょうか。テキストエディターには大抵「選択した行をソートする」という機能があり、「最初にimportキーワードが来て、その直後にimport対象の名前が来る」語順だとテキストエディターの機能でソートできて便利そうです。

というわけで、「テキストエディターでソートしやすい」というのがimport文の語順の合理性としてありうるのではないかと思います。もちろん、フォーマッターやLanguage Server等の、言語を認識できる特化ツールでソートさせればimport文の語順は重要ではない、という見方もできますが、皆さんはどう思われるでしょうか。

頭でっかちは嫌われるのか

C言語では関数の返り値の型は関数名の前に書きます。昔のC++でも同様でしたが、C++では返り値の型が長くなることがあります。例えば、

template<typename T>
typename std::vector<T>::const_iterator foo(std::vector<T> const&);

という関数宣言があったら関数名は foo で、返り値の型はその前の長ったらしい typename std::vector<T>::const_iterator です。

関数の定義を読むときは、「関数の返り値の型」よりも「関数の名前」を先に読みたい、という人が多いのではないでしょうか。そうすると、返り値の型が長々と書かれていて、関数の名前がその後に来る書き方だと読みづらそうです。

この問題は、ユーザー側での対処も可能で、関数の名前の前で改行すれば良いです:

template<typename T>
typename std::vector<T>::const_iterator
foo(std::vector<T> const&);

あるいは、C++11以降では auto を使って

template<typename T>
auto foo(std::vector<T> const&) -> typename std::vector<T>::const_iterator;

と書けます。

ここから得られる教訓があるなら、「重要度が高い部分と低い部分があって、低い部分が長くなりがちな場合、低い部分が一行の中で前の方に来ると読みにくい」となるでしょうか。

もう一つ具体例を挙げます。Haskellでは、多相な関数の型制約を型の一部として書くことができます。2引数関数 a -> a -> a の型変数 aNum a という制約をつけたかったら、

foo :: Num a => a -> a -> a

と書きます。

この例なら良いのですが、Haskellで高度な型技法を駆使したプログラミングを行うと、制約の部分が長くなりがちです。例えば、

matMul :: (Num a, KnownNat l, KnownNat m, KnownNat n) => Matrix l m a -> Matrix m n a -> Matrix l n a

という宣言があったら、関数の名前は matMul で、引数と返り値の型は Matrix l m a -> Matrix m n a -> Matrix l n a で、間にある (Num a, KnownNat l, KnownNat m, KnownNat n) は型制約となります。こういう場合、型制約と引数を別の行に書くことがあります。

matMul :: (Num a, KnownNat l, KnownNat m, KnownNat n)
       => Matrix l m a -> Matrix m n a -> Matrix l n a

普通は型制約よりも引数と返り値の型の方が重要なので、「型制約は後に書くようにした方が良かったのでは?」と思うことがあります。もちろん、Haskellの語順にも理由はあって、型制約を引数と混ぜて書きたくなることがあり、可読性だけで全てを判断するわけにはいきませんが。

「型制約を後に書く方が良かった」という話に対するアンサーとして、後発の言語であるRustやSwiftではwhere節により型制約を後置にできるようになっています。

そんな感じで、Haskellの型制約も、「重要度が高い部分と低い部分があって〜低い部分が一行の中で前に来ると読みにくい」の例となっていると言えそうです。

先頭にキーワードがあった方が良いか

PerlやRubyなど、一部の言語ではif文を後置にできます。私はPerlやRubyを書かないので使用者の肌感はわからないのですが、普段他の言語を書く人の意見も含めると賛否が分かれる機能になるのではないかと思います。

Pythonのif式も、「真の場合の式」が先に来て、その後にifキーワード、そして条件が後に来ます。これも賛否が分かれるのではないかと思います。

そういうわけで、「条件文や条件式の場合、『if』に相当するキーワードが先に来た方が良いのでは」という仮説が立てられます。

言語は変わって、Standard MLを見てみます。Standard MLで例外を捕捉するにはhandle式というものを使います。handle式の語順は、<exp> handle <match> という形で、捕捉対象の式が先に来て、その後に handle キーワード、その後に例外発生時の処理、となります。「捕捉対象の式」の前に特別なキーワードは置かれないことに注意してください。他の言語だったら try のようなキーワードが置かれると思います。

Standard MLのユーザーは多くはないので「世間の評判」を捉えることは難しいのですが、個人的にはStandard MLのhandle式に前置キーワードがないのは失敗だったと思います。

というわけで、「if」や「try」など、構文木の種類を表すキーワードが先頭にあった方が読みやすいのではないか、という仮説が立ちます。

ただ、Lisp/SchemeなどのS式はこの原理を突き詰めた構文ですが、S式を採用する言語が人気すぎて困っている、という話は聞かないので、何事にも限度はありそうです。

unless文の是非

PerlやRubyなど、一部の言語にはif文に加えてunless文があります。unless文の是非も結構意見が分かれるのではないでしょうか。

英語との類似性で考えるとunless文があっても良いような気がしますが、この記事では「自然言語との類似性」を必ずしも善とはしないので、unless文の是非は他の論点で判断したいです。

unless文はif文とnot式の組み合わせと等価です。論点としては、「構文の数を絞ってユーザーが覚える規則を減らした方が良いか」「他の基本的な構文に還元できる構文(糖衣構文)をどこまで追加するべきか」となるでしょうか。

not式に還元できる構文として、多くの言語には「値が等しくないこと」を判定する演算子(C系の言語では !=)があります。unless文が非難されるなら、!= 演算子も非難されるべきでしょうか?それとも、!= は「普及しすぎている」から違和感を持たれづらいのでしょうか?

この件については筆者からは「こうすべき」という原理は提示できません。

テストにshouldと書けると便利か

RSpecやそれにインスパイアされたテストフレームワークでは、expectやshouldなどのキーワードで「自然言語っぽく」書けるようになっています。例えば、値の比較であれば

expect(actual).to eq(expected)

という感じです。HaskellのHSpecでは、

actual `shouldBe` expected

と書けます。

「自然言語との類似性」を抜きにした時、これらの書き方にメリットはあるでしょうか?

筆者の考えでは、値の比較に関しては、== などの演算子と比べると「自然言語っぽい書き方」は「どちらのオペランドがどの役割か」を明示できるので優れている、ということになります。

自然言語とプログラミング言語の違い

次に、自然言語とプログラミング言語の違いについて思いついたことを雑多に挙げていきます。

大きな構造

大昔のプログラムがどうだったか知りませんが、近代のプログラムは、「モジュール」「クラス」「関数」などの構造があります。自然言語にも、章や節、段落などの構造があります。

プログラムは「大まかな構造を把握して必要な部分だけ読む」という読み方ができると便利そうです。自然言語の文章でも大まかな流れを事前に把握できることは望ましい性質ですが、プログラムの場合は望まれる度合いが大きいのではないかと思います。

自然言語でも、数学の文書だと「定理」「補題」などの構造を扱い、証明の部分は読み飛ばすこともあるので、プログラムと似ているかもしれません。

記号の活用

プログラミング言語では記号に意味を持たせることが多いです。自然言語でも約物を使いますが、プログラミングでの記号の活用の度合いは圧倒的ではないでしょうか。

帰結として、プログラムはスマホのキーボード(記号を打ち込むのに複数回のタップが必要)で入力しづらかったり、音声入力に適さない、などの不利益があります。もちろん、プログラム向けのスマホ用キーボードを作ったり、音声入力法を編み出したりすることはできるかもしれません。

普通のキーボードでのプログラミングでも、記号を使いすぎるとプログラムが暗号的になるという問題があります。Haskellでは記号からなる演算子を独自に定義できますが、使いすぎると暗号的になります。「記号はググりにくい」という問題もあります。新しくプログラミング言語を作るときは、それがなるべく効果的になる場面で記号を使うようにした方が良いでしょう。

インデントの活用

プログラムはインデントで構造の深さを明示することが多いです。自然言語でも引用などをインデントで表現することはありますが、プログラムのようにインデントを何段も深くすることは少ないでしょう。

テキストエディターではタブキーなどで固定幅のインデントができることが多いです。プログラムを書く上では、固定幅のインデントで上手くいくようなコーディングスタイルを使えると良さそうです。

筆者が課題だと感じているのは、関数型言語(あるいは、式指向の言語)で込み入った式を書くときにインデントの幅が一文字単位になる場合があることです。例えば、

someFunction x = if x >= 0
                 then x
                 else 0

というコードで if / then / else の位置を揃えるには2行目以降を17文字の位置に置く必要があります。これは典型的なインデント幅(2文字、3文字、4文字)の倍数ではありません。もちろん、この場合は

someFunction x =
  if x >= 0
  then x
  else 0

とすれば好きなインデント幅にできます。

もちろん、言語に特化したリッチなIDEを使えばインデントが1文字単位になっても問題ないかもしれません。

規則性

自然言語は「規則に対する例外」が多く、例えば英語学習者は動詞の不規則変化などを必死になって覚える羽目になります。一方、プログラミング言語は人間が設計するものなので、なるべく規則的になっている方が学習コスト的に有利そうです。「俺のプログラミング言語は自然言語の真似をして、規則に対する例外を多めにしたんだ」なんていう人がいたら張り倒されるでしょう。

一つの内容を複数で表現すること

自然言語では、一つの表現の繰り返しを避けて、同じ内容を別の言い方で表すことがあります。パラフレーズとかいうやつです。日本語よりも英語とかの方が顕著だと思います。パラフレーズ文化圏的には、同じ内容に対する表現が多い方が豊かな言語だ、ということになるでしょう。

一方、プログラミング言語の場合は、一つの内容に対する書き方は一つあれば十分です。むしろ、一つの内容に対する書き方が決まりきっていた方が(定型的に読み書きできた方が)人間に優しいかもしれません。

もちろん、Perlのように「There’s more than one way to do it」を掲げるプログラミング言語もあります。

違いはどうして生まれるのか/さまざまな合理性

自然言語は原則的には人間が話し、聞き、書き、読むものです。これらの制約から、「自然言語としての合理性」がいろいろ議論できるでしょう。プログラミング言語と比較する場合は、自然言語は(手話じゃなければ)「話せる必要がある」という制約が効いてきそうです。

プログラミング言語はどうでしょうか。プログラミング言語の合理性はいろいろな側面から議論できそうです:

  • 人間が書くときの合理性
    • 文字数が少なく書けると嬉しい
  • 人間が読むときの合理性
    • 視認性が高いと嬉しい
  • LLMが読み書きするときの合理性
  • コンパイラー・インタープリターで処理するときの合理性
    • パースしやすいと嬉しい
  • シンプルなエディターで編集するときの合理性
    • 単純なソートでimport文がいい感じになると嬉しい
    • インデント幅が何らかの倍数で済むと嬉しい
  • IDE/Language Serverの合理性
    • 補完の開始に使える記号(オブジェクト指向言語のドット等)があると嬉しい
  • バージョン管理するときの合理性
    • diffがいい感じになると嬉しい

そんな感じで、プログラムを扱う主体の数だけ合理性が議論できそうです。そして、それらは相反することもあります。一つの合理性を突き詰めると、他の観点で不便になることもあります。

まとまりのない文章になりました。「結論」というほどのオチはありませんが、「プログラミング言語と自然言語は違うのだから、自然言語に似せた構文を無条件に称賛しないでほしい」くらいのことは言っても良いでしょう。

Spread the love