Pull to refresh

Фокусируясь на владении

Reading time9 min
Views2.6K
Original author: Nicholas D. Matsakis

Прим. переводчика: запись датирована 13 мая 2014 года, поэтому некоторые детали, в том числе и исходный код, могут не соответствовать текущему положению вещей. Ответом на вопрос, зачем нужен перевод столь давнего поста, будет ценность его содержания для формирования понимания такой одной из основополагающих концепций языка Rust, как владение.


Со временем я убедился, что было бы лучше отказаться от различия между изменяемыми и неизменяемыми локальными переменными в Rust. По крайней мере, многие люди скептически относятся к этому вопросу. Я хотел изложить свою позицию публично. Я приведу различные мотивы: философский, технический и практический, а также обращусь к главной защите нынешней системы. (Примечание: я рассматривал это как Rust RFC, но решил, что тон лучше подходит для сообщения в блоге, и у меня нет времени переписывать его сейчас.)


Пояснение


Я написал эту статью довольно решительно и полагаю, что линия, которую я защищаю, будет правильной. Тем не менее, если мы не закончим поддерживать текущую систему, это не станет катастрофой или чем-то в этом роде. У неё есть свои преимущества, и в целом я нахожу её довольно приятной. Я просто думаю, что мы можем её улучшить.


Одним словом


Я хотел бы удалить различие между неизменяемыми и изменяемыми локальными переменными и переименовать &mut указатели в &my, &only или &uniq (мне без разницы). Лишь бы не было ключевого слова mut.


Философский мотив


Основная причина, по которой я хочу это сделать — это то, что я считаю, что это сделает язык более последовательным и простым для понимания. По существу, это переориентирует нас с разговоров об изменяемости на разговоры об использовании псевдонимов ("aliasing") (которое я назову "разделением" ("sharing"), см. ниже).


Изменяемость становится следствием, которое вытекает из уникальности: "Вы всегда можете изменять всё, к чему у вас есть уникальный доступ. Разделяемые данные, как правило, неизменяемы, но если вам нужно, вы можете изменить их, используя некий сорт типов Cell".


Иными словами, со временем мне стало ясно, что проблемы с гонками данных и безопасностью памяти возникают, когда у вас есть одновременно как использование псевдонимов, так и изменяемость. Функциональный подход к решению этой проблемы заключается в устранении изменяемости. Подход Rust состоял бы в том, чтобы удалить использование псевдонимов. Это дает нам историю, которую можно рассказать, и которая поможет нам разобраться.


Замечание по терминологии: я думаю, что мы должны ссылаться на использование псевдонимов как на разделение (прим. переводчика: далее везде вместо "aliasing" используется "sharing" в значении "разделения" или "разделяемого владения", поскольку ни "использование псевдонимов", ни "псевдонимирование" не дают понимания, о чем идет речь). В прошлом мы избегали этого из-за его многопоточных отсылок. Однако, если/когда мы реализуем планы распараллеливания данных, которые я предложил, то эта коннотация не совсем неуместна. На самом деле, учитывая тесную взаимосвязь между безопасностью памяти и гонками данных, я действительно хочу продвигать эту коннотацию.


Образовательный мотив


Я думаю, что нынешние правила сложнее для понимания, чем они должны быть. Неочевидно, например, что &mut T не подразумевает никакого разделяемого владения. Кроме того, обозначение &mut T предполагает, что &T не подразумевает никакой изменяемости, что не совсем точно, из-за таких типов, как Cell. И невозможно договориться, как их называть ("изменяемые/неизменяемые ссылки" — самое распространенное, но это не совсем правильно).


Напротив, тип вроде &my T или &only T, кажется, упрощает объяснения. Это уникальная ссылка — естественно, вы не можете заставить две из них указывать на одно и то же место. И изменяемость — это ортогональная вещь: она происходит от уникальности, но также справедлива и для ячеек. А тип &T — это как раз его противоположность, разделяемая ссылка. RFC PR # 58 дает ряд аналогичных аргументов. Я не буду повторять их здесь.


Практический мотив


В настоящее время существует разрыв между заимствуемыми указателями, которые могут быть либо разделяемыми, либо изменяемыми+уникальными, и локальными переменными, которые всегда уникальны, но могут быть изменяемыми или неизменяемыми. Конечным результатом этого является то, что пользователи должны размещать объявления mut на вещах, которые не изменяемы напрямую.


Локальные переменные не могут быть смоделированы с использованием ссылок


Этот феномен возникает из-за того, что ссылки не так выразительны, как локальные переменные. В общем и целом, это препятствует абстракции. Позвольте мне привести несколько примеров, чтобы объяснить, что я имею в виду. Представьте себе, что у меня есть структура окружения, которая хранит указатель на счетчик ошибок:


struct Env { errors: &mut usize }

Теперь я могу создавать экземпляры этой структуры (и использовать их):


let mut errors = 0;
let env = Env { errors: &mut errors };
...
if some_condition {
    *env.errors += 1;
}

OK, теперь представьте, что я хочу выделить код, который изменяет env.errors, в отдельную функцию. Я мог бы подумать, что, поскольку переменная env не объявлена как изменяемая, я могу использовать неизменяемую ссылку &:


let mut errors = 0;
let env = Env { errors: &mut errors };
helper(&env);

fn helper(env: &Env) {
  ...
  if some_condition {
      *env.errors += 1; // ОШИБКА
  }
}

Но это не так. Проблема заключается в том, что &Env является типом с разделяемым владением (прим. переводчика: как известно, единовременно может существовать более одной неизменяемой ссылки на объект), и, следовательно, env.errors появляется в пространстве, допускающем возможность раздельного владения объектом env. Чтобы этот код работал, я должен объявить env как изменяемую и использовать ссылку &mut (прим. переводчика: &mut для того, чтобы указать компилятору, что осуществляется уникальное владение env, поскольку единовременно может существовать только одна изменяемая ссылка на объект, и гонка данных исключена, а mut потому что нельзя создать изменяемую ссылку на неизменяемый объект):


let mut errors = 0;
let mut env = Env { errors: &mut errors };
helper(&mut env);

Эта проблема возникает из-за того, что мы знаем о том, что локальные переменные уникальны, но мы не можем поместить это знание в заимствуемую ссылку, не делая ее изменяемой.


Эта проблема возникает в ряде других мест. До сих пор мы писали об этом по-разному, но меня продолжает преследовать чувство, что мы говорим о разрыве, которого просто не должно быть.


Проверка типов применительно к замыканиям


Мы должны были обойти это ограничение в случае с замыканиями. Замыкания в основном рассахариваются в такие структуры, как Env, но не совсем. Это связано с тем, что я не хочу требовать, чтобы локальные переменные были объявлены как mut, если они используются через &mut в замыкании. Другими словами, возьмем некоторый код, например:


fn foo(errors: &mut usize) {
    do_something(|| *errors += 1)
}

Выражение, описывающее замыкание, фактически создаст экземпляр структуры Env:


struct ClosureEnv<'a, 'b> {
    errors: &uniq &mut usize
}

Обратите внимание на ссылку &uniq. Это не то, что может ввести конечный пользователь. Она означает "уникальный, но не обязательно изменяемый" указатель. Это необходимо, чтобы пройти проверку типов. Если бы пользователь попытался написать эту структуру вручную, ему пришлось бы написать &mut &mut usize, что в свою очередь потребовало бы, чтобы параметр errors был объявлен как mut errors: &mut usize.


Незапакованные замыкания и процедуры


Я предвижу, что это ограничение является проблемой для незапакованных замыканий. Позвольте мне подробно остановиться на дизайне, который я обдумывал. В принципе, идея заключалась в том, что выражение || эквивалентно некоторому новому структурному типу, который реализует один из типажей Fn:


trait Fn<A, R> { fn call(&self, ...); }
trait FnMut<A, R> { fn call(&mut self, ...); }
trait FnOnce<A, R> { fn call(self, ...); }

Точный типаж будет выбран по ожидаемому типу, по состоянию на сегодняшний день. В этом случае потребители замыканий могут написать одну из двух вещей:


fn foo(&self, closure: FnMut<usize, usize>) { ... }

fn foo<T: FnMut<usize, usize>>(&self, closure: T) { ... }

Мы … вероятно, захотим устаканить синтаксис, возможно, добавить сахар, такой как FnMut(usize) -> usize, или сохранить |usize| -> usize и т.д. Это не так важно, важно то, что мы будем передавать замыкание по значению. Обратите внимание, что в соответствии с действующими правилами DST (Dynamically-Sized Types) допустимо передавать в типаж тип по значению в качестве аргумента, поэтому аргумент FnMut<usize, usize> является допустимым DST и не является проблемой.


В сторону: этот проект не является полным, и я опишу все подробности в отдельном сообщении.


Проблема в том, что для вызова замыкания потребуется ссылка &mut. Поскольку замыкание передается по значению, пользователям снова придется писать mut там, где он выглядит не к месту:


fn foo(&self, mut closure: FnMut<usize, usize>) {
    let x = closure.call(3);
}

Это та же проблема, что и в примере с Env выше: то, что на самом деле происходит здесь, заключается в том, что типаж FnMut просто хочет уникальную ссылку, но поскольку это не является частью системы типов, он запрашивает изменяемую ссылку.


Теперь мы можем, возможно, обойти это по-разному. Одним из вариантов, что мы могли бы сделать, это чтобы || синтаксис раскрывался бы не в "некий структурный тип", а скорее в "структурный тип или указатель на структурный тип, как это диктуется выводом типов". В этом случае вызывающий мог бы писать:


fn foo(&self, closure: &mut FnMut<usize, usize>) {
    let x = closure.call(3);
}

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


Другие части API


Я не проделывал исчерпывающего исследования, но, естественно, это различие выползает и где-нибудь в другом месте. Например, чтобы читать из Socket, мне нужен уникальный указатель, поэтому я должен объявить его изменяемым. Поэтому иногда подобное не работает:


let socket = Socket::new();
socket.read() // ОШИБКА: нужна изменяемая ссылка

Естественно, согласно моему предложении такой код бы работал нормально. Вы все равно получили бы сообщение об ошибке, если попытались прочитать из &Socket, но тогда оно гласило бы что-то вроде "невозможно создать уникальную ссылку на разделяемую ссылку", что лично я считаю более понятным.


Но разве нам не нужен mut для безопасности?


Нет, вовсе нет. Программы на Rust были бы одинаково хороши, если бы вы просто объявили все привязки как mut. Компилятор отлично способен отслеживать, какие локальные переменные изменяются в любой момент времени — именно потому, что они локальны для текущей функции. То, что систему типов действительно волнует, — это уникальность.


Значение, которое я вижу в текущих правилах применения mut, и я не буду отрицать, что оно имеет ценность, это прежде всего то, что они помогают объявить намерение. То есть, когда я читаю код, я знаю, какие переменные могут быть переназначены. С другой стороны, я также трачу много времени на чтение кода на С++ и, честно говоря, никогда не замечал, чтобы это являлось крупным камнем преткновения. (То же самое касается времени, которое я потратил на чтение кода на Java, JavaScript, Python или Ruby.)


Также верно, что я иногда нахожу баги, потому что объявил переменную как mut и забыл ее изменить. Я думаю, что мы могли бы получить аналогичные преимущества с помощью других, более агрессивных проверок (например, ни одна из переменных, используемых в условии цикла, не изменяется в теле цикла). Я лично не могу вспомнить, чтобы сталкивался с противоположной ситуацией: то есть, если компилятор говорит, что что-то должно быть изменяемым, это в основном всегда означает, что я где-то забыл ключевое слово mut. (Подумайте: когда вы в последний раз реагировали на ошибку компилятора о недопустимом изменении, делая что-то другое, кроме реструктуризации кода, чтобы сделать изменение допустимым?)


Альтернативы


Я вижу три альтернативы текущей системе:


  1. Та, которую я представил, где вы просто выбрасываете "изменяемость" и отслеживаете только уникальность.
  2. Та, где вы имеете три ссылочных типа: &, &uniq и &mut. (Как я писал, фактически это та система типов, которую мы имеем на сегодня, по крайней мере с точки зрения поверщика заимствования (borrow checker).)
  3. Более строгий вариант, в котором "не-mut" переменные всегда считаются допускающими раздельное владение. Это означало бы, что вам придется писать:


    let mut errors = 0;
    let mut p = &mut errors; // Заметьте, что `p` должен быть объявлен, как `mut`.
    *p += 1;

    Вам нужно объявить p как mut, потому что иначе переменная будет считаться допускающей раздельное владение, хотя это и локальная переменная, и, следовательно, изменение *p недопустимо. Что странно в этой схеме, так это то, что локальная переменная НЕ допускает раздельное владение, и мы точно это знаем, поскольку при попытке создать её псевдоним произойдет перемещение, на ней запустится деструктор и т.д. То есть, у нас все еще есть понятие "владеемый", которое отличается от "не допускает раздельного владения".


    С другой стороны, если бы мы описали эту систему, сказав, что изменяемость наследуется через &mut указатели, вообще не заикаясь о разделяемом владении, это могло бы иметь смысл.



Из этих трех я определенно предпочитаю №1. Она самая простая, и сейчас меня больше всего интересует, как мы можем упростить Rust, сохранив его характер. В противном случае, я отдаю предпочтение той, которую мы имеем прямо сейчас.


Заключение


В принципе, я нахожу, что текущие правила, касающиеся изменяемости, имеют определенную ценность, но они дорого обходятся. Они представляют собой сорт протекающей абстракции: то есть они рассказывают простую историю, которая на поверку оказывается неполной. Это приводит к путанице, когда люди переходят от первоначального понимания, в котором &mut отображает то, как работает изменяемость, к полноценному пониманию: иногда mut нужен только для того, чтобы обеспечить уникальность, а иногда и изменямость достигается без ключевого слова mut.


Более того, мы должны действовать с оглядкой, чтобы поддерживать фикцию, что mut обозначает изменяемость, а не уникальность. Мы добавили специальные случаи для поверщика заимствования, чтобы проверять замыкания. Мы должны сделать правила, касающиеся &mut изменяемости, более сложными в целом. Мы должны либо добавить mut к замыканиям, чтобы можно было их вызывать, либо сделать синтаксис замыканий рассахаривающимся менее очевидным образом. И так далее.


В конце концов, всё оборачивается более сложным языком в целом. Вместо того, чтобы просто думать о разделяемом владении и уникальности, пользователь должен думать о разделяемом владении и изменяемости, и оба они каким-то образом перепутались.


Я не думаю, что оно того стоит.

Tags:
Hubs:
+9
Comments3

Articles

Change theme settings