Comments 30
0 ➜ cat a.rs
#[no_mangle]
pub fn test(a: i32) -> i32 {
if a == 0 {
42
} else if a == 7 {
43
} else {
test(a - 1)
}
}
0 ➜ rustc --crate-type dylib --emit llvm-ir -O a.rs
0 ➜ cat a.ll ; ModuleID = 'a.cgu-0.rs' source_filename = "a.cgu-0.rs" target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" ; Function Attrs: nounwind readnone uwtable define i32 @test(i32) unnamed_addr #0 { start: br label %tailrecurse tailrecurse: ; preds = %bb4, %start %.tr = phi i32 [ %0, %start ], [ %1, %bb4 ] switch i32 %.tr, label %bb4 [ i32 0, label %bb7.loopexit i32 7, label %bb7.loopexit2 ] bb4: ; preds = %tailrecurse %1 = add i32 %.tr, -1 br label %tailrecurse bb7.loopexit2: ; preds = %tailrecurse br label %bb7 bb7.loopexit: ; preds = %tailrecurse br label %bb7 bb7: ; preds = %bb7.loopexit, %bb7.loopexit2 %_0.0 = phi i32 [ 43, %bb7.loopexit2 ], [ 42, %bb7.loopexit ] ret i32 %_0.0 } attributes #0 = { nounwind readnone uwtable }
Другое дело, что из вашего кода непонятно, то ли вы его неправильно написали, так как первоначальный вариант функции execute содержит точку с запятой в самом конце, что подразумевает, что последняя инструкция не новый execute(...), а просто (), пустая инструкция, такой код вообще по идее не должен скомпилироваться, потому что возвращаемое значение не совпадает с сигнатурой функции. То ли по какой-то иной причине не отработала оптимизация.
if i == 0 {
1
}
А из маин вы подаете count(1), таким образом мы не сможем выйти из рекурсии
Если же говорить о вашем цикле из статьи, то вероятно, что ему тоже просто необходимо корректное условие выхода из цикла, потому что в функции execute его вообще нет. Мне лень лезть в код, но я подозреваю, что у вас предполагается, что в какой-то момент получение следующей инструкции выкинет панику или прямо сделает abort(), но это как раз плохой и нефункциональный подход.
Я не эксперт, но на самом деле я понимаю TCO вот так:
Видим, что функция возвращает вызов функции — не добавляем переход на эту функцию в стек вызовов.
И в этом случае что бы там не падало, или падало, или не важно — переполнения стека быть не может. Что угодно — но не это.
Возможно, я конечно ошибаюсь.
Видим, что функция возвращает вызов функции — не добавляем переход на эту функцию в стек вызовов.
Какой ещё "возврат вызова функции"?
Правило такое: Последовательность call addr ; ret
заменяем на jmp addr
.
(";" здесь — разделитель инструкций)
При этом можно явно заявить функцию, как не возвращающую значение:
#[no_mangle]
pub fn test(a: i32) -> ! {
println!("a");
test(a + 1)
}
И тогда компилятор штатно сгенерирует в конце функции:
... %6 = add i32 %0, 1 tail call void @test(i32 %6) unreachable
И при этом компилятор как раз предупредит о проблеме:
warning: function cannot return without recurring --> a.rs:2:1 | 2 | / pub fn test(a: i32) -> ! { 3 | | println!("a"); 4 | | test(a + 1) 5 | | } | |_^ | = note: #[warn(unconditional_recursion)] on by default note: recursive call site --> a.rs:4:5 | 4 | test(a + 1) | ^^^^^^^^^^^ = help: a `loop` may express intention better if this is on purpose
Но в функции же есть возврат — если аргумент равно нулю!
Другое дело, что до этого места никогда не доходит, но откуда об этом может знать компилятор?
Посмотрите пример с рандомом из соседней ветки — там даже есть реальный выход из функции, другое дело что переполнение стека по идее наступит раньше.
не помню, есть ли в Rust про это какой-то стандарт, а в C это называлось бы UB
Стандарта у раста (пока) в принципе нет, но сейчас поведение определено следующим образом: в дебаге переполнения паникуют, в релизе поведение определено как "two's complement".
TCO, внезапно, нет и в Common Lisp (из-за динамических областей видимости, насколько я понимаю).
https://github.com/rust-lang/rfcs/pull/1888 — без этого или аналогичного RFC сам язык гарантий не будет давать, все на милость LLVM.
Видите эти 4 закрывающие скобочки вконце? Выглядят страшно. Не зря всётаки многие функциональные языки не пользуются скобками. Бррр…
С if let… {} else {} было бы на один уровень вложенности меньше.
или можно вместо None => { CowVM{...
было бы написать None => CowVM{...
Это не "пофункциональному". В некоторых языках if вообще нету
Полностью функционально без сборки мусора всё-равно не получится. Так почему-бы не использовать то, что есть?
Я не понял, при чём тут сборка мусора.
Но в любом случае задача была "сделать максимально по функциональному феншую"
Я не понял, при чём тут сборка мусора.
Я вот не знаю ни одного чистого функционального языка общего назначения без сборки мусора — как оно работать-то должно?
Ну программу я же как-то написал. Все "нефункциональные вещи" — это заменил рекурсию на луп изза отсутствия ТСО, да ещё в массив писал через переменную.
Какая в общем то разница, как оно там внутри работает?
Вся фишка и программы и этой статьи — "пишем максимально по феншую, авось заработает. Надеемся что ниразу не придётся ни мутить переменную, ни писать лайфтаймы, на имплементить трейты"
Но это же очень простая программа, я бы не экстраполировал сильно.
По моим наблюдениям за ржавой экосистемой и экспериментами людей — чем кода будет больше и чем он будет сложнее, тем в общем случае больше головной боли будут приносить попытки писать на ржавчине в чистом функциональном стиле. Язык-то таки задуман как императивный, просто с элементами функциональщины.
Неизменяемые структуры данных без GC или аналога, к примеру, очень быстро перестают быть хоть немного практичными.
И, кстати, типы высшего порядка так и не завезли все еще и не факт что вообще звезут, как раз из-за отсутствия сборки мусора :( .
Так то ООП заносит ещё больше головной боли, как по мне.
Осталось только понять, как на нём писать. Я слишком молод, знаю только два подхода — либо ФП либо ООП. И ни один в Расте нормально почему-то не работает.
если что, if let
это просто тонкий сахар над match
— https://doc.rust-lang.org/book/if-let.html
Функциональный Rust: Готовим говядину