Замыкание и карирование

День добрый!
Читаю пример написания парсер-комбинаторов в 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);

playground

Нашел способ создать статическое замыкание завернутое в 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)));

playground

Изначально была идея использовать 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)
}

Goldbolt

С чего решил что пример с 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(). Если они будут возвращать разные значения, то совсем ерунда будет получаться. В книге это замыкание приведено просто для примера. Этот цикл нужно или совсем убрать или крутить в нем тестируемую функцию.