amarao: (Default)
[personal profile] amarao
А вот задачу инвертирования списка я считаю очень и очень полезной для понимания Rust'а. Ощупывание границ того, что можно делать с deconstructed structs - это очень и очень важно.

Об алгоритме.
Есть два алгоритма:

1) Сложить всё в стек и собрать обратно список в другом порядке. При том, что алгоритмически это такой же уровень сложности, на практике - это ~30-кратное замедление из-за необходимости иметь дополнительные аллокации и копирования.

2) Пройти по списку и поменять направление ссылок. Было (val1, Link_to_2) -> (val2, Link_to_2) -> (val3, None), должно стать (val1, None) <- (val2, Link_to_1) <- (val3, Link_to_2).

В условиях указателей (си-подобный язык) эта задача довольно простая, потому что мы казуально допускаем, что в какой-то момент у нас "всё висит" или есть два указателя на одно и то же место. (ну и чё такого?).

В Rust же всё куда более интригующе. Список - это

 
pub struct ListNode {
pub val: i32,
pub next: Option<Box<ListNode>>
}

При этом настоящий тип списка - это

Option<Box<ListNode>>

Проговорим это:

Список это:

Либо None
Либо enum, который владеет smart pointer'ом на heap, у которого тип Box, внутри которого находится структура ListNode.

Структура ListNode владеет val и next, который и есть остаток списка.

Чувствуете, как всё нюасно стало? Не ссылки, а есть владение. Чтобы взять содержимое val или next мы должны:
1. Деконструировать Option.
2. Деконструировать ListNode.

После того, как мы деконструировали  или Option, или ListNode, мы больше не можем передать его куда-то дальше!

То есть наивное: something.next = cur мы не можем сделать! У нас cur больше не существует.

В целом, эта задача была бы не решаема, если бы не возможность "реконструировать" ListNode. Если мы для каждого поля, из которого мы "взяли" значения положим назад значения (move, передадим владение туда), то мы получим "реконтструированную" структуру, которую мы можем передавать дальше.

Деконструированный же Option невозможно реконструировать (его больше нет), вместо этого мы можем создать новый. Что такое "новый Option" и чем он отличается от старого? В целом, ни чем. Я даже не уверен, что это создаст какие-то машинные инструкции, но с точки зрения компилятора новый Some и старый Some (который мы деконструировали) - две совершенно разные вещи.

Вот решение:

pub fn reverse_list(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
let mut prev = None;
while let Some(mut node) = head{
head = node.next;
node.next = prev;
prev = Some(node);
}
prev
}

Проговариваю его по шагам:

Мы деконструируем head (если есть что деконструировать), и создаём переменную node, которая получает владением над содержимым head. Option<Box<ListNode>> → Box<ListNode>.

node имеет тип Box - это умный указатель на кучу, который (за вычетом кучи) просто владеет ListNode.

К этому моменту у нас head не имеет значения (мы делали move из него в строчке while let).
Мы передаём в head значение поля node.next. В этот момент node становится частично деконструированной. next из неё забрали, а value всё ещё принадлежит node.

После этого мы перемещаем в node.next значение из prev. prev перестаёт иметь какое-либо значение (т.к. отдал владение значением). Зато node теперь полностью сконструирован, потому что он владеет всеми своими значениями.
Мы создаём новое значение Some(node), забирая значение из переменной node, node перестаёт иметь какое-либо значение (т.к. отдал владение значением в Some), после чего мы отдаём владение Some(node) переменной prev.

К концу цикла у нас: prev - содержит в себе Some(node), next содержит в себе либо None, либо Some со следующим элементом списка, то есть содержит остаток списка), node - не определён. Цикл заканчивается, у node заканчивается область видимости и она (он? оно?) уничтожается. Т.к. значение уже было move, то ничего делать не надо. Начинается следующая итерация.

В конце мы отдаём владение prev, внутри которого владение prev.next, внутри которого prev.next.next и т.д., то есть инвертированный список.

Вот, примерно так устроен процесс думания о Rust программе. Где-то тут был пропущен вопрос, как мы сквозь Box<ListNode> получаем поля ListNode. Я тщательно этот вопрос не продумывал, но насколько я понимаю, это неявно вызываемые функции deref и deref_mut, реализованные через трейты Deref и DeferMut.


Date: 2023-01-05 02:55 pm (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Спасибо за разъяснения. У меня все больше растет ощущение, что с растом что-то принципиально не так. Ну, линейная логика это хорошо, но вот интуиционизма не хватает. Правильных монад.

Date: 2023-01-05 03:24 pm (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Охренеть. The Ownership Monad. Если этот вопрос решен, и все получается выразить в интуиционизм, то вот и славненько.

Я-то не имел в виду, что раст плохой, я имел в виду, что он еще недоразвит - или его понимание недоразвито.

Date: 2023-01-07 01:23 pm (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Ну в целом да, Rust, с практической стороны, придуман, чтобы хардвер не мучать, а употреблять научно; а с теоретической стороны там внутре линейная логика. Я не большой фанат линейной логики (алхимия в ней тоже выражается вполне логично), но ценю. А вот с комбинацией, с линейным интуиционизмом, я не знаком. А жаль. Монады имеют большой смысл в смысле выразительности.

Date: 2023-01-07 03:31 pm (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

А! Спасибо. Не в курсе был.

Date: 2023-01-05 05:07 pm (UTC)
From: [personal profile] eterevsky
For the record, отмечу что рекомендованный способ реализовывать такие структуры данных -- в unsafe. В этом случае ты должен продолжать следовать "правилам хорошего тона", но так что компайлер не сможет их автоматически проверять. https://doc.rust-lang.org/nomicon/what-unsafe-does.html

Date: 2023-01-09 10:41 am (UTC)
From: [personal profile] eterevsky
Option Box — да. Но вот скажем RefCell и Rc уже не бесплатные.

Date: 2023-01-09 10:58 am (UTC)
From: [personal profile] eterevsky
Стопудово. Но скажем для связных списков без этого вероятно не обойтись, если не прибегать к unsafe.

Я собственно хотел только одно сказать: что если ты имплементируешь generic структуру данных, тебе не обязательно прикладывать отдельные усилия чтобы избежать unsafe, так как unsafe как раз создан для такого низкоуровневого кода.

Date: 2023-01-09 11:10 am (UTC)
From: [personal profile] eterevsky
Ну да, unsafe и есть некоторое подобие C. Но ты пишешь на нём только относительно небольшое количество кода. Плюс есть вполне определённые инварианты, которые девелопер должен соблюдать, чтобы unsafe код не ломал систему.

Profile

amarao: (Default)
amarao

August 2025

S M T W T F S
     12
345 6789
10111213141516
17181920212223
24252627282930
31      

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Aug. 16th, 2025 01:37 pm
Powered by Dreamwidth Studios