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

最近Rustを触っています。
GCに頼りきって生きてきた身なので、普段通りのコードを書いていたところ高階関数を実装しようとしたところで見事にエラーに遭遇しました。
未来の自分と、同じようなエラーに出くわした方のためにメモとして残しておこうと思います。

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


簡単のために「"ほげー!"という文字列を返す関数」を返す関数hogeを実装するという目的で話していきます。

直感的な実装

まずHaskellなんかで書くような気持ちで書いたのが以下(気持ちは分かってもらえるはず...)

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

fn hoge() -> (|| -> String) {
    || -> String {
        String::from("ほげー")
    }
}

以下のように怒られました。

prog.rs:5:16: 5:18 error: expected type, found `||`
prog.rs:5 fn hoge() -> (|| -> String) {
                         ^~

このあたりで上記のような書き方を見たのでこれでいけるものと思っていましたが、バーションアップの過程でダメになったようです。

返り値の型を変える

返り値の(|| -> String)は型としては認識されていないようなので、関数呼び出し形式で呼び出せる型を表すFnに書き換えたものが以下

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

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

これは以下のようになにやら長文で怒られました。

prog.rs:5:14: 5:30 error: the trait `core::marker::Sized` is not implemented for the type `core::ops::Fn() -> collections::string::String` [E0277]
prog.rs:5 fn hoge() -> (Fn() -> String) {
                       ^~~~~~~~~~~~~~~~
prog.rs:5:14: 5:30 note: `core::ops::Fn() -> collections::string::String` does not have a constant size known at compile-time
prog.rs:5 fn hoge() -> (Fn() -> String) {
                       ^~~~~~~~~~~~~~~~
prog.rs:6:5: 8:6 error: mismatched types:
 expected `core::ops::Fn() -> collections::string::String`,
    found `[closure prog.rs:6:5: 8:6]`
(expected trait core::ops::Fn,
    found closure) [E0308]
prog.rs:6     || -> String {
prog.rs:7         String::from("ほげー!")
prog.rs:8     }
error: aborting due to 2 previous errors

大事そうなのはこの部分

> the trait `core::marker::Sized` is not implemented for the type `core::ops::Fn() -> collections::string::String

>`core::ops::Fn() -> collections::string::String` does not have a constant size known at compile-time


Sizedというトレイトを実装した型ではないということと、コンパイル時に固定サイズを決められない(持ってない)というメッセージです。
どちらもコンパイル時に必要なサイズが分からないよということですね。
一応(関数)型としては認識されてそうです。

スタックとヒープ

確認として一応...

  • スタック(領域):コンパイラやOSが割り当てるメモリ領域
  • ヒープ(領域):アプリケーションやプログラム実行中に動的に確保するメモリ領域


関数の引数や返り値はスタックに積むのでコンパイル時に各変数などに対してそれぞれどれだけのメモリサイズを確保していればよいかを分からなければなりません。
i32とi64では割り当てられるサイズが違います。
Sizedというトレイトはi32のように型だけではサイズが分からない型が、「どれだけのメモリサイズを確保していればよいか分かっているよ」ということを示すトレイトらしい。


上のエラーメッセージを再度確認します。
() -> Stringな関数型はまず型からどれだけのメモリサイズを確保していればよいか分からない。またSizedトレイトも実装されていないのでスタックに詰めない。
なるほど。コンパイラの気持ち、分かってきた(嘘)


スタックがダメならヒープに積め(置け)ばいいじゃない

じゃあこの返り値である関数をヒープに置けばよいのではないか。
ここで使えるのがBoxです。

ヒープに置いていることを示すポインタ型のようです。
Boxを使うとこのようになる。

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

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

これをrust runすると

ほげー!

いけました。

所感的な

これで簡単な高階関数はできましたが、そんなカジュアルにヒープに置いていいのか?Box使っていいのか?ということが気になりました。
GCに頼りきってきたせいでこのあたりの塩梅がよく分かっていない、自信がないのは本当にダメなとこですね...。

僕の中で高階関数を使うといえばパーサーコンビネータが一番に出てくるので、Rustで有名なパーサーコンビネータを調べたところcombineというのが出て、その中でもちょろちょろ使われているよう。github.com



Rustは本当に色んな機能があるので、多分これ以外にも実装方法があると思います。
「これは良くない」「こっちの方がいい」とか「こういうやり方もある」なんかがあったら是非教えてください。(切実です)


続き(引数を渡したりする)kudohamu.hatenablog.com