Разработчик
0,0
рейтинг
17 октября 2014 в 00:01

Разработка → Почему ваша первая реализация FizzBuzz на Rust может не работать перевод

Полное оригинальное название статьи: «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, автор Chris Morgan.

Почему ваша первая реализация FizzBuzz может не работать: исследование некоторых особенностей Rust, которые изначально шокируют, но в действительности являются его лучшими сторонами (хотя они всё равно могут вам не понравиться)

http://chrismorgan.info/media/images/rust-fizzbuzz.svgFizzBuzz предполагается как простое задание для новичка, но в Rust присутствуют несколько подводных камней, о которых лучше знать. Эти подводные камни не являются проблемами Rust, а, скорее, отличиями от того, с чем знакомо большиство программистов, ограничениями, которые на первый взгляд могут показаться очень жёсткими, но в действительности дают громадные преимущества за малой ценой.

Rust это «подвижная мишень», тем не менее, язык становится стабильней. Код из статьи работает с версией 0.12. Если что-то сломается, пожалуйста, свяжитесь со мной. Касательно кода на Python, он будет работать как в двойке, так и в тройке.

Простая реализация


Ок, я сказал в заголовке, что ваша первая реализация FizzBuzz может не работать, чтож, она может и работать. Вы могли бы написать её как в примере ниже. Для краткости опустим fn main() { … }. Если вас беспокоит то, что код на Python оказывается короче, чем на Rust, то для вас есть специальная форма кода на Python, она доступна по нажатию на чекбокс. (прим. пер. в оригинальной статье есть чекбокс, который переключает код на Python в «специальную форму» from __future__ import braces, что-то вроде пасхалки от автора).
Реализация FizzBuzz с отдельными инструкциями print: всё просто работает.
Python Rust
for i in range(1, 101):
    if i % 15 == 0:
        print('FizzBuzz')
    elif i % 5 == 0:
        print('Buzz')
    elif i % 3 == 0:
        print('Fizz')
    else:
        print(i)

for i in range(1i, 101) {
    if i % 15 == 0 {
        println!("FizzBuzz");
    } else if i % 5 == 0 {
        println!("Buzz");
    } else if i % 3 == 0 {
        println!("Fizz");
    } else {
        println!("{}", i);
    }
}


Обе программы производят желаемый результат, и они, очевидно, очень схожи. Главное, что здесь стоит упомянуть, это то, что в Rust println!()[1] требует строковой литерал в качестве первого аргумента, строку формата; соответствующий код на Python будет выглядеть так: print('{}'.format(i)).

Но что если мы хотим избавиться от дублирования вызова print в коде? Вот как это могло бы выглядеть:
FizzBuzz с одной инструкцией print.
Python Rust (не скомпилируется)
for i in range(1, 101):
    if i % 15 == 0:
        result = 'FizzBuzz'
    elif i % 5 == 0:
        result = 'Buzz'
    elif i % 3 == 0:
        result = 'Fizz'
    else:
        result = i
    print(result)

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
    };
    println!("{}", result);
}


Обратите внимание, как в 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] (также известные как языки со сборкой мусора). То есть, у них в рантайме есть механизм, который управляет выделением и освобождением памяти в соответствующее время: когда никто больше не использует строку, она может быть особождена. Таким образом, они могут возвращать ссылку на строку, не волнуясь о её времени жизни: строки, которые ещё используются, не будут особождены.

Для этих языков здесь также есть одна уступка. Как правило, они имеют неизменяемые (иммутабельные) строки — если сконкатенировать две строки, произойдёт выделение памяти (аллокация) под новую строку нужного размера. (Это также означает, что для решения, которое будет конкатенировать Fizz и Buzz в соответствующих случаях, будет происходить две аллокации для чисел, делящихся на 15. Правда, некоторые языки могут немного сгладить этот негативный эффект, применяя то, что называется пулом строк или интернированием. Успех работы этого механизма зависит от оптимизатора и того, как написан код) Я полагаю, строки иммутабельны потому, что в качестве альтернативы у нас будет большее из зол — изменение строки может повлиять на другие строки, зависящие от неё. Это сильно бьёт по корректности программы и может вести к состояниям гонки в том, что по сути является примитивным типом.
Также, для unicode-строк это может вести к возникновению некорректных срезов строк. Конечно, эти проблемы возникают также и в других местах, но иметь их и в строках может быть намного хуже. (Я сказал, что эти языки имеют один строковой тип, но это не совсем так — есть также и специализированные строковые типы, например, как Java, так и .NET имеют механизм, называемый StringBuilder).

Модель Rust отличается от используемой в языках со сборкой мусора и основана на понятии владения (ownership). В этой модели каждый объект имеет одного владельца (прим. пер. is owned) в одном месте, в один момент времени, а в других местах можно безопасно получать указатель на него, одалживать (borrow).

collections::string::String это тип с владением. Это значит, что он имеет исключительное право владения содержимым стоки. Когда объект такого типа покидает свою область видимости (scope), строка освобождается. Поэтому, любая подстрока не может иметь тип String, потому что между строкой и подстрокой не будет связи, и когда первая покинет свою область видимости, вторая станет некорректной. Вместо этого, подстроки (или строковые срезы) используют тип, который является ссылкой на объект, которым владеет кто-то другой — &str. Rust, благодаря концепции времени жизни объекта, способен гарантировать, что ни один строковой срез не переживёт свою исходную строку, таким образом безопасность памяти сохраняется.

В гайде по времени жизни есть более детальное объяснение. Здесь же, если вы видите конструкцию 'такого_вида после ссылочного типа, знайте, это что так определяется время жизни ссылки. Есть специальное время жизни 'static, которое означает, что объект существует в течение всей работы программы. Такие объекты запекаются прямо в исполняемый файл, также как и строковые литералы, которые встречаются в коде — то есть тип строкового литерала &'static str.

Ранее, когда тип ~T был тем, чем сейчас является Box<T>, а str был фейковым, тип ~str представлял собой строковой тип изменяемого размера. Он хранил текущий (size) и максимальный (capacity) размер — как нынешний тип String (который заменил ~str). Предполагалось, что все типы-обёртки будут работать таким образом. Сейчас, Box<T> это простое обёрнутое значение. Вот почему он не используется — не имея дополнительной ёмкости, ему бы потребовалось перевыделять память каждый раз при дописывании в строку. String умеет перевыделять память и делает это по умолчанию. Поэтому разница между Box<str> и &str существенна.

Могу добавить, что во время этого изменения новый тип носил имя StrBuf. На самом деле ситуация не сильно отличается от таковой в других языках. В действительности, это влияние отсутствия обязательной сборки мусора, которая делает некоторые применения &str бестолковыми. В Rust вам придётся обращаться к строковому буферу несколько чаще, чем в других языках, просто потому что другие языки позволяют вам обращаться со своим основным строковым типом более легкомысленно.


Вернёмся к FizzBuzz


То есть, проблема в том, что в одной ветке мы имеем строку с владением, а строки в трёх других являются просто статическими строковыми срезами (ссылками на статически определённые строки). Как же нам разрешить эту проблему? Может попробуем сделать их все строковыми срезами (да-да, тип &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);
}
Делаем всё строками, но не бесплатно для рантайма.

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

Напишем функцию


Мы прошли столько, сколько могли в этом направлении, не скатывая код в абсурд. Как насчёт того, чтобы изменить саму постановку задачи, а именно, что мы не печатаем результат, а возвращаем его из функции?

Начнём с такого кода:
Функция fizz_buzz, возвращающая String.
Python Rust
def fizz_buzz(i):
    if i % 15 == 0:
        return 'FizzBuzz'
    elif i % 5 == 0:
        return 'Buzz'
    elif i % 3 == 0:
        return 'Fizz'
    else:
        return i
 
for i in range(1, 101):
    print(fizz_buzz(i))

fn fizz_buzz(i: int) -> String {
    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()
    }
}
 
for i in range(1i, 101) {
    println!("{}", fizz_buzz(i));
}

Теперь у нас есть дополнительный уровень инкапсуляции. Он демонстрирует как раз тот случай, когда решение с вынесением переменной на уровень выше не будет работать, потому что переменная будет покидать саму функцию.
(Можете попробовать сами; возвращаемое значение функции нельзя представить в системе типов Rust, так как нет подходящего времени жизни — x не получит время жизни 'static, и нет ничего, к чему мы могли бы его привязать.)

Также, так как мы поместили код в функцию, мы выделяем новые строки для тех случаев, когда это не нужно.

Введём SendStr


К счастью, Rust поддерживает алгебраические типы данных (также известные как enum). А также, в стандартной библиотеке есть подходящий тип, который может описать объект, являющийся либо строковым срезом, либо строкой с владением.

Ниже приведено определение такого типа (без описания методов, которые делают его ещё более полезным):
pub enum MaybeOwned<'a> {
    Slice(&'a str),
    Owned(String)
}

pub type SendStr = MaybeOwned<'static>;

Определения MaybeOwned и SendStr из std::str.

Send это ограничение, которое указывает, что объект можно безопасно пересылать между задачами (то есть между потоками, при этом не теряя безопасность по памяти); это также подразумевает, что объект самодостаточен, и может быть возвращён из функции. Пусть есть строка типа &'static str, как в определении SendStr; она не содержит ссылок на какие-либо объекты внутри функции, не так ли? Следовательно, она может существовать столько, сколько потребуется. То же самое верно и для String. Поэтому любой из этих двух объектов может быть захвачен внутри enum-типа, который говорит, что мы владеем одним или другим объектом. Следовательно SendStr удовлетворяет условию Send. Этот тип хранит в себе некоторое значение и пользователь может выполнять над ним разные операции. Сейчас самый примечательный факт в том, что мы можем извлекать строковой срез из этого типа с помощью 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. Это работает.
(.into_maybe_owned() взята из IntoMaybeOwned и доступна по умолчанию)

Круто! Теперь мы уменьшили количество работы, которую нужно выполнять компьютеру и сделали наш общеизвестный пример быстрей.
Но можем ли мы пойти дальше?

Написание собственного enum-типа и реализация типажа std::fmt::Show


Конечно, то, что мы передаём на самом деле не является «строкой», это некоторые значения «Fizz», «Buzz», «FizzBuzz», либо число. Мы просто преобразовали все варианты в строку заранее; мы можем запросто сделать это лениво, избегая лишних аллокаций (в действительности, всех аллокаций здесь можно избежать).

Давайте сделаем собственный enum.
В процессе также реализуем std::fmt::Show для него, что позволит выводить его напрямую в stdout, без необходимости в промежуточной строке.
Применение изолированного типа данных для эффективного представления возможных вариантов значения.
аналог на Python (чрезвычайно натянутый) Rust
class FizzBuzzItem:
 
    def __init__(self, value):
        self._value = value
 
    def __str__(self):
        if self is Fizz:
            return "Fizz"
        elif self is Buzz:
            return "Buzz"
        elif self is FizzBuzz:
            return "FizzBuzz"
        else:
            return str(self._value)
 
# притворимся, что эти типы непрозрачны
Fizz = FizzBuzzItem(object())
Buzz = FizzBuzzItem(object())
FizzBuzz = FizzBuzzItem(object())
 
def Number(number):
    return FizzBuzzItem(number)
 
def fizz_buzz(i):
    if i % 15 == 0:
        return FizzBuzz
    elif i % 5 == 0:
        return Buzz
    elif i % 3 == 0:
        return Fizz
    else:
        return Number(i)
 
for i in range(1, 101):
    print(fizz_buzz(i))

use std::fmt;
 
enum FizzBuzzItem {
    Fizz,
    Buzz,
    FizzBuzz,
    Number(int),
}
 
impl fmt::Show for FizzBuzzItem {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Fizz => f.write(b"Fizz"),
            Buzz => f.write(b"Buzz"),
            FizzBuzz => f.write(b"FizzBuzz"),
            Number(num) => write!(f, "{}", num),
        }
    }
}
 
fn fizz_buzz(i: int) -> FizzBuzzItem {
    if i % 15 == 0 {
        FizzBuzz
    } else if i % 5 == 0 {
        Buzz
    } else if i % 3 == 0 {
        Fizz
    } else {
        Number(i)
    }
}
 
for i in range(1i, 101) {
    println!("{}", fizz_buzz(i));
}


Обратите внимание, что это действительно хороший способ представления данных, хотя мы могли не заморачиваться так сильно в этом случае и просто заменить первые три ветви типом Word(&'static str): Word("FizzBuzz") итд. (По правде, это была первая версия, которую я написал на этом шаге. Даже я был повёрнут на использовании строк там, где этого не требуется!)

Мы могли бы пойти и дальше, написав отдельный итератор, но, учитывая то, как работают итераторы в Rust, это совсем необязательно — можно просто написать range(1, 101).map(fizz_buzz). Это даст намного больше гибкости. Как только где-то будет реализован Iterator<int>, можно будет просто дописать .map(fizz_buzz) в конец и вы получите тип, реализующий Iterator<FizzBuzzItem>.

Цикл можно переписать в этом стиле на раз-два:
Применяем функцию fizz_buzz по итератору целых чисел.
Python Rust
for f in map(fizz_buzz, range(1, 101)):
    print(f)

for f in range(1, 101).map(fizz_buzz) {
    println!("{}", f);
}


Какой бы из способов мы не выбрали, в результате мы получим старый-добрый выхлоп программы FizzBuzz.

Заключение


Теперь вы знаете почему ваша первая реализация FizzBuzz на Rust могла бы не работать. Некоторые из затруднений, описанных в статье, типичны для статически-типизированных языков, некоторые относятся к специфике Rust. (В действительности, ситуация аналогична той же в C++, за той разницей, что C++ позволит вам сделать кучу глупых ошибок и не даёт каких-либо гарантий работы с памятью. Не спорьте со мной по этому поводу, здесь я лишь цитирую других людей, я не знаю C++ в должной степени.)

Мы прошлись по теме модели владения в Rust, и тому как она может помешать вам писать в том стиле, к которому вы привыкли, и почему так (правда, без описания конкретных преимуществ). Также мы упомянули эффективную концепцию enum-типов (алгебраических типов данных), которая позволяет описывать данные более строго и эффективно.

Надеюсь, вы увидели силу всех этих вещей, и она вас заинтересовала.

Является ли описанное дополнительной смысловой нагрузкой? Да.

Это неприятно? Периодически. (Мой опыт говорит, что всё это спасает от затруднений также часто, как и создаёт их.)

Позволяет ли это улучшить эффективность ваших программ? Безусловно, и с полной уверенностью. Раньше эти вещи требовали потери безопасности и корректности, теперь в Rust, вам не требуются подобные компромиссы.

Позволяет ли это упростить пронимание кода? В простых случаях, как этот, особой разницы не видно, но в сложных эти механизмы становятся реальной помощью. (Мне правда недостаёт их в Python.)

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

Следует ли вам использовать Rust? Чтож, я предлагаю хотя бы попробовать его. Вы можете найти его сырым или непригодным для ваших целей из-за его акцента на системном программировании. Для многих высокоуровневых задач он может оказаться несколько громоздким. Но я верю, что придёт время, и это будет классный инструмент для вещей, вроде веб-программирования, о чём я говорил на докладе в StrangeLoop (можете также посмотреть слайды, 2MB SVG).

Наконец, если вы слабо знакомы с Rust или не поняли какую-то часть статьи, я предлагаю вам ознакомиться с официальной документацией; тридцатиминутное введение в Rust описывает концепцию владения достаточно хорошо, а в Гайде хорошо раскрыты enum-типы и многое другое. Также есть более детализованные гайды по конкретным вопросам. Если у вас всё ещё остались вопросы, места вроде канала #rust на irc.mozilla.org могут здорово помочь — я подолгу нахожусь там, мой ник ChrisMorgan.

Ну а если вы по-настоящему любите возиться с оптимизацией FizzBuzz


Да пожалуйста. Это финальная версия, с минимальными поправками, необходимыми, чтобы компилироваться с соверменной версией Rust, и строковой версией OUT для улучшенной читаемости (!?):
#![no_std]
#![feature(asm, lang_items)]
 
extern crate libc;
 
static OUT: &'static [u8] = b"\
    1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n\
    16\n17\nFizz\n19\nBuzz\nFizz\n22\n23\nFizz\nBuzz\n26\nFizz\n28\n29\nFizzBuzz\n\
    31\n32\nFizz\n34\nBuzz\nFizz\n37\n38\nFizz\nBuzz\n41\nFizz\n43\n44\nFizzBuzz\n\
    46\n47\nFizz\n49\nBuzz\nFizz\n52\n53\nFizz\nBuzz\n56\nFizz\n58\n59\nFizzBuzz\n\
    61\n62\nFizz\n64\nBuzz\nFizz\n67\n68\nFizz\nBuzz\n71\nFizz\n73\n74\nFizzBuzz\n\
    76\n77\nFizz\n79\nBuzz\nFizz\n82\n83\nFizz\nBuzz\n86\nFizz\n88\n89\nFizzBuzz\n\
    91\n92\nFizz\n94\nBuzz\nFizz\n97\n98\nFizz\nBuzz\n";
 
#[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.
2. При первом прочтении оригинала может сложиться впечатление, что идёт речь об управляемом коде, однако, здесь имеется ввиду управляемая память. Несмотря на различные формулировки внутри скобок и вне, речь идёт об одном и том же.

Материал достаточно большой, вполне возможны стилистические или смысловые ошибки перевода. Также, в силу того, что я не являюсь экспертом в Rust и статически-типизированных языках, могут возникать неточности в описании некоторых механизмов. В обоих случаях, я буду благодарен, если вы пришлёте мне свои поправки в личных сообщениях.
Спасибо за внимание.
Перевод: Chris Morgan
@StreetStrider
карма
29,7
рейтинг 0,0
Разработчик
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (24)

  • 0
    Кстати в Ruby тоже два типа строк, но там немного другие причины их существования.
    • 0
      Это какие такие 2 типа строк в Ruby?
      • –8
        Строка, которая и есть строка "привет, я строка" — она является самостоятельным объектом. А есть :вот_такая_строка — это уже некая константа (это не совсем так, но как определение сойдёт), она не создаёт объект в полном понимании этого слова. В следствии этого "строка" и "строка" — будут совершенно разными объектами с разными адресами в памяти, но :строка и :строка — будут ссылаться на одну и ту же ячейку памяти.

        Т.е. первый тип нужен для полного доступа к издевательствам над строковыми данными, а второй, допустим, для указания ключа для хеша (ассоциативного массива, например User[:admin])
        • +8
          Счего это символ стал строкой? Symbol это сахар вокруг Fixnum, вовсе не контанта это. Кроме того, в свежем руби символы еще и подвержены GC. Вы еще скажите, что замороженые строки это третий вид строк.
          • +1
            Честно, вообще не знаю что за замороженные строки.

            Просто меня в своё время удивило наличие двух разных типов, которые выглядят, как последовательность чаров, вот и решил упомянуть. Ну значит я ошибаюсь — в руби только один тип строк, а другой — это Symbol и не является набором чаров. Думаю Вам, как более сведующему в тонкостях этого, безоговорочно красивого языка — всё же виднее.
            • +3
              2.1.2 :002 > a = «a».freeze; b = «a».freeze; c = «a»
              2.1.2 :005 > a.object_id == b.object_id
              => true
              2.1.2 :006 > a.object_id == c.object_id
              => false

              hash1 = { «string» => «value»}
              hash2 = { «string» => «value»}
              Создаст 6 объектов

              hash1 = { :symbol => «value»}
              hash2 = { :symbol => «value»}
              Создаст 4-5 объекта (символ может быть уже создан)

              Символы же являются по-сути строкой только в редакторе. RubyVM с ними уже работает как с числами. Отсюда и профит их использования — они занимают меньше места (но не собираются GC, до недавнего времени), и поиск по hash для них быстрее.

              Можно даже вызвать Symbol#all_symbols и получить массив всех символов в системе.
            • +1
              Насколько подсказывает мой опыт, тип Символ это аналог перечислимого значения, уникального в пределах всей программы. Т.е., он выполняет роль некоторого уникального (но при этом читаемого в коде) идентификатора. Символ ближе к enum, чем к строке (просто мы его видим как строку), все символы лежат как бы внутри глобального enum на всю программу. Если сравнить это со статьёй, то автор тоже вводит FizzBuzzItem с перечислимыми значениями, которые суть символы, уникальные в рамках этого типа.

              Главная проблема, как указывают выше, это то, что символы во многих языках с gc не утилизируются (например, если мне не изменяет память, в erlang). Если в рубине утилизируются, то это очень хорошо, это очень здорово.
              • 0
                Как я писал выше, в рубине они собираются только в 2.2 и только MRI — bugs.ruby-lang.org/issues/9634
  • +5
    (В действительности, ситуация аналогична той же в C++, за той разницей, что C++ позволит вам сделать кучу глупых ошибок и не даёт каких-либо гарантий работы с памятью. Не спорьте со мной по этому поводу, здесь я лишь цитирую других людей, я не знаю C++ в должной степени.)

    Мне нравится, как в каждой статье о Rust, упоминается ужасный и неконтролируемый C++ с кучей проблем. В данной ситуации — статья — чисто о Rust — никаких сравнений с C++ — нет, и если Вы (автор оригинальной статьи) что-то, где-то слышали, что, мол C++ «Ужас и боль», то это не значит, что об этом нужно упоминать беспричинно и безаргументно. Просто ужасно надоело этот шаблон: «Вот — язык Х, а в С++ — будут страшные проблемы, если сделать так же». И в 90% приводится код, который на плюсах никто и никогда не пишет.
    • +2
      Учитывая, что очень многие видят в Rust`е потенциальную безопасную альтернативу C++, то и упоминания C++ в разговорах о Rust мне не кажутся чем-то странным. В данном случае C++ упомянут как пример другого относительного низкоуровневого языка со статической типизацией и схожими сложностями при работе со строками.
    • +4
      >> в 90% приводится код, который на плюсах никто и никогда не пишет.

      Обычно (в презентациях и статьях про Rust, которые мне вспомнились) приводятся короткие примеры, которые ясно демонстрируют проблему. Да, на практике никто прямо так не напишет, но реальные ошибки часто «размазаны» по большому количеству кода — не заталкивать же сотни строчек кода в презентацию.
    • +1
      Не воспринимайте эту фразу, как C++ — плохой, а X — хороший.
      Скорее, это значит, что C++, за свою долгую жизнь оброс различными здоровыми практиками и идиомами, но даже при всём желании комитета разработчиков он не сможет ввести некоторые фичи, в силу громадного груза обратной совместимости.
      С другой стороны, язык X (Rust, например), не имеет такого груза; он может свободно заимствовать лучшие идеи из C++ и других языков, и, возможно, когда-нибудь станет следующим словом в системном программировании.
      Не нужно всем сразу бросать C++ и кидаться писать на Rust. C++ это основа, которая всем нужна, на нём написано громадное количество жизненно важного для индустрии кода, но это не значит, что он никогда не будет заменён чем-то новым.
    • +3
      Я не фанат Rust, но совсем недавно сделал ошибку в C++, с которой довольно долго разбирался, и которую мог бы предотвратить borrow checker из Rust.
      Обсуждение проблемы — gcc.gnu.org/bugzilla/show_bug.cgi?id=60966
      Мой код был весьма похож на пример из баг-трекера:

      {
          std::promise<void> promise;
          auto future = promise.get_future();
          io.dispatch([this, &promise] {
              doSomething();
              promise.set_value();
          });
          future.get();
      }
      

      Суть в том, что future.get() получал управление до того, как завершался вызов promise.set_value(). В результате set_value() обращался к полям, которые деструктор ~std::promise() уже уничтожил.
      Нужно передавать владение promise в лямбда-функцию, но синтаксис С++11 не позволяет этого сделать, так можно только в C++14.

      Код ревьюили несколько человек, но ошибку никто не заподозрил.
      • 0
        С другой стороны, вполне возможно написать такую реализацию promise, в которой передача владения именно в этом случае не будет требоваться. А значит, должен быть способ указать компилятору, что некоторый объект можно использовать таким образом.

        Но тогда есть способ ошибочно указать компилятору то же самое — а значит, borrow checker не сможет гарантировать отсутствие ошибки в подобном коде при условии наличия бага в используемой библиотеке…
      • 0
        future.wait();
        

        Для кого придуман был?
        • 0
          А с ним что, такой ошибки не будет?
          • 0
            Да, что-то я сморозил — get блокирующий.
  • +3
    Напоминаю, что FuzzBuzz — это такая задача, которую надо решать не только правильно, но еще и быстро :) Так что автору надо было просто остановиться на первом варианте, и не пытаться избавиться от повторных вызовов println. Тем более, что в итоге код стал сложнее, а не проще.
    • +3
      Автор оригинальной статьи, думаю, никаких сложностей с fizzbuzz в любом варианте не испытывает, так как является одним из разработчиков Rust`а. Статья скорее о том, что многие новички, особенно имеющие только опыт работы с высокоуровневыми языками, часто спотыкаются на строках. Для работы с ними, все-таки, уже надо более-менее осознать систему типов и систему владения. Еще людей сильно смущает то, что просто типа «str» в языке нет.
      • 0
        Еще людей сильно смущает то, что просто типа «str» в языке нет.

        Вчера нет, а сегодня есть! После введения типов с динамическим размером (DST, dynamically sized types) `str` — полноценный и самостоятельный тип (так же как и срез `[T]`). Другое дело, что из-за его динамического размера с ним самим многого не сделаешь и вся работа ведётся через «толстые» ссылки `&str`, хранящие указатель на `str` и его размер. Не думаю, правда, что это сильно уменьшит смущение людей.
  • +1
    В статье не даётся ответ на поставленный в заголовке вопрос, т.к. первая приведённая программа (внезапно!) работает. Можно записать её короче, что далее и рассматривается, но если программа на известные входные данные выдаёт ожидаемые выходные — она, как правило, считается рабочей.
  • +4
    Это очень знакомый подход для рубистов, но не для питонистов, потому что в Python всё является инструкцией, а не выражением

    Пф.

    Не повторяйте это в продакшене
    for i in range(1, 101):
        res = 'FizzBuzz'  if i % 15 == 0 \ 
              else 'Buzz' if i % 5 == 0 \ 
              else 'Fizz' if i % 3 == 0 \ 
              else i
        print(res)

    И это тоже не повторяйте
    for res in ('FizzBuzz'  if i % 15 == 0 \
                else 'Buzz' if i % 5 == 0 \
                else 'Fizz' if i % 3 == 0 \
                else i
                for i in range (1, 101)):
        print(res)

    А сюда вообще не смотрите
    from collections import deque
    
    deque((print('FizzBuzz'  if i % 15 == 0 \
                 else 'Buzz' if i % 5 == 0 \
                 else 'Fizz' if i % 3 == 0 \
                 else i)
           for i in range (1, 101)),
          maxlen=1).pop()
    

    :-)
    • 0
      А зачем в последнем примере дека? Засуньте просто все это в list comprehension, а потом сделайте на него [0] — и кода меньше будет, и понятней.
      • 0
        При N=100 в принципе можно засунуть, но вообще мне жалко памяти :-)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.