演算子とステートメント

(この記事は Qiita に投稿した「Go 言語の ++-- は演算子ではない - Qiita」を大幅に修正して再構成したものです)

あるプログラミング言語を習得する際に最も早道なのは「たくさんの(他人の)コードを読むこと」であり「たくさんのコードを(コピペではなく自分で)書く」ことである。 これは間違いない。 しかし,その言語の仕様をきちんと把握してないとコードを読んでも間違って理解するかもしれないし,何より実際に自分でコードを書く際に躓く原因になる。

というわけで,少なくとも学ぶ言語の言語仕様を一度は眺めておくことをお勧めする。 Go 言語の場合は以下のページで言語仕様を見ることができる(“A Tour of Go” の後で読むと頭に入りやすいかもしれない)。

今回は「つまみ食い」的に演算子(operator)とステートメント(statement)1 について軽く紹介してみる。

ステートメント

Go 言語においては「ステートメント」は以下のように定義されている。

Statement =
    Declaration | LabeledStmt | SimpleStmt |
    GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
    FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
    DeferStmt .

SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .

まぁ名前で何か大体わかると思う。 ここでは SimpleStmt (simple statement) に絞って紹介しよう。

Empty Statements

EmptyStmt = .

文字通り空のステートメント。

Expression Statements

ExpressionStmt = Expression .

式(expression)を表すステートメント。 関数呼び出しや受信操作のコンテキスト内に記述できる。

さらに式は以下のように定義される。

Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .

binary_op  = "||" | "&&" | rel_op | add_op | mul_op .
rel_op     = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op     = "+" | "-" | "|" | "^" .
mul_op     = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .

unary_op   = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

PrimaryExpr (primary expression) については割愛する。詳細は「言語仕様」で確かめてみてください。ここでは Expression を構成する要素にはステートメントが含まれないことに注目)

binary_op, rel_op, add_op, mul_op, unary_op は演算子である。 演算子については後述する。

Send Statements

SendStmt = Channel "<-" Expression .
Channel  = Expression .

channel 送信のステートメント。

IncDec Statements

IncDecStmt = Expression ( "++" | "--" ) .

インクリメント(increment)およびデクリメント(decrement)のステートメント。 C/C++ のように ++x みたいな記述はできないので注意。

ちなみに IncDecStmt は次の代入ステートメントの以下の記述と同じである。

IncDec statement Assignment
x++ x += 1
x-- x -= 1

Assignments

Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .

代入。 add_op, mul_op は先ほど出た Expression の演算子を指す。

add_op     = "+" | "-" | "|" | "^" .
mul_op     = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .

定義だと assign_op は演算子っぽく見える。 そもそも代入を “assignment operation” と表記しているのだ。 どうなんだろう。 まぁ,いずれにしろ代入自体は間違いなくステートメントであり式の中には含められない。

ちなみに ExpressionListExpression を列挙したものである。

ExpressionList = Expression { "," Expression } .

これにより代入の左辺・右辺を組(tuple)で記述できる。 たとえば2つの変数の値を入れ替える場合は以下のように記述する。

x, y = y, x

Short Variable Declarations

ShortVarDecl = IdentifierList ":=" ExpressionList .

変数宣言の短縮表現。 var キーワードを使った以下の表現と同じ。

"var" IdentifierList = ExpressionList .

IdentifierListidentifier を列挙したもので

IdentifierList = identifier { "," identifier } .

これにより identifier で記述される複数の変数をまとめて宣言・初期化できる。 identifier の定義は以下の通り

identifier = letter { letter | unicode_digit } .

ちなみに変数名となる identifier は全ての Unicode 文字を許容する。 なので日本語交じりでこんな書き方もできる。

package main

import "fmt"

func main() {
    わーい := "わーい! たのしー!"
    fmt.Println(わーい)
}

演算子

さて,式と演算子の定義を再び掲げる。

Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .

binary_op  = "||" | "&&" | rel_op | add_op | mul_op .
rel_op     = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op     = "+" | "-" | "|" | "^" .
mul_op     = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .

unary_op   = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

Go 言語で式に使える演算子はここに挙げられているものが全てである。 このうち二項演算子(binary_op)には優先順位が付けられている。

Precedence Operator
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||

なお単項演算子(unary_op)は二項演算子よりも高い優先順位で機能する。 したがって全体としてはこんな感じだろうか。

Precedence Operator
6 unary_op
5 mul_op
4 add_op
3 rel_op
2 &&
1 ||

インクリメント/デクリメントは演算子ではない

たとえば C 言語の演算子と比較すると Go 言語ではインクリメント(++)/デクリメント(--)が演算子として扱われていないことに気付く2Go 言語ではインクリメント/デクリメント(および代入)はステートメントである。

これはどういうことかというと,たとえば C 言語のコードに似せて

package main

import "fmt"

func main() {
    i := 1
    fmt.Println(i++)
}

と書いてコンパイルしようとしても

syntax error: unexpected ++, expecting comma or )

とコンパイルエラーになるということである(式を構成する要素にステートメントは含まれないことを思い出してほしい)。 これはコードを,以下のように,代入に置き換えたほうが直感的で分かりやすいかもしれない。 (この場合も「syntax error: unexpected +=, expecting comma or )」でコンパイルエラーになる)

package main

import "fmt"

func main() {
    i := 1
    fmt.Println(i+=1)
}

私見で申し訳ないが,私は「式中の演算子は変数の状態を変えるべきではない」と考えている。 たとえば C/C++ では ++, -- 演算子を前置にすべきか後置にすべきかというのでよく議論になる3。 しかし,これはそもそも ++, -- 演算子が式の中で対象の変数の状態を変えてしまうことに問題があるのだ。

Go 言語ではインクリメントやデクリメント(あるいは代入)といった変数の状態を変える操作をステートメントとし,式の中に埋め込むことを禁止することでこの問題を回避しているように見える。 式の中で変数の状態が変わらないのであれば副作用を気にすることなく安全にコードを書くことができる。

ただし例外がある。

channel 操作

channel 操作では,送信はステートメントだが受信は <- 単項演算子を使う。 したがって,こんな記述もできる(意味があるかどうかはともかく)。

ch2 <- <-ch1

channel 受信を含んだ式では channel 変数の状態が変わる副作用(特に deadlock 関連)に注意を払う必要がある。

とまぁ,こんな感じで

手を動かしながら「言語仕様」を眺めていくと,いろいろ発見があって楽しいと思う。

ではまた。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. 言語仕様における “statement” の適切な日本語訳が思いつかなかったので,今回はカタカナにのばして「ステートメント」と表記する。教えて,英語得手の人。 ↩︎

  2. 言語仕様」では文章上の表現として operator と記述しているところが幾つかあるが定義としては演算子として扱われていない。 ↩︎

  3. C/C++ で ++, -- 演算子を前置にするか後置にするかという問題は挙動の分かりにくさと実行時パフォーマンスの2つの論点がある。いずれにしろ前置に統一する方がよいと言われているが,パフォーマンスに関しては異論もあるようだ。 ↩︎