はじめに
The Go BlogのGo's Declaration Syntaxを読もうと思ってついでにゆるーくふぃーりんぐで訳したので、記念にアップしておきます。
Cについては全然知らないです。
まじで適当です。間違いもたくさんあると思います。
結局、goの宣言の仕方はCに比べて読みやすいよ!ってことでした。
Go's Declaration Syntax
Introduction
go初心者は伝統的に構築されたC言語系と宣言の構文が違うことを不思議に思うだろう。この投稿では2つのアプローチを比較しなぜgoがこのような宣言を行うのか説明する。
C syntax
まず、Cの構文を見てみよう。Cは宣言構文に対して珍しい利口なアプローチを取る。特別な構文で型を記述する代わりに、宣言されたアイテムを含む式を記載し、式が持つ型を明示する。
int x;
のようにxをintとして宣言する(=式'x'はint型を持つ)。一般的に、新しい変数の型の書き方を理解するのに、基本の型を評価する変数を含む式を書き、それから基本の型を左、式を右に置く。
下記のように、'*p'がint型であるためpはintのポインタであり、a[3]がint型であるためaはintの配列であることを明示する。
int *p;
int a[3];
関数についてはどうだろう?元々、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)(a, b)を書く場合intを返却する関数を呼ぶため、fpは関数のポインタである。fpの引数の一つそれ自体が関数ならどうなるだろうか?
int (*fp)(int (*ff)(int x, int y), int b)
読むのが大変になってきた。
もちろん、関数宣言でパラメータ名を省くことはできるので、以下のようにできる。
int main(int, char *[])
argvの再起呼び出しはchar *argv[]
なので、型を構築するために宣言の中の名前を除外する。これは明らかではないが、char *[]の型の何かを、その名前を間に挟んで宣言する。 (ここなに言ってんのかよく分かんないです。dont think, feel...)
そしてパラメータに命名しない場合の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 syntax
C系以外の言語は宣言において明確な型構文を使用する。これは分かれるポイントだが、名前は普通はじめにきて、だいたいコロンがくっつく。以下が例だ(架空言語だけど分かりやすく書いたよ)
x: int
p: pointer to int
a: array[3] of int
これらの宣言はクリアだ、左から右に読めばいいだけだ。goはここからきっかけを掴んだが、簡潔さへの関心からコロンを省きいくつかのキーワードを削除した。
x int
p *int
a [3]int
[3]intとaの使い方に直接の関連はない。分裂した構文という犠牲を払って明確さを手に入れた。
さて関数に関して考えよう。mainの宣言をgoに書き直そう。(実際のgoのmain関数は引数を持たないけど)
func main(argc int, argv []string) int
一見charが配列から文字列に変わっただけで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)
Pointers
ポインタはルールを証明する例外だ。(?)
配列とスライスを例にすると、goの型構文は型の左に括弧を付けるが式構文では右に付けることに気付いてほしい。
var a []int
x = a[1]
なじみ深いものとして、goのポインタは*を使用するが、ポインタ型の反対に似たものにさせることはできない。
ポインタは以下のように動く。
var p *int
x = *p
以下ようには書けない。
var p *int
x = p*
なぜなら、接尾辞 * (アスタリスク) は掛け算と競合するからだ。
型と式の両方で*を使うことは複雑になるので、以下の例のようにパスカル^を使用することはできたし、そうするべきだった(そしてxorには他のオペレータが選ばれただろう)
var p ^int
x = p^
例えば[]int("hi")
と変換を書くことができるし、*から始まるなら型を挿入句に入れなければならない。
(*int)(nil)
*をポインタ構文として使うのは喜んでやめて、それらの括弧は不必要となった。
なのでgoのポインタ構文は有名なCの形式に近い、だが、それらの近さは文法において型と式の曖昧さをなくす括弧の使用を完璧にやめることができないということを意味している。
goの型構文が(特に複雑になってきたときに)Cよりも分かりやすいことを祈っている。