Goの宣言構文

Go’s Declaration Syntax by Rob Pike

はじめに

Goを新しく使い始めた人は、なぜGoの宣言構文がC系言語で確立されてきた伝統と違うのか疑問に思うことでしょう。 この記事では両方のアプローチを比較し、なぜGoの宣言がこのような見た目になっているのかを説明します。

C言語の構文

まずはC言語の構文について話しましょう。Cは宣言構文に対して、風変わりでいて巧妙なアプローチを取りました。 型を専用の構文で記述するのではなく、宣言する対象を含んだ式を書き、その式がどの型になるかを記述するのです。 つまり

int x;

はxをintとして宣言します。式「x」はint型を持つことになる、というわけです。一般に、新しい変数の型を どう書けばよいかを知るには、その変数を含み基本型に評価される式を書き、その基本型を左側に、式を右側に 置きます。

つまり、次の宣言

int *p;
int a[3];

は、pがintへのポインタであることを表しています。なぜなら*pはint型を持つからです。そしてaがintの配列で あることも表しています。なぜならa[3](この添字の具体的な値は無視してよく、これは配列のサイズを表すのに 流用されています)がint型を持つからです。

関数についてはどうでしょうか。もともとC言語の関数宣言では、引数の型を括弧の外に書いていました。 次のようにです。

int main(argc, argv)
    int argc;
    char *argv[];
{ /* ... */ }

ここでもやはり、式main(argc, argv)がintを返すことから、mainが関数であることがわかります。現代的な 記法で書けば

int main(int argc, char *argv[]) { /* ... */ }

となりますが、基本的な構造は同じです。

これは単純な型に対してはうまく機能する巧妙な構文上のアイデアですが、すぐにわかりにくくなってしまいます。 有名な例が関数ポインタの宣言です。ルールに従うと、次のようになります。

int (*fp)(int a, int b);

ここでfpは関数へのポインタです。なぜなら式(*fp)(a, b)と書けば、intを返す関数を呼び出すことになるから です。では、fpの引数の一つがそれ自身関数だったらどうなるでしょうか。

int (*fp)(int (*ff)(int x, int y), int b)

これはもう読みづらくなってきています。

もちろん、関数を宣言するときにパラメータの名前を省略することもできるので、mainは次のように宣言できます。

int main(int, char *[])

argvが次のように宣言されていたことを思い出してください。

char *argv[]

つまり、宣言の途中から名前を取り除くことでその型を構成しているわけです。しかし、char *[]という型の ものを宣言するのに、その名前を途中に置くのだということは、決して自明ではありません。

そして、パラメータに名前を付けなかった場合、fpの宣言がどうなるか見てみましょう。

int (*fp)(int (*)(int, int), int)

次の中のどこに名前を置けばよいのか自明でないだけでなく、

int (*)(int, int)

そもそもこれが関数ポインタの宣言であることすら、はっきりとはわかりません。では戻り値の型が関数ポインタ だったらどうなるでしょうか。

int (*(*fp)(int (*)(int, int), int))(int, int)

この宣言がfpについてのものだということすら見て取るのが困難です。

もっと手の込んだ例も作れますが、これだけでもC言語の宣言構文がもたらしうる難点のいくつかは示せている でしょう。

しかし、もう一点触れておくべきことがあります。型の構文と宣言の構文が同じであるため、式の途中に型が 現れるとパースが難しくなることがあるのです。たとえばCのキャストが常に型を括弧で囲むのは、これが理由 です。

(int)M_PI

Goの構文

C系以外の言語では、宣言において型のための別個の構文を使うのが普通です。これとは別の話ですが、名前が 先に来て、そのあとにコロンが続くことが多いです。したがって、先ほどの例は(架空の、しかし説明のための 言語で書けば)次のようになります。

x: int
p: pointer to int
a: array[3] of int

これらの宣言は、多少冗長ではありますが明快です。左から右へと読んでいくだけで済みます。Goはここから ヒントを得ていますが、簡潔さのためにコロンを取り除き、キーワードのいくつかも削っています。

x int
p *int
a [3]int

[3]intという見た目と、式の中でaをどう使うかの間には直接の対応関係はありません。(ポインタについては 次の節で改めて触れます。)別個の構文を採用する代わりに、明快さを手に入れているのです。

次に関数について考えてみましょう。Goでの記法に沿ってmainの宣言を書き写してみます。もっとも、実際の Goのmain関数は引数を取りませんが。

func main(argc int, argv []string) int

表面的にはchar配列がstringに変わったこと以外、Cとそう変わりません。しかし左から右へとよく読めます。

関数mainはintとstringのスライスを引数に取り、intを返します。

パラメータの名前を落としても、同じように明快です。名前は常に最初に来るので、混乱の余地がありません。

func main(int, []string) int

この左から右へ読むスタイルの利点の一つは、型が複雑になっていってもうまく機能し続けることです。次は 関数型の変数の宣言です(Cにおける関数ポインタに相当します)。

f func(func(int,int) int, int) int

あるいはfが関数を返す場合は、

f func(func(int,int) int, int) func(int, int) int

これでも左から右へと明快に読めますし、どの名前が宣言されているのかは常に明らかです。名前が先に来る からです。

型の構文と式の構文が区別されているおかげで、Goではクロージャを書いてその場で呼び出すのも簡単です。

sum := func(a, b int) int { return a+b } (3, 4)

ポインタ

ポインタはこの規則の例外であり、むしろそれによって規則の正しさが際立ちます。たとえば配列やスライスでは、 Goの型構文は角括弧を型の左側に置きますが、式の構文では角括弧は式の右側に置かれることに注目してください。

var a []int
x = a[1]

親しみやすさのため、GoのポインタはC由来の*という表記を使っていますが、ポインタ型についても同様に 前後を入れ替えるところまでは踏み切れませんでした。そのため、ポインタは次のように動作します。

var p *int
x = *p

次のようには書けませんでした。

var p *int
x = p*

なぜなら、この後置の*は乗算と混同されてしまうからです。たとえばPascalの^を使うこともできたでしょう。

var p ^int
x = p^

そして、おそらくそうすべきだったのでしょう(xor用には別の演算子を選んで)。というのも、型と式の両方に 前置のアスタリスクを使うことは、いろいろな形で物事をややこしくするからです。たとえば、次のように変換 として書くことはできますが、

[]int("hi")

型が*で始まる場合は、その型を括弧で囲む必要があります。

(*int)(nil)

もしポインタの構文として*を使うことをあきらめる覚悟があったなら、これらの括弧は不要だったはずです。

このように、Goのポインタ構文は馴染み深いCの形式に結びついていますが、その結びつきのせいで、文法上 型と式を区別するための括弧の使用から完全に脱却することはできていません。

とはいえ全体としては、Goの型構文はCのものよりも理解しやすいと私たちは考えています。特に物事が複雑に なってきたときにはなおさらです。

補足

Goの宣言は左から右へと読めます。一方でCの宣言は螺旋を描くように読むのだ、と指摘されています。 David Andersonによる「時計回り・螺旋の法則」を参照してください。

By Rob Pike