関数
関数は、Rustのコードにおいてよく見かける存在です。既に、言語において最も重要な関数のうちの一つを目撃していますね:
そう、main
関数です。これは、多くのプログラムのエントリーポイント(訳注
: プログラム実行時に最初に走る関数のこと)になります。
fn
キーワードもすでに見かけましたね。これによって新しい関数を宣言することができます。
Rustの関数と変数の命名規則は、スネークケース(訳注
: some_variableのような命名規則)を使うのが慣例です。
スネークケースとは、全文字を小文字にし、単語区切りにアンダースコアを使うことです。
以下のプログラムで、サンプルの関数定義をご覧ください:
ファイル名: src/main.rs
fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); // 別の関数 }
Rustにおいて関数定義は、fn
キーワードで始まり、関数名の後に丸かっこの組が続きます。
波かっこが、コンパイラに関数本体の開始と終了の位置を伝えます。
定義した関数は、名前に丸かっこの組を続けることで呼び出すことができます。
another_function
関数がプログラム内で定義されているので、main
関数内から呼び出すことができるわけです。
ソースコード中でanother_function
をmain
関数の後に定義していることに注目してください;
勿論、main関数の前に定義することもできます。コンパイラは、関数がどこで定義されているかは気にしません。
どこかで定義されていることのみ気にします。
functionsという名前の新しいバイナリ生成プロジェクトを始めて、関数についてさらに深く探究していきましょう。
another_function
の例をsrc/main.rsファイルに配置して、走らせてください。
以下のような出力が得られるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.28 secs
Running `target/debug/functions`
Hello, world!
Another function.
行出力は、main
関数内に書かれた順序で実行されています。最初に"Hello, world"メッセージが出て、
それからanother_function
が呼ばれて、こちらのメッセージが出力されています。
関数の引数
関数は、引数を持つようにも定義できます。引数とは、関数シグニチャの一部になる特別な変数のことです。
関数に引数があると、引数の位置に実際の値を与えることができます。技術的にはこの実際の値は
実引数と呼ばれますが、普段の会話では、仮引数("parameter")と実引数("argument")を関数定義の変数と関数呼び出し時に渡す実際の値、
両方の意味に区別なく使います(訳注
: 日本語では、特別区別する意図がない限り、どちらも単に引数と呼ぶことが多いでしょう)。
以下の書き直したanother_function
では、Rustの仮引数がどのようなものかを示しています:
ファイル名: src/main.rs
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {}", x); // xの値は{}です }
このプログラムを走らせてみてください; 以下のような出力が得られるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21 secs
Running `target/debug/functions`
The value of x is: 5
another_function
の宣言には、x
という名前の仮引数があります。x
の型は、
i32
と指定されています。値5
がanother_function
に渡されると、println!
マクロにより、
フォーマット文字列中の1組の波かっこがあった位置に値5
が出力されます。
関数シグニチャにおいて、各仮引数の型を宣言しなければなりません。これは、Rustの設計において、 意図的な判断です: 関数定義で型注釈が必要不可欠ということは、コンパイラがその意図するところを推し量るのに、 プログラマがコードの他の箇所で使用する必要がないということを意味します。
関数に複数の仮引数を持たせたいときは、仮引数定義をカンマで区切ってください。 こんな感じです:
ファイル名: src/main.rs
fn main() { another_function(5, 6); } fn another_function(x: i32, y: i32) { println!("The value of x is: {}", x); println!("The value of y is: {}", y); }
この例では、2引数の関数を生成しています。そして、引数はどちらもi32
型です。それからこの関数は、
仮引数の値を両方出力します。関数引数は、全てが同じ型である必要はありません。今回は、
偶然同じになっただけです。
このコードを走らせてみましょう。今、functionプロジェクトのsrc/main.rsファイルに記載されているプログラムを先ほどの例と置き換えて、
cargo run
で走らせてください:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/functions`
The value of x is: 5
The value of y is: 6
x
に対して値5
、y
に対して値6
を渡して関数を呼び出したので、この二つの文字列は、
この値で出力されました。
関数本体は、文と式を含む
関数本体は、文が並び、最後に式を置くか文を置くという形で形成されます。現在までには、 式で終わらない関数だけを見てきたわけですが、式が文の一部になっているものなら見かけましたね。Rustは、式指向言語なので、 これは理解しておくべき重要な差異になります。他の言語にこの差異はありませんので、文と式がなんなのかと、 その違いが関数本体にどんな影響を与えるかを見ていきましょう。
実のところ、もう文と式は使っています。文とは、なんらかの動作をして値を返さない命令です。 式は結果値に評価されます。ちょっと例を眺めてみましょう。
let
キーワードを使用して変数を生成し、値を代入することは文になります。
リスト3-1でlet y = 6;
は文です。
ファイル名: src/main.rs
fn main() { let y = 6; }
関数定義も文になります。つまり、先の例は全体としても文になるわけです。
文は値を返しません。故に、let
文を他の変数に代入することはできません。
以下のコードではそれを試みていますが、エラーになります:
ファイル名: src/main.rs
fn main() {
let x = (let y = 6);
}
このプログラムを実行すると、以下のようなエラーが出るでしょう:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
(エラー: 式を予期しましたが、文が見つかりました (`let`))
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: variable declaration using `let` is a statement
(注釈: `let`を使う変数宣言は、文です)
このlet y = 6
という文は値を返さないので、x
に束縛するものがないわけです。これは、
CやRubyなどの言語とは異なる動作です。CやRubyでは、代入は代入値を返します。これらの言語では、
x = y = 6
と書いて、x
もy
も値6になるようにできるのですが、Rustにおいては、
そうは問屋が卸さないわけです。
式は何かに評価され、これからあなたが書くRustコードの多くを構成します。
簡単な数学演算(5 + 6
など)を思い浮かべましょう。この例は、値11
に評価される式です。式は文の一部になりえます:
リスト3-1において、let y = 6
という文の6
は値6
に評価される式です。関数呼び出しも式です。マクロ呼び出しも式です。
新しいスコープを作る際に使用するブロック({}
)も式です:
ファイル名: src/main.rs
fn main() { let x = 5; let y = { let x = 3; x + 1 }; println!("The value of y is: {}", y); }
以下の式:
{
let x = 3;
x + 1
}
は今回の場合、4
に評価されるブロックです。その値が、let
文の一部としてy
に束縛されます。
今まで見かけてきた行と異なり、文末にセミコロンがついていないx + 1
の行に気をつけてください。
式は終端にセミコロンを含みません。式の終端にセミコロンを付けたら、文に変えてしまいます。そして、文は値を返しません。
次に関数の戻り値や式を見ていく際にこのことを肝に銘じておいてください。
戻り値のある関数
関数は、それを呼び出したコードに値を返すことができます。戻り値に名前を付けはしませんが、
矢印(->
)の後に型を書いて確かに宣言します。Rustでは、関数の戻り値は、関数本体ブロックの最後の式の値と同義です。
return
キーワードで関数から早期リターンし、値を指定することもできますが、多くの関数は最後の式を暗黙的に返します。
こちらが、値を返す関数の例です:
ファイル名: src/main.rs
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {}", x); }
five
関数内には、関数呼び出しもマクロ呼び出しも、let
文でさえ存在しません。数字の5が単独であるだけです。
これは、Rustにおいて、完璧に問題ない関数です。関数の戻り値型が-> i32
と指定されていることにも注目してください。
このコードを実行してみましょう; 出力はこんな感じになるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
Running `target/debug/functions`
The value of x is: 5
five
内の5
が関数の戻り値です。だから、戻り値型がi32
なのです。これについてもっと深く考察しましょう。
重要な箇所は2つあります: まず、let x = five()
という行は、関数の戻り値を使って変数を初期化していることを示しています。
関数five
は5
を返すので、この行は以下のように書くのと同義です:
# #![allow(unused_variables)] #fn main() { let x = 5; #}
2番目に、five
関数は仮引数をもたず、戻り値型を定義していますが、関数本体はセミコロンなしの5
単独です。
なぜなら、これが返したい値になる式だからです。
もう一つ別の例を見ましょう:
ファイル名: src/main.rs
fn main() { let x = plus_one(5); println!("The value of x is: {}", x); } fn plus_one(x: i32) -> i32 { x + 1 }
このコードを走らせると、The value of x is: 6
と出力されるでしょう。しかし、
x + 1
を含む行の終端にセミコロンを付けて、式から文に変えたら、エラーになるでしょう:
ファイル名: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
このコードを実行すると、以下のようにエラーが出ます:
error[E0308]: mismatched types
(型が合いません)
--> src/main.rs:7:28
|
7 | fn plus_one(x: i32) -> i32 {
| ____________________________^
8 | | x + 1;
| | - help: consider removing this semicolon
9 | | }
| |_^ expected i32, found ()
| (i32を予期したのに、()型が見つかりました)
|
= note: expected type `i32`
found type `()`
メインのエラーメッセージである「型が合いません」でこのコードの根本的な問題が明らかになるでしょう。
関数plus_one
の定義では、i32
型を返すと言っているのに、文は値に評価されないからです。このことは、
()
、つまり空のタプルとして表現されています。それゆえに、何も戻り値がなく、これが関数定義と矛盾するので、
結果としてエラーになるわけです。この出力内で、コンパイラは問題を修正する手助けになりそうなメッセージも出していますね:
セミコロンを削除するよう提言しています。そして、そうすれば、エラーは直るわけです。