関数
関数は、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.28s
Running `target/debug/functions`
Hello, world!
Another function.
行出力は、main関数内に書かれた順序で実行されています。最初に"Hello, world"メッセージが出て、
それからanother_functionが呼ばれて、こちらのメッセージが出力されています。
引数
関数は、仮引数 (parameter) を持つよう定義することもできます。仮引数とは、関数シグニチャの一部になる特別な変数のことです。
関数に仮引数があると、仮引数に対して具体的な値を与えることができます。
厳密にはこの具体的な値は実引数 (argument) と呼ばれますが、普段の会話では、関数定義内の変数と関数呼び出し時に渡す実際の値の両方の意味で、
parameterとargumentを区別なく使う傾向にあります (訳注: 日本語では、特別区別する意図がない限り、どちらも単に引数と呼ぶことが多いでしょう)。
次の版のanother_functionでは、仮引数を追加しています:
ファイル名: src/main.rs
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {x}"); // xの値は{x}です }
このプログラムを走らせてみてください; 以下のような出力が得られるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
another_functionの宣言には、xという名前の仮引数があります。xの型は、
i32と指定されています。値5をanother_functionに渡すと、println!マクロにより、
フォーマット文字列中のxを含む1組の波かっこがあった位置に値5が出力されます。
関数シグニチャにおいて、各仮引数の型を宣言しなければなりません。これは、Rustの設計において、 意図的な判断です: 関数定義で型注釈が必要不可欠ということは、コンパイラがその意図する型を推し量るのに、 プログラマがコードの他の箇所で使用する必要がないということを意味します。 コンパイラも、関数が期待する型を知っていれば、より役に立つエラーメッセージを与えることができます。
関数に複数の仮引数を定義したいときは、仮引数定義をカンマで区切ってください。 こんな感じです:
ファイル名: src/main.rs
fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }
この例では、print_labeled_measurementという名前の2引数の関数を生成しています。
第1引数はvalueという名前でi32です。第2引数はunit_labelという名前でchar型です。
この関数はvalueとunit_labelの両方を含むテキストを出力します。
このコードを走らせてみましょう。今、functionプロジェクトのsrc/main.rsファイルに記載されているプログラムを先ほどの例と置き換えて、
cargo runで走らせてください:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
valueに対して値5、unit_labelに対して値'h'を渡して関数を呼び出したので、
プログラムの出力にはこれらの値が含まれます。
文と式
関数本体は、文が並び、最後に式を置くか文を置くという形で形成されます。今のところ、 私たちが見てきた関数は式で終わることはありませんでしたが、式が文の一部になっているものなら見かけましたね。Rustは、式指向言語なので、 これは理解しておくべき重要な差異になります。他の言語にこの差異はありませんので、文と式がなんなのかと、 その違いが関数本体にどんな影響を与えるかを見ていきましょう。
- 文 (statement) はなんらかの動作をして値を返さない命令です。
- 式 (expression) は結果値に評価されます。ちょっと例を眺めてみましょう。
実のところ、もう文と式は使っています。
letキーワードを使用して変数を生成し、値を代入することは文になります。
リスト3-1でlet y = 6;は文です。
ファイル名: src/main.rs
fn main() { let y = 6; }
リスト3-1: 1文を含むmain関数宣言
関数定義も文になります。つまり、先の例は全体としても文になるわけです。
文は値を返しません。故に、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 `let` statement
(エラー: 式を予期しましたが、`let`文が見つかりました)
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
(注釈: `if` および `while` 式の条件部直下でのみ対応しています
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
この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 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.30s
Running `target/debug/functions`
The value of x is: 5
five内の5が関数の戻り値です。だから、戻り値型がi32なのです。これについてもっと深く考察しましょう。
重要な箇所は2つあります: まず、let x = five()という行は、関数の戻り値を使って変数を初期化していることを示しています。
関数fiveは5を返すので、この行は以下のように書くのと同義です:
#![allow(unused)] 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;
}
このコードをコンパイルすると、以下のようにエラーが出ます:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
(型が合いません)
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
メインのエラーメッセージであるmismatched types (型が合いません)でこのコードの根本的な問題が明らかになるでしょう。
関数plus_oneの定義では、i32型を返すと言っているのに、文は値に評価されないからです。このことは、
()、つまりユニット型として表現されています。それゆえに、何も戻り値がなく、これが関数定義と矛盾するので、
結果としてエラーになるわけです。この出力内で、コンパイラは問題を修正する手助けになりそうなメッセージも出していますね:
セミコロンを削除するよう提言しています。そして、そうすれば、エラーは直るわけです。