Предыдущий пост Поделиться Следующий пост
На пример
sceptic
slobin

Под катом иллюстрация к моему предыдущему посту про "об учиться". Это я попытался заглянуть в случайный репозиторий на Расте, зная только самые основы языка, и посмотреть, как на нём пишут простые смертные (то есть, не примеры из учебника и не стандартная библиотека). Первоначально это было письмом к конкретному человеку, поэтому где-то могут остаться обращения во втором лице.

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

Нижеследующая функция, судя по комментариям, должна проверить, есть ли в начале массива целых возрастающий интервал, и если да, выдать его максимальное значение. Пример из автора: [1, 2, 3, 5] -- выдать (true, 3). Ну я бы задачу вообще по-другому декомпозировал (декомпонировал?), но не будем прыгать выше джуниора -- что сказали, на то и смотрим. Вот как решает её автор:

fn find_range(list: &Vec<u32>) -> (bool, u32) {
    if list.len() == 0 {
        return (false, 0);
    }

    let mut iterable = list.iter().peekable();
    let mut range_end = match iterable.next() {
        Some(n) => *n,
        None => return (false, 0),
    };

    let mut has_range = false;

    while iterable.peek().is_some() {
        let n = *iterable.next().unwrap();
        if n != range_end + 1 {
            break;
        }

        has_range = true;
        range_end = n;
    }

    (has_range, range_end)
}

fn main() {
    println!("{:?}", find_range(&vec![]));
    println!("{:?}", find_range(&vec![1]));
    println!("{:?}", find_range(&vec![1, 2]));
    println!("{:?}", find_range(&vec![1, 2, 3]));
    println!("{:?}", find_range(&vec![1, 2, 3, 5]));
}

Да, пример взят вот отсюда:

https://github.com/torproject/tor/blob/master/src/rust/protover/protover.rs

У автора честные юниттесты через assert_eq!, я для наглядности вывожу результат просто принтом. Должно получиться:

(false, 0)
(false, 1)
(true, 2)
(true, 3)
(true, 3)

Работать всё работает, но автор явно контужен требованиями стайлгайдов "не обращайтесь к компонентам массива по индексам, пользуйтесь итераторами", а для него это тема явно не родная. Начать с того, что первая проверка (на list.len() == 0) полностью дублируется второй: взять первый элемент итератора и проверить, был ли этот первый элемент вообще. Раст язык хороший, не сделать такой проверки он тебе просто не даст. Но вот первая, на длину, в результате просто лишняя. Выкидываем, результат не изменяется:

fn find_range(list: &Vec<u32>) -> (bool, u32) {
    let mut iterable = list.iter().peekable();
    let mut range_end = match iterable.next() {
        Some(n) => *n,
        None => return (false, 0),
    };

    let mut has_range = false;

    while iterable.peek().is_some() {
        let n = *iterable.next().unwrap();
        if n != range_end + 1 {
            break;
        }

        has_range = true;
        range_end = n;
    }

    (has_range, range_end)
}

Дальше. Автор задействует какой-то peekable. Я эту штуку у него в первый раз вижу (я не знаю языка! а уж библиотек тем более не знаю!), но видно, что товарищ опять же не справился с итераторами, и пытается подглядывать (peek) через дырочку вместо честного решения. Давайте избавимся от лишней сущности, заодно и короче станет:

fn find_range(list: &Vec<u32>) -> (bool, u32) {
    let mut iterable = list.iter();
    let mut range_end = match iterable.next() {
        Some(n) => *n,
        None => return (false, 0),
    };
    let mut has_range = false;
    while let Some(&n) = iterable.next() {
        if n != range_end + 1 {
            break;
        }
        has_range = true;
        range_end = n;
    }
    (has_range, range_end)
}

Ну, я заодно пустые строки убрал, но это я зря, лучше бы оставить. Работает как раньше. Может быть, автор испугался конструкции while let? Может быть, он про неё вообще не знал? А что нам говорит растовский проверяльщик стиля? (...) Он говорит, что я идиот^w слишком умный, и хитрая while let здесь полностью эквивалента обычному for!

fn find_range(list: &Vec<u32>) -> (bool, u32) {
    let mut iterable = list.iter();
    let mut range_end = match iterable.next() {
        Some(n) => *n,
        None => return (false, 0),
    };
    let mut has_range = false;
    for &n in iterable {
        if n != range_end + 1 {
            break;
        }
        has_range = true;
        range_end = n;
    }
    (has_range, range_end)
}

С точки зрения питониста здесь всё понятно, но слегка да, кривовато: мы сначала вызываем первую итерацию итератора вручную, через .next(), а все последующие неявно зовёт цикл for. В общем, не знаю, может быть, так и нормально, но мне слегка некомфортно. Но в любом случае лучше, чем то, что было в самом начале, правда ведь?

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

fn find_range(list: &Vec<u32>) -> (bool, u32) {
    if list.is_empty() {
        return (false, 0);
    }
    let mut range_end = list[0];
    let mut has_range = false;
    for &n in list[1..].iter() {
        if n != range_end + 1 {
            break;
        }
        has_range = true;
        range_end = n;
    }
    (has_range, range_end)
}

По-моему, получился совсем питон (обратите внимание на list[1..]), но, возможно, пуристы будут на явное индексирование ругаться. В общем, не знаю, какой из двух последних вариантов лучше. А ещё всё это можно хардкорно переписать на адаптерах итераторов (все эти map, scan, find), но я не буду, я добрый! ;-)

Ну и чисто по мелочи: is_empty() вместо len() == 0 посоветовал проверяльщик стиля (говорит, так идиоматичнее). Язык достаточно умный, чтобы при попытке сделать цикл прямо по массиву попытаться преобразовать его в итератор самому, но, к сожалению, там получается НЕ ТОТ итератор (подставляется неявное into_iter(), а нам нужен iter()). Поэтому приходится руками.

Все примеры проверены на https://play.rust-lang.org/ , локально себе я его пока так и не поставил.

P.S. После получаса возни с языком убедился, что let mut (вместо var) -- гениальное решение. Все муты ДОЛЖНЫ бросаться в глаза, их должно хотеться убрать! Если бы было var, как в некоторых других современных языках, они бы так и оставались в коде навсегда. А так (в терминах автокодов полувековой давности) любой мут -- это АККУМУЛЯТОР. Не то чтобы аккумуляторы не нужны, но полезно и нужно отличать их от всего остального.

... Привет от Лианта! ...

Метки:

  • 1
А что за странная религия "к элементам массива по индексам не обращаться"? Чем абстракция нумерованного списка, которую люди используют не одно столетие, не угодила? Только тем, что можно выстрелить себе в ногу неправильным индексом? Ну, так во первых, у программиста должна быть голова, чтобы за корректностью индекса проследить, чай не бином Ньютона. А во-вторых, высокоуровневые конструкции предоставляют гораздо более изысканные способы ногостреляния, на первый взгляд гораздо менее заметные.
Ну, а pickable, или как оно там - это вообще за гранью добра и зла, имхо

Ну вот я очень удивлюсь, если в данном примере (мой последний вариант) компилятор не поймёт, что нулевой элемент у массива гарантировано есть, и не выкинет проверку. Ну то есть понятно, что одно обращение вне цикла вообще ни на что не влияет, но чисто на принцип. Компилятор точно знает, что я именно содержательно проверяю массив на пустоту, а не просто что-то непонятное сравниваю (он сам мне об этом сказал!), он точно умеет анализировать поток управления (не инициализированные переменные он ловит), ну и он точно знает, что массив меняться не будет -- вся фишка Раста в том, чтобы для как можно большего числа вещей знать, что они меняться не будут. Если всего этого компилятору мало -- это какой-то совсем игрушечный компилятор. Но в порождаемый код я не смотрел.

... Мои мечты совсем не так просты ...


Вообще, по моему нескромному мнению, эту задачу надо решать или в лоб императивно, и решение займет три строчки. Или уж хардкорно функционально на каком-нибудь Хаскеле. В те же три строчки...

Edited at 2018-03-07 16:40 (UTC)

Если "три строчки" обозначают "пятнадцать" (это нормальное программистское словоупотребление! ;-), то последний вариант -- это оно и есть? В смысле, в лоб императивно? Ну а если "три" -- это хотя бы "десять", то покажите, как? На любом "обычном" языке, только чур, выигрыш за счёт форматирования кода не считаем, считаем содержательно. У меня не выходит.

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

... Эх, Додоша, а ещё математик ...


Три строчки это я слегка утрирую, но в четыре, наверное, влезет.
Первая - обработка случая длины 0 и 1.
Вторая - обработка длины 2. Один сишный оператор "знак вопроса".
Третья - цикл for(), выражающий логику наращивания возрастающего интервала с помощью пары индексов или указателей.
Четвертая - упаковка того, что получилось, из двух указателей в пару из булевской переменной и интервала.
Сорри, я сейчас слегка нездоров, писать реальный код немного тяжело.

gavno kakojeto etot vash Rust.

the_prefix([A, B | Tail]) where A+1 == B -> the_prefix([B|Tail]);
the_prefix([B | _]) -> {true, B};
the_prefix([]) -> {false, 0}.

Ну, всё-таки автор специфицировал свою функцию более хитровы***но, поэтому вот так:

find_prefix([A, B | T], _) when A + 1 == B -> find_prefix([B | T], true);
find_prefix([], _) -> {false, 0};
find_prefix([B | _], Q) -> {Q, B}.

find_prefix(X) -> find_prefix(X, false).

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

ПыСы... как там назывался язычок для финансовой аналитики от Моргана Стэнли, который промелькнул в новостях и исчез? Чтобы делать то, что они раньше делали на k, но потом, видимо, убедились, что найти людей, мозги которых заточены под k, близко к невозможно. А на образцах и "задача сводится к предыдущей" любой дурак писать может (это я о себе, если что).

... И вам семь футов под передними колёсами! ...

tak ponjatneje, i bystree:

the_prefix([]) -> {false, 0};
the_prefix([B]) -> {false, B};
the_prefix(B) -> _prefix(B).
_prefix([A, B | Tail]) when A+1 == B -> _prefix([B|Tail]);
_prefix([B | _]) -> {true, B}.

i kstati vidno chto specifikacija ujebanskaja,
kogda slishkom doxera osobyx sluchajev v shapke, eto znachit zakazchik ne znal kakoje emu na samom dele xotelosj poluchitj znachenije pri "oshibke"

Что за волшебство происходит в этой ветке? Это ещё Rust или уже Haskell?

Эрланг. Я не знаю, при чём тут Эрланг, мы как-то с темы сбились. :-) Но вообще он хорош как "функциональный псевдокод". Когда хочется показать идею, и в общем пофиг на чём.

... Государственный разъяснитель третьего ранга ...


(без темы) (Анонимно) Развернуть
(Анонимно)
И смешно и грустно.

21-го века уже треть скоро,
а люди до сих пор вот такое пишут...

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

ironija v tom chto vse popytki standartizacii "boltov" priveli k kromeshnomu pizdecu (chastiju kojego zaglavnyj primer jestj)

(Анонимно)
Значит чего-то не того стандартизировали... %)

зировать (Анонимно) Развернуть
Re: зировать (Анонимно) Развернуть

Случайное не встречали

(Анонимно)
Раст, как и все функциональщики -- очень похваляются что оно лучшее.

А вот, есть ли где такое, чтобы взяли какой существующий кода,
ну например Юниксовые утилиты,
и переписали на функциональщине.

И Чтоб вот так сразу наглядно было,
какое УГ легаси,
и какое шайнинг ФП? %)

  • 1
?

Log in

No account? Create an account