
Статья о том, как портировали на консоли игру, первоначально написанную на JavaScript. Спойлер: динамичность мешается.
habr.com/ru/post/489974/
Size: a a a
let closures = [|x| x + 1, |x| x + 2];
не компилируется.Box<dyn Fn(Foo, Bar) -> Baz>
. Очевидный недостаток — падение производительности на ровном месте. Более того, выделение памяти в куче и частичное стирание типа препятствует работе оптимизатора.EitherN
не прибавляют наглядности. Можно реализовать нужные Fn
-трейты для перечисления самому, но на сегодняшний день этот вариант доступен лишь на nightly.let func = if some_condition {
|x| x + foo
} else {
|x| x + bar
};
Решение — явный вынос захватываемого значения в отдельную переменную:let added = if some_condition { foo } else { bar };
let func = |x| x + added;
2) Захват одних и тех же значений, но разные используемые операции. Пример:let some_var = ...;
let func = if some_condition {
|x| x + some_var
} else {
|x| x * some_var
};
Решение — захват самой операции:let some_var = ...;
let func = |x| {
if some_condition {
x + some_var
} else {
x * some_var
}
};
Почему захватывается флаг, а не функция? Потому что это влияет на размер получаемого замыкания. В случае захвата функции компилятору ничего не остаётся, кроме как хранить адрес самой функции, который по размеру совпадает с usize
, флаг же занимает лишь один байт. Наглядное доказательство.let lambda = match a.cmp(&b) {
Less => || x.do_stuff(),
Equal => || y.do_other_stuff(),
Greater => || z.do_stuff_too(),
};
Для этого случая я адаптировал вариант б), с той разницей, что разбор происходит внутри замыкания:enum Either3<X, Y, Z> {
X(X),
Y(Y),
Z(Z),
}
// или просто
// use either::Either3;
use Either3::*;
let datum = match a.cmp(&b) {
Less => X(x),
Equal => Y(y),
Greater => Z(z),
};
let lambda = || match datum {
X(x) => x.do_stuff(),
Y(y) => y.do_other_stuff(),
Z(z) => z.do_stuff_too(),
};
Разумеется, я не охватил все возможные варианты использования замыканий, но я надеюсь, что это поможет вам меньше боксить в коде только для того, чтобы удовлетворить тайпчекеру.let closures = [|x| x + 1, |x| x + 2];
не компилируется.Box<dyn Fn(Foo, Bar) -> Baz>
. Очевидный недостаток — падение производительности на ровном месте. Более того, выделение памяти в куче и частичное стирание типа препятствует работе оптимизатора.EitherN
не прибавляют наглядности. Можно реализовать нужные Fn
-трейты для перечисления самому, но на сегодняшний день этот вариант доступен лишь на nightly.let func = if some_condition {
|x| x + foo
} else {
|x| x + bar
};
Решение — явный вынос захватываемого значения в отдельную переменную:let added = if some_condition { foo } else { bar };
let func = |x| x + added;
2) Захват одних и тех же значений, но разные используемые операции. Пример:let some_var = ...;
let func = if some_condition {
|x| x + some_var
} else {
|x| x * some_var
};
Решение — захват самой операции:let some_var = ...;
let func = |x| {
if some_condition {
x + some_var
} else {
x * some_var
}
};
Почему захватывается флаг, а не функция? Потому что это влияет на размер получаемого замыкания. В случае захвата функции компилятору ничего не остаётся, кроме как хранить адрес самой функции, который по размеру совпадает с usize
, флаг же занимает лишь один байт. Наглядное доказательство.let lambda = match a.cmp(&b) {
Less => || x.do_stuff(),
Equal => || y.do_other_stuff(),
Greater => || z.do_stuff_too(),
};
Для этого случая я адаптировал вариант б), с той разницей, что разбор происходит внутри замыкания:enum Either3<X, Y, Z> {
X(X),
Y(Y),
Z(Z),
}
// или просто
// use either::Either3;
use Either3::*;
let datum = match a.cmp(&b) {
Less => X(x),
Equal => Y(y),
Greater => Z(z),
};
let lambda = || match datum {
X(x) => x.do_stuff(),
Y(y) => y.do_other_stuff(),
Z(z) => z.do_stuff_too(),
};
Разумеется, я не охватил все возможные варианты использования замыканий, но я надеюсь, что это поможет вам меньше боксить в коде только для того, чтобы удовлетворить тайпчекеру.Clone
, но это настолько грязный хак, что я его даже рассматривать не буду). С другой стороны, несмотря на то, что литералы замыканий имеют уникальные типы, типы их возвращаемых значений совпадают. В частности, вызов одного и того замыкания с разными аргументами возвращает значение одного и того же типа (в этом Rust отличается от C++, в котором начиная с C++14, лямбда-функции могут быть полиморфными).let make_closure = |added| {Аналогичные "фабричные" функции (не обязательно замыкания) можно сделать и для остальных примеров. Естественно, в этом случае несколько страдает эргономика, поскольку ранее захваченные переменные приходится передавать явно, но динамическая аллокация всё же не требуется
move |x: i32| x + added
};
let closures = [make_closure(10), make_closure(20)];
s.chars()
.scan(false, |in_string, ch| {
*in_string ^= ch == '"';
Some((ch, *in_string))
})
.some_other_adapter(...)
Для демонстрации полученного кода рассмотрим функцию, которая ищет символ вне закавыченной строки в строке:fn find_outside_string(s: &str, needle: char) -> Option<usize> {
// .chars() поменялось на .char_indices()
// для корректного отслеживания позиции.
s.char_indices()
.scan(false, |in_string, (i, ch)| {
*in_string ^= ch == '"';
Some((i, ch, *in_string))
})
.find_map(|(i, ch, in_string)| {
if ch == needle && !in_string {
Some(i)
} else {
None
}
})
}
И немного тестов:assert_eq!(find_outside_string("Hey!", '!'), Some(3));
assert_eq!(find_outside_string(r#"Outside "Inside!" Outside!"#, '!'), Some(25));
assert_eq!(find_outside_string("Nope", '?'), None);
Как видите, весь код, относящийся непосредственно к отслеживанию строки, инкапсулирован в одном месте и не отвлекает на себя внимания.s.chars()
.scan(false, |prev_was_slash, ch| {
let escaped = *prev_was_slash;
*prev_was_slash = ch == '\\';
Some((ch, escaped))
})
// если слеш экранирован, то второй символ
// не надо пропускать дальше
.filter_map(|(ch, escaped)| if ch == '\\' && escaped {
None
} else {
Some((ch, escaped))
})
.scan(false, |in_string, (ch, escaped)| {
// экранированые кавычки не переключают строку
if !escaped {
*in_string ^= ch == '"';
}
Some((ch, in_string))
.some_other_adapter(...)
Для демонстрации перепишем find_outside_string с поддержкой экранирования:fn find_outside_string(s: &str, needle: char) -> Option<usize> {
s.char_indices()
.scan(false, |prev_was_slash, (i, ch)| {
let escaped = *prev_was_slash;
*prev_was_slash = ch == '\\';
Some((i, ch, escaped))
})
.filter_map(|(i, ch, escaped)| {
if ch == '\\' && escaped {
None
} else {
Some((i, ch, escaped))
}
})
.scan(false, |in_string, (i, ch, escaped)| {
if !escaped {
*in_string ^= ch == '"';
}
Some((i, ch, *in_string))
})
.find_map(|(i, ch, in_string)| {
if ch == needle && !in_string {
Some(i)
} else {
None
}
})
}
Проверим, что оно действительно работает:let s = r#"An "Escaped \" quote" quote"#;
assert_eq!(find_outside_string(s, 'q'), Some(22));
// результат отличается от результата str::find
assert_ne!(find_outside_string(s, 'q'), s.find('q'));
Замечательной особенностью этого паттерна является то, что он без особых усилий расширяется на экранирование других символов.