なんかアドベントカレンダーって前後の人の紹介すべきなんですか?前日は@Piffett、翌日は@Piffettです。
前回の復習
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
match option {
None => None,
Some(value) => Some(f(value)),
}
}
前回はジェネリックパラメーターのFとAに関しては、「FnOnce(T) -> A」という制約がついているという話でした。この制約について理解するために、クロージャについて説明していきます。
クロージャの構文
さて、前述した「FnOnce(T) -> A」という意味不明なものを理解するためにここからクロージャについて解説していきます。外から来た自由変数を関数内で閉じ込める物をクロージャと言います。ラムダ式とも言われたりします。以下の例を見てください。
let plus_one = |x: i32| x + 1;
assert_eq!(2, plus_one(1));
クロージャはパイプ||に囲まれた引数と、式からなります。関数と似ています。なので、複数行のクロージャも作れます。
let plus_two = |x| {
let mut result: i32 = x;
result += 1;
result += 1;
result
};
assert_eq!(4, plus_two(2));
クロージャでは引数や返り値の型を示す必要がありません。もちろん以下のように型を示すこともできます。
fn plus_one_v1 (x: i32) -> i32 { x + 1 }
let plus_one_v2 = |x: i32| -> i32 { x + 1 };
let plus_one_v3 = |x: i32| x + 1 ;
クロージャは匿名で、名前付き関数が深くネストして定義から離れたところで発生するようなエラーを起こさないので、型の省略が許されています。
もちろん以下のような型が異なる型で推論されるクロージャはコンパイルエラーします。
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5)
クロージャと環境
次のクロージャを見てください
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
重い処理をクロージャにしています。今回は2秒待機処理を入れて、受け取った値をそのまま返り値にしています。
それを基に以下のコードを書きました。
use std::thread;
use std::time::Duration;
fn heavy_task(num1: u32, num2: u32) {
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if num1 == 3 {
println!("No Task");
}else{
println!("Task {} Done", expensive_closure(num2));
println!("Next Task {} Done", expensive_closure(num2));
}
}
fn main() {
let num1 = 10;
let num2 = 5;
heavy_task(num1, num2);
}
関数heavy_taskには二つの引数をわたし、num1でif文を通り、num2で結果をタスクとして終了させています。ここでは重いクロージャを二回読んでいます。この問題を解決するために、重いクロージャの結果を変数にい入れておくことが考えられます。
今回は、クロージャにクロージャの呼び出し結果の値を保存させてることを考えましょう。ジェネリック引数とFnトレイトを使ってクロージャを保存します。
まず、クロージャとオプションの結果値を保持する構造体の定義を示します。
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation: T,
value: Option<u32>,
}
前回出てきたwhere句が出てきました。where句はトレイト境界を定めるもので、TがFn(u32) -> u32のトレイトであることを示します。このTがcalculationフィールドに対応します。そしてvalueフィールドはOptionなので、クロージャ実行前のvalueはNoneになりm実行されるとOptionのSomeに保存されます。そして、二度目以降のクロージャの実行ではSomeに保持された結果を返します。ということをロジックに起こすと、以下の通りになります
impl<T> Cacher<T>
where T: Fn(u32) -> u32
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
},
}
}
}
さて、この構造体を用いて先ほどのソースコードを書き直すと、
// ~省略~
fn heavy_task(num1: u32, num2: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
if num1 == 3 {
println!("No Task");
}else{
println!("Task {} Done", expensive_result.value(num2));
println!("Next Task {} Done", expensive_result.value(num2));
}
}
fn main() {
let num1 = 10;
let num2 = 5;
heavy_task(num1, num2);
}
こうすることで、重い計算を再計算する必要がなくなりました。ここで、例のソースコードに戻ります。
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
match option {
None => None,
Some(value) => Some(f(value)),
}
}
すべてのクロージャはFnOnceを実装しており、この例ではFnOnce(T) -> Aのトレイト境界を定めていることがわかります。
そして、この関数は、Option型とクロージャを引数にとり、その返り値の型がOptionのSome(A)のAの型になります。
で。クロージャのFn,FnOnce,FnMutの話ができてないから続編が不可避な気がする(ひとつの例とは)