質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

Q&A

解決済

1回答

835閲覧

クロージャの引数としてクロージャを受け取りたい

Watching

総合スコア56

Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

0グッド

0クリップ

投稿2021/09/27 03:58

同じようで少しづつ違うけれどその関数内でしか使わない処理をいくつか書くとき、匿名関数を使って実装するという手法を良く使っていて、Rustでもそれがしたいというのが目的です。
そうした時に、クロージャの引数としてクロージャを取りたいという時が良くあります。

Rust

1let func =|f:Fn(u8)->()|{f()}; 2//つまりこういうこと 3func(|i|{});

しかし、これを実装しようと思うとFn(u8)->()のサイズが不定だと怒られ、このように変更しました。

Rust

1let func =|f:&(dyn Fn(u8)->())|{f()}; 2func(&|i|{});

しかし、この実装はかなり気持ち悪い気がします。一時的にしか使わないデータに参照がくっついているのがひどく不格好な気がします。

まず、この方法がRustのお作法通りなのかどうかを教えていただきたいです。そのうえで、綺麗な解決法があるならば教えてください。この方法で問題ないのならそれで結構です。

よろしくお願いいたします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

私はRustを5年ほど使ってますが、クロージャーを引数に取るクロージャーは今まで見たことがないですねぇ。

Rustのクロージャーはジェネリクスにできないので、ご質問のコードのように、funcをクロージャーにするのなら、引数fのクロージャー|i| {}&|i| {}のようにトレイトオブジェクトにするしかなさそうです。

このようにした場合、&|i| {}は動的ディスパッチという方法で呼び出されます。

もしfuncをジェネリックな関数にするのなら、引数fのクロージャーはトレイトオブジェクトにする必要はありません。つまり|i| {}を直接引数に取れます。その場合fは静的ディスパッチという方法で呼び出されます。

後者の方がコンパイラーの最適化が効きやすいので、Rustでは後者が好まれる傾向があります。ただ、それと引き換えに柔軟性が失われるので、柔軟性が必要な場合は前者が用いられます。とはいえ、前者と後者の性能差は一般的にはごく僅かで、多くの場合は測定できないレベルだと思います。なので&|i| {}のようなトレイトオブジェクトを使っても、特に問題はないはずです。

同じようで少しづつ違うけれどその関数内でしか使わない処理をいくつか書くとき、匿名関数を使って実装するという手法を良く使っていて、

単にその関数内でしか使わない処理を表現したいのなら、関数内に関数を定義する方法もあります。

rust

1pub fn outer_func() { 2 この関数はouter_func内だけで使用できる 3 関数なのでジェネリクスにできる(implトレイトを引数に取れる) 4 fn func(f: impl Fn(u8) -> ()) { 5 f(0); 6 } 7 8 func(|_i| {}); 9}

クロージャーとの違いは

  • クロージャーは自由変数(その外側にある変数)を捕捉(キャプチャー)できるが、関数はできない
  • クロージャーはジェネリクスにできないが、関数はできる

なお関数ポインターはFnなどのクロージャーのトレイトを実装しますので、クロージャーが要求されるところに関数ポインターを渡すことができます。以下のようなことも可能です。

rust

1pub fn outer_func() { 2 fn func(f: impl Fn(u8) -> ()) { 3 f(0); 4 } 5 6 fn inner_func(_i: u8) {} 7 8 // impl Fn(u8) -> ()の代わりに関数ポインターfn(u8) -> ()を 9 // 渡すこともできる 10 func(inner_func); 11}

用途によってはこれらも使えるかもしれません。

追記

funcクロージャーの引数fをクロージャー(impl Fn(u8) -> ())ではなくて、関数ポインター(fn(u8) -> ())にすることで、トレイトオブジェクトを作らないで済むことを思い出しました。自由変数を捕捉しないクロージャー(いわゆる無名関数)なら、関数ポインターに型強制できるので、そういうクロージャーなら受け付けられます。

rust

1fn outer_func() { 2 // funcクロージャーは引数に関数ポインターをとる 3 let func = |f: fn(u8)| { 4 f(0); 5 }; 6 7 // このクロージャーは自由変数を捕捉していないので 8 // 関数ポインターへ型強制できる 9 func(|_i| {}); 10 11 // このクロージャーは自由変数(j)を捕捉しているので 12 // 関数ポインターへは型強制できない 13 let j = 10; 14 func(|i| i + j); // error[E0308]: mismatched types 15 // ^^^^^^^^^ expected fn pointer, found closure 16 // note: expected fn pointer `fn(u8)` 17 // found closure `[closure@src/main.rs:15:10: 15:19]` 18}

まとめ

  • クロージャーを引数に取るクロージャー(func)では、ご質問のコードのように、引数となるクロージャー(f)をトレイトオブジェクトにする必要がある。(トレイトオブジェクトは柔軟性がある反面、動的ディスパッチによる性能上のオーバーヘッドがあるため、Rustでは一般的に避けられる傾向がある)
  • 柔軟性を多少犠牲にできるなら、以下のどちらかの方法で動的ディスパッチを避けることができる
  • func側をジェネリックなローカル関数にする。(fの型をimpl Fn(u8)にする) その場合、funcは(クロージャーではないので)自由変数を捕捉できなくなる
  • fの型を関数ポインター(fn(u8))にする。その場合、fは(クロージャーではないので)自由変数を捕捉できなくなる

まず最後の2つのどちらかにできないかを検討し、無理なら最初の方法(トレイトオブジェクトによる動的ディスパッチ)にするのがいいかもしれません。なお文法上は、最後の関数ポインターを使う方法が、ご質問にある「こう書きたいコード」に一番近くなります。

投稿2021/09/27 05:37

編集2021/09/27 14:21
tatsuya6502

総合スコア2046

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問