Я вам больше скажу. Бывают случаи, когда нужен не map, а action - некоторое действие с каждым элементов коллекции.
И для этого также есть разные средства (к сожалению, в статье не показанные). Если этот action просто использует значение (скорее, ссылку на него), то for, который работает на всём, что реализует IntoIterator, который включает в себя (но не ограничивается этим) и сами итераторы:
for item in &vec {
println!("Item is {item}");
}
Если же нужно in-place, то... Снова for, потому что итерироваться можно ещё и по mut-ссылкам:
for item in &mut vec {
// in-place увеличиваем каждый элемент на 10
*item += 10;
}
Ну это уже сферические map/filter в вакууме. В реальности все ограничивается размерами памяти и накладными расходами на ее выделение/освобождение
Вовсе нет. В этом и прекрасная особенность итераторов: они не обязаны соответствовать где-то выделенному куску данных, а являются генераторами некого потока данных, который на каждой итерации порождает как-то следующее значение, причём далеко не обязательно в дальнейшем все эти значения куда-то складывать (в статье, к сожалению, преимущественно такое показано).
И это прекрасно, потому что зачастую функции не нужно собирать все элементы в памяти, ей достаточно единожды обходимого потока данных (самый простой пример - нахождения минимума/максимума). И таким функция достаточно принимать просто некий итератор, а не фиксированный кусок памяти.
Например, в std есть iter::repeat, который просто неограниченно генерирует один и тот же элемент, а уж что с ним будет делать получатель данных - его выбор. И при этом самому итератор нужно памяти на своё внутренне представление. Или, например, в крейте rand ГПСЧ позволяют получить из них итераторы, возвразающие бесконечный итератор псевдослучайных чисел.
Создание объектов и запуск в отдельных потоках run выполняется одним потоком.
А исполнение тела run() в другом, и именно в этом теле идёт доступ к записанной в другом потоке переменной.
Компайлер может попросить закэшить что-то, если это что-то очень часто и много пишется с одного потока.
А может не попросить, а может и не может. Это никем не гарантируется. А вот то, что изменение (запись строки) может быть не замечено явно описано, причём не в какой-то отдельной спеке, а прямо таки в спецификации языка.
За всю свою многолетнюю (больше 20 лет) практику от платёжных и до высоконагруженных систем, я ни разу не воспользовался этим словом.
Апелляция к опыту это, конечно, прикольно, но она ничего не говорит, кроме того, что лично вы на такое не наткнулись (а, может, просто не заметили). Вот вам конкретный контрпример из Lucene LUCENE-8780.
За повсеместное втыкание final, надо бить по рукам. Что final, что модификаторы видимости - это для библиотек больше. Люди, имеющие доступ к этим модификатором удалят их, если им действительно нужно будет что-то сделать public или перереференсировать.
Плохие разработчики мешают стрелять по ногам. Эти вещи придумали не просто так, а для решения конкретных проблем. Если что-то не должно изменяться, то мне, как разработчику, гораздо удобнее явно иметь признак этого (final на поле) и отсюда какую-то гарантию того, что никто мне поле не поменяет [1]. Как типовой пример, что final -- это хорошо, вспомните, как устроены строки и как они полагаются на свою иммутабельность.
Причём эти вещи позволяют (и тут мы уже переходим к реальным системам) рантайму быть эффективнее, например компилятору (JITу) с чистой совестью решать, что он может кэшировать, а что нет, что он может держать локально, а что должен закоммитить в память.
Надо нанимать людей, которые понимают зачем изменчивость, зачем куча и стек, сильные и слабые стороны ООП и ФП и т.д.
А неизменяемость это не какая-то уникальная особенность ФП. Ей есть место много где, включая ООП. Да и Java это не чисто-ООП язык, а мультипарадигмальный. И если посмотреть на вектор развития языка, то многие нововведений предпочитают иммутабельность (records, scoped values, frozen arrays).
[1]: На самом деле, есть штуки три способов в обход final записать поле, но они больно экзотические, плюс могут вести к деоптимизациям, плюс с ними постепенно пытаются бороться в OpenJDK.
Также стоит обратить внимание на трейты TryFrom и TryInto, которые также используются для конверсий, но на этот раз таких, которые могут быть неуспешными (они, кстати, также автоматически реализуются с типом невозможной ошибки Infallible, если реализованы обычные конверсии From).
В случае с if-let-else ещё происходит shadowing предыдущей локальной переменной cthulhu: Option<Cthulhu> новой cthulhu: Cthulhu, полученной в качестве результата if-let-else выражения.
По-моему и логично локализовать fast-math'ную логику в отдельных частях программы (чем бы она не была), а не делать глобальной для всей программы, ломая те вещи, которым разумно полагаться на точность работы с float'ами.
Тем более, что тут ничего сложного изобретать не надо:
можно использовать быстрые функции только тогда, когда это действительно уместно (см. fast-math);
можно в конкретных задачах использовать свои типы, реализованные с быстрой математикой (например, таковые из fast-floats);
можно делать свои типы обощёнными относительно некого генерика, реализованного как на стандартных флотах, так и на ускоренных, а ля
struct Rotation<F: Float32> {
yaw: F,
pitch: F,
}
где некий Float32 реализован как на f32, так и на каком-ниубудь FF32.
И? То, что оптимизация, на которую я сослался, есть, не значит, что надо изобретать бессмысленные структуры, как Option<NonZero<Num>> (на самом деле, вполне себе осмысленные, но не для данной задачи).
0 для отсутствия индекса вполне элегантно, быстро и компактно. В отличие от ADT, который превращается в discriminated union.
Контрпример, демонстрирующий, что сум-тип -- очень даже удобно.
Оптимизация были упомянуты дополнительно как что-то, что, в целом, хорошо дружит с последними, но не как инструкция к написанию нелогичного -- для задачи инексирования -- типа.
В дополнение к тому, что индексация с нуля удобна, она ещё и естественна для многих прикладных вещей.
А в чём конкретно проблема discriminated union, если он нормально реализован и, тем более, хорошо подлежит оптимизациям (TL;DR, если у вас есть дырка из невозможного битового представления, то это невозможное значение и будет представлять собой пустой вариант)?
Как верно написал , просто так рассматривать &Vec<String> как &[&str] не получится. Причина в том, что слайс &[T] подразумевает последовательно идущие данные типа T (с учётом выравниания), в то время как у String представление включает в себя&str, но не только его, из-за чего сугубо такой варинт без копирования (в новую структуру с новым представлением) не реализуем.
Но есть гораздо более гибкий вариант сделать метод более универсальным без необходимости в лишних копированиях, к которому мы придём в несколько шагов улучшения изначального метода:
пусть вместо строго строки (а, фактически, нас волнует даже не String, а то, что мы получаем ссылку ан её str-составляющую) будет абстрактное нечто, что можно преобразовать в &str:
fn show_notes<T: AsRef<str>>(notes: &Vec<T>) {
// Выводим пустую строку.
println!();
// Для каждой заметки в заметках ...
for note in notes {
// выводим её на экран.
println!("{}", note.as_ref())
}
}
Я метода появился типовой параметр T, который должен быть чем-то, что может быть представлено как ссылка на str.
на место вектора действительно можно поставить просто слайс (фактически, для вектора Vec<T> верно, что он AsRef[T], но тут в этом нет необходимости):
fn show_notes<T: AsRef<str>>(notes: &[T]) {
// Выводим пустую строку.
println!();
// Для каждой заметки в заметках ...
for note in notes {
// выводим её на экран.
println!("{}", note.as_ref())
}
}
наконец, вво имя красоты и компактности, воспользуемся синтаксическим сахаром, позволяющим описать п.1 более компактно:
fn show_notes(notes: &[impl AsRef<str>]) {
// Выводим пустую строку.
println!();
// Для каждой заметки в заметках ...
for note in notes {
// выводим её на экран.
println!("{}", note.as_ref())
}
}
По итогу, имеем метод, который принимает слайс, содержащий что-то, что можно представить как ссылку на str, работающий как со String, так со str, так и с произвольными типами, реализующими AsRef<str> (при этом лежащими в любом контейнере, который представим как слайс):
fn main() {
let notes = vec!["foo".to_string(), "bar".to_string()];
show_notes(¬es);
let notes = vec!["baz", "qux"];
show_notes(¬es);
let notes = vec!["dora", "prgrm"];
show_notes(¬es);
}
В качестве более продвинутого варианта, можно принимать даже не слайс (потому что непосредственной необходимости в последовательности данных у нас нет), а нечто, что можно последовательно обходить (итерироваться по этому), или даже то, почему можно устроить обход:
fn show_notes(notes: impl IntoIterator<Item = impl AsRef<str>>) {
// Выводим пустую строку.
println!();
// Для каждой заметки в заметках ...
for note in notes {
// выводим её на экран.
println!("{}", note.as_ref())
}
}
Разумеется, это не следует из этой статьи, поскольку в ней трейты не рассматривались, но, с точки зрения общего развития (и заманивания в секту Раста :) ) считаю это достаточно интересным.
Если интересен вопрос производительности, то, де факто, компилятор выполнит мономорфизацию, а именно создаст реализацию метода под каждый использующийся с ним тип (не каждый потенциально доступный, а именно каждый, который реально используется в программе), то есть, фактически, для оригинального варианта там будет всё тот же &Vec<String>, для других &[String], &Vec<&str> и так далее.
Так ведь, в данном случае, это ещё больший костыль, который внешнему пользователю (=тому, кто структурой пользуется) даёт ложное чувство того, что поле действительно необязательно, а пользователю структуры всё так же оставляет необходимость каждое из этих полей проверять на отсутствие.
И для этого также есть разные средства (к сожалению, в статье не показанные).
Если этот action просто использует значение (скорее, ссылку на него), то
for
, который работает на всём, что реализуетIntoIterator
, который включает в себя (но не ограничивается этим) и сами итераторы:Если же нужно in-place, то... Снова
for
, потому что итерироваться можно ещё и поmut
-ссылкам:Вовсе нет. В этом и прекрасная особенность итераторов: они не обязаны соответствовать где-то выделенному куску данных, а являются генераторами некого потока данных, который на каждой итерации порождает как-то следующее значение, причём далеко не обязательно в дальнейшем все эти значения куда-то складывать (в статье, к сожалению, преимущественно такое показано).
И это прекрасно, потому что зачастую функции не нужно собирать все элементы в памяти, ей достаточно единожды обходимого потока данных (самый простой пример - нахождения минимума/максимума). И таким функция достаточно принимать просто некий итератор, а не фиксированный кусок памяти.
Например, в std есть
iter::repeat
, который просто неограниченно генерирует один и тот же элемент, а уж что с ним будет делать получатель данных - его выбор. И при этом самому итератор нужно памяти на своё внутренне представление. Или, например, в крейтеrand
ГПСЧ позволяют получить из них итераторы, возвразающие бесконечный итератор псевдослучайных чисел.Как один из спикеров, ответственно заявляю, что буду рассказывать именно про то, что реально используем, и почему именно так
Благо (на мой взгляд) нет, здесь речь только о тех вещах, которые явно используются в FFI (
extern "C"
).А исполнение тела
run()
в другом, и именно в этом теле идёт доступ к записанной в другом потоке переменной.А может не попросить, а может и не может. Это никем не гарантируется. А вот то, что изменение (запись строки) может быть не замечено явно описано, причём не в какой-то отдельной спеке, а прямо таки в спецификации языка.
Апелляция к опыту это, конечно, прикольно, но она ничего не говорит, кроме того, что лично вы на такое не наткнулись (а, может, просто не заметили). Вот вам конкретный контрпример из Lucene LUCENE-8780.
Плохие разработчики мешают стрелять по ногам. Эти вещи придумали не просто так, а для решения конкретных проблем. Если что-то не должно изменяться, то мне, как разработчику, гораздо удобнее явно иметь признак этого (
final
на поле) и отсюда какую-то гарантию того, что никто мне поле не поменяет [1]. Как типовой пример, чтоfinal
-- это хорошо, вспомните, как устроены строки и как они полагаются на свою иммутабельность.Причём эти вещи позволяют (и тут мы уже переходим к реальным системам) рантайму быть эффективнее, например компилятору (JITу) с чистой совестью решать, что он может кэшировать, а что нет, что он может держать локально, а что должен закоммитить в память.
А неизменяемость это не какая-то уникальная особенность ФП. Ей есть место много где, включая ООП. Да и Java это не чисто-ООП язык, а мультипарадигмальный. И если посмотреть на вектор развития языка, то многие нововведений предпочитают иммутабельность (records, scoped values, frozen arrays).
[1]: На самом деле, есть штуки три способов в обход
final
записать поле, но они больно экзотические, плюс могут вести к деоптимизациям, плюс с ними постепенно пытаются бороться в OpenJDK.Гитхаб Вам даже отдельную кнопочку рисует в описании проекта
Да и для лицензии есть вполне себе официальный перевод
Да, для конверсий стоит использовать именно трейты
From
иInto
.Единственное, что из этой пары следует реализовывать именно
From
, посколькуInto
в таком случае реализуется автоматически (обратное неверно), то есть:Также стоит обратить внимание на трейты
TryFrom
иTryInto
, которые также используются для конверсий, но на этот раз таких, которые могут быть неуспешными (они, кстати, также автоматически реализуются с типом невозможной ошибкиInfallible
, если реализованы обычные конверсииFrom
).Ну и, как верно подметил выше @medvedevia, с недавних пор есть компактный
let-else
:Но ведь ничто не мешает в, условно, одну строчку конвертироваться из
Option<T>
вResult<T, E>
и далее сразу его вернуть try-оператором.Почему же? Оба варианта правильные.
В случае с
if-let-else
ещё происходит shadowing предыдущей локальной переменнойcthulhu: Option<Cthulhu>
новойcthulhu: Cthulhu
, полученной в качестве результатаif-let-else
выражения.Магия есть, но не чёрная. Это конкретный lang item, для которого (и только) компилятор знает об interior mutabilty.
Высокоуровневые же примитивы для interior mutabilty строятся на его основе.
Ну так ведь это не эквивалентное сравнение. Можно же сделать
По-моему и логично локализовать fast-math'ную логику в отдельных частях программы (чем бы она не была), а не делать глобальной для всей программы, ломая те вещи, которым разумно полагаться на точность работы с float'ами.
Тем более, что тут ничего сложного изобретать не надо:
можно использовать быстрые функции только тогда, когда это действительно уместно (см. fast-math);
можно в конкретных задачах использовать свои типы, реализованные с быстрой математикой (например, таковые из fast-floats);
можно делать свои типы обощёнными относительно некого генерика, реализованного как на стандартных флотах, так и на ускоренных, а ля
где некий
Float32
реализован как наf32
, так и на каком-ниубудьFF32
.Java: jOOQ: query builder с максимально близким к SQL DSL и проверкой типов на этапе компиляции
Rust: Diesel: гибрид ORM и query builder'а, работающий поверх статически генерируемой схемы
Rust: Sqlx: статический валидатор и маппер SQL-запросов
Del
И? То, что оптимизация, на которую я сослался, есть, не значит, что надо изобретать бессмысленные структуры, как
Option<NonZero<Num>>
(на самом деле, вполне себе осмысленные, но не для данной задачи).Суть моего комментария -- противопоставление Вашему
Контрпример, демонстрирующий, что сум-тип -- очень даже удобно.
Оптимизация были упомянуты дополнительно как что-то, что, в целом, хорошо дружит с последними, но не как инструкция к написанию нелогичного -- для задачи инексирования -- типа.
В дополнение к тому, что индексация с нуля удобна, она ещё и естественна для многих прикладных вещей.
А с чего вдруг
NonZero<Num>
, если речь, как раз, о том, чтобы оставить 0 как индекс начинающего элемента?А в чём конкретно проблема discriminated union, если он нормально реализован и, тем более, хорошо подлежит оптимизациям (TL;DR, если у вас есть дырка из невозможного битового представления, то это невозможное значение и будет представлять собой пустой вариант)?
Как верно написал , просто так рассматривать
&Vec<String>
как&[&str]
не получится. Причина в том, что слайс&[T]
подразумевает последовательно идущие данные типаT
(с учётом выравниания), в то время как уString
представление включает в себя&str
, но не только его, из-за чего сугубо такой варинт без копирования (в новую структуру с новым представлением) не реализуем.Но есть гораздо более гибкий вариант сделать метод более универсальным без необходимости в лишних копированиях, к которому мы придём в несколько шагов улучшения изначального метода:
пусть вместо строго строки (а, фактически, нас волнует даже не
String
, а то, что мы получаем ссылку ан еёstr
-составляющую) будет абстрактное нечто, что можно преобразовать в&str
:Я метода появился типовой параметр
T
, который должен быть чем-то, что может быть представлено как ссылка наstr
.на место вектора действительно можно поставить просто слайс (фактически, для вектора
Vec<T>
верно, что онAsRef[T]
, но тут в этом нет необходимости):наконец, вво имя красоты и компактности, воспользуемся синтаксическим сахаром, позволяющим описать п.1 более компактно:
По итогу, имеем метод, который принимает слайс, содержащий что-то, что можно представить как ссылку на
str
, работающий как соString
, так соstr
, так и с произвольными типами, реализующимиAsRef<str>
(при этом лежащими в любом контейнере, который представим как слайс):В качестве более продвинутого варианта, можно принимать даже не слайс (потому что непосредственной необходимости в последовательности данных у нас нет), а нечто, что можно последовательно обходить (итерироваться по этому), или даже то, почему можно устроить обход:
за счёт чего будет ещё большая гибкость:
Разумеется, это не следует из этой статьи, поскольку в ней трейты не рассматривались, но, с точки зрения общего развития (и заманивания в секту Раста :) ) считаю это достаточно интересным.
Если интересен вопрос производительности, то, де факто, компилятор выполнит мономорфизацию, а именно создаст реализацию метода под каждый использующийся с ним тип (не каждый потенциально доступный, а именно каждый, который реально используется в программе), то есть, фактически, для оригинального варианта там будет всё тот же
&Vec<String>
, для других&[String]
,&Vec<&str>
и так далее.Так ведь, в данном случае, это ещё больший костыль, который внешнему пользователю (=тому, кто структурой пользуется) даёт ложное чувство того, что поле действительно необязательно, а пользователю структуры всё так же оставляет необходимость каждое из этих полей проверять на отсутствие.
Не говоря о НЕтипобеопасности
Уже не просят