【Rust】高階関数を作るときのメモ その2

前回、簡単な高階関数hogeを書きましたが、もう少しだけ分かったことがあるのでまたメモとして残しておきます。

kudohamu.hatenablog.com


今回は高階関数というよりはRustのクロージャについてです。

※ バーション1.3.0です。Rustは結構頻繁に破壊的な変更が起きるようなので将来ここに書いてある内容は通らない可能性あり。


前回できたもの

前回は以下のように「"ほげー!"という文字列を返す関数」を返す関数hogeを作成しました。

fn main() {
    println!("{}", hoge()());
}

fn hoge() -> Box<Fn() -> String> {
    Box::new(|| -> String {
        String::from("ほげー!")
    })
}

ただ、高階関数を作る時は大抵返り値となる関数の中でゴニョゴニョしたいものです。

返り値の関数に引数を付ける

ということで、hogeを改変して「"ほげー"という文字に引数の文字列をsuffixとして付けた文字を返す関数」を返す関数hogeを作ってみます。

fn main() {
    println!("{}", hoge()("?"));
}

fn hoge() -> Box<Fn(&str) -> String> {
    Box::new(|suffix: &str| -> String {
        format!("ほげー{}", suffix)
    })
}

結果

ほげー?

想定通りの動きをしました。

高階関数hoge)に引数を付ける

ではhogeにも引数prefixを作り、"{prefix}、ほげー{suffix}"となるような関数を返す関数にしてみます。

fn main() {
    println!("{}", hoge(String::from("ほ"))("?"));
}

fn hoge(prefix: String) -> Box<Fn(&str) -> String> {
    Box::new(|suffix: &str| -> String {
        format!("{}、ほげー{}", prefix, suffix)
    })
}

普段通り書いてみましたが、以下のように怒られました。

prog.rs:6:14: 8:6 error: closure may outlive the current function, but it borrows `prefix`, which is owned by the current function [E0373]
prog.rs:6     Box::new(|suffix: &str| -> String {
prog.rs:7         format!("{}、ほげー{}", prefix, suffix)
prog.rs:8     })
prog.rs:7:29: 7:35 note: `prefix` is borrowed here
prog.rs:7         format!("{}、ほげー{}", prefix, suffix)
                                          ^~~~~~
note: in expansion of format_args!
<std macros>:2:26: 2:57 note: expansion site
<std macros>:1:1: 2:61 note: in expansion of format!
prog.rs:7:9: 7:44 note: expansion site
prog.rs:6:14: 8:6 help: to force the closure to take ownership of `prefix` (and any other referenced variables), use the `move` keyword, as shown:
prog.rs:      Box::new(move |suffix: &str| -> String {
prog.rs:          format!("{}、ほげー{}", prefix, suffix)
prog.rs:      })
error: aborting due to previous error

大事そうなメッセージはこの2つ

closure may outlive the current function, but it borrows `prefix`, which is owned by the current function

prog.rs:6:14: 8:6 help: to force the closure to take ownership of `prefix` (and any other referenced variables), use the `move` keyword, as shown:
prog.rs:      Box::new(move |suffix: &str| -> String {
prog.rs:          format!("{}、ほげー{}", prefix, suffix)
prog.rs:      })

クロージャはcurrent function(hoge)の外でも生きるけど、prefixの所有権はcurrent functionにあるよというのと
move キーワードを付ければ?というもの。

move キーワードを付ける

メッセージ通りつけてみます。

fn main() {
    println!("{}", hoge(String::from("ほ"))("?"));
}

fn hoge(prefix: String) -> Box<Fn(&str) -> String> {
    Box::new(move |suffix: &str| -> String {
        format!("{}、ほげー{}", prefix, suffix)
    })
}

結果

ほ、ほげー?

解決した。

引数として文字列を渡しやすいようにStringを&strにリファクタしておく

fn main() {
    println!("{}", hoge("ほ")("?"));
}

fn hoge(prefix: &str) -> Box<Fn(&str) -> String> {
    let string_prefix = prefix.to_string();
    Box::new(move |suffix: &str| -> String {
        format!("{}、ほげー{}", string_prefix, suffix)
    })
}

prefixをto_stringするのがあれだけど呼ぶ側が綺麗になるので良しとしておく。

moveキーワードってなによ

Rustのクロージャはエンクロージャの環境(の変数)を借用するらしい。

少し見てみます。

fn main() {
    let mut out_var = String::from("あー");
    
    let hoge = || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", hoge()); // あー、ほげー
    println!("{}", out_var); // あー

    // mutableである必要がないという警告が出ますが、これ以降のためにmutableしておきます
}
fn main() {
    let mut out_var = String::from("あー");
    
    let hoge = || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", hoge()); // あー、ほげー
    out_var.push_str("いー"); // エラー: out_varはまだhogeが借りている状態なので変更できない
    println!("{}", out_var);
}
fn main() {
    let mut out_var = String::from("あー");
    
    {
        let hoge = || -> String {
            format!("{}、ほげー", out_var)
        };
        println!("{}", hoge()); // あー、ほげー
    } // ここでhogeのlifetimeが終わるのでout_varの所有権が返される
    
    out_var.push_str("いー");
    println!("{}", out_var); // あーいー
}

確かに、Rustのクロージャはエンクロージャの環境(の変数)を借用しています。

振り返る

これを踏まえるとさっきのエラーメッセージ

closure may outlive the current function, but it borrows `prefix`, which is owned by the current function

の意味が分かってきます。
moveキーワードを付ける前の状態ではクロージャはエンクロージャであるhogeの環境にある変数prefixを借りていますが、クロージャがprefixを使い終わって返す前にhogeのlifetimeが終わってしまいますね。
prefixの所有権はhogeにあるのでメモリの開放もhogeの寿命が尽きるのと同時に行われますがその後にクロージャにprefixを参照されるとマズいのでこの動作はもっともです。

でも、prefixを使いたい。

ここでコンパイラに教えてもらったmoveキーワードです。
これのキーワードをクロージャにつけると、エンクロージャの環境(の変数)を借用するのではなく所有権を移動するように変更されるとのこと。

つまりmoveキーワードを付けることでprefixの所有権がhogeからクロージャに移る。よってprefixのメモリの開放はhogeの寿命が尽きるときには行われず、クロージャの寿命が尽きるときに一緒に開放されるようになるということですね。

少し見てみましょう。

fn main() {
    let mut out_var = String::from("あー");
    
    let hoge = move || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", hoge()); // あー、ほげー
}
fn main() {
    let mut out_var = String::from("あー");
    
    println!("{}", out_var); // あー
    
    let hoge = move || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", hoge()); // あー、ほげー
}
fn main() {
    let mut out_var = String::from("あー"); 
    
    let hoge = move || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", out_var); // エラー:out_varの所有権がhogeに移ったので使えない
    println!("{}", hoge());
    println!("{}", out_var); // 同様にエラー
}
fn main() {
    let mut out_var = String::from("あー");
    
    let cloned_var = out_var.clone();
    
    let hoge = move || -> String {
        format!("{}、ほげー", out_var)
    };
    
    println!("{}", cloned_var); // あー(クローンした変数の所有権は持っているので使える)
    println!("{}", hoge()); // あー、ほげー
    println!("{}", cloned_var); // あー(out_varが解放されても当然使える)
}
fn main() {
    let mut out_var = String::from("あー");
    
    let cloned_var = out_var.clone();
    
    let hoge = move || -> String {
        format!("{}、ほげー", cloned_var) //クローンした変数の方を使う(hogeはcloned_varの所有権をもらう)
    };
    
    println!("{}", out_var); // あー(out_varの所有権は依然として持っているので使える)
    println!("{}", hoge()); // あー、ほげー
    println!("{}", out_var); // あー
}

所有権が移動されています。

所感的な

少しずつ、少しずつだけどRustのメモリ周りについて分かってきた。
この程度の内容はmallocやfreeを書き慣れている人達は息を吸うように分かっているだろうなぁ...。


ただこう、なんというかクロージャは名前の通りエンクロージャの環境を閉包するものだと思っていたのでRustのクロージャクロージャと呼んでいいのかというモヤモヤがあります。
自由変数を実行時の環境ではなく定義したときの環境で使うもので、環境(よく意味論(よく分かってない)とかEnvで表されてるやつ)をまるっとコピーしてもっておくようなイメージでした。
定義とかいう言葉を持ち出すのは怖いのであまりやらないのですが、これも従来のクロージャなのでしょうか?


いつも通り、「ここが間違っている」「こうした方が良い」、「こういうやり方もある」等ありましたらご教示ください。

参考

qiita.com