LINUX.ORG.RU

Rust Alpha

 


1

3

Не следил активно за изменениями, но в своё время прочёл уже устаревшую Rust by Example и туториалы с официального сайта. Теперь вот начал неспешно читать новую «книгу», которая заменила собой старые гайды. Но некоторые моменты вызывают удивление. Знаю, что тут есть пару человек, которые в языке неплохо разбираются, так что может обьяснят почему сделали именно так.

1. Функции высшего порядка. Старый вариант:

fn test(f: |int| -> int) -> int
Новый:
fn test<F: Fn(i32) -> i32>(f: F) -> i32
Второй вариант, имхо, приближается к шаблонам С++ (в плохом смысле). Но ок, первая мысль - «подумали над уменьшением многословности для функций с одинаковыми сигнатурами». Не самый частый случай, но возможно, смысл есть. Потом читаю дальше:
fn test<F, G>(x: i32, f: F, g: G) -> i32
    where F: Fn(i32) -> i32, G: Fn(i32) -> i32 {
}
И объяснение:

That is because in Rust each closure has its own unique type. So, not only do closures with different signatures have different types, but different closures with the same signature have different types, as well!

По моему, они взяли худшее из обоих вариантов. Есть в этом какой-то смысл?

Опять же where тут, в отличии от хаскеля, выглядит коряво. Мы и так сначала заявляем, что вот у нас есть типы (<F, G>), потом ещё раз про них говорим (f: F, g: G) и наконец-то определяем их. Я бы ещё понял, если бы where позволяло отказаться от указания типов в угловых скобках.

2. Closures.

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

3. Named arguments, variable arguments.

В описании методов присутствует следующее:

Rust doesn't have method overloading, named arguments, or variable arguments. We employ the builder pattern instead.

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

tailgunner ozkriff numas13

★★★★★

Мне кажется, в 1 у тебя конструкции разного назначения. Может:

fn test(f: &Fn(i32) -> i32) -> i32

?

Ссылки в тему: https://github.com/rust-lang/rfcs/blob/master/text/0114-closures.md https://github.com/rust-lang/rfcs/pull/114

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

Да.

А на уровне отдельных переменных можно?

Думаю, нет. Да и зачем?

Кстати: http://cosmic.mearie.org/2014/01/periodic-table-of-rust-types/

AFAIK, перегрузки не будет, переменное число параметров - может быть, named arguments - очень вероятно. Точно не знаю - после убиения rust-dev@ следить за разработкой очень неудобно.

tailgunner ★★★★★
()

мда, а так все хорошо начиналось :-/

anonymous
()

о замыканиях

Есть два принципиально разных вида замыканий: boxed и unboxed. И для каждого из них свой синтаксис. Те, что boxed, сейчас Box<Fn(i32) -> i32> или &Fn(i32) -> i32 и передаются как аргумент.
Кроме того, есть контроль над тем, как будет использоваться замыкание: лишь один раз, много раз, с изменяемым окружением или неизменяемым.
unboxed замыкания нужны, чтобы избежать косвенной адресации.

В книге этот момент не поясняется, но всюду используется вариант |&:|

Обычно можно использовать и ||, компилятор сам выведет что именно нужно.

quantum-troll ★★★★★
()
Последнее исправление: quantum-troll (всего исправлений: 1)

В Rust функции высшего порядка = замыкания, отсюда все нюансы описания прототипов функций.

numas13
()

в этом языке термин «синтаксический сахар» заменен на «синтаксический кал»

anonymous
()

Деградировавшие плюсики.

anonymous
()
Ответ на: комментарий от tailgunner

Мне кажется, в 1 у тебя конструкции разного назначения. Может:

Почему разного? Проверил - твой вариант тоже работает, но в книге предлагают именно «мой» вариант.

Думаю, нет. Да и зачем?

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

переменное число параметров - может быть, named arguments - очень вероятно.

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

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

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от Virtuos86

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

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

DarkEld3r ★★★★★
() автор топика

Кстати, ещё один момент. Раньше, если ничего не путаю, для «числовых констант» надо было указывать тип (если он, конечно, не мог быть выведен):

let a = 10; // нельзя, непонятный тип
let b = 10i; // ок - int
То теперь сделали «int» (isize) по умолчанию. Не очень понравилось - изначально язык заставлял задуматься о типе. Хотя теперь стало менее многословно, конечно.

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от numas13

В Rust функции высшего порядка = замыкания, отсюда все нюансы описания прототипов функций.

Это ничего не объясняет. Как раз в старом варианте было точно как ты говоришь: объявление замыканий и «параметров функций» выглядело одинаково. Теперь нет.

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от quantum-troll

Есть два принципиально разных вида замыканий: boxed и unboxed.

Много раз встречал упоминание этого, но прочитанные мануалы как-то не раскрывали тему. Вот сейчас нагуглил:

Short version: current closures in Rust are «boxed» in that they consist of a function pointer, and a pointer to the closure environment (which contains captured variables). LLVM has trouble inlining and optimising around these indirections.

Unboxed closures work by having each closure expand to an anonymous type which is effectively a struct that contains the captured variables, and implements one of the three kinds of «call» operator. Because there's no indirection involved, LLVM can much more easily inline and optimise these calls.

Eventually, it won't be unboxed vs. boxed closures, there will just be unboxed closures and everything will be puppies and unicorns.

Если всё так и осталось, а не устарело, то это просто детали реализации? И непонятно зачем они торчат наружу, если от них потом всё равно собираются избавиться. Смысл тогда придумывать новые синтаксисы?..

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

Про это в курсе. Раньше было proc(), теперь стало move перед обьявлением замыкания.

с изменяемым окружением или неизменяемым.

Это в смысле, |&:| и |&mut:| или как-то иначе?

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от tailgunner

Думаю, нет. Да и зачем?

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

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от DarkEld3r

Если всё так и осталось, а не устарело, то это просто детали реализации?

Не просто. Прочти внимательно вот это: http://smallcultfollowing.com/babysteps/blog/2014/11/26/purging-proc/
Так как размер замыкания заранее неизвестен (так как неизвестен размер окружения), для статической диспечеризации нужна мономорфизация. Но в то же время это позволяет функциям вроде map обходиться без косвенной адресации и не уступать циклам в производительности.
Если же требуется большая гибкость, используются boxed замыкания. Они подразумевают косвенную адресацию, но мономорфизации не требуют.

Про это в курсе. Раньше было proc(), теперь стало move перед обьявлением замыкания.

Неверно. move задаёт то, как захватывается окружение, а не то, как его потом используют.

Это в смысле, |&:| и |&mut:| или как-то иначе?

FnOnce, Fn и FnMut. Им соответствуют |:|, |&:| и |&mut:|.

Не стоит забывать, что раст — системный язык, поэтому в нём уделяется внимание тому, чему обычно не уделяют внимания в несистемных языках.

quantum-troll ★★★★★
()
Ответ на: комментарий от DarkEld3r

Мне кажется, в 1 у тебя конструкции разного назначения. Может:

Почему разного?

Первая конструкция, со старой формой замыкания - функция; вторая, из книги - дженерик. Почему так - не знаю.

tailgunner ★★★★★
()
Ответ на: комментарий от DarkEld3r

Я неправильно выразился, все лямбды = замыкания.

numas13
()
Ответ на: комментарий от mix_mix

Ты откопал что-то ОЧЕНЬ древнее, где move имеет совсем другое значение.

quantum-troll ★★★★★
()

Не перестаю охреневать от синтаксиса. Он по набору магических символов и нечитаемости скоро перл перегонит.

entefeed ☆☆☆
()
Ответ на: комментарий от quantum-troll

Например?


Старое-доброе:


и привычнее и читается проще.


fn test(f: |int| -> int) -> int

опять же:

int test( function<int(int)> f )

не требует дополнительных || и ->, а <> в rust и так есть

anonymous
()
Ответ на: комментарий от anonymous
int test( function<int(int)> f )

И как в этой парадигме выразить ещё три трайта для замыканий?

O02eg ★★★★★
()
Ответ на: комментарий от anonymous

обозначение типов как в си

Чтобы читать их можно было только по спирали? Нет уж, спасибо.

Старое-доброе

Запись типов через двоеточие есть во многих не С-подобных языках (например, в ML).

function<int(int)>

Хорошо, теперь пусть эта функция возвращает другую функцию.

не требует дополнительных ||

|| — довольно удобный синтаксис для замыканий. А fn(i32, i32) -> i32 уже используется для типа-функции (да, функции и замыкания — разные вещи).

quantum-troll ★★★★★
()
Последнее исправление: quantum-troll (всего исправлений: 1)

О, Rust-тред. Чтоб два раза не создавать, как в ржавчине принято хранить данные с циклическими ссылками? Хотя бы простой случай: дерево, в котором у каждого узла есть ссылка на родителя.

anonymous
()
Ответ на: комментарий от quantum-troll

Чтобы читать их можно было только по спирали? Нет уж, спасибо.

Порядок никак не связан с ':'. Просто перенеси то, что справа от ':' в начало.

int[5] xs = { 1, 2, 3, 4, 5 };

явно лучше наркоманского:

let xs: [i32; 5] = [1, 2, 3, 4, 5];

Хорошо, теперь пусть эта функция возвращает другую функцию.

alias спасет отца русской демократии:

function<foo(int)>

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

|| — довольно удобный синтаксис для замыканий

Внезапно авторы rust в данном конкретном случае с тобой не согласны:

error: obsolete syntax note: use unboxed closures instead

anonymous
()
Ответ на: комментарий от anonymous

int[5] xs = { 1, 2, 3, 4, 5 };

Ну или [int 5], если уж хочется читать код как машина, а не человек. Для кого-то и лисповый синтаксис самый удобный и правильный.

anonymous
()
Ответ на: комментарий от anonymous

явно лучше

«Как в С» ≠ явно лучше. Может ты ещё и говоришь в стиле «целых массивом с длинной пять является переменная xs, и равна она...»?
Даже в том же Go отошли от традиции С, и сделали по-человечески: http://blog.golang.org/gos-declaration-syntax

alias спасет

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

Внезапно авторы rust в данном конкретном случае с тобой не согласны

Он был удобен, но был недостаточно общим. Но для замыканий всё ещё используется, пример: http://goo.gl/qWdVRk

quantum-troll ★★★★★
()
Ответ на: комментарий от quantum-troll

«Как в С» ≠ явно лучше

Там не как в С.

Может ты ещё и говоришь в стиле «целых массивом с длинной пять является переменная xs, и равна она...»?

Человеческий (русский в частности) язык в качестве языка программирования был бы крайне ужасен.

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

fn<fn<int(int)> (int, string, foo)>

Давай свою человеческую запись для сравнения. Ну и «на каждый чих» - это явное преувеличение.

Но для замыканий всё ещё используется

Не в том случае, который мы обсуждаем.

anonymous
()
Ответ на: комментарий от quantum-troll

Прочти внимательно вот это

Спасибо, почитаю.

Если же требуется большая гибкость, используются boxed замыкания.

Возможно, когда прочитаю, то вопросы отпадут, но смущает, что мы в сигнатуре должны задавать какое замыкание ожидается. В С++ как-то удобнее - функция может понятия не иметь, что ей передали: лямбду или другую функцию. Или я опять не так понял?

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

FnOnce, Fn и FnMut. Им соответствуют |:|, |&:| и |&mut:|.

А «||» - это, судя по ссылке, «|&mut:|»?

Не стоит забывать, что раст — системный язык, поэтому в нём уделяется внимание тому, чему обычно не уделяют внимания в несистемных языках.

Да я как бы на С++ пишу.

DarkEld3r ★★★★★
() автор топика
Последнее исправление: DarkEld3r (всего исправлений: 2)
Ответ на: комментарий от quantum-troll

Замыкание используется несколько раз, но строка `s` перемещена из `main` в замыкание.

Понял, невнимательно прочёл мануал.

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от ozkriff

Эээ, вроде нет.

Хм... по моему, раньше такой код давал ошибку:

let a = 10;
println!("{}", a);
И требовалось указать тип или в обьявлении «переменной» или как суффикс к числу.

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от quantum-troll

Прочти внимательно вот это: http://smallcultfollowing.com/babysteps/blog/2014/11/26/purging-proc/

Почитал. Кое-что понял, но вопросы ещё остались:

1. Получается сейчас невозможно написать функцию, которой всё равно boxed или unboxed замыкание ей передадут? Предлагается писать врапперы и/или ждать изменений в языке?

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

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

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от DarkEld3r

функцию, которой всё равно boxed или unboxed замыкания

Хм. А каким функциям могут понадобиться и те, и те?

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

Потому, что каждое замыкание — отдельный тип. Хотя, наверное, синтаксис можно было немного причесать.

quantum-troll ★★★★★
()
Ответ на: комментарий от DarkEld3r

Да, там чего-то поменяли в выводе типов для чисел, но литералы без суффиксов не становятся isize по-умолчанию, я об этом:

fn main() {
    let _: bool = 1;
}

<anon>:2:19: 2:20 error: mismatched types:
 expected `bool`,
    found `_`
(expected bool,
    found integral variable) [E0308]
<anon>:2     let _: bool = 1;
                           ^

Оно просто некое «целое число», пока вывод типов их не подхватит:

fn n(_: &u8) {}

fn main() {
    let a = 1;
    n(&a);
    let _: bool = a;
}

<anon>:6:19: 6:20 error: mismatched types:
 expected `bool`,
    found `u8`
(expected bool,
    found u8) [E0308]
<anon>:6     let _: bool = a;
                           ^
ozkriff
()
Ответ на: комментарий от DarkEld3r

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

В смысле? Например?

Мы и так сначала заявляем, что вот у нас есть типы (<F, G>), потом ещё раз про них говорим (f: F, g: G) и наконец-то определяем их. Я бы ещё понял, если бы where позволяло отказаться от указания типов в угловых скобках.

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

ozkriff
()
Ответ на: комментарий от quantum-troll

Хм. А каким функциям могут понадобиться и те, и те?

Да ведь в статье даже говорится про возможность написания враппера. Как раз, чтобы иметь возможность передавать разные виды замыканий.

А надо это для того, чтобы писать обобщённые библиотечные функции. А уже пользователь библиотеки выберет какое ему замыкание надо. А получается наоборот - советуют всегда использовать дженерик, а внутри, если надо «конвертировать».

Есть ощущение, что я не совсем понимаю идею всё-таки. Буду разбираться...

Потому, что каждое замыкание — отдельный тип.

В С++ каждая лямбда тоже имеет отдельный тип. Тем не менее, нет никаких проблем: функция может принимать std::function, хоть шаблоны. А передать можно что угодно: хоть свободную функцию, хоть лямбду, хоть «функциональный объект», хоть результат std::bind. Главное чтобы сигнатура была совместима.

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

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от ozkriff

Да, там чего-то поменяли в выводе типов для чисел, но литералы без суффиксов не становятся isize по-умолчанию, я об этом:

Хм. Интересно как, в таком случае, println влияет и во что выводит.

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

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от ozkriff

В смысле? Например?

Ну я там понимаю, что where добавили из-за того, что если всё сразу в угловые скобки пихать, то получается некрасиво. Вот над этим и поработали бы.

Ну или пусть наличие where не требовало бы угловых скобок вообще.

DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от DarkEld3r

Да ведь в статье даже говорится про возможность написания враппера

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

А уже пользователь библиотеки выберет какое ему замыкание надо.

Так ведь наоборот, пользователю всё равно, какой тип замыканий используется, в отличие от разработчика библиотеки.

В С++ каждая лямбда тоже имеет отдельный тип.

Не знаю, как там в С++ сделано. Там функции параметризируются относительно лямбд?

quantum-troll ★★★★★
()
Ответ на: комментарий от DarkEld3r

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

Можно пример текстом, а не ссылкой на нечто глючащее?

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Можно пример текстом, а не ссылкой на нечто глючащее?

Я не виноват, что play.rust-lang.org сглючил. Кстати, сейчас всё работает. На всякий случай, дублирую:

fn test<F>(a: F, b: F) -> i32
    where F : FnMut() -> i32 {
    a() + b()
}

fn main() {
    let a = test(||{ 10 }, || { 20 });
    println!("{}", a);
}

<anon>:9:28: 9:37 error: mismatched types:
 expected `[closure <anon>:9:18: 9:26]`,
    found `[closure <anon>:9:28: 9:37]`
(expected closure,
    found a different closure) [E0308]
<anon>:9     let a = test(||{ 10 }, || { 20 });
                                    ^~~~~~~~~
DarkEld3r ★★★★★
() автор топика
Ответ на: комментарий от anonymous

У тебя аналогичный код и в C++ не скомпилится. У тебя у двух лямбд разный тип.

Хм... в чём-то ты прав. Тем не менее, в С++ можно так:

template<typename T>
int test(T t1, T t2)
{
    return t1() + t2();
}

int main()
{
    test<int (*)()>([](){ return 10;}, [](){ return 20;});
}

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

int test(function<int()> t1, function<int()> t2)
{
    return t1() + t2();
}

Хотя я, кажется, понял.

DarkEld3r ★★★★★
() автор топика
Последнее исправление: DarkEld3r (всего исправлений: 2)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.