1 Востаннє редагувалося koala (25.09.2016 19:13:04)

Тема: Вивчення Rust

Вирішив таки повивчати Rust, побачивши це.

Щоб зайвий раз не переходити
http://replace.org.ua/extensions/om_images/img/57dc6ccbb6acc/5459e951816323ef6deb3af1d6e5bd3a.jpg

Треба ж кудись рухатися, правильно?
Спробую сюди писати думки і враження. Це стосується не стільки Rust, скільки взагалі концепцій, які я вивчатиму - гадаю, вони для Rust не унікальні.
Отже, в двох словах. Rust - пам'яте- і нитевобезпечна мова з абстракціями нульової вартості. Тобто немає ніяких збиральників сміття, неявних параметрів, виключних ситуацій, зате є багатий інструментарій, покликаний це все замінити.

Тепер - від чого в мене фрустрації. Перше - Result::unwrap(). Коли функція може повернути значення або помилку, Rust використовує тип Result, який "обгортає" результат (чи помилку), що часто призводить до конструкцій вигляду "змінна = якщо результат нормальний, то вміст "обгортки", інакше припинити програму з повідомленням про помилку". Мовою Rust це записується так:

x = match func() {                   // match - розширений аналог switch
  Ok(value) => value,                //якщо все гаразд, то match поверне value
  Error(message) => panic!(message), //якщо помилка, програма завершиться з повідомленням про неї
}

Оскільки ця конструкція є загальним місцем, її загнали у функцію unwrap, і вся попередня конструкція записується просто

x = func().unwrap();

Ці unwrap мене поки засмучують. Я навіть пробував ставити їх у наступний рядок, сприймаючи як "перевірити на помилку":

x = func()
    .unwrap();

але трохи розібравшися, таки зрозумів, що це неправильно: основне призначення unwrap - це "розгорнути" конверт-Result, а що при цьому відбувається падіння - це альтернативна гілка.

Друге - це Builder-и. В Rust, як я вже писав, немає типових значень параметрів функцій. Отже, для створення складного об'єкта потрібно вказати всі його параметри, а це дуже незручно. Вихід такий: будується примітивний об'єкт Builder з типовими значеннями параметрів, деякі замінюються на потрібні нам, після чого викликається метод build(). Наприклад:

let car = CarFactory::orderCar()   //повертає carBuilder з типовими параметрами
                      .horses(200) //встановлює цьому carBuilder параметр horses
                      .color(Red)  //встановлює color
                      .build()     //викликає повний конструктор Car з усіма параметрами, повертає Result
                      .unwrap();   //дістає нашу нову машину з фабричного пакування

Функції horses і color встановлюють тільки два значення, решта лишаються типовими. Що мене тут засмучує:
- функція orderCar за логікою має повертати машину, але повертає тільки її "креслення" (CarBuilder), які ще треба "побудувати" (build). Трохи незвично.
- в цьому ланцюжку функцій (принаймні, мені зараз) легко загубити, з чим ми працюємо. Перші три функції повертають один і той самий CarBuilder, build повертає Result і unwrap - Car. Мені здається, мало б певний сенс додати якесь позначення хоча б для тих функцій, які повертають об'єкт, для якого викликані (&self), щоб бачити, який фрагмент ланцюжка працює над одним об'єктом, а де об'єкт вже інший.
І так, така конструкція ефективна, оскільки функції (зокрема, конструктори) Builder-ів є по суті простими setter-ами, які компілятор інлайнить у код, а оптимізатор прибирає зайве присвоювання типового значення.

І це я ще про те, скільки видів дужок використовується, не розповідав...

2

Re: Вивчення Rust

Теж на Rust дивився, але поки вирішив помаленьку збочуватися з Elixir/Phoenix. Ще якщо Фенікс - Рельсоподібний веб-фреймворк, то Еліксир - абсолютний розрив шаблону і винесення мізків. Але Ви пишіть сюди ваші враження та досвід, хто зна, що буде популярним через років 5.

Мій блог про ОС сімейства *nix - http://nixtravelling.blogspot.com/
Подякували: 0xDADA11C7, koala2

3

Re: Вивчення Rust

З нетерпінням чекатиму продовження про дужки, бо мене на тій схемі також в Rust занесло.

4

Re: Вивчення Rust

Дужок (поки що, я ще не дочитав) використовується 5 типів: ()[]{}<>||. Причому при застосуванні макросів перші три види працюють майже однаково і можуть замінювати один одного. Останній - проголошення лямбда-функції. В решті ж вони схожі на C/C++.

Дуже потужна штука - enum-и із даними; фактично, це аналоги C/C++ union, першим елементом яких є enum. Приклад з документації:

enum Message {
    Quit,
    ChangeColor(i32, i32, i32),
    Move { x: i32, y: i32 },
    Write(String),
}

Щоб працювати із ними, використовуються конструкції match, приклад був вище (Result - це enum з Ok і Error); але воно дуже цікаве і трохи схоже на регулярки, це не простий switch, воно перевіряє гілки на відповідність патернам.
До речі, макроси теж використовують такі патерни, цілком нормальна ситуація, коли параметром в макрос передається ціле дужкове дерево різних значень. А ще макроси гігієнічні - проголошення внутрішніх змінних у них екрановане від зовнішніх, і ситуації на кшталт (C/C++)

#define macro(x)               \
{                              \
    int y=f(x);                \
    print("f(%d)=%d",x,y);     \
}
...
int y = 3;
macro(y);//тут стається щось не те - виводиться внутрішній y два рази

не виникають.

Подякували: 0xDADA11C7, Betterthanyou2

5

Re: Вивчення Rust

Класно зробили. Такий собі низькорівневий хаскель

МАКЕ ЦКЯАІИЕ БЯЕАТ АБАІИ
Подякували: 0xDADA11C71

6

Re: Вивчення Rust

Як прострелити собі ногу на Rust:
Відкриваєте ящик, витягаєте з нього спаковану коробку. Розгортаєте коробку, відкриваєте, витягаєте з неї кобуру. Відкриваєте кобуру, дістаєте пістолета. Намагаєтеся прострелити собі ногу. Під час кожної з цих операцій ви можете запанікувати і втекти, але цього не стається, бо ви отримуєте повідомлення компілятора, що ви неправильно позичили пістолет і не можете вносити зміни одночасно в пістолет і в ногу.

Подякували: Betterthanyou, leofun012

7 Востаннє редагувалося koala (06.12.2016 11:51:23)

Re: Вивчення Rust

Вирішив цьогорічний Advent of code написати на rust. Враження дуже цікаві. Поки що написав тільки перше завдання (добре, із його продовженням) і маленьку бібліотечку для завантаження вводу з сайту. Довелося трохи покопирсатися в стандартних бібліотеках (ini для налаштувань і hyper для завантаження даних), але впорався, код працює.
Що дуже сподобалося: в файлі проекту (.toml) дописуєш вимоги до бібліотеки, на кшталт

ini=">=0.6"

тобто бібліотека ini версії не нижче за 0.6, і компілятор при збірці сам(!) її завантажує. Можна навіть прописувати посилання на GitHub.
Що приємно: невикористані змінні не викликають нарікань компілятора, якщо починаються на _. Наприклад, цикл в 10 повторень виглядає як

for _ in 0..10 {print!("*")}

Якщо замість _ написати i, компілятор лається на невикористану змінну. Так само останній варіант match-ів зазвичай позначають _.
Із чим довелося позависати:
1. Обробка помилок. В Rust немає виключних ситуацій і вважається дурним тоном використовувати unwrap-и. Функція, що може повернути помилку, має повертати тип Result<T,E>, зі значеннями Ok(T), якщо все гаразд і Err(E), якщо щось пішло не так. unwrap, насправді, працює так:

match result {
  Ok(value) => value,
  Err(error) => panic!(error),
}

Але панікувати - це не наш метод. Замість панікувати використовується така схема:
- створюємо власний тип помилки із переліком можливих проблем, проблеми, що йдуть "транзитом" з інших функцій, при цьому обгортаються в наш тип:

enum Error {
  MyError, //це наша помилка
  IoError(io::Error), //а це помилка введення даних, яка виникає в іншій функції, викликаній з нашої
}

- визначаємо кілька функцій для виведення повідомлень про наші помилки та перетворення з чужих типів помилок (як от io::Error) на наш; гадаю, тут скоро виникне (а швидше за все, вже виник, я просто поки не знайшов) типовий макрос для визначення таких функцій в типових випадках, а як ні, то мені варто його написати;
- замість методу .unwrap() використовуємо макрос try!, який робить замість panic! звичайний return. У версії 1.13 - наразі останній - цей макрос можна замінити знаком ? (читається "Carrier", це звично для rust, ! в макросах читається як "Bang"), для спрощення ось таких виразів:

let x=try!(try!(try!(a().b()).c()).d().e());

якщо не зрозуміли (а це реально важко), тут відбувається ланцюжок викликів a().b().c().d().e() (це типово для rust), але функції b(), c() та e() повертають не простий тип, а result. Із керіером це виглядає так:

let x=a().b()?.c()?.d().e()?;

2. Rust бореться із Q&D code (програмуванням на тяп-ляп). Звісно, повністю його не перебореш, он ті ж unwrap-и є якраз прикладом такого Q&D; але деякі речі дуже правильні, хоча звикати до цього важко. Отже, ситуація: я написав функцію для завантаження мого input-у з сайту Advent of code

fn get_input(url:&str, session:&str)

Відлагодив (мої поневіряння із Hyper поки що описувати не буду), запрацювало. Але трохи нецікаво писати ці речі в кожному файлі, тому я вирішив підвантажувати їх із .ini. Ну і перша думка:

fn get_input(url:&str, session:&str){
  if str.is_empty() || session.is_empty() {
    //завантажити ini, замінити url чи session на значення з ini
  }
  //завантажуємо дані, як зазвичай
}

А от не виходить. Треба додатково проголошувати змінні перед перевіркою і копіювати у них значення з ini - бо інакше ini припинить існувати при виході з блоку if. А це вже зовсім некрасиво. Тому я проголосив додаткову функцію

fn get_input_from_ini(){
  //завантажити ini, взяти з нього url та session
  get_input(url, session);
}

Тепер, якщо викликати нову функцію, ini живе достатньо довго для того, щоб брати посилання на рядки з нього. А якщо ini не потрібен - треба брати стару.

Подякували: 0x9111A, Betterthanyou2

8 Востаннє редагувалося koala (11.12.2016 11:02:09)

Re: Вивчення Rust

Враження продовжують вражати :)
Сильно змінився процес налагодження - тепер неправильні програми, переважно, взагалі не компілюються. Стандартна ситуація - накидаю зараз абичого, скомпілюю, а потім подивлюся, що треба виправити - стала вже нетиповою: щоб програма просто запрацювала, треба докласти зусиль, а ці зусилля самі течуть в потрібне річище - на виправлення коду. А що робити, якщо на прості речі, на кшталт виведення хешу md5 в 16-річній формі, доводиться створювати структури на кшталт

if i%1000000==0 {
  let s:String=md5.iter()
                  .map(|x|format!("{:X} ",x))
                  .collect();
  println!("i={}, now have {}",i, s );
}

(так, виглядає не дуже страшно, але знали б ви, скільки я мучився заради цього рядка, підбираючи різні варіанти)
О! До речі, ось і підібралося:

if i%1000000==0 {
  println!("i={}, now have {}",i, md5.iter()
                                     .map(|x|format!("{:2X} ",x))
                                     .collect::<String>() );
}

Одразу поясню, що тут відбувається: є масив беззнакових байт фіксованого розміру
let mut md5 = [0u8; 16];
в нього заноситься md5 (тому він mut - придатний для змін) і далі він обробляється у відповідності до умов Дня 5 Advent of Code. А щоб було цікавіше дивитися на обробку, я вирішив виводити кожен мільйонний md5. Ну проста ж задача, правда?

if i%1000000==0 {//кожен мілйьонний
  println!("i={}, now have {}",i,//ніби все зрозуміло
  md5.iter()//ітератор - семантика "ідемо по об'єкту", а фактично - для масивів пара "поточна позиція - кінець"
     .map(|x|format!("{:2X} ",x)) //застосувати до кожного елементу, поверненого ітератором, лямбда-функцію
             //format! - це макрос, що перетворює параметр на рядок (підставляє відповідні функції) у відповідності до рядка форматування
     .collect::<String>()//і ось тут нарешті це все запускається: collect збирає елементи з ітератора в єдиний об'єкт. 
             //А оскільки компілятор не знає, що у саме треба збирати (вектор? рядок? множина?), доводится уточнювати
  );
}

Можливо, справа в тому, що я ще не дуже добре знаю всі можливості мови, а деякий досвід програмування у мене є :) Поки що програмування на rust нагадує мені спроби підхопити щойно спійману рибу - слизька вона, крутиться і б'ється. Зате коли нарешті приготувати - дуже смачна :D

Подякували: Betterthanyou1

9

Re: Вивчення Rust

А ще маю відзначити роботу оптимізатора. Якщо в test/debug-збірці перебір md5 тягнувся по 100000 за пару секунд, то в release - пара мільйонів на секунду.

10 Востаннє редагувалося koala (11.12.2016 11:46:52)

Re: Вивчення Rust

А все ж він не ідеальний :)
Код:

      .filter_map(|x| match x.0 % 2 {
                        0=>Some(x.1),
                        1=>None,
                      } )

Відповідь компілятора:

11 |       .filter_map(|x| match x.0 % 2 {
   |                             ^^^^^^^ pattern `_` not covered

Компілятор вважає, що x.0 % 2 може набувати інших значень, окрім 0 та 1...

Коротше, звісно, зробити

                        _=>None,

але, можливо, коректніше

      .filter_map(|x| match x.0 % 2 == 0 {
                        true =>Some(x.1),
                        false=>None,
                      } )

?

Подякували: 0xDADA11C71

11

Re: Вивчення Rust

А в результаті обійшлося взагалі без match.

12

Re: Вивчення Rust

Поспішайте, саме сьогодні книга на халяву по Rust - https://www.packtpub.com/packt/offers/free-learning

Мій блог про ОС сімейства *nix - http://nixtravelling.blogspot.com/

13

Re: Вивчення Rust

Master_Sergius написав:

Поспішайте, саме сьогодні книга на халяву по Rust - https://www.packtpub.com/packt/offers/free-learning

Дякую, встиг.

14

Re: Вивчення Rust

Вирішив почати перекладати офіційну книжку по Rust. Хтось іще хоче долучитися?

15

Re: Вивчення Rust

koala написав:

Вирішив почати перекладати офіційну книжку по Rust. Хтось іще хоче долучитися?

Навіть не знаю.. бо не знаю навіщо мені Rust..

Віддамся на один вечір в хороші дівочі руки.. не дорого, в у.о. .. Якщо сподобається, то залишуся безкоштовно назавжди..

16

Re: Вивчення Rust

HetmanNet написав:
koala написав:

Вирішив почати перекладати офіційну книжку по Rust. Хтось іще хоче долучитися?

Навіть не знаю.. бо не знаю навіщо мені Rust..

Хіба я питав "хто не знає" чи "хто не хоче"?

Подякували: 0xDADA11C71

17

Re: Вивчення Rust

Написання типової програми на Rust для мене складається з таких етапів:
- написати функціональний код;
- скомпілювати, переконатися, що не працює;
- довго і нудно розбирати на імперативний, виправляючи помилки, знайдені компілятором;
- довести до компіляції, переконатися, що працює, як слід (у Rust якщо скомпілювалося - 90%, що запрацює як слід);
- зібрати назад у функціональний.

Функція, що робить з "abc" рядок "A-Bb-Ccc", на Python:

def accum(s):
    return '-'.join((c*(i+1)).title() for i,c in enumerate(s))

і на Rust (тільки зі стандартною бібліотекою):

use std::iter::*;

fn accum(s:&str)->String {
    s.chars()
     .enumerate()
     .map(|(i,c)|{
          let mut word = String::new();        
          word.extend(c.to_uppercase());
          for _ in 0..i {
              word.extend(c.to_lowercase());
          }
          word
       })
     .collect::<Vec<_>>()
     .join("-")
}
Подякували: Betterthanyou, /KIT\2