- PVSM.RU - https://www.pvsm.ru -
Полное оригинальное название статьи: «Why your first FizzBuzz implementation may not work: an exploration into some initially surprising but great parts of Rust (though you still might not like them)»
tl;dr;-версия: На первый взгляд некоторые аспекты Rust могут показаться странными и даже неудобными, однако, они оказываются весьма удачными для языка, который позиционируется как системный. Концепции владения (ownership) и времени жизни (lifetime) позволяют привнести в язык сильные статические гарантии и сделать программы на нём эффективными и безопасными, как по памяти, так и по времени.
Лицензия: CC-BY [1], автор Chris Morgan [2].
FizzBuzz предлагается как простое задание для новичка, но в Rust присутствуют несколько подводных камней, о которых лучше знать. Эти подводные камни не являются проблемами Rust, а, скорее, отличиями от того, с чем знакомо большиство программистов, ограничениями, которые на первый взгляд могут показаться очень жёсткими, но в действительности дают громадные преимущества за малой ценой.
Rust это движущаяся цель, тем не менее, язык становится стабильней. Код из статьи работает с версией 0.12. Если что-то сломается, пожалуйста, свяжитесь со мной [2]. Касательно кода на Python, он будет работать как в двойке, так и в тройке.
Ок, я сказал в заголовке, что ваша первая реализация FizzBuzz может не работать, чтож, она может и работать. Вы могли бы написать её как в примере ниже. Для краткости опустим fn main() { … }
. Если вас беспокоит то, что код на Python оказывается короче, чем на Rust, то для вас есть специальная форма кода на Python, она доступна по нажатию на чекбокс. (прим. пер. в оригинальной статье есть чекбокс, который переключает код на Python в «специальную форму» from __future__ import braces
, что-то вроде пасхалки от автора).
Python | Rust |
---|---|
|
|
Обе программы производят желаемый результат, и они, очевидно, очень схожи. Главное, что здесь стоит упомянуть, это то, что в Rust println!()
[1] [3] требует строковой литерал в качестве первого аргумента, строку формата; соответствующий код на Python будет выглядеть так: print('{}'.format(i))
.
Но что если мы хотим избавиться от дублирования вызова print
в коде? Вот как это могло бы выглядеть:
Python | Rust (не скомпилируется) |
---|---|
|
|
Обратите внимание, как в Rust мы можем использовать целый блок
if
как выражение. Даже результат присваивания в действительности не нужен, мы могли бы просто запихнуть весь блок в ту конструкцию, где он используется. Это очень знакомый подход для рубистов, но не для питонистов, потому что в Python всё является инструкцией, а не выражением. Если вы скептически относитесь к этому подходу, я понимаю; когда я начал знакомство с Rust, его уклон в выражения и возможность опускать инструкциюreturn
показались мне чудн ́ыми. Но, используя Rust, я осознал, что это совсем не так. На самом деле это здорово.Python был моим любимым языком в течении пяти лет, но, несмотря на то, что я продолжаю профессионально писать на этом языке (правда, хотел бы переключиться на Rust), я обнаружил, что мне всё чаще не хватает фишек из Rust. При работе с Rust я не чувствую такого же недостатка чего-либо из Python, если не считать нужды в библиотеках, которых ещё нет в Rust. Вообще, Rust очень сильно принизил Python в моих глазах.
Код на Rust выглядит хорошо, но в действительности он не работает из-за строгих правил типизации в этом языке. Так какой же тип переменной result
? Первые три ветви if
возвращают строки, а четвёртая — целое число:
f.rs:7:12: 11:6 error: if and else have incompatible types: expected `&'static str`, found `int` (expected &-ptr, found int) f.rs:7 } else if i % 3 == 0 { f.rs:8 "Fizz" f.rs:9 } else { f.rs:10 i f.rs:11 }; error: aborting due to previous error
Это не работает. Как насчёт того, чтобы превратить число в строку?
for i in range(1i, 101) {
let result = if i % 15 == 0 {
"FizzBuzz"
} else if i % 5 == 0 {
"Buzz"
} else if i % 3 == 0 {
"Fizz"
} else {
i.to_string()
};
println!("{}", result);
}
Здесь мы утянули фишку, с которой многие знакомы из других языков программирования (to_string
) и применили её в той области, в которой не так много кто разбирается. В общем, это не работает.
f.rs:7:12: 11:6 error: if and else have incompatible types: expected `&'static str`, found `collections::string::String` (expected &-ptr, found struct collections::string::String) f.rs:7 } else if i % 3 == 0 { f.rs:8 "Fizz" f.rs:9 } else { f.rs:10 i.to_string() f.rs:11 }; error: aborting due to previous error
«Что?» Я так и слышу, как вы говорите «разве теперь они не все строки? В чём дело с этим &'static str
(да как это вообще, чёрт возьми, произнести?) и collections::string::String
?». На этом этапе нам стоит более тщательно подойти к анализу типов значений, производимых ветвями: первые три ветви не производят просто какую-то «строку», они производят &'static str
, а четвёртая ветвь не производит просто «целое», а int
. В языках навроде Python, Ruby и JavaScript типы целых объединены (а JS пошёл ещё дальше, и вовсе объединил все числовые типы), в то время как C#, Java, Go имеют множество типов целых, различающихся размером. Но даже языки типа C#, Java, Go имеют всего один тип для строки.
А Rust — нет. У него их два.
Здесь мы могли бы ограничиться простым объяснением и пойти дальше, но раз уж мы спустились так глубоко, то почему бы не спуститься до конца, и понять, что именно было сделано, и почему оно абсолютно того стоит. Так почему же C♯, Java и Go смогли удовлетвориться одним строковым типом, а Rust нет? Чтобы ответить на этот вопрос мы должны спуститься на уровень управления памятью.
Как C♯, так Java и Go — все они являются управляемыми языками[2] [4] (также известные как языки со сборкой мусора). То есть, у них в рантайме есть механизм, который управляет выделением и освобождением памяти в соответствующее время: когда никто больше не использует строку, она может быть особождена. Таким образом, они могут возвращать ссылку на строку, не волнуясь о её времени жизни: строки, которые ещё используются, не будут особождены.
Для этих языков здесь также есть одна уступка. Как правило, они имеют неизменяемые (иммутабельные) строки — если сконкатенировать две строки, произойдёт выделение памяти (аллокация) под новую строку нужного размера. (Это также означает, что для решения, которое будет конкатенировать Fizz и Buzz в соответствующих случаях, будет происходить две аллокации для чисел, делящихся на 15. Правда, некоторые языки могут немного сгладить этот негативный эффект, применяя то, что называется пулом строк [5] или интернированием. Успех работы этого механизма зависит от оптимизатора и того, как написан код) Я полагаю, строки иммутабельны потому, что в качестве альтернативы у нас будет большее из зол — изменение строки может повлиять на другие строки, зависящие от неё. Это сильно бьёт по корректности программы и может вести к состояниям гонки [6] в том, что по сути является примитивным типом.
Также, для unicode-строк это может вести к возникновению некорректных срезов строк. Конечно, эти проблемы возникают также и в других местах, но иметь их и в строках может быть намного хуже. (Я сказал, что эти языки имеют один строковой тип, но это не совсем так — есть также и специализированные строковые типы, например, как Java, так и .NET имеют механизм, называемый StringBuilder
).
Модель Rust отличается от используемой в языках со сборкой мусора и основана на понятии владения (ownership). В этой модели каждый объект имеет одного владельца (прим. пер. is owned) в одном месте, в один момент времени, а в других местах можно безопасно получать указатель на него, одалживать (borrow).
collections::string::String
это тип с владением. Это значит, что он имеет исключительное право владения содержимым стоки. Когда объект такого типа покидает свою область видимости (scope), строка освобождается. Поэтому, любая подстрока не может иметь тип String
, потому что между строкой и подстрокой не будет связи, и когда первая покинет свою область видимости, вторая станет некорректной. Вместо этого, подстроки (или строковые срезы) используют тип, который является ссылкой на объект, которым владеет кто-то другой — &str
. Rust, благодаря концепции времени жизни объекта, способен гарантировать, что ни один строковой срез не переживёт свою исходную строку, таким образом безопасность памяти сохраняется.
В гайде по времени жизни [7] есть более детальное объяснение. Здесь же, если вы видите конструкцию 'такого_вида
после ссылочного типа, знайте, это что так определяется время жизни ссылки. Есть специальное время жизни 'static
, которое означает, что объект существует в течение всей работы программы. Такие объекты запекаются прямо в исполняемый файл, также как и строковые литералы, которые встречаются в коде — то есть тип строкового литерала &'static str
.
Ранее, когда тип
~T
был тем, чем сейчас являетсяBox<T>
, аstr
был фейковым, тип~str
представлял собой строковой тип изменяемого размера. Он хранил текущий (size) и максимальный (capacity) размер — как нынешний типString
(который заменил~str
). Предполагалось, что все типы-обёртки будут работать таким образом. Сейчас,Box<T>
это простое обёрнутое значение. Вот почему он не используется — не имея дополнительной ёмкости, ему бы потребовалось перевыделять память каждый раз при дописывании в строку.String
умеет перевыделять память и делает это по умолчанию. Поэтому разница междуBox<str>
и&str
существенна.Могу добавить, что во время этого изменения новый тип носил имя
StrBuf
. На самом деле ситуация не сильно отличается от таковой в других языках. В действительности, это влияние отсутствия обязательной сборки мусора, которая делает некоторые применения&str
бестолковыми. В Rust вам придётся обращаться к строковому буферу несколько чаще, чем в других языках, просто потому что другие языки позволяют вам обращаться со своим основным строковым типом более легкомысленно.
То есть, проблема в том, что в одной ветке мы имеем строку с владением, а строки в трёх других являются просто статическими строковыми срезами (ссылками на статически определённые строки). Как же нам разрешить эту проблему? Может попробуем сделать их все строковыми срезами (да-да, тип &str
любого времени жизни 'a
мы можем неявно привести к 'b
, если 'a
дольше, чем 'b
. Так как 'static
дольше, чем что-либо, компилятор может свободно преобразовать его к подходящему времени жизни):
for i in range(1i, 101) {
let result = if i % 15 == 0 {
"FizzBuzz"
} else if i % 5 == 0 {
"Buzz"
} else if i % 3 == 0 {
"Fizz"
} else {
i.to_string().as_slice()
};
println!("{}", result);
}
Выглядит как хорошая идея, да? Простите, это тоже не сработает:
f.rs:10:9: 10:22 error: borrowed value does not live long enough f.rs:10 i.to_string().as_slice() ^~~~~~~~~~~~~ f.rs:2:25: 13:2 note: reference must be valid for the block at 2:24... f.rs:2 for i in range(1i, 101) { f.rs:3 let result = if i % 15 == 0 { f.rs:4 "FizzBuzz" f.rs:5 } else if i % 5 == 0 { f.rs:6 "Buzz" f.rs:7 } else if i % 3 == 0 { ... f.rs:9:12: 11:6 note: ...but borrowed value is only valid for the expression at 9:11 f.rs:9 } else { f.rs:10 i.to_string().as_slice() f.rs:11 }; error: aborting due to previous error
Здесь мы упираемся во время жизни: строка, порождённая в i.to_string()
не хранится достаточное время и освобождается в конце блока. Таким образом, ссылка на неё также не может покинуть блок. Это потенциальный баг, связанный с ссылкой на невалидную память, который компилятор Rust успешно поймал. В некоторых языках это называется «висящий указатель» и это Очень Плохо.
Здесь мы можем просто поднять строковую переменную за блок, нам достаточно, чтобы строка была валидной в течение тела цикла. Иногда вы будете сталкиваться с ситуациями, в которых этого будет достаточно, но зачастую — нет.
for i in range(1i, 101) {
let x;
let result = if i % 15 == 0 {
"FizzBuzz"
} else if i % 5 == 0 {
"Buzz"
} else if i % 3 == 0 {
"Fizz"
} else {
x = i.to_string();
x.as_slice()
};
println!("{}", result);
}
Размещаем ссылку в охватывающем блоке, это работает.
String
?Мы можем пойти и в обратном направлении, обязав все ветви возвращать строки с владением:
for i in range(1i, 101) {
let result = if i % 15 == 0 {
"FizzBuzz".to_string()
} else if i % 5 == 0 {
"Buzz".to_string()
} else if i % 3 == 0 {
"Fizz".to_string()
} else {
i.to_string()
};
println!("{}", result);
}
Делаем всё строками, но не бесплатно для рантайма.
Этот подход работает хорошо, но он означает, что для каждой итерации будет выделяться память, не только для тех, в которых мы получаем число.
Мы прошли столько, сколько могли в этом направлении, не скатывая код в абсурд. Как насчёт того, чтобы изменить саму постановку задачи, а именно, что мы не печатаем результат, а возвращаем его из функции?
Начнём с такого кода:
Python | Rust |
---|---|
|
|
Теперь у нас есть дополнительный уровень инкапсуляции. Он демонстрирует как раз тот случай, когда решение с вынесением переменной на уровень выше не будет работать, потому что переменная будет покидать саму функцию.
(Можете попробовать сами [8]; возвращаемое значение функции нельзя представить в системе типов Rust, так как нет подходящего времени жизни — x
не получит время жизни 'static
, и нет ничего, к чему мы могли бы его привязать.)
Также, так как мы поместили код в функцию, мы выделяем новые строки для тех случаев, когда это не нужно.
SendStr
К счастью, Rust поддерживает алгебраические типы данных [9] (также известные как enum
). А также, в стандартной библиотеке есть подходящий тип, который может описать объект, являющийся либо строковым срезом, либо строкой с владением.
Ниже приведено определение такого типа (без описания методов, которые делают его ещё более полезным):
pub enum MaybeOwned<'a> {
Slice(&'a str),
Owned(String)
}
pub type SendStr = MaybeOwned<'static>;
Определения MaybeOwned
[10] и SendStr
[11] из std::str
[12].
Send
это ограничение, которое указывает, что объект можно безопасно пересылать между задачами (то есть между потоками, при этом не теряя безопасность по памяти); это также подразумевает, что объект самодостаточен, и может быть возвращён из функции. Пусть есть строка типа &'static str
, как в определении SendStr
; она не содержит ссылок на какие-либо объекты внутри функции, не так ли? Следовательно, она может существовать столько, сколько потребуется. То же самое верно и для String
. Поэтому любой из этих двух объектов может быть захвачен внутри enum
-типа, который говорит, что мы владеем одним или другим объектом. Следовательно SendStr
удовлетворяет условию Send
. Этот тип хранит в себе некоторое значение и пользователь может выполнять над ним разные операции [13]. Сейчас самый примечательный факт в том, что мы можем извлекать строковой срез из этого типа с помощью as_slice()
. Данный тип также реализует std::fmt::Show
, что означает, что мы можем использовать его в форматированном выводе напрямую, указывая {}
(типаж Show
это прямой аналог __str__()
в Python или to_s()
, toString()
, &c
в других языках, но он работает напрямую с объектом writer
, что позволяет избавиться от промежуточного строкового объекта. Вызов to_string()
на любом типе, реализующем Show
также вызывает этот механизм).
Вот как выглядит применение:
use std::str::SendStr;
fn fizz_buzz(i: int) -> SendStr {
if i % 15 == 0 {
"FizzBuzz".into_maybe_owned()
} else if i % 5 == 0 {
"Buzz".into_maybe_owned()
} else if i % 3 == 0 {
"Fizz".into_maybe_owned()
} else {
i.to_string().into_maybe_owned()
}
}
for i in range(1i, 101) {
println!("{}", fizz_buzz(i));
}
Функция fizz_buzz возвращает SendStr
[11]. Это работает.
(.into_maybe_owned()
взята из IntoMaybeOwned
[14] и доступна по умолчанию [15])
Круто! Теперь мы уменьшили количество работы, которую нужно выполнять компьютеру и сделали наш общеизвестный пример быстрей.
Но можем ли мы пойти дальше?
enum
-типа и реализация типажа std::fmt::Show
Конечно, то, что мы передаём на самом деле не является «строкой», это некоторые значения «Fizz», «Buzz», «FizzBuzz», либо число. Мы просто преобразовали все варианты в строку заранее; мы можем запросто сделать это лениво, избегая лишних аллокаций (в действительности, всех аллокаций здесь можно избежать).
Давайте сделаем собственный enum
.
В процессе также реализуем std::fmt::Show
для него, что позволит выводить его напрямую в stdout
, без необходимости в промежуточной строке.
аналог на Python (чрезвычайно натянутый) | Rust |
---|---|
|
|
Обратите внимание, что это действительно хороший способ представления данных, хотя мы могли не заморачиваться так сильно в этом случае и просто заменить первые три ветви типом Word(&'static str)
: Word("FizzBuzz")
итд. (По правде, это была первая версия, которую я написал на этом шаге. Даже я был повёрнут на использовании строк там, где этого не требуется!)
Мы могли бы пойти и дальше, написав отдельный итератор, но, учитывая то, как работают итераторы в Rust, это совсем необязательно — можно просто написать range(1, 101).map(fizz_buzz)
. Это даст намного больше гибкости. Как только где-то будет реализован Iterator<int>
, можно будет просто дописать .map(fizz_buzz)
в конец и вы получите тип, реализующий Iterator<FizzBuzzItem>
.
Цикл можно переписать в этом стиле на раз-два:
Python | Rust |
---|---|
|
|
Какой бы из способов мы не выбрали, в результате мы получим старый-добрый выхлоп программы FizzBuzz.
Теперь вы знаете почему ваша первая реализация FizzBuzz на Rust могла бы не работать. Некоторые из затруднений, описанных в статье, типичны для статически-типизированных языков, некоторые относятся к специфике Rust. (В действительности, ситуация аналогична той же в C++, за той разницей, что C++ позволит вам сделать кучу глупых ошибок и не даёт каких-либо гарантий работы с памятью. Не спорьте со мной по этому поводу, здесь я лишь цитирую других людей, я не знаю C++ в должной степени.)
Мы прошлись по теме модели владения в Rust, и тому как она может помешать вам писать в том стиле, к которому вы привыкли, и почему так (правда, без описания конкретных преимуществ). Также мы упомянули эффективную концепцию enum
-типов (алгебраических типов данных), которая позволяет описывать данные более строго и эффективно.
Надеюсь, вы увидели силу всех этих вещей, и она вас заинтересовала.
Является ли описанное дополнительной смысловой нагрузкой? Да.
Это неприятно? Периодически. (Мой опыт говорит, что всё это спасает от затруднений также часто, как и создаёт их.)
Позволяет ли это улучшить эффективность ваших программ? Безусловно, и с полной уверенностью. Раньше эти вещи требовали потери безопасности и корректности, теперь в Rust, вам не требуются подобные компромиссы.
Позволяет ли это упростить пронимание кода? В простых случаях, как этот, особой разницы не видно, но в сложных эти механизмы становятся реальной помощью. (Мне правда недостаёт их в Python.)
Подводя итог по этим концепциям, можно сказать, что есть плохая и хорошая стороны: иногда вы будете любить их, иногда ненавидеть. Но, по крайней мере, я ненавижу их не так часто.
Следует ли вам использовать Rust? Чтож, я предлагаю хотя бы попробовать его. Вы можете найти его сырым или непригодным для ваших целей из-за его акцента на системном программировании. Для многих высокоуровневых задач он может оказаться несколько громоздким. Но я верю, что придёт время, и это будет классный инструмент для вещей, вроде веб-программирования, о чём я говорил на докладе в StrangeLoop [16] (можете также посмотреть слайды, 2MB SVG [17]).
Наконец, если вы слабо знакомы с Rust или не поняли какую-то часть статьи, я предлагаю вам ознакомиться с официальной документацией [18]; тридцатиминутное введение в Rust [19] описывает концепцию владения достаточно хорошо, а в Гайде [20] хорошо раскрыты enum
-типы и многое другое. Также есть более детализованные гайды по конкретным вопросам [21]. Если у вас всё ещё остались вопросы, места вроде канала #rust на irc.mozilla.org [22] могут здорово помочь — я подолгу нахожусь там, мой ник ChrisMorgan.
Да пожалуйста [23]. Это финальная версия, с минимальными поправками, необходимыми, чтобы компилироваться с соверменной версией Rust, и строковой версией OUT
для улучшенной читаемости (!?):
#![no_std]
#![feature(asm, lang_items)]
extern crate libc;
static OUT: &'static [u8] = b"
1n2nFizzn4nBuzznFizzn7n8nFizznBuzzn11nFizzn13n14nFizzBuzzn
16n17nFizzn19nBuzznFizzn22n23nFizznBuzzn26nFizzn28n29nFizzBuzzn
31n32nFizzn34nBuzznFizzn37n38nFizznBuzzn41nFizzn43n44nFizzBuzzn
46n47nFizzn49nBuzznFizzn52n53nFizznBuzzn56nFizzn58n59nFizzBuzzn
61n62nFizzn64nBuzznFizzn67n68nFizznBuzzn71nFizzn73n74nFizzBuzzn
76n77nFizzn79nBuzznFizzn82n83nFizznBuzzn86nFizzn88n89nFizzBuzzn
91n92nFizzn94nBuzznFizzn97n98nFizznBuzzn";
#[start]
fn start(_argc: int, _argv: *const *const u8) -> int {
unsafe {
asm!(
"
mov $$1, %rax
mov $$1, %rdi
mov $0, %rsi
mov $$0x19d, %rdx
syscall
"
:
: "r" (&OUT[0])
: "rax", "rdi", "rsi", "rdx"
:
);
}
0
}
#[lang = "stack_exhausted"] extern fn stack_exhausted() {}
#[lang = "eh_personality"] extern fn eh_personality() {}
#[lang = "fail_fmt"] extern fn fail_fmt() {}
Примечания переводчика:
1. Rust имеет развитую систему макросов, в данном случае println!
в compile-time разворачивается в специализированный под конкретный тип вызов println
.
управляемом коде [24], однако, здесь имеется ввиду управляемая память. Несмотря на различные формулировки внутри скобок и вне, речь идёт об одном и том же.
Материал достаточно большой, вполне возможны стилистические или смысловые ошибки перевода. Также, в силу того, что я не являюсь экспертом в Rust и статически-типизированных языках, могут возникать неточности в описании некоторых механизмов. В обоих случаях, я буду благодарен, если вы пришлёте мне свои поправки в личных сообщениях.
Спасибо за внимание.
Автор: StreetStrider
Источник [25]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/71934
Ссылки в тексте:
[1] CC-BY: http://creativecommons.org/licenses/by/3.0/
[2] Chris Morgan: http://chrismorgan.info/
[3] [1]: #r1
[4] [2]: #r2
[5] пулом строк: http://ru.wikipedia.org/wiki/Пул строк
[6] состояниям гонки: http://ru.wikipedia.org/wiki/Состояние гонки
[7] гайде по времени жизни: http://doc.rust-lang.org/guide-lifetimes.html
[8] Можете попробовать сами: http://is.gd/vd6Amb
[9] алгебраические типы данных: http://ru.wikipedia.org/wiki/Алгебраический тип данных
[10] MaybeOwned
: http://doc.rust-lang.org/std/str/enum.MaybeOwned.html
[11] SendStr
: http://doc.rust-lang.org/std/str/type.SendStr.html
[12] std::str
: http://doc.rust-lang.org/std/str/index.html
[13] выполнять над ним разные операции: http://doc.rust-lang.org/std/str/enum.MaybeOwned.html#methods
[14] IntoMaybeOwned
: http://doc.rust-lang.org/std/str/trait.IntoMaybeOwned.html
[15] доступна по умолчанию: http://doc.rust-lang.org/std/prelude/index.html
[16] я говорил на докладе в StrangeLoop: https://www.youtube.com/watch?v=jVoFws7rp88
[17] 2MB SVG: http://chrismorgan.info/media/misc/fast-secure-safe-the-web-that-can-still-be.svg
[18] официальной документацией: http://doc.rust-lang.org/
[19] тридцатиминутное введение в Rust: http://doc.rust-lang.org/intro.html
[20] Гайде: http://doc.rust-lang.org/guide.html
[21] более детализованные гайды по конкретным вопросам: http://doc.rust-lang.org/#guides
[22] #rust на irc.mozilla.org: http://irc://irc.mozilla.org/#rust
[23] Да пожалуйста: http://www.reddit.com/r/rust/comments/27ziqs/some_issue_regarding_obsolete_tilde_syntax/ci5xlrq
[24] управляемом коде: http://http://ru.wikipedia.org/wiki/Управляемый код
[25] Источник: http://habrahabr.ru/post/240617/
Нажмите здесь для печати.