День добрый!
Читаю пример написания парсер-комбинаторов в rust где активно используются замыкания и возник вопрос:
Правильно понимаю что:
когда вызываем функцию возвращающую замыкание, при каждом её вызове создаётся и возвращается новый экземпляр замыкания ?
И коим образом, тогда, решить вопрос с желанием написать общую функцию возвращающую замыкание и карировать её(частично применить) получив с десяток частных функций но так чтобы не строились новые экземпляры функций каждый раз при их вызове?
Пробовал описать константы:
pub const space1: BoxedParser<&str> = sequence1(&|c:char| c.is_ascii_whitespace());
Компилятор говорить что константные функции не могут возвращать замыкания.
Возврат замыкания не создаёт новую функцию: замыкание содержит только значения (или ссылки на значения) захваченных переменных. Функция, которая вызывается при вызове замыкания, одна и та же.
Так что создание константных замыканий особого смысла не имеет, и пока невозможно, так как в константных функциях нельзя использовать выделение памяти в куче (для Box<dyn Fn()>
) и нельзя использовать impl Fn()
как тип константы (пока не реализовано).
Вы хотите сказать что функции f1()
и f2()
идентичны и в f2()
не будет сто раз создаваться анонимная функция а будет вызываться одна и та же ?
pub fn f() -> Box<Fn(i32, i32) -> i32> {
Box::new(move |a:i32, b: i32| a + b)
}
pub fn f1() -> i32 {
let a = f();
let mut result: i32 = 0;
for i in 1..100 {
result += a(4, i);
}
result
}
pub fn f2() -> i32 {
let mut result: i32 = 0;
for i in 1..100 {
result += f()(4, i);
}
result
}
@Cergo, по-моему вы получите тоже самое если просто напишете функцию:
fn space1(inp: &str) -> &str {
sequence1(&|c:char| c.is_ascii_whitespace())(inp)
}
Не знаю точно какие тут должны быть типы аргументов и возвращаемого значения. Это нужно смотреть по сигнатуре BoxedParser
.
Посмотрел, что такое BoxedParser
. Так не получится.
Вообще, в ночном Rust в принципе можно сделать константное замыкание, только без Box
:
#![feature(impl_trait_in_bindings)]
#![feature(const_fn)]
const fn apply(fun: impl Fn(usize, usize) -> usize, x: usize) -> impl Fn(usize) -> usize {
move |y| { fun(x, y) }
}
const mul_2:impl Fn(usize)->usize = apply(|x, y| x * y, 2);
Нашел способ создать статическое замыкание завернутое в Box
с помощью thread_local!
:
fn apply(fun: impl Fn(usize, usize) -> usize + 'static, x: usize) -> Box<dyn Fn(usize) -> usize> {
Box::new(move |y| { fun(x, y) })
}
thread_local!{ static mul_2: Box<dyn Fn(usize) -> usize> = apply(|x, y| x * y, 2); }
println!("mul_2(10) = {}", mul_2.with(|f| f(10)));
Изначально была идея использовать lazy_static!
, но Rust ругается что Box
не является Sync
. В принципе замыкание можно и в Arc
завернуть…
На практике такой способ наверно будет малополезен т.к. получившееся статическое замыкание просто так уже не вызовешь, только через .with()
.
Кстати, насчет практики. Rust довольно неплохо оптимизирует код и в примере который привел @Cergo обе функции компилируются в константу.
Вот еще пример, все три функции fn_mul_***
после компиляции дают одинаковый код:
fn apply_impl(fun: impl Fn(usize, usize) -> usize, x: usize) -> impl Fn(usize) -> usize {
move |y| { fun(x, y) }
}
fn apply_box(fun: impl Fn(usize, usize) -> usize + 'static, x: usize) -> Box<dyn Fn(usize) -> usize> {
Box::new(move |y| { fun(x, y) })
}
pub fn fn_mul_simple(z:usize) -> usize {
2 * z
}
pub fn fn_mul_impl(z:usize) -> usize {
apply_impl(|x, y| x * y, 2)(z)
}
pub fn fn_mul_box(z:usize) -> usize {
apply_box(|x, y| x * y, 2)(z)
}
С чего решил что пример с f1()
и f2()
компилируется в один код?
Бенчмарк с твоими выводами не согласен.
#[bench]
fn bench_f1(b: &mut Bencher) {
b.iter(|| {
let n = test::black_box(1000);
f1();
(0..n).fold(0, |a, b| a ^ b)
});
}
#[bench]
fn bench_f2(b: &mut Bencher) {
b.iter(|| {
let n = test::black_box(1000);
f2();
(0..n).fold(0, |a, b| a ^ b)
});
}
Смотрел ассемблер в goldbolt. Поменял константу во второй функции иначе вообще одна функция генерится.
Да и бенчи у меня одно и тоже дают:
running 2 tests
test bench_f1 ... bench: 124 ns/iter (+/- 9)
test bench_f2 ... bench: 124 ns/iter (+/- 9)
Странные какие-то тесты. Зачем там замыкание в цикле гоняется? Разве не так должно быть:
#[bench]
fn bench_f1(b: &mut Bencher) {
b.iter(|| {
for _ in 0..1000{
test::black_box(f1());
}
});
}
#[bench]
fn bench_f2(b: &mut Bencher) {
b.iter(|| {
for _ in 0..1000{
test::black_box(f2());
}
});
}
Так тоже разница в пределах погрешности.
В докак советуют так проводить бенчмарки, о… я поправил их
всё одно разница в 25% на такой маленькой ананимной функции, думаю если она будет сложнее и разница увеличится.
test tests::bench_f1 ... bench: 549 ns/iter (+/- 29)
test tests::bench_f2 ... bench: 722 ns/iter (+/- 39)
#[bench]
fn bench_f1(b: &mut Bencher) {
b.iter(|| {
let n = test::black_box(f1());
(0..n).fold(0, |a, b| a ^ b)
});
}
#[bench]
fn bench_f2(b: &mut Bencher) {
b.iter(|| {
let n = test::black_box(f2());
(0..n).fold(0, |a, b| a ^ b)
});
}
Хм, у меня все равно одинаковый результат:
running 2 tests
test bench_f1 ... bench: 656 ns/iter (+/- 23)
test bench_f2 ... bench: 660 ns/iter (+/- 41)
Попробуй f1
и f2
поменять местами. Вот так:
#[bench]
fn bench_f1(b: &mut Bencher) {
b.iter(|| {
let n = test::black_box(f2());
(0..n).fold(0, |a, b| a ^ b)
});
}
#[bench]
fn bench_f2(b: &mut Bencher) {
b.iter(|| {
let n = test::black_box(f1());
(0..n).fold(0, |a, b| a ^ b)
});
}
Теперь у тебя bench_f1
теперь должен быть медленнее.
Просто по-моему врут эти бенчмарки иногда очень сильно.
Пробовал, проверял, всё верно, результат меняется
ноут dell e7470 rustc 1.39.0-nightly
Вообще так нельзя тестировать. Здесь цикл с этим замыканием крутится столько раз сколько возвращает f1()
или f2()
. Если они будут возвращать разные значения, то совсем ерунда будет получаться. В книге это замыкание приведено просто для примера. Этот цикл нужно или совсем убрать или крутить в нем тестируемую функцию.