Rust: Проблема отслеживания для impl Trait (RFC 1522, RFC 1951, RFC 2071)

Созданный на 27 июн. 2016  ·  417Комментарии  ·  Источник: rust-lang/rust

НОВАЯ ПРОБЛЕМА ОТСЛЕЖИВАНИЯ = https://github.com/rust-lang/rust/issues/63066

Статус реализации

Базовая функция, указанная в RFC 1522 , реализована, однако были изменения, которые все еще нуждаются в доработке:

RFC

Было несколько RFC, касающихся черты impl, и все они отслеживаются этой центральной проблемой отслеживания.

  • https://github.com/rust-lang/rfcs/pull/1522

    • оригинал, который охватывал только impl Trait в позиции возврата для встроенных функций

  • https://github.com/rust-lang/rfcs/pull/1951

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

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

    • добавление атрибута impl в позицию аргумента.

  • https://github.com/rust-lang/rfcs/pull/2071

    • с именем abstract type в модулях и реализациях



      • [x] Завершить синтаксис (https://github.com/rust-lang/rfcs/pull/2515)


      • [ ] Обновите RFC https://github.com/rust-lang/rfcs/pull/2289, чтобы он соответствовал этому синтаксису, если этот RFC будет объединен.



    • использование признака impl в позициях let , const и static

  • https://github.com/rust-lang/rfcs/pull/2250

    • Завершение синтаксиса impl Trait и dyn Trait с несколькими границами

Нерешенные вопросы

Реализация также подняла ряд интересных вопросов:

  • [x] Каков приоритет ключевого слова impl при анализе типов? Обсуждение: 1
  • [ ] Должны ли мы разрешать impl Trait после -> в типах fn или сахара в скобках? №45994
  • [ ] Должны ли мы наложить DAG на все функции, чтобы обеспечить автоматическую безопасную утечку, или мы можем использовать какую-то отсрочку. Обсуждение: 1

    • Настоящая семантика: DAG.

  • [x] Как нам интегрировать черту impl в regionck? Обсуждение: 1 , 2
  • [ ] Должны ли мы разрешать указывать типы, если некоторые параметры являются неявными, а некоторые явными? например, fn foo<T>(x: impl Iterator<Item = T>>) ?
  • [ ] [Некоторые опасения по поводу использования вложенных трейтов impl](https://github.com/rust-lang/rust/issues/34511#issuecomment-350715858)
  • [x] Должен ли синтаксис в реализации быть existential type Foo: Bar или type Foo = impl Bar ? ( см. здесь для обсуждения )
  • [ ] Должен ли набор «определяющих применений» для existential type в реализации быть просто элементами реализации или включать вложенные элементы внутри функций реализации и т. д.? ( см. здесь, например )
B-RFC-implemented B-unstable C-tracking-issue T-lang disposition-merge finished-final-comment-period

Самый полезный комментарий

Так как это последний шанс перед закрытием FCP, я хотел бы привести последний аргумент против автоматических автоматических трейтов. Я понимаю, что это немного в последнюю минуту, поэтому я хотел бы в лучшем случае официально решить эту проблему, прежде чем мы примем текущую реализацию.

Чтобы прояснить для тех, кто не следил за impl Trait , это проблема, которую я представляю. Тип, представленный типами impl X настоящее время автоматически реализует автоматические черты тогда и только тогда, когда конкретный тип, стоящий за ними, реализует указанные автоматические черты. Конкретно, если сделать следующее изменение кода, функция продолжит компилироваться, но любое использование функции, полагающееся на тот факт, что тип, который она возвращает, реализует Send завершится ошибкой.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(более простой пример: работа , внутренние изменения вызывают сбой )

Этот вопрос не однозначен. Было очень обдуманное решение сделать автоматические черты "утечкой": если бы мы этого не сделали, нам пришлось бы поставить + !Send + !Sync на каждую функцию, которая возвращает что-то не-Send или не-Sync, и мы бы иметь неясную историю с потенциальными другими пользовательскими авто-чертами, которые просто невозможно реализовать для конкретного типа, возвращаемого функцией. Это две проблемы, которых я коснусь позже.

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

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

Когда я знакомлю людей с Rust, я, конечно, могу показать им скорость, производительность, безопасность памяти. Но в го есть скорость. Ада имеет безопасность памяти. У Python есть производительность. Что в Rust превосходит все это, так это удобство сопровождения. Когда автор библиотеки хочет изменить алгоритм, чтобы сделать его более эффективным, или когда он хочет переделать структуру ящика, у него есть твердая гарантия от компилятора, что он сообщит ему об ошибках. В Rust я могу быть уверен, что мой код продолжит функционировать не только с точки зрения безопасности памяти, но и с точки зрения логики и интерфейса. _Каждый интерфейс функции в Rust полностью представлен объявлением типа функции_.

Стабилизация impl Trait как есть имеет большой шанс пойти против этого убеждения. Конечно, это очень удобно для быстрого написания кода, но если я хочу создать прототип, я буду использовать python. Rust — это предпочтительный язык, когда вам нужна долгосрочная ремонтопригодность, а не краткосрочный код, предназначенный только для записи.


Я говорю, что есть только «большая вероятность» того, что здесь все плохо, потому что, опять же, проблема не ясна. Сама идея «автопризнаков» не является явной. Отправка и синхронизация реализуются на основе содержимого структуры, а не публичного объявления. Поскольку это решение сработало для ржавчины, impl Trait действующий аналогичным образом, также может сработать.

Однако функции и структуры используются в кодовой базе по-разному, и это разные проблемы.

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

Совершенно ясно, что при изменении внутренних компонентов функции это может повлиять как на производительность, так и на правильность. Однако в Rust нам не нужно проверять, что мы возвращаем правильный тип. Объявления функций — это жесткий контракт, который мы должны соблюдать, и rustc следит за нами. Это тонкая грань между автоматическими типажами в структурах и возвратами функций, но изменение внутренних компонентов функции является гораздо более рутинным. Когда у нас будут полностью работающие от генератора Future s, изменение функций, возвращающих -> impl Future , станет еще более рутинным. Все это будут изменения, которые авторы должны проверять на наличие модифицированных реализаций отправки/синхронизации, если компилятор их не улавливает.

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

Я уже изложил свой основной ответ на это, но вот последнее замечание. Изменение макета структуры не так уж распространено; от этого можно защититься. Бремя обслуживания, обеспечивающее, чтобы функции продолжали реализовывать одни и те же автоматические черты, больше, чем для структур, просто потому, что функции изменяются намного больше.


В качестве последнего замечания, я хотел бы сказать, что автоматические автоматические черты — не единственный вариант. Это вариант, который мы выбрали, но альтернатива автоматических признаков отказа все еще остается альтернативой.

Мы могли бы потребовать, чтобы функции, возвращающие элементы, отличные от отправки/несинхронизации, либо указывали состояние + !Send + !Sync либо возвращали трейт (возможно, псевдоним?), который имеет эти границы. Это не было бы хорошим решением, но оно может быть лучше, чем то, которое мы сейчас выбираем.

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


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

Спасибо, что прочитали, и я надеюсь, что окончательное решение направит Rust в наилучшем направлении, в котором он может двигаться.

Все 417 Комментарий

@aturon Можем ли мы поместить RFC в репозиторий? ( @mbrubeck прокомментировал там, что это проблема.)

Сделанный.

Первая попытка реализации — #35091 (вторая, если считать мою прошлогоднюю

Одна проблема, с которой я столкнулся, связана с продолжительностью жизни. Вывод типа любит размещать переменные региона _везде_, и без каких-либо изменений, проверяющих регион, эти переменные не выводят ничего, кроме локальных областей.
Тем не менее, конкретный тип _должен_ быть экспортируемым, поэтому я ограничил его до 'static и явно назвал ранние параметры времени жизни, но _никогда_ ни один из них, если задействована какая-либо функция - даже строковый литерал не подразумевает 'static , это совершенно бесполезно.

Одна вещь, о которой я подумал, которая не повлияет на саму проверку региона, - это стереть время жизни:

  • ничто, раскрывающее конкретный тип impl Trait должно заботиться о времени жизни - быстрый поиск Reveal::All предполагает, что это уже имеет место в компиляторе
  • необходимо установить границу для всех конкретных типов impl Trait в возвращаемом типе функции, что она переживет вызов этой функции - это означает, что любое время жизни по необходимости равно либо 'static или один из параметров времени жизни функции — _even_, если мы не знаем, какой (например, «кратчайшее из 'a и 'b »)
  • мы должны выбрать дисперсию для неявного параметризма времени жизни impl Trait (т. е. для всех параметров времени жизни в области видимости, так же, как и для параметров типа): инвариантность проще всего и дает больший контроль вызываемому, в то время как контравариантность позволяет вызывающему больше и потребует проверки того, что каждое время жизни в возвращаемом типе находится в контравариантной позиции (то же самое с параметризмом ковариантного типа вместо инвариантного)
  • механизм автоматической утечки трейта требует, чтобы привязка трейта могла быть помещена в конкретный тип, в другую функцию - поскольку мы стерли времена жизни и не знаем, какое время жизни куда идет, каждое стертое время жизни в конкретном типе придется заменить с новой переменной вывода, которая гарантированно не короче самого короткого времени жизни из всех фактических параметров времени жизни; проблема заключается в том, что импликации свойств могут в конечном итоге потребовать более прочных жизненных отношений (например, X<'a, 'a> или X<'static> ), которые должны быть обнаружены и исправлены, так как они не могут быть доказаны для тех жизни

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

копия @rust-язык/язык

@эддиб

Но время жизни _are_ важно с impl Trait - например

fn get_debug_str(s: &str) -> impl fmt::Debug {
    s
}

fn get_debug_string(s: &str) -> impl fmt::Debug {
    s.to_string()
}

fn good(s: &str) -> Box<fmt::Debug+'static> {
    // if this does not compile, that would be quite annoying
    Box::new(get_debug_string())
}

fn bad(s: &str) -> Box<fmt::Debug+'static> {
    // if this *does* compile, we have a problem
    Box::new(get_debug_str())
}

Я упоминал об этом несколько раз в ветках RFC.

версия без трейт-объекта:

fn as_debug(s: &str) -> impl fmt::Debug;

fn example() {
    let mut s = String::new("hello");
    let debug = as_debug(&s);
    s.truncate(0);
    println!("{:?}", debug);
}

Это либо UB, либо нет, в зависимости от определения as_debug .

@arielb1 arielb1 А, да, я забыл, что одна из причин, по которой я сделал то, что сделал, заключалась в том, чтобы фиксировать только параметры времени жизни, а не анонимные параметры с поздней привязкой, за исключением того, что это на самом деле не работает.

@arielb1 arielb1 Есть ли у нас строгое отношение срока службы, которое мы можем установить между временем жизни, найденным в конкретном типе до стирания, и временем жизни с поздней привязкой в ​​подписи? В противном случае может быть неплохой идеей просто посмотреть на пожизненные отношения и сразу же сбросить любые прямые или _косвенные_ 'a outlives 'b где 'a — это _что угодно_, кроме 'static или параметра времени жизни. и 'b появляется в конкретном типе impl Trait .

Извините, что понадобилось время, чтобы написать сюда. Так что я думал
об этой проблеме. Я чувствую, что мы, в конечном счете, должны (и
хотите) расширить regionck с новым типом ограничений — я назову это
ограничение \in , потому что оно позволяет вам сказать что-то вроде '0 \in {'a, 'b, 'c} , что означает, что регион, используемый для '0 должен быть
либо 'a , 'b , либо 'c . Я не уверен в лучшем способе интеграции
это в решение само по себе - конечно, если набор \in является одноэлементным
множество, это просто отношение равенства (которого у нас в настоящее время нет как
первоклассная вещь, но которая может быть составлена ​​из двух граней), но
в противном случае это все усложняет.

Это все связано с моим желанием сделать набор региональных ограничений
более выразительным, чем то, что мы имеем сегодня. Конечно, можно было бы составить
Ограничение \in из ограничений OR и == . Но конечно больше
выразительные ограничения сложнее решить, и \in ничем не отличается.

В любом случае, позвольте мне просто изложить здесь немного своих мыслей. Давайте работать с этим
пример:

pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...}

Я думаю, что наиболее точное обессахаривание для impl Trait , вероятно,
Новый тип:

pub struct FooReturn<'a, 'b> {
    field: XXX // for some suitable type XXX
}

impl<'a,'b> Iterator for FooReturn<'a,'b> {
    type Item = <XXX as Iterator>::Item;
}

Теперь impl Iterator<Item=u32> в foo должны вести себя так же, как
FooReturn<'a,'b> будет себя вести. Хотя это не идеальное совпадение. Один
разница, например, есть дисперсия, как Эддыб подвел -- я
предполагая, что мы сделаем impl Foo -подобные типы инвариантными относительно типа
параметры foo . Однако поведение автоматической черты работает.
(Еще одна область, где совпадение может быть не идеальным, — если мы когда-нибудь добавим
возможность «пробить» абстракцию impl Iterator , чтобы код
«внутри» абстракция знает точный тип — тогда она будет сортировать
имеет место неявная операция "развертки".)

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

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    type Type = XXX;
}

Теперь мы можем рассматривать тип impl Iterator как <() as FooReturn<'a,'b>>::Type . Это тоже не идеальное совпадение, потому что мы
обычно нормализует его. Вы можете представить себе использование специализации
чтобы предотвратить это:

trait FooReturn<'a,'b> {
    type Type: Iterator<Item=u32>;
}

impl<'a,'b> FooReturn<'a,'b> for () {
    default type Type = XXX; // can't really be specialized, but wev
}

В этом случае <() as FooReturn<'a,'b>>::Type не нормализуется,
и у нас есть гораздо более близкий матч. Дисперсия, в частности, ведет себя
правильно; если бы мы когда-либо хотели иметь какой-то тип, который находится "внутри"
абстракция, они были бы одинаковыми, но им разрешено
нормализовать. Тем не менее, есть одна загвоздка: вещи с автоматическими чертами не
вполне рабочий. (Возможно, мы захотим рассмотреть возможность согласования вещей здесь,
фактически.)

В любом случае, моя цель в изучении этих потенциальных обессахаривания состоит не в том, чтобы
предложить реализовать "impl Trait" как _actual_ desugaring
(хотя это может быть приятно...), но дать интуицию для нашей работы. я
думаю, что второе обессахаривание — с точки зрения прогнозов — является
довольно полезный для направления нас вперед.

Одно место, где это проекционное обескураживание является действительно полезным руководством, это
отношение «переживает». Если бы мы хотели проверить, <() as FooReturn<'a,'b>>::Type: 'x , RFC 1214 говорит нам, что мы можем это доказать.
пока выполняется 'a: 'x _and_ 'b: 'x . Это я думаю, как мы хотим
также обрабатывать вещи для свойства impl.

Во время транса и для авто-черт мы должны будем знать, что такое XXX
есть, конечно. Я полагаю, что основная идея здесь состоит в том, чтобы создать тип
переменная для XXX и убедитесь, что фактические значения, которые возвращаются
все можно объединить с XXX . Теоретически переменная этого типа должна
скажите нам наш ответ. Но, конечно, проблема в том, что этот тип
переменная может относиться ко многим областям, которые не входят в область действия в
сигнатура fn -- например, области тела fn. (Эта же проблема
не встречается с типами; хотя технически можно поставить
например, объявление структуры в теле fn, и оно будет безымянным,
это какое-то искусственное ограничение -- с тем же успехом можно было бы двигаться
структура вне fn.)

Если вы посмотрите как на struct desugaring, так и на impl, есть
(неявное в лексической структуре Rust) ограничение, что XXX может
назовите только 'static или времена жизни, такие как 'a и 'b , которые
появляются в сигнатуре функции. Это то, чем мы не являемся
моделирование здесь. Я не уверен, что это лучший способ сделать это - какой-то тип
схемы вывода имеют более прямое представление области видимости, и
Я всегда хотел добавить это в Rust, чтобы помочь нам с замыканиями. Но
давайте сначала подумаем о меньших дельтах, я думаю.

Вот откуда берется ограничение \in . Можно представить добавление
правило проверки типа, которое (в основном) FR(XXX) \subset {'a, 'b} --
это означает, что «свободные регионы», появляющиеся в XXX, могут быть только 'a и
'b . Это приведет к переводу требований на \in для
различные регионы, которые появляются в XXX .

Давайте посмотрим на реальный пример:

fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Здесь типом, если condition является true, будет что-то вроде
Cloned<SliceIter<'a, i32>> . Но если condition ложно, мы бы
хочу Cloned<SliceIter<'b, i32>> . Конечно, в обоих случаях мы бы
завершите что-то вроде (используя числа для переменных типа/региона):

Cloned<SliceIter<'0, i32>> <: 0
'a: '0 // because the source is x.iter()
Cloned<SliceIter<'1, i32>> <: 0
'b: '1 // because the source is y.iter()

Если мы затем создадим экземпляр переменной 0 до Cloned<SliceIter<'2, i32>> ,
у нас есть '0: '2 и '1: '2 , или полный набор региональных отношений
как:

'a: '0
'0: '2
'b: '1
'1: '2
'2: 'body // the lifetime of the fn body

Итак, какое значение мы должны использовать для '2 ? У нас также есть доп.
ограничение, что '2 in {'a, 'b} . С фн, как написано, я думаю, мы
должен был сообщить об ошибке, так как ни 'a ни 'b не являются
правильный выбор. Интересно, однако, что если бы мы добавили ограничение 'a: 'b , тогда было бы правильное значение ( 'b ).

Обратите внимание, что если мы просто запустим алгоритм _normal_, мы получим
'2 равно 'body . Я не уверен, как обращаться с отношениями \in
кроме полного перебора (хотя могу себе представить какие-то специальные
случаи).

Хорошо, это то, что я получил. знак равно

В PR #35091 @arielb1 написал:

Мне не нравится подход «захват всех жизней в свойстве impl», и я бы предпочел что-то более похожее на жизненный элизион.

Я подумал, что имеет смысл обсудить это здесь. @arielb1 , можете ли вы подробнее рассказать о том, что вы имеете в виду? С точки зрения аналогий, которые я провел выше, я предполагаю, что вы в основном говорите об «обрезке» набора жизней, которые будут отображаться либо как параметры в новом типе, либо в проекции (т.е. <() as FooReturn<'a>>::Type вместо <() as FooReturn<'a,'b>>::Type что ли?

Я не думаю, что правила исключения времени жизни в том виде, в каком они существуют, были бы хорошим руководством в этом отношении: если бы мы просто выбрали время жизни &self только для включения, то мы не обязательно смогли бы включить параметры типа из структуры Self , а также параметры типа из метода, поскольку они могут иметь условия WF, которые требуют, чтобы мы назвали некоторые другие времена жизни.

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

Я нигде не видел обсуждения взаимодействия impl Trait с конфиденциальностью.
Теперь fn f() -> impl Trait может возвращать приватный тип S: Trait аналогично трейт-объектам fn f() -> Box<Trait> . Т.е. объекты приватных типов могут свободно ходить вне своего модуля в анонимизированном виде.
Это кажется разумным и желательным - сам тип является деталью реализации, только его интерфейс, доступный через трейт public Trait является общедоступным.
Однако есть одно различие между типаж-объектами и impl Trait . Только с трейт-объектами все трейт-методы частных типов могут получить внутреннюю связь, они по-прежнему будут вызываться через указатели на функции. С типажами типа impl Trait s частные типы могут напрямую вызываться из других единиц трансляции. Алгоритму, выполняющему «интернализацию» символов, придется приложить больше усилий, чтобы интернализировать методы только для типов, не анонимизированных с помощью impl Trait , или быть очень пессимистичным.

@никомацакис

«Явный» способ написать foo был бы

fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
    if condition { x.iter().cloned() } else { y.iter().cloned() }
}

Здесь нет вопроса о сроке службы. Очевидно, что необходимость каждый раз записывать время жизни будет довольно повторяющейся. Однако то, как мы справляемся с такого рода повторениями, — это, как правило, элизия на протяжении всей жизни. В случае foo elision потерпит неудачу и вынудит программиста явно указать время жизни.

Я против добавления времени жизни, зависящего от это сделал impl Trait а не иначе.

@ arielb1 arielb1 хм, я не уверен на 100%, как думать об этом предложенном синтаксисе с точки зрения «обезуглероживания», которое я обсуждал. Это позволяет вам указать то, что кажется привязанным к времени жизни, но мы пытаемся сделать вывод, в основном, о том, какие времена жизни появляются в скрытом типе. Означает ли это, что максимум одно время жизни может быть «спрятано» (и что оно должно быть точно указано?)

Похоже, что не всегда достаточно «одного параметра времени жизни»:

fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
    x.iter().chain(y).cloned()
}

В этом случае скрытый тип итератора относится как к 'a и к 'b (хотя он _является_ вариантом в обоих случаях; но я думаю, мы могли бы придумать пример, который является инвариантным).

Итак, @aturon и я немного обсудили этот вопрос, и я хотел поделиться. Здесь действительно есть несколько ортогональных вопросов, и я хочу их выделить. Первый вопрос: «какие параметры типа/времени жизни потенциально можно использовать в скрытом типе?» С точки зрения (квази)обессахаривания в default type , это сводится к тому, «какие параметры типа появляются в свойстве, которое мы вводим». Так, например, если эта функция:

fn foo<'a, 'b, T>() -> impl Trait { ... }

будет обессахарен до чего-то вроде:

fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
  type Type: Trait;
}
impl<...> Foo<...> for () {
  default type Type = /* inferred */;
}

тогда этот вопрос сводится к тому, «какие параметры типа появляются в признаке Foo и его реализации»? В основном, ... здесь. Ясно, что это включает в себя набор параметров типа, которые используются самим Trait , но какие дополнительные параметры типа? (Как я уже отмечал ранее, это удаление сахара на 100% верно, за исключением утечки автоматических признаков, и я бы сказал, что мы должны упускать автоматические признаки также и для специализированных импликаций.)

Ответ по умолчанию, который мы использовали, — «все», поэтому здесь ... будет 'a, 'b, T (вместе с любыми анонимными параметрами, которые могут появиться). Это _может_ быть разумным значением по умолчанию, но оно _обязательно_ не является лучшим значением по умолчанию. (Как указал @arielb1 .)

Это влияет на отношение срока жизни, так как для того, чтобы определить, что <() as Foo<...>>::Type (ссылаясь на некую непрозрачную реализацию impl Trait ) переживет 'x , мы фактически должны показать что ...: 'x (то есть каждый параметр времени жизни и типа).

Вот почему я говорю, что недостаточно учитывать параметры времени жизни: представьте, что у нас есть некоторый вызов foo например foo::<'a0, 'b0, &'c0 i32> . Это означает, что все три времени жизни, '[abc]0 , должны пережить 'x -- другими словами, до тех пор, пока используется возвращаемое значение, это прологирует заимствование всех данных, переданных в функцию. . Но, как указал @arielb1 , elision предполагает, что обычно это будет длиннее, чем необходимо.

Итак, я предполагаю, что нам нужно:

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

@aturon выдал что-то вроде impl<...> Trait в качестве явного синтаксиса, что кажется разумным. Следовательно, можно было бы написать:

fn foo<'a, 'b, T>(...) -> impl<T> Trait { }

чтобы указать, что скрытый тип на самом деле не относится к 'a или 'b а только к T . Или можно написать impl<'a> Trait чтобы указать, что ни 'b ни T не захвачены.

Что касается значений по умолчанию, кажется, что иметь больше данных было бы весьма полезно, но общая логика исключения предполагает, что нам было бы лучше захватить все параметры, названные в типе self , когда это применимо. Например, если у вас есть fn foo<'a,'b>(&'a self, v: &'b [u8]) и тип Bar<'c, X> , то тип self будет &'a Bar<'c, X> и, следовательно, мы получим 'a , 'c и X по умолчанию, но не 'b .


Еще одна связанная с этим заметка о том, что означает пожизненная связь. Я думаю, что разумные границы времени жизни имеют существующее значение, которое не следует менять: если мы пишем impl (Trait+'a) это означает, что скрытый тип T переживет 'a . Точно так же можно написать impl (Trait+'static) чтобы указать, что заимствованные указатели отсутствуют (даже если некоторые времена жизни захвачены). При выводе скрытого типа T это подразумевает привязку времени жизни, например $T: 'static , где $T — это переменная вывода, которую мы создаем для скрытого типа. Это будет обработано обычным способом. С точки зрения вызывающей стороны, где скрытый тип, ну, скрытый, граница 'static позволит нам заключить, что impl (Trait+'static) переживет 'static даже если есть захваченные параметры времени жизни.

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

fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
  type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
  default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
}

Все это ортогонально из вывода. Мы по-прежнему хотим (я думаю) добавить понятие ограничения «выбрать из» и модифицировать вывод с помощью некоторых эвристик и, возможно, исчерпывающего поиска (опыт RFC 1214 показывает, что эвристика с консервативным запасным вариантом может на самом деле дать нам очень далеко; Я не знаю, чтобы люди сталкивались с ограничениями в этом отношении, хотя, вероятно, где-то есть проблема). Конечно, добавление ограничений времени жизни, таких как 'static или 'a`, может повлиять на вывод и, таким образом, быть полезным, но это не идеальное решение: во-первых, они видны вызывающей стороне и становятся частью API, что может быть нежелательно.

Возможные варианты:

Явное время жизни, связанное с выходным параметром elision

Подобно сегодняшним типаж-объектам, объекты impl Trait имеют единственный связанный параметр времени жизни, который выводится с помощью правил исключения.

Недостаток: неэргономичный
Преимущество: ясно

Явные границы времени жизни с «всем общим» исключением

Как и сегодняшние трейт-объекты, объекты impl Trait имеют один ограниченный параметр времени жизни.

Однако elision создает новые параметры с ранней привязкой, которые переживают все явные параметры:

fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total

Недостаток: добавляет параметр ранней привязки

более.

Я столкнулся с этой проблемой с помощью impl Trait +'a и заимствования: https://github.com/rust-lang/rust/issues/37790

Если я правильно понимаю это изменение (и вероятность этого, вероятно, мала!), то я думаю, что этот код игровой площадки должен работать:

https://play.rust-lang.org/?gist=496ec05e6fa9d3a761df09c95297aa2a&version=nightly&backtrace=0

И ThingOne и ThingTwo реализуют трейт Thing . build говорит, что вернет что-то, что реализует Thing , что он и делает. Тем не менее, он не компилируется. Так что я явно что-то недопонимаю.

Это «что-то» должно иметь тип, но в вашем случае у вас есть два конфликтующих типа. @nikomatsakis ранее предлагал сделать эту работу в целом, создавая, например, ThingOne | ThingTwo мере появления несоответствий типов.

@eddyb, не могли бы вы ThingOne | ThingTwo ? Разве вам не нужно иметь Box если мы знаем тип только во время выполнения? Или это своего рода enum ?

Да, это может быть специальный enum подобный тип, который делегирует вызов метода типажа, где это возможно, его вариантам.

Я тоже раньше хотел такую ​​штуку. Анонимные перечисления RFC: https://github.com/rust-lang/rfcs/pull/1154

Это редкий случай, когда что-то работает лучше, если оно основано на выводах, потому что, если вы создаете эти типы только при несоответствии, варианты будут разными (что является проблемой с обобщенной формой).
Также вы можете получить что-то от отсутствия сопоставления с образцом (за исключением явно непересекающихся случаев?).
Но сахар делегирования IMO будет «просто работать» во всех соответствующих случаях, даже если вам удастся получить T | T .

Не могли бы вы объяснить другие, неявные половины этих предложений? Я не понимаю большую часть этого и подозреваю, что упускаю какой-то контекст. Вы неявно реагировали на проблемы с типами объединения? Этот RFC - это просто анонимные перечисления, а не типы объединения - (T|T) было бы точно так же проблематично, как Result<T, T> .

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

Я нахожу (позиционные, т.е. T|U != U|T ) анонимные перечисления интригующими, и я считаю, что с ними можно было бы экспериментировать в библиотеке, если бы у нас были вариативные дженерики (вы можете обойти это, используя hlist ) и const generics (то же самое, с числами Пеано).

Но в то же время, если бы у нас была языковая поддержка чего-то, это были бы типы объединения, а не анонимные перечисления. Например, не Result а типы ошибок (чтобы обойти утомительные именованные обертки для них).

Я не уверен, что это правильное место, чтобы спросить, но зачем нужно ключевое слово вроде impl ? Я не смог найти обсуждение (может быть моя вина).

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

С

fn bar(a: &Foo) {
  ...
}

означает «принять ссылку на тип, который реализует черту Foo », я ожидал бы

fn bar() -> Foo {
  ...
}

означать "вернуть тип, реализующий трейт Foo ". Это невозможно?

@ kud1ing, причина в том, чтобы не Trait если в будущем будет добавлена ​​поддержка возвращаемых значений с динамическим размером. В настоящее время Trait уже является допустимым DST, просто невозможно вернуть DST, поэтому вам нужно упаковать его, чтобы сделать его типом размера.

РЕДАКТИРОВАТЬ: есть некоторые обсуждения по этому поводу в связанной ветке RFC.

Ну, во-первых, независимо от того, будут ли добавляться возвращаемые значения с динамическим размером, я предпочитаю текущий синтаксис. В отличие от того, что происходит с типаж-объектами, это не стирание типа, и любые совпадения типа «параметр f: &Foo принимает что-то, что подразумевает Foo , тогда как это возвращает что-то, что подразумевает Foo ». может ввести в заблуждение.

Из обсуждения RFC я понял, что сейчас impl является реализацией заполнителя, и очень желательно, чтобы impl не было. Есть ли причина, по которой _не_ нужен признак impl если возвращаемое значение не является DST?

Я думаю, что текущая техника реализации для обработки «автоматической утечки признаков» проблематична. Вместо этого мы должны обеспечить порядок DAG, чтобы, если вы определяете fn fn foo() -> impl Iterator и у вас есть вызывающий объект fn bar() { ... foo() ... } , мы должны были проверить тип foo() до bar() (чтобы мы знали, что такое скрытый тип). Если в результате получится цикл, мы сообщим об ошибке. Это консервативная позиция — мы, вероятно, можем добиться большего успеха — но я думаю, что текущая техника, когда мы собираем обязательства авто-черт и проверяем их в конце, в целом не работает. Например, это не будет хорошо работать со специализацией.

(Еще одна возможность, которая может быть более допустимой, чем требование строгого DAG, — это до некоторой степени проверять тип обоих fns «вместе». Я думаю, что это следует учитывать только после того, как мы немного перестроим систему признаков.)

@Nercury Я не понимаю. Вы спрашиваете, есть ли причины не хотеть, чтобы fn foo() -> Trait означало -> impl Trait ?

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

Разница в том, что функции, возвращающие impl Trait всегда возвращают один и тот же тип — это в основном вывод типа возврата. IIUC, функции, возвращающие только Trait могли бы динамически возвращать любую реализацию этого трейта, но вызывающая сторона должна быть готова выделить место для возвращаемого значения с помощью чего-то вроде box foo() .

@Nercury Простая причина в том, что синтаксис -> Trait уже имеет значение, поэтому мы должны использовать что-то еще для этой функции.

На самом деле я видел, как люди ожидают обоих типов поведения по умолчанию, и такого рода путаница возникает достаточно часто. Честно говоря, я бы предпочел, чтобы fn foo() -> Trait ничего не значило (или было предупреждением по умолчанию), ключевые слова как для случая «какой-то тип, известный во время компиляции, который я могу выбрать, но вызывающий не видит», так и для случая «объект типа, который может быть динамически отправлен любому типу, реализующему тип», например fn foo() -> impl Trait против fn foo() -> dyn Trait . Но очевидно, что эти корабли уплыли.

Почему компилятор не генерирует перечисление, которое содержит все различные возвращаемые типы функции, реализует передачу типажа через аргументы для каждого варианта и вместо этого возвращает его?

Это позволит обойти единственное разрешенное правило возвращаемого типа.

@NeoLegends Выполнение этого вручную довольно распространено, и некоторый сахар для этого может быть приятным и предлагался в прошлом, но это третий совершенно отличный набор семантики от возврата impl Trait или типаж-объекта, так что это не так. действительно имеет отношение к этому обсуждению.

@Ixrec Да, я знаю, что это делается вручную, но реальный вариант использования анонимных перечислений в качестве возвращаемых типов, сгенерированных компилятором, — это типы, которые вы не можете расшифровать, например, длинные цепочки итераторов или будущих адаптеров.

Чем отличается эта семантика? Анонимные перечисления (насколько их генерирует компилятор, а не в соответствии с анонимными перечислениями RFC) в качестве возвращаемых значений действительно имеют смысл только в том случае, если существует общий API, такой как черта, которая абстрагирует разные варианты. Я предлагаю функцию, которая по-прежнему выглядит и ведет себя как обычная черта impl, только с удалением ограничения на один тип через сгенерированное компилятором перечисление, которое потребитель API никогда не увидит напрямую. Потребитель всегда должен видеть только «Impl Trait».

Анонимные, автоматически генерируемые перечисления дают impl Trait скрытую стоимость, которую легко не заметить, так что это следует учитывать.

Я подозреваю, что «передача автоматического перечисления» имеет смысл только для объектно-безопасных признаков. Верно ли то же самое и с самим impl Trait ?

@rpjohnst Если это не так, фактический вариант метода находится в метаданных ящика и мономорфизирован на сайте вызова. Конечно, для этого требуется, чтобы переход от одного варианта к другому не нарушал работу вызывающего объекта. И это может быть слишком волшебно.

@glaebhoerl

Я подозреваю, что «передача автоматического перечисления» имеет смысл только для объектно-безопасных признаков. Верно ли то же самое для самого impl Trait?

это интересный момент! Я обсуждал, как правильно "обессахарить" свойство impl, и был на грани того, чтобы предположить, что, возможно, мы хотели бы думать об этом больше как о "структуре с частным полем", а не о "проекции абстрактного типа". "интерпретация. Однако это, кажется, подразумевает что-то очень похожее на обобщенное наследование новых типов, которое, как известно, оказалось несостоятельным в Haskell в сочетании с семействами типов . Признаюсь, у меня нет полного понимания этой несостоятельности «в кеше», но кажется, что мы должны быть очень осторожны здесь всякий раз, когда мы хотим автоматически сгенерировать реализацию трейта для некоторого типа F<T> из реализации. за T .

@никомацакис

Проблема в том, что в терминах Rust

trait Foo {
    type Output;
    fn get() -> Self::Output;
}

fn foo() -> impl Foo {
    // ...
    // what is the type of return_type::get?
}

Суть в том, что обобщенное наследование нового типа было (и есть) реализовано простым transmute в vtable — в конце концов, vtable состоит из функций над типом, а тип и его новый тип имеют одно и то же представление , так должно быть хорошо, верно? Но это не работает, если эти функции также используют типы, которые определяются ветвлением на уровне типа по идентификатору (а не по представлению) данного типа — например, с использованием функций типов или связанных типов (или в Haskell, GADT). Потому что нет гарантии, что представления этих типов также совместимы.

Обратите внимание, что эта проблема возможна только из-за использования небезопасной трансмутации. Если вместо этого он просто сгенерирует скучный шаблонный код, чтобы обернуть/развернуть новый тип везде и отправить каждый метод в его реализацию из базового типа (как некоторые из предложений по автоматическому делегированию для Rust IIRC?), то наихудшим возможным результатом будет тип ошибка или, может быть, ICE. В конце концов, по конструкции, если вы не используете небезопасный код, у вас не может быть небезопасного результата. Точно так же, если бы мы сгенерировали код для какого-то «автоматического прохождения перечисления», но не использовали для этого какие-либо примитивы unsafe , не было бы никакой опасности.

(Я не уверен, относится ли это к моему первоначальному вопросу о том, должны ли трейты, используемые с impl Trait , и/или автоматическое прохождение перечисления по необходимости быть объектно-безопасными?)

@rpjohnst Можно сделать так, чтобы случай enum был

fn foo() -> enum impl Trait { ... }

Это почти наверняка пища для другого RFC.

@glaebhoerl да, я потратил некоторое время на

Извиняюсь, если это что-то очевидное, но я пытаюсь понять причины, по которым impl Trait не может появляться в возвращаемых типах методов типов, или имеет ли это вообще смысл? Например:

trait IterInto {
    type Output;
    fn iter_into(&self) -> impl Iterator<Item=impl Into<Self::Output>>;
}

@aldanor Это полностью имеет смысл, и, насколько мне известно, цель состоит в том, чтобы заставить это работать, но это еще не реализовано.

Это вроде как имеет смысл, но это не та же основная функция (кстати, это много обсуждалось):

// What that trait would desugar into:
trait IterInto {
    type Output;
    type X: Into<Self::Output>;
    type Y: Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y;
}

// What an implementation would desugar into:
impl InterInto for FooList {
    type Output = Foo;
    // These could potentially be left unspecified for
    // a similar effect, if we want to allow that.
    type X = impl Into<Foo>;
    type Y = impl Iterator<Item=Self::X>;
    fn iter_into(&self) -> Self::Y {...}
}

В частности, impl Trait в RHS ассоциированных типов impl Trait for Type будет аналогичен функции, реализованной сегодня, в том смысле, что ее нельзя обессахарить до стабильной версии Rust, тогда как в trait это может быть.

Я знаю, что это, вероятно, и слишком поздно, и в основном байкшеринг, но было ли где-нибудь задокументировано, почему было введено ключевое слово impl ? Мне кажется, что в текущем коде Rust уже есть способ сказать «компилятор определяет, какой тип здесь идет», а именно _ . Не могли бы мы повторно использовать это здесь, чтобы дать синтаксис:

fn foo() -> _ as Iterator<Item=u8> {}

@jonhoo Это не то, что делает функция, это не тот тип, который возвращается из функции, а скорее «семантическая оболочка», которая скрывает все, кроме выбранных API (и OIBIT, потому что это боль).

Мы могли бы разрешить некоторым функциям выводить типы в своих сигнатурах, заставляя DAG, но такая функция никогда не была одобрена и вряд ли когда-либо будет добавлена ​​в Rust, поскольку она затрагивала бы «глобальный вывод».

Предложите использовать синтаксис @Trait для замены impl Trait , как указано здесь .

Его легче распространить на другие позиции типа и в композиции, например Box<@MyTrait> или &@MyTrait .

@Trait для any T where T: Trait и ~Trait для some T where T: Trait :

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> ~Fn(T) -> V {
    move |x| g(f(x))
}

В fn func(t: T) -> V нет необходимости различать какие-либо t или какие-то v, так как trait.

fn compose<T, U, V>(f: @Fn(T) -> U, g: @Fn(U) -> V) -> @Fn(T) -> V {
    move |x| g(f(x))
}

еще работает.

@JF-Liu Я лично против того, чтобы any и some объединялись в одно ключевое слово/знак, но технически вы правы в том, что мы могли бы иметь один знак и использовать его как исходный impl Trait RFC.

@JF-Liu @eddyb Была причина, по которой символы были удалены из языка. Почему эта причина не применима к этому делу?

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

Я имел в виду, что сигилами AFAIK злоупотребляли.

Синтаксический велосипед: я очень недоволен нотацией impl Trait , потому что использование ключевого слова (жирный шрифт в редакторе) для названия типа звучит слишком громко. Помните наблюдение C struct и громкого синтаксиса Страустроупа (слайд 14)?

В https://internals.rust-lang.org/t/ideas-for-making-rust-easier-for-beginners/4761 @konstin предложил <Trait> . Выглядит очень красиво, особенно в позиции ввода:

fn take_iterator(iterator: <Iterator<Item=i32>>)

Я вижу, что это несколько будет конфликтовать с UFCS, но, может быть, это можно проработать?

Я тоже считаю, что использование угловых скобок вместо impl Trait будет лучшим выбором, по крайней мере, в позиции возвращаемого типа, например:

fn returns_iter() -> <Iterator<Item=i32>> {...}
fn returns_closure() -> <FnOnce() -> bool> {...}

Синтаксис <Trait> конфликтует с дженериками, учтите:

Vec<<FnOnce() -> bool>> против Vec<@FnOnce() -> bool>

Если Vec<FnOnce() -> bool> разрешено, то <Trait> — хорошая идея, это означает эквивалентность параметру универсального типа. Но поскольку Box<Trait> отличается от Box<@Trait> , придется отказаться от синтаксиса <Trait> .

Я предпочитаю синтаксис ключевого слова impl потому что при быстром чтении документации это позволяет меньше ошибиться в понимании прототипов.
Что вы думаете ?

Я только что понял, что предложил надмножество этого rfc в ветке внутренних дел (спасибо

Разрешить использование трейтов в параметрах функций и возвращаемых типах, заключив их в угловые скобки, как в следующем примере:

fn transform(iter: <Iterator>) -> <Iterator> {
    // ...
}

Затем компилятор мономорфизировал бы параметр, используя те же правила, которые в настоящее время применяются к дженерикам. Тип возвращаемого значения может быть получен, например, из реализации функций. Это означает, что вы не можете просто вызвать этот метод для Box<Trait_with_transform> или использовать его для динамически отправляемых объектов в целом, но это все равно сделало бы правила более разрешительными. Я не читал все обсуждения RFC, так что, возможно, там уже есть лучшее решение, которое я пропустил.

Я предпочитаю синтаксис ключевого слова impl, потому что при быстром чтении документации это позволяет меньше пропустить прототипы.

Другой цвет подсветки синтаксиса должен помочь.

В этой статье Страуструпа обсуждается аналогичный синтаксический выбор для понятий C++ в разделе 7: http://www.stroustrup.com/good_concepts.pdf

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

fn transform(iter: <Iterator>) -> <Iterator>

должен быть эквивалентен этому

fn transform<T: Iterator, U: Iterator>(iter: T) -> U

или это должно быть эквивалентно этому

fn transform(iter: impl Iterator) -> impl Iterator

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

Возможно, синтаксис должен быть похожим, но он не должен быть одинаковым.

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

  • используйте Type , &Type , Box<Type> для конкретных типов данных, статическая диспетчеризация
  • используйте @Trait , &@Trait , Box<@Trait> и общий параметр типа для абстрактного типа данных, статической отправки
  • используйте &Trait , Box<Trait> для абстрактного типа данных, динамической отправки

fn func(x: @Trait) эквивалентно fn func<T: Trait>(x: T) .
fn func<T1: Trait, T2: Trait>(x: T1, y: T2) можно просто записать как fn func(x: <strong i="22">@Trait</strong>, y: @Trait) .
T по-прежнему необходим в fn func<T: Trait>(x: T, y: T) .

struct Foo { field: <strong i="28">@Trait</strong> } эквивалентно struct Foo<T: Trait> { field: T } .

При использовании в переменных, аргументах и ​​полях структуры всегда принимается любой из T, при использовании в возвращаемом типе fn всегда принимается какой-либо из T.

Вы можете вернуть любой признак прямо сейчас в стабильной версии Rust, используя существующий общий синтаксис. Это очень часто используемая функция. serde_json::de::from_slice принимает &[u8] в качестве параметра и возвращает T where T: Deserialize .

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

Для более знакомого примера Iterator::collect может возвращать любое значение T where T: FromIterator<Self::Item> , что подразумевает мою предпочтительную запись: fn collect(self) -> any FromIterator<Self::Item> .

Как насчет синтаксиса
fn foo () -> _ : Trait { ... }
для возвращаемых значений и
fn foo (m: _1, n: _2) -> _ : Trait where _1: Trait1, _2: Trait2 { ... }
для параметров?

Для меня действительно ни одно из новых предложений не приближается к impl Trait по своей элегантности. impl — это ключевое слово, уже известное каждому программисту на rust, и, поскольку оно используется для реализации трейтов, оно на самом деле предполагает, что эта функция делает сама по себе.

Да, использование существующих ключевых слов кажется мне идеальным; Я хотел бы видеть impl для экзистенциалов и for для универсалов.

Лично я против объединения any и some в одно ключевое слово/символ.

@eddyb Я бы не считал это смешением. Это естественно следует из правила:

((∃ T . F⟨T⟩) → R)  →  ∀ T . (F⟨T⟩ → R)

Изменить: это односторонний, а не изоморфизм.


Несвязанный: есть ли какое-либо связанное предложение, позволяющее также разрешить impl Trait в других ковариантных позициях, таких как

~ ржавчинафн фу(обратный вызов: F) -> Rгде F: FnOnce(импл SomeTrait) -> R {обратный вызов (создать_что-то())}~

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

Но если функция RFC 1522 стабилизируется, то будет невозможно назначить сигнатуру типа программам, таким как выше, если create_something приводит к impl SomeTrait (по крайней мере, без упаковки). Я думаю, это проблематично.

@Rufflewind В реальном мире все не так однозначно, и эта функция представляет собой очень специфический вид экзистенциалов (в Rust их уже несколько).

Но даже в этом случае все, что у вас есть, — это использование ковариации для определения того, что означает impl Trait внутри и снаружи аргументов функции.

Этого недостаточно для:

  • используя противоположное значение по умолчанию
  • устранение неоднозначности внутри типа поля (где одинаково желательны как any и some )

@Rufflewind Похоже, это неправильный брекетинг для того, что такое impl Trait . Я знаю, что Haskell использует это отношение, чтобы использовать только ключевое слово forall для представления как универсалий, так и экзистенциалов, но это не работает в обсуждаемом нами контексте.

Возьмем, к примеру, это определение:

fn foo(x: impl ArgTrait) -> impl ReturnTrait { ... }

Если мы используем правило, что " impl в аргументах является универсальным, impl в возвращаемых типах является экзистенциальным", то тип элемента функции foo логически такой (в обозначение выдуманного типа):

forall<T: ArgTrait>(exists<R: ReturnTrait>(fn(T) -> R))

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

forall<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

Или это:

exists<T: ArgTrait, R: ReturnTrait>(fn(T) -> R)

И ни одно из них не сводится к тому, что мы хотим по логическим правилам. Так в конечном счете any / some захватывают важное различие вы не можете захватить с одного ключевого слова. В std есть даже разумные примеры, когда вам нужны универсальные символы в позиции возврата. Например, этот метод Iterator :

fn collect<B>(self) -> B where B: FromIterator<Self::Item>;
// is equivalent to
fn collect(self) -> any FromIterator<Self::Item>;

И нет никакого способа написать это с помощью impl и правила аргумента/возврата.

tl; dr, имеющий impl контекстуально обозначающий либо универсальный, либо экзистенциальный, действительно придает ему два разных значения.


Для справки, в моей записи отношение forall/exists, упомянутое @Rufflewind, выглядит так:

fn(exists<T: Trait>(T)) -> R === forall<T: Trait>(fn(T) -> R)

Это связано с концепцией трейт-объектов (экзистенциалов), эквивалентных дженерикам (универсалиям), но не с этим impl Trait вопросом.

Тем не менее, я больше не сторонник any / some . Я хотел быть точным в том, о чем мы говорим, и any / some имеют эту теоретическую и визуальную привлекательность, но я был бы в порядке с использованием impl с контекстным правило. Я думаю, что он охватывает все распространенные случаи, позволяет избежать проблем с грамматикой контекстных ключевых слов, а в остальном мы можем перейти к параметрам именованного типа.

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

В общем, я был бы доволен:

  • impl Trait как сокращение как для универсалий, так и для экзистенциалов (контекстно).
  • Параметры именованного типа как полностью общее сокращение как для универсалов, так и для экзистенциалов. (Реже требуется.)

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

@solson Для меня «наивный» перевод привел бы к тому, что кванторы существования были бы прямо рядом с определяемым типом. Следовательно

~ ржавчина(импл MyTrait)~

просто синтаксический сахар для

~ ржавчина(существуютТ)~

что представляет собой простое локальное преобразование. Таким образом, наивный перевод, подчиняющийся правилу « impl всегда является экзистенциальным», приведет к следующему результату:

~ ржавчинафн(существуетТ) -> (существуетР)~

Затем, если вы вытащите квантификатор из аргумента функции, он станет

~ ржавчиназаfn(T) -> (существуетР)~

Таким образом, несмотря на то, что T всегда экзистенциальна по отношению к самой себе, она кажется универсальной по отношению ко всему функциональному типу.


ИМО, я думаю, что impl может стать ключевым словом де-факто для экзистенциальных типов. В будущем, возможно, можно было бы создать более сложные экзистенциальные типы, такие как:

~~ржавчина(импл(Век, Т))~ ~

по аналогии с универсальными типами (через HRTB)

~ ржавчина(для <'a> FnOnce(&'a T))~

@Rufflewind Это представление не работает, потому что fn(T) -> (exists<R: ReturnTrait>(R)) логически не эквивалентно exists<R: ReturnTrait>(fn(T) -> R) , что impl Trait самом деле означает возвращаемый тип

(По крайней мере, не в конструктивной логике, обычно применяемой к системам типов, где важен конкретный свидетель, выбранный для экзистенциала. Первое подразумевает, что функция может выбирать разные типы для возврата на основе, скажем, аргументов, а второе подразумевает, что существует один конкретный тип для всех вызовов функции, как в случае с impl Trait .)

Я чувствую, что мы тоже немного отдалились. Я думаю, что контекстуальное impl — это приемлемый компромисс, и я не думаю, что такое обоснование необходимо или особенно полезно (мы, конечно, не будем учить этому правилу с точки зрения такого рода логической связи). ).

@solson Да, вы правы: экзистенциалы не могут быть раскрыты. Этот вообще не держится:

(T → ∃R. f(R))  ⥇  ∃R. T → f(R)

тогда как они выполняются в целом:

(∃R. T → f(R))  →   T → ∃R. f(R)
(∀A. g(A) → T)  ↔  ((∃A. g(A)) → T)

Последний отвечает за переинтерпретацию экзистенциалов в аргументах как дженериков.

Изменить: К сожалению, (∀A. g(A) → T) → (∃A. g(A)) → T делает удержание.

Я опубликовал RFC с подробным предложением по расширению и стабилизации impl Trait . Это основано на большом количестве дискуссий в этой и предыдущих темах.

Стоит отметить, что https://github.com/rust-lang/rfcs/pull/1951 был принят.

Каков статус этого в настоящее время? У нас есть готовый RFC, есть люди, использующие первоначальную реализацию, но я не совсем понимаю, что нужно сделать.

В #43869 было обнаружено, что функция -> impl Trait не поддерживает чисто расходящееся тело:

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

Ожидается ли это (поскольку ! не подразумевает Iterator ) или считается ошибкой?

Как насчет определения выведенных типов, которые можно было бы использовать не только как возвращаемые значения, но и как что-либо (я думаю), для чего тип может использоваться в настоящее время?
Что-то типа:
type Foo: FnOnce() -> f32 = #[infer];
Или с ключевым словом:
infer Foo: FnOnce() -> f32;

Затем тип Foo можно было бы использовать как возвращаемый тип, тип параметра или что-то еще, для чего может использоваться тип, но было бы незаконным использовать его в двух разных местах, где требуется другой тип, даже если это type реализует FnOnce() -> f32 в обоих случаях. Например, следующее не будет компилироваться:

infer Foo: FnOnce() -> f32;

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo {
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

Это не должно компилироваться, потому что даже если возвращаемые типы из return_closure и return_closure2 оба являются FnOnce() -> f32 , их типы на самом деле разные, потому что в Rust нет двух замыканий одного типа. . Таким образом, для компиляции вышеизложенного вам необходимо определить два разных выводимых типа:

infer Foo: FnOnce() -> f32;
infer Foo2: FnOnce() -> f32; //Added this line

fn return_closure() -> Foo {
    || 0.1
}

fn return_closure2() -> Foo2 { //Changed Foo to Foo2
    || 0.2
}

fn main() {
    println!("{:?}, {:?}", return_closure()(), return_closure2()());
}

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

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

@cramertj Ааа , вот почему эта проблема стала такой тихой..

Итак, @cramertj спрашивал меня о том, как, по моему мнению, было бы лучше всего решить проблему регионов с поздним связыванием, с которой они столкнулись в своем PR. Я считаю, что мы, вероятно, хотим немного «переоборудовать» нашу реализацию, чтобы попытаться с нетерпением ждать модели anonymous type Foo .

Для контекста идея примерно такая

fn foo<'a, 'b, T, U>() -> impl Debug + 'a

был бы (вроде) обессахарен до чего-то вроде этого

anonymous type Foo<'a, T, U>: Debug + 'a
fn foo<'a, 'b, T, U>() -> Foo<'a, T, U>

Обратите внимание, что в этой форме вы можете увидеть, какие универсальные параметры захвачены, потому что они появляются как аргументы для Foo — в частности, 'b не захватывается, потому что он не появляется в ссылке на трейт в как угодно, но параметры типа T и U всегда есть.

В любом случае, в настоящее время в компиляторе, когда у вас есть ссылка impl Debug , мы создаем def-id, который фактически представляет этот анонимный тип. Затем у нас есть запрос generics_of , который вычисляет его общие параметры. Прямо сейчас это возвращает то же самое, что и «охватывающий» контекст, то есть функцию foo . Это то, что мы хотим изменить.

На «другой стороне», то есть в подписи foo , мы представляем impl Foo как TyAnon . Это в основном правильно - TyAnon представляет собой ссылку на Foo которую мы видим в приведенном выше дешугаринге. Но способ получения «субстанций» для этого типа заключается в использовании функции «идентичности» , что явно неверно — или, по крайней мере, не обобщает.

Так что, в частности, здесь происходит своего рода «нарушение пространства имен». Когда мы генерируем «идентификационные» подстановки для элемента, это обычно дает нам замены, которые мы использовали бы при проверке типа этого элемента, то есть со всеми его общими параметрами в области видимости. Но в этом случае мы создаем ссылку на Foo которая появляется внутри функции foo() , поэтому мы хотим, чтобы общие параметры foo() появлялись в Substs , а не Foo . Это работает, потому что сейчас это одно и то же, но это не совсем правильно .

Я думаю, что мы должны делать что-то вроде этого:

Во-первых, когда мы вычисляем параметры универсального типа Foo (то есть самого анонимного типа), мы начинаем создавать новый набор универсальных типов. Естественно, это будет включать типы. Но на протяжении всей жизни мы перешагиваем границы признаков и идентифицируем каждую из областей, которые появляются внутри. Это очень похоже на существующий код, который написал Cramertj , за исключением того, что мы не хотим накапливать идентификаторы def, потому что не все регионы в области видимости имеют идентификаторы def.

Я думаю, что мы хотим собрать набор появляющихся регионов и расположить их в некотором порядке, а также отслеживать значения для этих регионов с точки зрения foo() . Это немного раздражает, потому что у нас нет единой структуры данных, представляющей логическую область. (Раньше у нас было понятие FreeRegion , которое почти сработало бы, но мы больше не используем FreeRegion для вещей с ранним связыванием, только для вещей с поздним связыванием.)

Возможно, самым простым и лучшим вариантом было бы просто использовать Region<'tcx> , но вам придется сместить глубину индекса debruijn, чтобы «отменить» любые введенные связующие. Хотя это, пожалуй, лучший выбор.

Таким образом, в основном, когда мы получаем обратные вызовы в visit_lifetime , мы преобразуем их в Region<'tcx> выраженные в начальной глубине (нам придется отслеживать, когда мы проходим через связующие). Мы будем накапливать их в векторе, удаляя дубликаты.

Когда мы закончим, у нас есть две вещи:

  • Во-первых, для каждой области в векторе нам нужно создать общий параметр области. Все они могут иметь анонимные имена или что-то еще, это не имеет большого значения (хотя, может быть, нам нужно, чтобы у них были def-id или что-то в этом роде...? Мне нужно посмотреть на структуры данных RegionParameterDef ...) .
  • Во-вторых, области в векторе также являются теми вещами, которые мы хотим использовать для «подстроек».

Хорошо, извините, если это загадочно. Я не могу понять, как сказать это более ясно. Хотя в чем-то я не уверен — прямо сейчас я чувствую, что наша обработка регионов довольно сложна, так что, может быть, есть способ реорганизовать вещи, чтобы сделать их более однородными? Готов поспорить на 10 долларов, что у @eddyb есть какие-то мысли. ;)

@nikomatsakis Я считаю, что многое из этого похоже на то, что я сказал @cramertj , но более конкретизировано!

Я думал об экзистенциальном impl Trait и столкнулся с любопытным случаем, когда, как мне кажется, нам следует действовать с осторожностью. Рассмотрим эту функцию:

trait Foo<T> { }
impl Foo<()> for () { }
fn foo() -> impl Foo<impl Debug> {
  ()
}

Как вы можете убедиться в игре , этот код компилируется сегодня. Однако, если мы углубимся в то, что происходит, это выделит то, что имеет опасность «совместимости вперед», которая меня беспокоит.

В частности, ясно, как мы выводим возвращаемый здесь тип ( () ). Менее ясно, как мы выводим тип параметра impl Debug . То есть вы можете думать об этом возвращаемом значении как о чем-то вроде -> ?T где ?T: Foo<?U> . Мы должны вывести значения ?T и ?U основываясь только на том факте, что ?T = () .

Прямо сейчас мы делаем это, используя тот факт, что существует только один импл. Однако это хрупкое свойство. Если добавится новый импл, код перестанет компилироваться , потому что теперь мы не можем однозначно определить, какой должна быть ?U .

Это может случиться во многих сценариях в Rust — что достаточно важно, но ортогонально — но в случае impl Trait есть кое-что другое. В случае impl Trait у пользователя нет возможности добавлять аннотации типов, чтобы направлять вывод! И у нас действительно нет плана для такого пути. Единственное решение — изменить интерфейс fn на impl Foo<()> или что-то другое явное.

В будущем, используя abstract type , можно было бы представить, что пользователи могут явно указать скрытое значение (или, возможно, просто неполные подсказки, используя _ ), что затем может помочь в выводе, сохраняя при этом примерное значение. тот же публичный интерфейс

abstract type X: Debug = ();
fn foo() -> impl Foo<X> {
  ()
}

Тем не менее, я думаю, что было бы благоразумно избегать стабилизации "вложенных" применений экзистенциальной реализации Trait, за исключением привязок связанных типов (например, impl Iterator<Item = impl Debug> не страдает от этих двусмысленностей).

В случае impl Trait у пользователя нет возможности добавлять аннотации типов, чтобы направлять вывод! И у нас действительно нет плана для такого пути.

Возможно, это могло выглядеть как UFCS? например, <() as Foo<()>> -- не меняет тип, как голый as , просто устраняет неоднозначность. В настоящее время это недопустимый синтаксис, так как ожидается, что :: и другие будут следовать.

Я только что нашел интересный случай, касающийся вывода типов с помощью impl Trait для Fn :
Следующий код компилируется просто отлично :

fn op(s: &str) -> impl Fn(i32, i32) -> i32 {
    match s {
        "+" => ::std::ops::Add::add,
        "-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    }
}

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

error[E0308]: match arms have incompatible types
 --> src/main.rs:4:5
  |
4 | /     match s {
5 | |         "+" => ::std::ops::Add::add,
6 | | //         "-" => ::std::ops::Sub::sub,
7 | |         "<" => |a,b| (a < b) as i32,
8 | |         _ => unimplemented!(),
9 | |     }
  | |_____^ expected fn item, found closure
  |
  = note: expected type `fn(_, _) -> <_ as std::ops::Add<_>>::Output {<_ as std::ops::Add<_>>::add}`
             found type `[closure@src/main.rs:7:16: 7:36]`
note: match arm with an incompatible type
 --> src/main.rs:7:16
  |
7 |         "<" => |a,b| (a < b) as i32,
  |                ^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

@oberien Это не похоже на impl Trait - это верно для умозаключений в целом. Попробуйте эту небольшую модификацию вашего примера:

fn main() {
    let _: i32 = (match "" {
        "+" => ::std::ops::Add::add,
        //"-" => ::std::ops::Sub::sub,
        "<" => |a,b| (a < b) as i32,
        _ => unimplemented!(),
    })(5, 5);
}

Похоже, это закрыто сейчас:

ICEs при взаимодействии с elision

Одна вещь, которую я не вижу в этом выпуске или в обсуждении, — это возможность хранить замыкания и генераторы, которые не предоставляются вызывающей стороной, в полях структуры. Прямо сейчас это возможно, но выглядит некрасиво: вам нужно добавить параметр типа в структуру для каждого поля замыкания/генератора, а затем в сигнатуре функции-конструктора заменить этот параметр типа на impl FnMut/impl Generator . Вот пример , и он работает, что довольно круто! Но оставляет желать лучшего. Было бы намного лучше, если бы вы могли избавиться от параметра типа:

struct Counter(impl Generator<Yield=i32, Return=!>);

impl Counter {
    fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

impl Trait может быть неправильным способом сделать это - вероятно, абстрактные типы, если я правильно прочитал и понял RFC 2071. Что нам нужно, так это то, что мы можем написать в определении структуры, чтобы можно было вывести фактический тип ( [generator@src/main.rs:15:17: 21:10 _] ).

Я считаю, что абстрактные типы

abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        Counter(|| {
            let mut x: i32 = 0;
            loop {
                yield x;
                x += 1;
            }
        })
    }
}

Есть ли запасной путь, если это чужой impl Generator который я хочу поместить в свою структуру, но они не создали для меня abstract type ?

@scottmcm Вы все еще можете объявить свои собственные abstract type :

// library crate:
fn foo() -> impl Generator<Yield = i32, Return = !> { ... }

// your crate:
abstract type MyGenerator: Generator<Yield = i32, Return = !>;

pub struct Counter(MyGenerator);

impl Counter {
    pub fn new() -> Counter {
        let inner: MyGenerator = foo();
        Counter(inner)
    }
}

@cramertj Подождите, абстрактные типы уже в ночных ?! Где пиар?

@alexreg Нет, это не так.

Редактировать: Приветствую вас, гости из будущего! Проблема ниже была решена.


Я хотел бы обратить внимание на этот причудливый крайний случай использования, который появляется в # 47348.

use ::std::ops::Sub;

fn test(foo: impl Sub) -> <impl Sub as Sub>::Output { foo - foo }

Должен ли вообще быть разрешен возврат такой проекции на impl Trait ? (потому что в настоящее время __это.__)

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

@ExpHP Хм. Это кажется проблематичным по той же причине, что и impl Foo<impl Bar> . По сути, у нас нет никаких реальных ограничений на рассматриваемый тип — только на то, что из него проецируется.

Я думаю, мы хотим повторно использовать логику «параметров ограниченного типа» из impls. Короче говоря, указание возвращаемого типа должно «ограничивать» impl Sub . Функция, о которой я говорю, такова:

https://github.com/rust-lang/rust/blob/a0dcecff90c45ad5d4eb60859e22bb3f1b03842a/src/librustc_typeck/constrained_type_params.rs#L89 -L93

Небольшая сортировка для людей, которым нравятся флажки:

  • #46464 готово -> флажок
  • #48072 готово -> флажок

@rfcbot fcp слияние

Я предлагаю стабилизировать функции conservative_impl_trait и universal_impl_trait с одним ожидающим изменением (исправление для https://github.com/rust-lang/rust/issues/46541).

Тесты, документирующие текущую семантику

Тесты для этих функций можно найти в следующих каталогах:

run-pass/impl-trait
пользовательский интерфейс/импл-черта
компиляция-ошибка/внедрение-черта

Вопросы, решаемые в ходе реализации

Детали парсинга impl Trait были разрешены в RFC 2250 и реализованы в https://github.com/rust-lang/rust/pull/45294.

impl Trait запрещены позиции вложенного-несвязанного типа и определенные квалифицированные позиции пути, чтобы предотвратить двусмысленность. Это было реализовано в https://github.com/rust-lang/rust/pull/48084.

Оставшиеся нестабильные функции

После этой стабилизации можно будет использовать impl Trait в позиции аргумента и позиции возврата функций, не являющихся трейтами. Однако использование impl Trait любом месте синтаксиса Fn по-прежнему запрещено, чтобы обеспечить возможность будущих итераций дизайна. Кроме того, ручное указание параметров типа функций, которые используют impl Trait в позиции аргумента, не допускается.

Член команды @cramertj предложил объединить это. Следующий шаг — просмотр остальными отмеченными командами:

  • [x] @атурон
  • [х] @cramertj
  • [х] @eddyb
  • [x] @никомацакис
  • [х] @nrc
  • [х] @pnkfelix
  • [x] @без лодок

В настоящее время в списке нет проблем.

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

См. этот документ для получения информации о том, какие команды могут давать мне члены команды, отмеченные тегами.

После этой стабилизации можно будет использовать impl Trait в позиции аргумента и возвращать позицию функций, не являющихся трейтами. Тем не менее, использование черты impl в любом месте синтаксиса Fn по-прежнему запрещено, чтобы обеспечить будущую итерацию дизайна. Кроме того, ручное указание параметров типа функций, которые используют impl Trait в позиции аргумента, не допускается.

Каков статус использования impl Trait в аргументе / возвратные позиции в функции признака, или в синтаксисе Fn, по этому вопросу?

@alexreg Возвратная позиция impl Trait в типажах заблокирована в RFC, хотя RFC 2071 разрешит аналогичную функциональность после реализации. Аргумент-позиция impl Trait в типажах не заблокирована для каких-либо технических функций, о которых мне известно, но это не было явно разрешено в RFC, поэтому на данный момент оно опущено.

impl Trait в позиции аргумента синтаксиса Fn заблокировано на уровне типа HRTB, так как некоторые люди думают, что T: Fn(impl Trait) следует обесценить до T: for<X: Trait> Fn(X) . impl Trait в позиции возврата синтаксиса Fn не заблокирован по какой-либо технической причине, о которой я знаю, но это было запрещено в RFC в ожидании дальнейшей работы по проектированию - я ожидаю, что см. другой RFC или, по крайней мере, отдельный FCP, прежде чем стабилизировать это.

@cramertj Хорошо, спасибо за обновление. Надеюсь, мы увидим, что эти две функции, которые ничем не заблокированы, скоро получат добро после некоторого обсуждения. Дешугаринг имеет смысл в позиции аргумента, аргумент foo: T где T: Trait эквивалентен foo: impl Trait , если я не ошибаюсь.

Проблема: https://github.com/rust-lang/rust/issues/34511#issuecomment -322340401 остается прежней. Можно ли разрешить следующее?

fn do_it_later_but_cannot() -> impl Iterator<Item=u8> { //~ ERROR E0227
    unimplemented!()
}

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

Вопрос о турбофишах решен лишь наполовину. Мы должны хотя бы предупредить о impl Trait в аргументах фактически общедоступных функций, считая impl Trait в аргументах закрытым типом для нового частного в общедоступной проверке .

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

Мотивация состоит в том, чтобы не дать библиотекам сломать турбофиш пользователей, изменив аргумент с явного универсального на impl Trait .

Я надеюсь, что когда это начнет происходить и люди начнут жаловаться, люди из lang-команды, которые в настоящее время сомневаются, будут убеждены, что impl Trait должен поддерживать явное предоставление аргументов типа с помощью turbofish.

@leodasvacas

Вопрос о турбофишах решен лишь наполовину. Мы должны, по крайней мере, предупредить об имплементации Trait в аргументах фактически общедоступных функций, считая импл Trait в аргументах частным типом для новой частной проверки в общедоступной.

Я не согласен - это было решено. На данный момент мы полностью запрещаем использование Turbofish для этих функций. Изменение подписи общедоступной функции для использования impl Trait вместо явных универсальных параметров является критическим изменением.

Если мы позволим использовать Turbofish для этих функций в будущем, это, скорее всего, позволит указывать только параметры, отличные от типа impl Trait .

:bell: Это сейчас вступает в свой последний период комментариев , в соответствии с обзором выше . :колокол:

Я должен добавить, что я не хочу стабилизироваться, пока https://github.com/rust-lang/rust/pull/49041 не приземлится. (Но, надеюсь, это будет скоро.)

Таким образом, № 49041 содержит исправление для № 46541, но это исправление имеет большее влияние, чем я ожидал — например, теперь компилятор не загружается — и это дает мне некоторую паузу в отношении правильного курса здесь. Проблема в #49041 заключается в том, что мы можем случайно допустить утечку жизней, которых не должны были делать. Вот как это проявляется в компиляторе. У нас может быть такой метод:

impl TyCtxt<'cx, 'gcx, 'tcx>
where 'gcx: 'tcx, 'tcx: 'cx
{
    fn foos(self) -> impl Iterator<Item = &'tcx Foo> + 'cx {
        /* returns some type `Baz<'cx, 'gcx, 'tcx>` that captures self */
    }
}

Ключевым моментом здесь является то, что TyCtxt инвариантны относительно 'tcx и 'gcx , поэтому они должны появляться в возвращаемом типе. И все же только 'cx и 'tcx появляются в границах признака impl, поэтому предполагается, что только эти два времени жизни должны быть «захвачены». Старый компилятор принимал это, потому что 'gcx: 'cx , но это не совсем правильно, если вы думаете о дешугаризации, которую мы имеем в виду. Эта дешугаризация создаст такой абстрактный тип:

abstract type Foos<'cx, 'tcx>: Iterator<Item = &'tcx Foo> + 'cx;

и все же значение для этого абстрактного типа будет Baz<'cx, 'gcx, 'tcx> -- но 'gcx не входит в область действия!

Обходной путь здесь заключается в том, что мы должны назвать 'gcx в границах. Это немного раздражает; мы не можем использовать 'cx + 'gcx . Мы можем, я полагаю, сделать фиктивную черту:

trait Captures<'a> { }
impl<T: ?Sized> Captures<'a> for T { }

а затем верните что-то вроде этого impl Iterator<Item = &'tcx Foo> + Captures<'gcx> + Captures<'cx> .

Кое-что, что я забыл отметить: если бы объявленный возвращаемый тип был dyn Iterator<Item = &'tcx Foo> + 'cx , это было бы нормально, потому что ожидается, что типы dyn стирают время жизни. Поэтому я не верю, что здесь возможна какая-либо несостоятельность, предполагая, что вы не можете сделать ничего проблематичного с impl Trait что было бы невозможно с dyn Trait .

Можно смутно представить себе идею, что значение абстрактного типа аналогично экзистенциальному: exists<'gcx> Baz<'cx, 'gcx, 'tcx> .

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

ОБНОВЛЕНИЕ: Чтобы прояснить мое значение черт dyn : я говорю, что они могут «скрывать» всю жизнь, например, 'gcx пока граница ( здесь 'cx ) гарантирует, что 'gcx будет по-прежнему активен везде, где используется dyn Trait .

@nikomatsakis Это интересный пример, но я не чувствую, что он меняет здесь основное исчисление, то есть мы хотим, чтобы все соответствующие времена жизни были ясны только из возвращаемого типа.

Черта Captures кажется хорошим и легким подходом для этой ситуации. Кажется, что на данный момент он может перейти в std::marker как нестабильный?

@nikomatsakis Ваш последующий комментарий заставил меня понять, что я не совсем собрал все части здесь, чтобы понять, почему вы можете ожидать, что в этом случае вы упустите 'gcx , т.е. 'gcx не является "соответствующий срок службы" с точки зрения клиента. В любом случае начинать с консервативного кажется нормальным.

Мое личное мнение заключается в том, что https://github.com/rust-lang/rust/issues/46541 на самом деле не является ошибкой - это поведение, которого я ожидал, я не понимаю, как его можно сделать ненадежным, и это боль, чтобы обойти. IMO должна быть возможность вернуть тип, который реализует Trait и переживает время жизни 'a как impl Trait + 'a , независимо от того, какие другие времена жизни он содержит. Тем не менее, я согласен стабилизировать более консервативный подход для начала, если это то, что предпочитает @rust-lang/lang.

(Еще одно уточнение: единственный раз, когда вы получите ошибки с исправлением в #49041, это когда скрытый тип инвариантен по отношению к отсутствующему времени жизни 'gcx , так что это, вероятно, срабатывает относительно редко.)

@cramertj

Мое личное мнение состоит в том, что # 46541 на самом деле не является ошибкой - это поведение, которого я ожидал, я не понимаю, как его можно сделать ненадежным, и это проблематично обойти.

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

@rfcbot касается сайтов с несколькими возвратами

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

fn foo(empty: bool) -> impl Iterator<Item = u32> {
    if empty { None.into_iter() } else { &[1, 2, 3].cloned() }
}

Конечно, сегодня это не работает, и это определенно выходит за рамки того, чтобы заставить это работать. Тем не менее, то, как черта impl работает сейчас, фактически закрывает дверь для того, чтобы она когда-либо работала (с таким синтаксисом). Это связано с тем, что в настоящее время вы можете накапливать ограничения с нескольких сайтов возврата:

fn foo(empty: bool) -> (impl Debug, impl Debug) {
    if empty { return (22, Default::default()); }
    return (Default::default(), false);
}

Здесь предполагаемый тип (i32, bool) , где первый return ограничивает часть i32 , а второй return ограничивает часть bool .

Это означает, что мы никогда не сможем поддерживать случаи, когда два оператора return не объединяются (как в моем первом примере), иначе это было бы очень неприятно.

Интересно, должны ли мы ввести ограничение, которое требует, чтобы каждый return (в общем, каждый источник ограничения) был полностью определен независимо? (И мы объединяем их постфактум?)

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

@rfcbot разрешает

Поэтому я немного поговорил с @cramertj на #rust-lang . Мы обсуждали идею сделать «ранний возврат» нестабильным для impl Trait , чтобы в конечном итоге изменить его.

Они пришли к выводу, что было бы лучше иметь явное согласие на такой синтаксис, особенно потому, что есть другие случаи (например, let x: impl Trait = if { ... } else { ... } ), когда это нужно, и мы не можем ожидать обрабатывать их все неявно (определенно нет).

Я нахожу это довольно убедительным. До этого я предполагал, что у нас в любом случае будет некоторый синтаксис согласия, но я просто хотел убедиться, что мы не закрыли двери преждевременно. В конце концов, объяснить, когда вам нужно вставить «динамическую прокладку», довольно сложно.

@nikomatsakis Просто мое, возможно, менее информированное мнение: хотя возможность функции возвращать один из нескольких типов, возможных во время выполнения, может быть полезной, я бы не хотел иметь один и тот же синтаксис как для статического вывода типа возвращаемого значения для одного типа, так и разрешение ситуаций, когда требуется какое-то внутреннее решение во время выполнения (то, что вы только что назвали «динамической прокладкой»).

Этот первый пример foo , насколько я понял проблему, может либо разрешить (1) коробочный + стертый тип Iterator<Item = u32> , либо (2) тип суммы std::option::Iter или std::slice::Iter , что, в свою очередь, приведет к реализации Iterator . Постараюсь быть кратким, поскольку в обсуждении были некоторые обновления (а именно, я сейчас читаю журналы IRC), и становится все труднее понять: я, безусловно, согласен с dyn-подобным синтаксисом для динамической прокладки, хотя я также поймите, что называть это dyn может быть не идеально.

Бесстыдная вилка и небольшая заметка для протокола: вы можете легко получить «анонимные» типы сумм и продуктов с помощью:

@Centril Да, эта штука от frunk очень крутая. Однако обратите внимание, что для того, чтобы CoprodInjector::inject работало, результирующий тип должен быть выводимым, что обычно невозможно без именования результирующего типа (например, -> Coprod!(A, B, C) ). Часто бывает так, что вы работаете с безымянными типами, поэтому вам понадобится -> Coprod!(impl Trait, impl Trait, impl Trait) , что приведет к ошибке вывода, потому что он не знает, какой вариант должен содержать какой тип impl Trait .

@cramertj Совершенно верно (примечание: каждый «вариант» может быть не полностью безымянным, а только частично, например: Map<Namable, Unnameable> ).

Идея enum impl Trait обсуждалась ранее в https://internals.rust-lang.org/t/pre-rfc-anonymous-enums/5695.

@Centril Да, это правда. В частности, я думаю о будущем, где я часто пишу такие вещи, как

fn foo(x: Foo) -> impl Future<Item = (), Error = Never> {
    match x {
        Foo::Bar => do_request().and_then(|res| ...).left().left(),
        Foo::Baz => do_other_thing().and_then(|res| ...).left().right(),
        Foo::Boo => do_third_thing().and_then(|res| ...).right(),
    }
}

@cramertj Я бы не сказал, что анонимное перечисление похоже на enum impl Trait , потому что мы не можем заключить, что X: Tr && Y: Tr(X|Y): Tr (встречный пример: Default черта impl Future for (X|Y|Z|...) .

@kennytm Предположительно, мы хотели бы автоматически сгенерировать некоторые черты характера для анонимных перечислений, поэтому похоже, что в основном это одна и та же функция.

@cramertj Поскольку анонимное перечисление может быть названо (хе-хе), если импл Default сгенерирован для (i32|String) , мы сможем написать <(i32|String)>::default() . OTOH <enum impl Default>::default() просто не будет компилироваться, поэтому независимо от того, что мы автоматически сгенерируем, он все равно будет безопасным, поскольку его вообще нельзя вызвать.

Тем не менее, в некоторых случаях автогенерация все еще может вызывать проблемы с enum impl Trait . Учитывать

pub trait Rng {
    fn next_u32(&mut self) -> u32;
    fn gen<T: Rand>(&mut self) -> T where Self: Sized;
    fn gen_iter<'a, T: Rand>(&'a mut self) -> Generator<'a, T, Self> where Self: Sized;
}

Совершенно нормально, что если у нас есть mut rng: (XorShiftRng|IsaacRng) мы можем вычислить rng.next_u32() или rng.gen::<u64>() . Тем не менее, rng.gen_iter::<u16>() невозможно сконструировать, потому что автогенерация может дать только (Generator<'a, u16, XorShiftRng>|Generator<'a, u16, IsaacRng>) , тогда как на самом деле нам нужно Generator<'a, u16, (XorShiftRng|IsaacRng)> .

(Возможно, компилятор может автоматически отклонить небезопасный вызов делегирования точно так же, как проверка Sized .)

FWIW эта функция кажется мне более близкой по духу к замыканиям, чем к кортежам (которые, конечно, являются анонимным аналогом struct гипотетическим анонимным enum s). Способы, которыми эти вещи становятся «анонимными», различны.

Для анонимных struct s и enum s (кортежи и «разъединения») «анонимные» в смысле «структурных» (в отличие от «номинальных») типов — они повторно встроены, полностью универсальны для своих типов компонентов и не являются именованным объявлением ни в одном исходном файле. Но программист по-прежнему записывает их и использует как любой другой тип, реализации трейтов для них записываются явно, как обычно, и они не особенно волшебны (за исключением встроенного синтаксиса и «вариативности», в отличие от других типов). еще не может быть). В каком-то смысле у них есть имя, но вместо того, чтобы быть буквенно-цифровым, их «имя» — это синтаксис, используемый для их записи (круглые скобки и запятые).

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

Возможность возвращать разные типы из разных ветвей if за impl Trait кажется ближе к последнему — компилятор неявно генерирует тип для хранения разных ветвей, реализует запрошенный трейт. на нем для отправки соответствующему, и программист никогда не записывает и не видит, что это за тип, и не может сослаться на него, и у него нет реальной причины этого хотеть.

(На самом деле, эта функция кажется чем-то связанным с гипотетическими «литералами объектов», которые для других трейтов были бы тем же, чем существующий синтаксис замыкания для Fn . То есть вместо одного лямбда-выражения вы реализовали бы каждый метод данного типажа (с неявным self ), используя переменные в области видимости, а компилятор сгенерировал бы анонимный тип для хранения upvars и реализовал бы для него данный тип, он бы таким же образом иметь необязательный режим move и т. д. В любом случае, я подозреваю, что другим способом выражения if foo() { (some future) } else { (other future) } будет object Future { fn poll() { if foo() { (some future).poll() } else { (other future).poll() } } } (ну, вам также понадобится чтобы вывести результат foo() в let чтобы он запускался только один раз). Это довольно менее эргономично и, вероятно, не должно рассматриваться как фактическая *альтернатива другому особенность, но это предполагает, что есть связь. Может быть, первое может превратиться во второе или что-то в этом роде.)

@glaebhoerl это очень интересная идея! Здесь также есть некоторые предшествующие разработки Java.

Некоторые мысли в голове (поэтому не очень испеченные):

  1. [bikeshed] префикс object предполагает, что это трейт-объект, а не просто экзистенциальный объект, но это не так.

Возможный альтернативный синтаксис:

impl Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
// ^ --
// this conflicts with inherent impls for types, so you have to delay
// things until you know whether `Future` is a type or a trait.
// This might be __very__ problematic.

// and perhaps (but probably not...):
dyn Future { fn poll() { if foo() { a.poll() } else { b.poll() } } }
  1. [макросы/сахар] вы можете предоставить некоторый тривиальный синтаксический сахар, чтобы получить:
future!(if foo() { a.poll() } else { b.poll() })

Да, вопрос о синтаксисе - это беспорядок, потому что неясно, хотите ли вы черпать вдохновение из литералов struct , замыканий или блоков impl :) сакэ. (Во всяком случае, моя главная мысль заключалась не в том, что мы должны пойти и добавить литералы объектов [хотя мы должны это сделать], а в том, что я думаю, что анонимные enum являются отвлекающим маневром [хотя мы должны добавить и их].)

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

Хм. Поэтому я предположил, что вместо того, чтобы генерировать «свежие имена» для типов перечислений, мы вместо этого будем использовать типы | , соответствующие таким импликациям:

impl<A: IntoIterator, B: IntoIterator> IntoIterator for (A|B)  { /* dispatch appropriately */ }

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

-> impl IntoIterator<Item = Y>

но где-то еще мы делаем

-> impl IntoIterator<IntoIter = X, Item = Y>

Думаю, это будут два перекрывающихся импликации, которые нельзя «объединить»; ну может со специализацией.

Во всяком случае, понятие «секретные перечисления» кажется мне более понятным.

Я хотел бы отметить еще одну последнюю проблему с чертой экзистенциальной реализации. В значительной части случаев, когда я хочу использовать черту impl, я на самом деле хочу вернуть более одного типа.

@nikomatsakis : Справедливо ли сказать, что в этом случае возвращаемое значение ближе к dyn Trait чем к impl Trait , потому что синтетическое/анонимное возвращаемое значение реализует нечто похожее на динамическую отправку?

cc https://github.com/rust-lang/rust/issues/49288 , проблема, с которой я часто сталкивался в последнее время, работая с методами Future s и Future -returning типажа.

Так как это последний шанс перед закрытием FCP, я хотел бы привести последний аргумент против автоматических автоматических трейтов. Я понимаю, что это немного в последнюю минуту, поэтому я хотел бы в лучшем случае официально решить эту проблему, прежде чем мы примем текущую реализацию.

Чтобы прояснить для тех, кто не следил за impl Trait , это проблема, которую я представляю. Тип, представленный типами impl X настоящее время автоматически реализует автоматические черты тогда и только тогда, когда конкретный тип, стоящий за ними, реализует указанные автоматические черты. Конкретно, если сделать следующее изменение кода, функция продолжит компилироваться, но любое использование функции, полагающееся на тот факт, что тип, который она возвращает, реализует Send завершится ошибкой.

 fn does_some_operation() -> impl Future<Item=(), Error=()> {
-    let data_stored = Arc::new("hello");
+    let data_stored = Rc::new("hello");

     return some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

(более простой пример: работа , внутренние изменения вызывают сбой )

Этот вопрос не однозначен. Было очень обдуманное решение сделать автоматические черты "утечкой": если бы мы этого не сделали, нам пришлось бы поставить + !Send + !Sync на каждую функцию, которая возвращает что-то не-Send или не-Sync, и мы бы иметь неясную историю с потенциальными другими пользовательскими авто-чертами, которые просто невозможно реализовать для конкретного типа, возвращаемого функцией. Это две проблемы, которых я коснусь позже.

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

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

Когда я знакомлю людей с Rust, я, конечно, могу показать им скорость, производительность, безопасность памяти. Но в го есть скорость. Ада имеет безопасность памяти. У Python есть производительность. Что в Rust превосходит все это, так это удобство сопровождения. Когда автор библиотеки хочет изменить алгоритм, чтобы сделать его более эффективным, или когда он хочет переделать структуру ящика, у него есть твердая гарантия от компилятора, что он сообщит ему об ошибках. В Rust я могу быть уверен, что мой код продолжит функционировать не только с точки зрения безопасности памяти, но и с точки зрения логики и интерфейса. _Каждый интерфейс функции в Rust полностью представлен объявлением типа функции_.

Стабилизация impl Trait как есть имеет большой шанс пойти против этого убеждения. Конечно, это очень удобно для быстрого написания кода, но если я хочу создать прототип, я буду использовать python. Rust — это предпочтительный язык, когда вам нужна долгосрочная ремонтопригодность, а не краткосрочный код, предназначенный только для записи.


Я говорю, что есть только «большая вероятность» того, что здесь все плохо, потому что, опять же, проблема не ясна. Сама идея «автопризнаков» не является явной. Отправка и синхронизация реализуются на основе содержимого структуры, а не публичного объявления. Поскольку это решение сработало для ржавчины, impl Trait действующий аналогичным образом, также может сработать.

Однако функции и структуры используются в кодовой базе по-разному, и это разные проблемы.

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

Совершенно ясно, что при изменении внутренних компонентов функции это может повлиять как на производительность, так и на правильность. Однако в Rust нам не нужно проверять, что мы возвращаем правильный тип. Объявления функций — это жесткий контракт, который мы должны соблюдать, и rustc следит за нами. Это тонкая грань между автоматическими типажами в структурах и возвратами функций, но изменение внутренних компонентов функции является гораздо более рутинным. Когда у нас будут полностью работающие от генератора Future s, изменение функций, возвращающих -> impl Future , станет еще более рутинным. Все это будут изменения, которые авторы должны проверять на наличие модифицированных реализаций отправки/синхронизации, если компилятор их не улавливает.

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

Я уже изложил свой основной ответ на это, но вот последнее замечание. Изменение макета структуры не так уж распространено; от этого можно защититься. Бремя обслуживания, обеспечивающее, чтобы функции продолжали реализовывать одни и те же автоматические черты, больше, чем для структур, просто потому, что функции изменяются намного больше.


В качестве последнего замечания, я хотел бы сказать, что автоматические автоматические черты — не единственный вариант. Это вариант, который мы выбрали, но альтернатива автоматических признаков отказа все еще остается альтернативой.

Мы могли бы потребовать, чтобы функции, возвращающие элементы, отличные от отправки/несинхронизации, либо указывали состояние + !Send + !Sync либо возвращали трейт (возможно, псевдоним?), который имеет эти границы. Это не было бы хорошим решением, но оно может быть лучше, чем то, которое мы сейчас выбираем.

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


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

Спасибо, что прочитали, и я надеюсь, что окончательное решение направит Rust в наилучшем направлении, в котором он может двигаться.

Расширение обзора @daboross относительно . псевдонимы трейтов, можно было бы улучшить эргономику авто трейтов без утечек следующим образом:

trait FutureNSS<T, E> = Future<Item = T, Error= E> + !Send + !Sync;

fn does_some_operation() -> impl FutureNSS<(), ()> {
     let data_stored = Rc::new("hello");
     some_long_operation.and_then(|other_stuff| {
         do_other_calculation_with(data_stored)
     });
}

Это не так уж и плохо — вам придется придумать хорошее имя (которое FutureNSS нет). Основное преимущество заключается в том, что он уменьшает обрезки бумаги, связанные с повторением границ.

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

Как насчет запроса Send если он не помечен как !Send , но не предоставления Sync если он не помечен как Sync? Разве Send не должен быть более распространенным по сравнению с Sync?

Нравится:

fn provides_send_only1() -> impl Trait {  compatible_with_Send_and_Sync }
fn provides_send_only2() -> impl Trait {  compatible_with_Send_only }
fn fails_to_complile1() -> impl Trait {  not_compatible_with_Send }
fn provides_nothing1() -> !Send + impl Trait { compatible_with_Send}
fn provides_nothing2() -> !Send + impl Trait { not_compatible_with_Send }
fn provides_send_and_sync() -> Sync + impl Trait {  compatible_with_Send_and_Sync }
fn fails_to_compile2() -> Sync + impl Trait { compatible_with_Send_only }

Есть ли несоответствие между impl Trait в позиции аргумента и позиции возврата относительно. характеристики авто?

fn foo(x: impl ImportantTrait) {
    // Can't use Send cause we have not required it...
}

Это имеет смысл для позиции аргумента, потому что если бы вам было позволено предположить «Отправить здесь», вы получили бы ошибки мономорфизации после публикации. Конечно, здесь не обязательно, чтобы правила для позиции возврата и позиции аргумента совпадали, но это представляет проблему с точки зрения обучаемости.

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

Ну, это верно для предстоящего авто черта Unpin (только не-implmented для автореферентных генераторов), но ... это , кажется, немой удачи? Действительно ли это ограничение, с которым мы можем жить? Я не могу поверить, что в будущем не будет чего-то, что нужно будет отключить, например, для &mut или Rc ...

Я считаю, что это обсуждалось, и это, конечно, очень поздно, но я все еще недоволен impl Trait в позиции аргумента.

Способности а) работать с замыканиями/фьючерсами по значению и б) рассматривать некоторые типы как «выходные данные» и, таким образом, детали реализации, являются идиоматическими и существовали еще до версии 1.0, потому что они напрямую поддерживают основные ценности Rust: производительность, стабильность, и безопасность.

Таким образом, -> impl Trait просто выполняет обещание, данное версией 1.0, или удаляет пограничный случай, или обобщает существующие функции: он добавляет выходные типы к функциям, используя тот же механизм, который всегда использовался для обработки анонимных типов, и применяя его. в большем количестве случаев. Возможно, было бы более принципиально начать с abstract type , то есть типов вывода для модулей, но, учитывая, что в Rust нет системы модулей ML, порядок не имеет большого значения.

Вместо этого fn f(t: impl Trait) кажется, что он был добавлен «только потому, что мы можем», делая язык больше и страннее, не давая взамен достаточно. Я изо всех сил пытался и не смог найти какую-то существующую структуру, в которую можно было бы вписать ее. Я понимаю спор вокруг краткости fn f(f: impl Fn(...) -> ...) и обоснование того, что границы уже могут быть в предложениях <T: Trait> и where , но они кажутся пустыми. Они не отменяют недостатков:

  • Теперь вам нужно выучить два синтаксиса для границ — как минимум <> / where имеют один синтаксис.

    • Это также создает препятствие для обучения и скрывает идею использования одного и того же универсального типа в нескольких местах.

    • Новый синтаксис затрудняет определение того, какая функция является универсальной — вам нужно просмотреть весь список аргументов.

  • Теперь то, что должно быть деталью реализации функции (как она объявляет параметры своего типа), становится частью ее интерфейса, потому что вы не можете написать ее тип!

    • Это также связано с обсуждаемыми в настоящее время усложнениями auto-типа — дальнейшее запутывание того, что является общедоступным интерфейсом функции, а что нет.

  • Аналогия с dyn Trait , честно говоря, ложная:

    • dyn Trait всегда означает одно и то же и не «заражает» окружающие его объявления, кроме как через существующий механизм автоматического типажа.

    • dyn Trait можно использовать в структурах данных, и это действительно один из его основных вариантов использования. impl Trait в структурах данных не имеет смысла, если не рассматривать все варианты использования структуры данных.

    • Частью того, что означает dyn Trait является стирание типа, но impl Trait ничего не говорит о его реализации.

    • Предыдущий пункт будет еще более запутанным, если мы введем немономорфные дженерики. На самом деле, в такой ситуации fn f(t: impl Trait) , скорее всего, а) не будут работать с новой функцией и/или б) потребуют еще более тщательного рассмотрения крайних случаев, например проблемы с автоматическими чертами. Представьте себе fn f<dyn T: Trait>(t: T, u: dyn impl Urait) ! :крик:

Итак, для меня это сводится к тому, что impl Trait в позиции аргумента добавляет крайние случаи, использует больше бюджета странности, заставляет язык казаться больше и т. д., в то время как impl Trait в позиции возврата объединяет, упрощает и делает язык более тесным.

Как насчет того, чтобы требовать отправки, если она не помечена как !Send, но не предоставлять синхронизацию, если она не помечена как синхронизация? Разве Send не должен быть более распространенным по сравнению с Sync?

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

Идея велосипедного сарая здесь, чтобы не отвлекать от моих пунктов выше: вместо impl используйте type ? Это ключевое слово, используемое для связанных типов, вероятно (одно из) ключевых слов, используемых для abstract type , оно все еще довольно естественно и больше намекает на идею «типов вывода для функций»:

// keeping the same basic structure, just replacing the keyword:
fn f() -> type Trait

// trying to lean further into the concept:
fn f() -> type R: Trait
fn f() -> type R where R: Trait
fn f() -> (i32, type R) where R: Trait
// or perhaps:
fn f() -> type R: Trait in R
// or maybe just:
fn f() -> type: Trait

Спасибо, что прочитали, и я надеюсь, что окончательное решение направит Rust в наилучшем направлении, в котором он может двигаться.

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

Мне кажется , что важным вопросом является степень , в которой функции действительно отличаются от структур:

Изменение макета структуры не так уж распространено; от этого можно защититься. Бремя обслуживания, обеспечивающее, чтобы функции продолжали реализовывать одни и те же автоматические черты, больше, чем для структур, просто потому, что функции изменяются намного больше.

Очень трудно понять, насколько это будет правдой. Похоже, общее правило будет заключаться в том, что вводить Rc нужно с осторожностью — вопрос не столько в том, где вы его храните. (На самом деле, случай, над которым я действительно работаю, — это не Rc а введение dyn Trait , так как это может быть менее очевидным.)

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

В любом случае, это расстраивает, потому что об этом трудно знать заранее, независимо от того, как долго мы даем ему период стабилизации.

В качестве последнего замечания, я хотел бы сказать, что автоматические автоматические черты — не единственный вариант. Это вариант, который мы выбрали, но альтернатива автоматических признаков отказа все еще остается альтернативой.

Верно, хотя я определенно нервничаю из-за идеи «выделять» определенные автоматические черты, такие как Send . Также следует помнить, что существуют и другие варианты использования черты impl помимо фьючерсов. Например, возврат итераторов или замыканий — и в этих случаях не очевидно, что вы хотите отправлять или синхронизировать по умолчанию. В любом случае, то, что вам действительно нужно, и то, что мы пытаемся отложить =), является своего рода "условной" привязкой (Отправить, если T - это Отправить). Это именно то, что дают авто черты.

@rpjohnst

по-моему это обсуждалось

Действительно, так оно и есть :) с тех пор, как много лет назад был опубликован первый RFC impl Trait . (Вау, 2014. Я чувствую себя старым.)

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

Я чувствую совершенно противоположное. Для меня без impl Trait в позиции аргумента impl Trait в позиции возврата выделяется тем более. Объединяющая нить, которую я вижу, это:

  • impl Trait -- там, где он появляется, он указывает, что будет "некоторый мономорфизированный тип, реализующий Trait ". (Вопрос о том, кто определяет этот тип — вызывающий или вызываемый — зависит от того, где появляется impl Trait .)
  • dyn Trait -- там, где он появляется, он указывает, что будет какой-то тип, который реализует Trait , но выбор типа осуществляется динамически.

Также есть планы расширить набор мест, где могут появиться impl Trait , опираясь на эту интуицию. Например, https://github.com/rust-lang/rfcs/pull/2071 разрешает

let x: impl Trait = ...;

Применяется тот же принцип: выбор типа известен статически. Точно так же тот же RFC вводит abstract type (для которого impl Trait можно понимать как своего рода синтаксический сахар), который может появляться в импликациях трейтов и даже как элементы в модулях.

Идея велосипедного сарая здесь, чтобы не отвлекать от моих пунктов выше: вместо impl используйте type ?

Лично я не склонен восстанавливать здесь велопарковку. Мы потратили довольно много времени на обсуждение синтаксиса на https://github.com/rust-lang/rfcs/pull/2071 и в других местах. Кажется, не существует «идеального ключевого слова», но чтение impl как «какой-то тип, который реализует» работает довольно хорошо.

Позвольте мне добавить еще немного об утечке автоматических признаков:

Во-первых, в конечном счете я думаю, что автоматическая утечка признаков — это правильное решение именно потому, что оно согласуется с остальной частью языка. Ауто-черты всегда были, как я уже говорил, азартной игрой, но, похоже, они окупились. Я просто не вижу, чтобы impl Trait были такими уж разными.

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

Наконец, давайте рассмотрим последствия, если я ошибаюсь: в основном мы говорим здесь о том, что semver становится еще более тонким для суждений. Я думаю, это проблема, но ее можно смягчить различными способами. Например, мы можем использовать линты, которые предупреждают, когда вводятся типы !Send или !Sync . Мы давно говорили о введении средства проверки semver, которое поможет вам предотвратить случайные нарушения semver — похоже, это еще один случай, когда это могло бы помочь. Короче проблема, но не думаю критичная.

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

Лично я не склонен восстанавливать здесь велопарковку.

Я тоже не очень в него вложился; это была запоздалая мысль, основанная на моем впечатлении, что impl Trait в позиции аргумента, похоже, мотивировано «заполнением дыр» синтаксически, а не семантически , что кажется правильным, учитывая ваш ответ. :)

Для меня без impl Trait в позиции аргумента impl Trait в позиции возврата выделяется тем более.

Учитывая аналогию со связанными типами, это очень похоже на «без type T в позиции аргумента связанные типы выделяются еще больше». Я подозреваю, что конкретное возражение возникло не потому, что выбранный нами синтаксис кажется бессмысленным — существующий там синтаксис достаточно хорош, чтобы никто не чувствовал необходимости в синтаксическом сахаре, таком как trait Trait<type SomeAssociatedType> .

У нас уже есть синтаксис для «некоторого мономорфного типа, реализующего Trait ». В случае трейтов у нас есть как «вызывающий», так и «вызываемый» варианты. В случае функций у нас есть только вариант, указанный вызывающим, поэтому нам нужен новый синтаксис для варианта, указанного вызывающим.

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

Как я уже упоминал в своем предыдущем комментарии, я также являюсь поклонником abstract type . Это, опять же, просто расширение концепции «типа вывода» на модули. И применение использования вывода -> impl Trait , let x: impl Trait и abstract type вывода к типам, связанным с типами импликаций, также прекрасно.

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

@никомацакис

Очень трудно понять, насколько это будет правдой.

Мне кажется, что тогда мы должны ошибаться в сторону консерватизма? Можем ли мы обрести больше уверенности в дизайне с большим количеством времени (позволив автоматической утечке свойств находиться в отдельном шлюзе функций и только в ночное время, пока мы стабилизируем остальную часть impl Trait )? Мы всегда можем добавить поддержку автоматической утечки трейтов позже, если мы не утечка сейчас.

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

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

Например, мы можем использовать линты, которые предупреждают, когда вводятся типы !Send или !Sync . Мы давно говорили о введении средства проверки semver, которое поможет вам предотвратить случайные нарушения semver — похоже, это еще один случай, когда это могло бы помочь. Короче проблема, но не думаю критичная.

Это приятно слышать! 🎉 И я думаю, что это в основном смягчает мои опасения.

Верно, хотя я определенно нервничаю из-за идеи «выделять» определенные автоматические черты, такие как Send .

Я полностью согласен с этим мнением 👍.

В любом случае, то, что вам действительно нужно, и то, что мы пытаемся отложить =), является своего рода "условной" привязкой (Отправить, если T - это Отправить). Это именно то, что дают авто черты.

Я чувствую, что T: Send => Foo<T>: Send было бы лучше понято, если бы в коде это было явно указано.

fn foo<T: Extra, trait Extra = Send>(x: T) -> impl Bar + Extra {..}

Хотя, как мы обсуждали в WG-Traits, здесь вы можете вообще не получить вывод, поэтому вам всегда нужно указывать Extra если вы хотите что-то другое, кроме Send , что было бы полным обломом. .

@rpjohnst

Аналогия с dyn Trait , честно говоря, ложная:

В отношении impl Trait в позиции аргумента это неверно, но не в случае с -> impl Trait поскольку оба являются экзистенциальными типами.

  • Теперь то, что должно быть деталью реализации функции (как она объявляет параметры своего типа), становится частью ее интерфейса, потому что вы не можете написать ее тип!

Я хотел бы отметить, что порядок параметров типа никогда не был деталью реализации из-за turbofish, и в этом отношении я думаю, что impl Trait может помочь, поскольку он позволяет вам оставлять определенные аргументы типа неуказанными в turbofish. .

[..] существующий синтаксис достаточно хорош, чтобы никто не чувствовал необходимости в синтаксическом сахаре, таком как trait Trait.

Никогда не говори никогда? https://github.com/rust-lang/rfcs/issues/2274

Как и @nikomatsakis , я очень ценю внимание к этим комментариям в последнюю минуту; Я знаю, что это может быть похоже на попытку броситься под грузовой поезд, особенно для такой долгожданной функции, как эта!


@daboross , я хотел немного больше

Однако, к сожалению, он сталкивается с некоторыми проблемами, когда вы начинаете смотреть на картину в целом:

  • Если автоматические черты рассматривались как отказ для impl Trait , они также должны быть для dyn Trait .
  • Это, конечно, применимо даже тогда, когда эти конструкции используются в позиции аргумента.
  • Но тогда было бы довольно странно, если бы дженерики вели себя по-другому. Другими словами, для fn foo<T>(t: T) вы могли бы разумно ожидать T: Send по умолчанию.
  • У нас, конечно, есть механизм для этого, в настоящее время применяемый только к Sized ; это черта, которая везде предполагается по умолчанию, и от которой вы отказываетесь, написав ?Sized

Механизм ?Sized остается одним из самых неясных и трудных для изучения аспектов Rust, и в целом нам очень не хотелось расширять его на другие концепции. Использование его для такой центральной концепции, как Send кажется рискованным — не говоря уже о том, что это будет серьезное изменение.

Более того: мы действительно не хотим зацикливаться на допущении об авто-чертах для дженериков, потому что часть красоты сегодняшних дженериков заключается в том, что вы можете эффективно использовать дженерики в зависимости от того, реализует ли тип авто-трейт, и просто получить эту информацию. "протекать". Например, рассмотрим fn f<T>(t: T) -> Option<T> . Мы можем передать T независимо от того, равно ли это Send , и на выходе будет Send если и только если T было. Это чрезвычайно важная часть истории дженериков в Rust.

Есть также проблемы с dyn Trait . В частности, из-за отдельной компиляции нам пришлось бы ограничить эту природу «отказа» исключительно «хорошо известными» автоматическими трейтами, такими как Send и Sync ; это, вероятно, означало бы никогда не стабилизировать auto trait для внешнего использования.

Наконец, стоит повторить, что дизайн «утечки» был явно смоделирован после того, что происходит сегодня, когда вы создаете оболочку нового типа для возврата непрозрачного типа. По сути, я считаю, что «утечка» — это в первую очередь неотъемлемый аспект автоматических черт; у него есть компромиссы, но это то, что лежит в основе этой функции, и я думаю, что мы должны стремиться к тому, чтобы новые функции взаимодействовали с ней соответствующим образом.


@rpjohnst

Мне нечего добавить по вопросу позиции аргумента после обширных обсуждений RFC и сводного комментария @nikomatsakis выше.

Теперь то, что должно быть деталью реализации функции (как она объявляет параметры своего типа), становится частью ее интерфейса, потому что вы не можете написать ее тип!

Я не понимаю, что вы хотите этим сказать. Можете ли вы расширить?

Также хочу отметить, что такие фразы, как:

fn f(t: impl Trait) вместо этого кажется, что он был добавлен «только потому, что мы можем»

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

Компромиссы существуют, действительно есть недостатки, но это не помогает нам прийти к аргументированному выводу, чтобы изобразить карикатуру на «другую сторону» дебатов.

Спасибо всем за подробные комментарии! Я очень рад наконец-то выпустить impl Trait в стабильной версии, поэтому я сильно склоняюсь к текущей реализации и дизайнерским решениям, которые к ней привели. Тем не менее, я постараюсь ответить как можно беспристрастнее и рассматривать вещи так, как будто мы начинаем с нуля:

auto Trait Утечка

Идея утечки auto Trait беспокоила меня в течение долгого времени — в некотором смысле она может показаться противоположной многим целям дизайна Rust. По сравнению со своими предшественниками, такими как C++ или семейство ML, Rust отличается тем, что требует явного указания общих границ в объявлениях функций. На мой взгляд, это упрощает чтение и понимание универсальных функций Rust, а также делает относительно понятным, когда вносятся изменения, несовместимые с предыдущими версиями. Мы продолжили этот шаблон в нашем подходе к const fn , требуя, чтобы функции явно определяли себя как const вместо того, чтобы делать выводы о const из тел функций. Подобно явным ограничениям трейтов, это упрощает определение того, какие функции и каким образом можно использовать, и дает авторам библиотек уверенность в том, что небольшие изменения реализации не сломают пользователей.

Тем не менее, я широко использовал return-position impl Trait в своих собственных проектах, в том числе в своей работе над операционной системой Fuchsia, и я считаю, что автоматическая утечка типажа является здесь правильным значением по умолчанию. Практически последствием устранения утечки будет то, что мне придется вернуться и добавить + Send практически к каждой impl Trait -using-функции, которую я когда-либо писал. Отрицательные границы (требующие + !Send ) — интересная идея для меня, но тогда я бы написал + !Unpin почти для всех одних и тех же функций. Явность полезна, когда она информирует пользователей о решениях или делает код более понятным. В данном случае я думаю, что это не будет делать ни то, ни другое.

Send и Sync — это «контексты», в которых пользователи программируют: крайне редко я пишу приложение или библиотеку, которые используют оба типа Send и !Send (особенно при написании асинхронного кода для запуска на центральном исполнителе, который либо является многопоточным, либо нет). Выбор быть потокобезопасным или нет — это один из первых выборов, которые необходимо сделать при написании приложения, и с этого момента выбор потокобезопасности означает, что все мои типы должны быть Send . Для библиотек почти всегда я предпочитаю типы Send , поскольку их неиспользование обычно означает, что моя библиотека непригодна для использования (или требует создания выделенного потока) при использовании в многопоточном контексте. Неоспоримый parking_lot::Mutex будет иметь почти идентичную производительность с RefCell при использовании на современных процессорах, поэтому я не вижу никакой мотивации подталкивать пользователей к специализации функциональности библиотеки для использования !Send случаи. По этим причинам я не думаю, что важно уметь различать типы Send и !Send на уровне сигнатуры функций, и я не думаю, что это будет обычным делом для авторы библиотек могут случайно ввести типы !Send типы impl Trait , которые ранее были Send . Это правда, что этот выбор сопряжен с затратами на удобочитаемость и ясность, но я считаю, что компромисс стоит того, чтобы получить преимущества эргономики и удобства использования.

Аргумент-позиция impl Trait

Мне нечего здесь сказать, за исключением того, что каждый раз, когда я достигал аргумента-позиции impl Trait , я обнаруживал, что это значительно увеличивает читабельность и общее удобство сигнатур моих функций. Это правда, что он не добавляет новой возможности, которая невозможна в сегодняшнем Rust, но это отличное улучшение качества жизни для сложных сигнатур функций, концептуально он хорошо сочетается с return-position impl Trait , и это облегчает переход ООП-программистов в счастливых пользователей Rustace. В настоящее время существует много избыточности, связанной с необходимостью введения именованного универсального типа только для обеспечения границы (например, F в fn foo<F>(x: F) where F: FnOnce() против fn foo(x: impl FnOnce()) ). Это изменение решает эту проблему и приводит к тому, что сигнатуры функций легче читать и писать, и IMO кажется естественным дополнением к -> impl Trait .

тл; Д.Р.: Я думаю, что наши первоначальные решения были правильными, хотя они, несомненно, сопряжены с компромиссами.
Я очень ценю всех, кто говорит и тратит столько времени и усилий, чтобы убедиться, что Rust — лучший язык, которым он может быть.

@Центрил

Что касается impl Trait в позиции аргумента, это неверно, но не так с -> impl Trait, поскольку оба являются экзистенциальными типами.

Да, это то, что я имел в виду.

@атурон

фразы типа... подрывают добросовестное обсуждение

Вы правы, извините за это. Я считаю, что я сделал свою точку зрения лучше в другом месте.

Теперь то, что должно быть деталью реализации функции (как она объявляет параметры своего типа), становится частью ее интерфейса, потому что вы не можете написать ее тип!

Я не понимаю, что вы хотите этим сказать. Можете ли вы расширить?

С поддержкой impl Trait в позиции аргумента вы можете написать эту функцию двумя способами:

fn f(t: impl Trait)
fn f<T: Trait>(t: T)

Выбор формы определяет, может ли потребитель API даже записать имя любого конкретного экземпляра (например, взять его адрес). Вариант impl Trait не позволяет вам сделать это, и это не всегда можно обойти без перезаписи подписи для использования синтаксиса <T> . Кроме того, переход на синтаксис <T> — это кардинальное изменение!

С риском дальнейшей карикатуры мотивация этого заключается в том, что его легче учить, изучать и использовать. Однако, поскольку выбор между ними также является важной частью интерфейса функции, как и порядок параметров типа, я не чувствую, что это было адекватно решено — я на самом деле не согласен с тем, что его проще использовать или что он приводит к более приятным сигнатурам функций.

Я не уверен, что какие-либо другие наши «простые, но ограниченные -> сложные, но общие» изменения, мотивированные обучаемостью/эргономикой, включают изменения интерфейса именно таким образом. Либо сложный эквивалент простого способа ведет себя идентично, и вам нужно переключаться только тогда, когда вы уже меняете интерфейс или поведение (например, исключение продолжительности жизни, соответствие эргономике, -> impl Trait ), либо изменение является столь же общим и предназначено для применяться повсеместно (например, модули/пути, внутриполосное время жизни, dyn Trait ).

Чтобы быть более конкретным, я беспокоюсь, что мы начнем сталкиваться с этой проблемой в библиотеках, и это будет очень похоже на «все должны помнить, чтобы получить Copy / Clone », но хуже, потому что а) это будет кардинальное изменение, и б) всегда будет напряжение, чтобы вернуться назад, особенно потому, что эта функция была разработана для этого!

@cramertj Что касается избыточности подписи функций ... можем ли мы избавиться от нее каким-то другим способом? Внутриполосное время жизни удалось обойтись без обратных ссылок; возможно, мы могли бы каким-то образом сделать моральный эквивалент «параметров внутриполосного типа». Или, другими словами, «изменение столь же общее и предназначено для универсального применения».

@rpjohnst

Кроме того, переход на синтаксис <T> — это серьезное изменение!

Не обязательно, с https://github.com/rust-lang/rfcs/pull/2176 вы можете добавить дополнительный параметр типа T: Trait в конец, и Turbofish все равно будет работать (если вы не имеете в виду поломку от какие-то другие средства, кроме турбофиш-брейка).

Вариант impl Trait не позволяет вам сделать это, и это не всегда можно обойти, не переписав подпись для использования синтаксиса <T> . Кроме того, переход на синтаксис <T> — это кардинальное изменение!

Кроме того, я думаю, вы имеете в виду, что переход от синтаксиса <T> является критическим изменением (поскольку вызывающие абоненты больше не могут явно указывать значение T с помощью turbofish).

ОБНОВЛЕНИЕ: обратите внимание, что если функция использует черту impl, то в настоящее время мы вообще не разрешаем использовать turbofish, даже если у нее есть некоторые обычные общие параметры.

@nikomatsakis Переход к явному синтаксису также может быть критическим изменением, если старая подпись содержала смесь явных и неявных параметров типа — любой, кто предоставил параметры типа n , теперь должен будет предоставить n + 1 вместо этого. Это был один из случаев, когда RFC

ОБНОВЛЕНИЕ: обратите внимание, что если функция использует черту impl, то в настоящее время мы вообще не разрешаем использовать turbofish, даже если у нее есть некоторые обычные общие параметры.

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

@никомацакис

Спасибо за искреннее отношение к этой проблеме.

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

В основном я рассматривал вариант использования фьючерсов, но вряд ли он единственный. Без утечек Send/Sync из локальных типов не очень хорошая история для использования impl Trait во многих различных контекстах. Учитывая это и учитывая дополнительные автоматические черты, мое предложение не очень жизнеспособно.

Я не хотел выделять Sync и Send и _только_ предполагать их, так как это немного произвольно и подходит только для _одного_ варианта использования. Тем не менее, альтернатива, заключающаяся в том, чтобы принять все автоматические черты, также не будет хорошей. + !Unpin + !... для каждого типа не звучит как жизнеспособное решение.

Если бы у нас было еще пять лет разработки языка, чтобы придумать систему эффектов и другие идеи, о которых я сейчас не знаю, мы могли бы придумать что-то получше. Но на данный момент и для Rust кажется, что 100% «автоматические» автоматические черты — лучший путь вперед.

@lfairy

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

В настоящее время это не разрешено. Если вы используете impl Trait , вы не получите турбофиша ни по каким параметрам (как я уже отмечал). Однако это не задумано как долгосрочное решение, а скорее консервативный шаг, чтобы избежать разногласий о том, как действовать, пока у нас не будет времени придумать округлый дизайн. (И, как заметил @rpjohnst , у него есть свои недостатки .)

Дизайн, который я хотел бы видеть, это (а) да, чтобы принять RFC @centril или что-то в этом роде, и (б) сказать, что вы можете использовать turbofish для явных параметров (но не типов impl Trait ). Однако мы этого не сделали, отчасти потому, что нам было интересно, может ли быть история, которая позволила бы перейти от явного параметра к трейту impl.

@lfairy

Это был один из случаев, когда RFC

_[Trivia]_ Между прочим, именно @nikomatsakis обратил мое внимание на то, что частичный турбофиш может сгладить разрывы между <T: Trait> и impl Trait ;) Это не было целью RFC на все с самого начала, но это был приятный сюрприз. 😄

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

Последний период комментариев завершен.

Если это будет поставляться в 1.26, то https://github.com/rust-lang/rust/issues/49373 кажется мне очень важным, Future и Iterator — два основных варианта использования. -cases, и они оба очень зависят от знания связанных типов.

Выполнил быстрый поиск в системе отслеживания проблем, и № 47715 — это ICE, который все еще нужно исправить. Можем ли мы получить это до того, как оно войдет в стабильную версию?

Что-то, с чем я столкнулся сегодня с impl Trait:
https://play.rust-lang.org/?gist=69bd9ca4d41105f655db5f01ff444496&version=stable

Похоже, impl Trait несовместим с unimplemented!() — это известная проблема?

да, см. #36375 и #44923

Я только что понял, что предположение 2 RFC 1951 противоречит некоторым из моих запланированных применений impl Trait с асинхронными блоками. В частности, если вы берете общий параметр AsRef или Into чтобы иметь более эргономичный API, а затем преобразуете его в некоторый собственный тип, прежде чем возвращать блок async , вы все равно получите возвращенный Тип impl Trait связан любым временем жизни в этом параметре, например

impl HttpClient {
    fn get(&mut self, url: impl Into<Url>) -> impl Future<Output = Response> + '_ {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

fn foo(client: &mut HttpClient) -> impl Future<Output = Response> + '_ {
    let url = Url::parse("http://foo.example.com").unwrap();
    client.get(&url)
}

При этом вы получите error[E0597]: `url` does not live long enough потому что get включает время жизни временной ссылки в возвращенном impl Future . Этот пример немного надуман, так как вы можете передать URL-адрес по значению в get , но почти наверняка в реальном коде возникнут подобные случаи.

Насколько я могу судить, ожидаемое исправление для этого - абстрактные типы, в частности

impl HttpClient {
    abstract type Get<'a>: impl Future<Output = Response> + 'a;
    fn get(&mut self, url: impl Into<Url>) -> Self::Get<'_> {
        let url = url.into();
        async {
            // perform the get
        }
    }
}

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

Мне интересно, есть ли потенциально более краткий способ написать это, или это просто закончится тем, что абстрактные типы будут использоваться почти для каждой функции, а не голый возвращаемый тип impl Trait ?

Итак, если я понимаю комментарий @cramertj по этому вопросу, вы получите ошибку в определении HttpClient::get что-то вроде `get` returns an `impl Future` type which is bounded to live for `'_`, but this type could potentially contain data with a shorter lifetime inside the type of `url` . (Поскольку в RFC явно указано, что impl Trait фиксирует _все_ параметры универсального типа, и это ошибка, что вам разрешено захватывать тип, время жизни которого может быть короче, чем ваше явно объявленное время жизни).

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

На самом деле, кажется, что это будет кардинальное изменение. Так что если ошибка в этом случае будет добавлена, то лучше сделать это в ближайшее время.

РЕДАКТИРОВАТЬ: И перечитывая комментарий, я не думаю, что это то, о чем он говорит, поэтому я все еще не понимаю, есть ли потенциальный способ обойти это без использования абстрактных типов или нет.

@Nemo157 Nemo157 Да, исправление # 42940 устранит вашу проблему со сроком службы, поскольку вы можете указать, что возвращаемый тип должен жить до тех пор, пока заимствование self, независимо от времени жизни Url . Это определенно то изменение, которое мы хотим внести, но я считаю, что оно обратно совместимо: оно не позволяет возвращаемому типу иметь более короткое время жизни, оно чрезмерно ограничивает способы использования возвращаемого типа.

Например, следующие ошибки с «параметр Iter может не прожить достаточно долго»:

fn foo<'a, Iter>(_: &'a mut u32, iter: Iter) -> impl Iterator<Item = u32> + 'a
    where Iter: Iterator<Item = u32>
{
    iter
}

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

Похоже, #46541 готово. Кто-нибудь может обновить ОП?

Есть ли причина, по которой синтаксис abstract type Foo = ...; был выбран вместо type Foo = impl ...; ? Я предпочел последнее из-за согласованности синтаксиса, и я помню некоторое обсуждение этого вопроса некоторое время назад, но, похоже, не могу его найти.

Я неравнодушен к type Foo = impl ...; или type Foo: ...; , abstract кажется ненужным чудаком.

Если я правильно помню, одной из основных проблем было то, что люди научились интерпретировать type X = Y как текстовую замену («замените X на Y где это применимо»). Это не работает для type X = impl Y .

Я сам предпочитаю type X = impl Y потому что моя интуиция подсказывает, что type работает как let , но...

@alexreg Существует много дискуссий на тему RFC 2071 . TL;DR: type Foo = impl Trait; разрушает возможность обесценивания impl Trait в какой-то «более явной» форме и ломает интуицию людей о том, что псевдонимы типов работают как немного более умная синтаксическая замена.

Я неравнодушен к типу Foo = impl ...; или введите Foo: ...;, abstract кажется ненужным чудаком

Вы должны присоединиться к моему лагерю exists type Foo: Trait; :wink:

@cramertj Хм. Я только что освежил в памяти кое-что из этого, и, если честно, я не могу сказать, что понимаю рассуждения @withoutboats . Мне это кажется наиболее интуитивным (у вас есть контрпример?), и немного о дешугаринге, которого я просто не понимаю. Думаю, моя интуиция работает как @lnicola. Я также считаю, что этот синтаксис лучше всего подходит для таких вещей, как https://github.com/rust-lang/rfcs/pull/2071#issuecomment -319012123 — можно ли это сделать в текущем синтаксисе?

exists type Foo: Trait; — это небольшое улучшение, хотя ключевое слово exists я бы все же исключил. type Foo: Trait; не будет беспокоить меня настолько, чтобы жаловаться. 😉 abstract просто лишнее/ странное , как говорит

@алексрег

можно ли это сделать даже в текущем синтаксисе?

Да, но это гораздо более неудобно. Это была основная причина, по которой я предпочел синтаксис = impl Trait (по модулю ключевого слова abstract ).

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item=impl Display>;

// can be written like this:

exists type Foo1: Bar;
exists type Foo2: Baz;
exists type Foo: (Foo1, Foo2);

exists type IterDisplayItem: Display;
exists type IterDisplay: Iterator<Item=IterDisplayItem>;

Редактировать: exists type Foo: (Foo1, Foo2); выше должно было быть type Foo = (Foo1, Foo2); . Извините за путаницу.

@cramertj Синтаксис кажется приятным. Должно ли exists быть подходящим ключевым словом?

@cramertj Верно, я думал, что вам придется сделать что-то подобное ... я думаю, хорошая причина предпочесть = impl Trait ! Честно говоря, если люди думают, что интуиция о подстановке здесь в достаточной степени нарушается для экзистенциальных типов (по сравнению с псевдонимами простых типов), то почему бы не пойти на следующий компромисс?

exists type Foo = (impl Bar, impl Baz);

(Честно говоря, я бы предпочел просто использовать одно ключевое слово type для всего.)

Я нахожу:

exists type Foo: (Foo1, Foo2);

глубоко странно. Использование Foo: (Foo1, Foo2) где RHS не является границей, не согласуется с тем, как Ty: Bound используется в другом месте языка.

Мне кажутся приемлемыми следующие формы:

exists type Foo: Bar + Baz;  // <=> "There exists a type Foo which satisfies Bar and Baz."
                             // Reads super well!

type Foo = impl Bar + Baz;

type Bar = (impl Foo, impl Bar);

Я также предпочитаю не использовать здесь слово abstract .

Я нахожу exists type Foo: (Foo1, Foo2); очень странным

Это определенно выглядит как ошибка для меня, и я думаю, что здесь должно быть написано type Foo = (Foo1, Foo2); .

Если мы сталкиваемся здесь с abstract type против exists type , я определенно поддержу первое. В основном потому, что «абстрактный» работает как прилагательное. Я мог бы легко назвать что-то «абстрактным типом» в разговоре, тогда как странно говорить, что мы делаем «существующий тип».

Я также предпочел бы : Foo + Bar : (Foo, Bar) , = Foo + Bar , = impl Foo + Bar или = (impl Foo, impl Bar . Использование + хорошо работает со всеми другими местами, где могут быть границы, а отсутствие = действительно означает, что мы не можем записать полный тип. Здесь мы не создаем псевдоним типа, мы создаем имя для чего-то, что, как мы гарантируем, имеет определенные границы, но что мы не можем назвать явно.


Мне также все еще нравится предложение по синтаксису с https://github.com/rust-lang/rfcs/pull/2071#issuecomment -318852774:

type ExistentialFoo: Bar;
type Bar: Baz + Bax;

Хотя это, как упоминалось в этой ветке, слишком мало и не очень явно.

Я должен интерпретировать (impl Foo, impl Bar) совсем иначе, чем некоторые из вас... для меня это означает, что тип представляет собой двойку некоторых экзистенциальных типов и полностью отличается от impl Foo + Bar .

@alexreg Если бы это было намерением @cramertj , я бы все равно нашел это очень странным с синтаксисом : :

exists type Foo: (Foo1, Foo2);

кажется, все еще очень неясно, что он делает - границы обычно не определяют кортеж возможных типов в любом случае, и его можно легко спутать со значением синтаксиса Foo: Foo1 + Foo2 .

= (impl Foo, impl Bar) — интересная идея. Было бы интересно разрешить создание экзистенциальных кортежей с типами, которые сами по себе неизвестны. Я не думаю, что нам _нужно_ поддерживать их, поскольку мы можем просто ввести два экзистенциальных типа для impl Foo и impl Bar затем псевдоним третьего типа для кортежа.

@daboross Ну, вы создаете «экзистенциальный тип» , а не «существующий тип» ; так это называется в теории типов. Но я думаю, что фраза «существует тип Foo, который...» хорошо работает как с ментальной моделью, так и с точки зрения теории типов.

Я не думаю, что нам нужно их поддерживать, поскольку мы можем просто ввести два экзистенциальных типа для impl Foo и impl Bar затем псевдоним третьего типа для кортежа.

Это кажется неэргономичным... временные не так хороши.

@alexreg Примечание: я не хотел сказать, что impl Bar + Baz; совпадает с (impl Foo, impl Bar) , последнее, очевидно, представляет собой двойку.

@daboross

Если бы это было намерением @cramertj , я бы все равно нашел это очень странным с синтаксисом ::

exists type Foo: (Foo1, Foo2);

кажется, все еще очень неясно, что он делает - границы обычно не определяют кортеж возможных типов в любом случае, и его можно легко спутать со значением синтаксиса Foo: Foo1 + Foo2.

Это может быть немного неясно (не так явно, как (impl Foo, impl Bar) , что я сразу понял бы интуитивно) – но я не думаю, что когда-либо перепутал бы это с Foo1 + Foo2 лично.

= (impl Foo, impl Bar) — интересная идея. Было бы интересно разрешить создание экзистенциальных кортежей с типами, которые сами по себе неизвестны. Я не думаю, что нам нужно их поддерживать, поскольку мы можем просто ввести два экзистенциальных типа для impl Foo и impl Bar, а затем псевдоним третьего типа для кортежа.

Да, это было раннее предложение, и оно мне до сих пор очень нравится. Было отмечено, что это можно сделать в любом случае, используя текущий синтаксис, но для этого требуется 3 строки кода, что не очень удобно. Я также утверждаю, что некоторый синтаксис, такой как ... = (impl Foo, impl Bar) является наиболее понятным для пользователя, но я знаю, что здесь есть разногласия.

@Centril Сначала я так не думал, но это было немного двусмысленно, а затем @daboross, похоже, интерпретировал это именно так, ха. В любом случае, рад, что мы это прояснили.

Упс, посмотрите мое редактирование на https://github.com/rust-lang/rust/issues/34511#issuecomment -386763340. exists type Foo: (Foo1, Foo2); должно было быть type Foo = (Foo1, Foo2); .

@cramertj Ах, теперь это имеет больше смысла. В любом случае, не кажется ли вам, что возможность сделать следующее будет наиболее эргономичной? Даже просматривая эту другую ветку, я действительно не видел веских аргументов против нее.

type A = impl Foo;
type B = (impl Foo, impl Bar, String);

@alexreg Да, я думаю , что это самый эргономичный синтаксис.

Используя RFC https://github.com/rust-lang/rfcs/pull/2289 , я бы переписал фрагмент @cramertj следующим образом :

type Foo = (impl Bar, impl Baz);
type IterDisplay = impl Iterator<Item: Display>;

// alternatively:

exists type IterDisplay: Iterator<Item: Display>;

type IterDisplay: Iterator<Item: Display>;

Однако я думаю, что для псевдонимов типов отказ от exists помог бы сохранить выразительную мощь, не усложняя синтаксис языка без необходимости; поэтому из бюджета сложности POV impl Iterator кажется лучше, чем exists . Однако последний вариант на самом деле не вводит новый синтаксис, а также является самым коротким, но при этом понятным.

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

type Foo = (impl Bar, impl Baz);
type IterDisplay: Iterator<Item: Display>;

РЕДАКТИРОВАТЬ: Какой синтаксис следует использовать? IMO, clippy однозначно должен предпочесть синтаксис Type: Bound когда его можно использовать, поскольку он наиболее эргономичен и прямолинеен.

Я предпочитаю вариант type Foo: Trait варианту type Foo = impl Trait . Он соответствует синтаксису связанного типа, что хорошо, поскольку он также является «типом вывода» модуля, который его содержит.

Синтаксис impl Trait уже используется как для входных, так и для выходных типов, а это означает, что он рискует создать впечатление полиморфных модулей. :(

Если бы impl Trait использовались исключительно для выходных типов, то я мог бы предпочесть вариант type Foo = impl Trait на том основании, что синтаксис связанного типа больше подходит для трейтов (которые примерно соответствуют сигнатурам ML), в то время как type Foo = .. Синтаксис

@rpjohnst

Я предпочитаю вариант type Foo: Trait варианту type Foo = impl Trait .

Я согласен, его следует использовать, когда это возможно; но как насчет случая (impl T, impl U) когда связанный синтаксис нельзя использовать напрямую? Мне кажется, что введение псевдонимов временных типов вредит читабельности.

Использование только type Name: Bound кажется запутанным при использовании внутри блоков impl :

impl Iterator for Foo {
    type Item: Display;

    fn next(&mut self) -> Option<Self::Item> { Some(5) }
}

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

pub abstract type First: Display;
pub abstract type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

против

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);

    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@Nemo157 Nemo157 Почему бы не разрешить оба

pub type First: Display;
pub type Second: Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

и:

impl Iterator for Foo {
    type Item = (impl Display, impl Debug);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

?

Я не понимаю, почему должны быть два синтаксиса для одной и той же функции, используя только синтаксис type Name = impl Bound; явно предоставляющий имена для двух частей:

pub type First = impl Display;
pub type Second = impl Debug;

impl Iterator for Foo {
    type Item = (First, Second);
    fn next(&mut self) -> Option<Self::Item> { Some((5, 6)) }
}

@ Nemo157 Я согласен, что не нужно (и не должно) быть двух разных синтаксисов. Я не нахожу type (без ключевого слова префикса) вообще запутанным, должен сказать.

@rpjohnst Что, черт возьми, такое полиморфный модуль? :-) В любом случае, я не понимаю, почему мы должны моделировать синтаксис после связанных определений типов, которые устанавливают границы типажей для типа. Это не имеет ничего общего с границами .

@alexreg Полиморфный модуль — это модуль с параметрами типа, как и у fn foo(x: impl Trait) . Это не то, что существует, и поэтому я не хочу, чтобы люди думали, что это существует.

abstract type ( редактировать: чтобы назвать функцию, а не предлагать использовать ключевое слово) имеет прямое отношение к границам! Границы — это единственное, что вы знаете о типе. Единственная разница между ними и связанными типами заключается в том, что они выводятся, поскольку обычно не имеют имени.

@ Nemo157 Nemo157, синтаксис Foo: Bar уже более знаком в других контекстах (ограничения связанных типов и параметров типов), он более эргономичен и (IMO) понятен, когда его можно использовать без введения временных элементов.

Письмо:

type IterDisplay: Iterator<Item: Display>;

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

type IterDisplay = impl Iterator<Item = impl Display>;

Я думаю, что это просто последовательное применение синтаксиса, который у нас уже есть; так что это не совсем ново.

РЕДАКТИРОВАТЬ 2: Первый синтаксис также соответствует тому, как я хотел бы, чтобы он отображался в rustdoc.

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

trait Foo {
    type Bar: Baz;
    // stuff...
}

struct Quux;

impl Foo for Quux {
    type Bar: Baz; // Oh look! Same as in the trait; I had to do nothing!
    // stuff...
}

Синтаксис impl Bar кажется лучше, когда в противном случае вам пришлось бы вводить временные файлы, но он также последовательно применяет синтаксис.

Возможность использовать оба синтаксиса на самом деле не сильно отличается от возможности использовать impl Trait в позиции аргумента, а также иметь явный параметр типа T: Trait который затем используется аргументом.

EDIT1: На самом деле, наличие только одного синтаксиса было бы особым регистром, а не наоборот.

@rpjohnst Я прямого отношения к границам.

В любом случае, я не против синтаксиса type Foo: Bar; , но, ради бога, давайте избавимся от ключевого слова abstract . type само по себе вполне понятно при любых обстоятельствах.

Лично я считаю, что использование = и impl — хороший визуальный намек на то, что вывод происходит. Это также облегчает обнаружение этих мест при просмотре большого файла.

Кроме того, если я увижу type Iter: Iterator<Item = Foo> мне придется найти Foo и выяснить, является ли это типом или чертой, прежде чем я узнаю, что происходит.

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

Так что я думаю, что вариант = / impl решает немного больше бумажных вырезок.

@файлон

Кроме того, если я увижу тип Iter: Iterator<Item = Foo> мне придется найти Foo и выяснить, тип это или черта, прежде чем я узнаю, что происходит.

Этого я не понимаю; Item = Foo всегда должен быть типом в настоящее время, учитывая, что dyn Foo является стабильным (и голая черта постепенно исчезает...)?

@Центрил

Этого я не понимаю; Item = Foo всегда должен быть типом в настоящее время, учитывая, что dyn Foo стабилен (и голый трейт постепенно упраздняется...)?

Да, но в предлагаемом варианте impl less это может быть выведенный тип с привязкой или конкретный тип. Например, Iterator<Item = String> против Iterator<Item = Display> . Я должен знать черты, чтобы знать, происходит ли вывод.

Редактировать: Ах, не заметил, чтобы кто-то использовал : . Вроде того, что я имею в виду под словом «легко пропустить» :) Но вы правы, что они разные.

Редактировать 2: я думаю, что эта проблема будет сохраняться за пределами связанных типов. Учитывая type Foo: (Bar, Baz) вам нужно знать Бар и Баз, чтобы знать, где происходит вывод.

@Центрил

EDIT1: На самом деле, наличие только одного синтаксиса было бы особым регистром, а не наоборот.

В настоящее время существует только один способ объявить _экзистенциальные_ типы, -> impl Trait . Есть два способа объявить _универсальные_ типы ( T: Trait и : impl Trait в списке аргументов).

Если бы у нас были полиморфные модули, которые принимали бы универсальные типы, я мог бы увидеть некоторые аргументы вокруг этого, но я считаю, что текущее использование type Name = Type; как в модулях, так и в определениях признаков — это параметр выходного типа, который должен быть экзистенциальным. тип.


@файлон

Да, но в предлагаемом варианте на impl less это может быть выведенный тип с привязкой или конкретный тип. Например, Iterator<Item = String> против Iterator<Item = Display> . Я должен знать черты, чтобы знать, происходит ли вывод.

Я считаю, что вариант с меньшим количеством impl использует : Bound во всех случаях для экзистенциальных типов, поэтому вы можете иметь Iterator<Item = String> или Iterator<Item: Display> качестве границ признака, но Iterator<Item = Display> будет недопустимым объявлением.

@Немо157
Вы правы в отношении случая связанного типа, это плохо. Но (как указано в моем редактировании) я думаю, что проблема с type Foo: (A, B) . Поскольку либо A либо B могут быть типом или чертой.

Я считаю, что это также хорошая причина использовать = . : только говорит вам, что некоторые вещи предполагаются, но не говорит вам, какие именно. type Foo = (A, impl B) мне кажется понятнее.

Я также предполагаю, что чтение и предоставление фрагментов кода проще с impl , поскольку никогда не требуется предоставлять дополнительный контекст о том, что является чертой, а что нет.

Редактировать: некоторые кредиты: мой аргумент в основном такой же, как @alexreg здесь , я просто хотел уточнить, почему я думаю, что impl предпочтительнее.

В настоящее время существует только один способ объявить экзистенциальные типы — -> impl Trait . Есть два способа объявить универсальные типы ( T: Trait и : impl Trait в списке аргументов).

Вот что я говорю :PI Почему универсальная квантификация должна иметь два пути, а экзистенциальная - только одна (игнорируя dyn Trait ) в других местах ?

Мне кажется одинаково вероятным, что пользователь пойдет и напишет type Foo: Bound; и type Foo = impl Bound; изучив разные части языка, и я не могу сказать, что один синтаксис явно лучше во всех случаях; Мне ясно, что для одних вещей лучше подходит один синтаксис, а для других — другой.

@файлон

Я считаю, что это также хорошая причина, чтобы использовать =. : только говорит вам, что некоторые вещи предполагаются, но не говорит вам, какие именно. тип Foo = (A, impl B) кажется мне более понятным.

Да, наверное, это еще одна веская причина. Чтобы понять, что экзистенциально квантифицируется, действительно требуется некоторая распаковка — переход от определения к определению.

Другое дело: можно ли вообще разрешить : в связанной привязке типа при таком синтаксисе? Мне это показалось бы немного странным частным случаем, учитывая, что экзистенциальные типы не могут быть составлены/объединены каким-либо другим способом в этом предлагаемом синтаксисе. Я бы предположил, что следующий подход будет наиболее последовательным с использованием этого синтаксиса:

type A: Foo;
type B: Bar;
type C: Baz;
type D: Iterator<Item = C>; 
type E = (A, Vec<B>, D);

Используя синтаксис, который я (и некоторые другие здесь) предпочитаю, мы могли бы записать все это в одну строку, и, кроме того, сразу становится ясно, где происходит количественная оценка!

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item = impl Baz>);

Независимо от вышеизложенного: когда мы играем, чтобы внедрить let x: impl Trait в nightly? Мне давно не хватало этой функции.

@алексрег

Другое дело: можно ли вообще разрешить : в связанной привязке типа при таком синтаксисе?

Да, почему бы и нет; Это было бы естественным эффектом rust-lang/rfcs#2289 + type Foo: Bound .

Вы также можете сделать:

type E = (impl Foo, Vec<impl Bar>, impl Iterator<Item: Baz>);

@Centril Я думаю, что разрешать два альтернативных синтаксиса - плохая идея. Пахнет синдромом "мы не смогли решить, поэтому просто поддержим обоих". Видеть код, который смешивает и сочетает их, будет настоящим бельмом на глазу!

@Centril Я вроде как с @nikomatsakis по этому твоему RFC, кстати, извини. Лучше написать impl Iterator<Item = impl Baz> . Красиво и откровенно.

@alexreg Это справедливо;

Но (не)к счастью (в зависимости от вашей точки зрения), мы уже начали «разрешить два альтернативных синтаксиса» с impl Trait в позиции аргумента, так что у нас есть как Foo: Bar и impl Bar работает, чтобы означать одно и то же;

Это для универсальной количественной оценки, но нотация impl Trait самом деле не заботится о том, на какой стороне двойственности она находится; в конце концов, мы не использовали any Trait и some Trait .

Учитывая, что мы уже сделали выбор «мы не можем решить» и «сторона двойственности синтаксически не имеет значения» , мне кажется последовательным применять «мы не можем решить» везде, чтобы пользователь не получил в "но я мог бы написать это так там, почему бы и нет?" ;)


PS:

Ре. impl Iterator<Item = impl Baz> не работает как граница в предложении where; так что вам придется смешать это как Iter: Iterator<Item = impl Baz> . Вы должны разрешить: Iter = impl Iterator<Item = impl Baz> чтобы он работал равномерно (может быть, мы должны?).

Использование : Bound вместо = impl Bound также является явным, просто короче ^,-
Я думаю, что разница в интервалах между X = Ty и X: Ty делает синтаксис разборчивым.

Проигнорировав мой собственный совет, продолжим этот разговор на RFC ;)

Но (не)к счастью (в зависимости от вашего POV), мы уже начали «разрешить два альтернативных синтаксиса» с атрибутом impl Trait в позиции аргумента, так что у нас есть и Foo: Bar, и impl Bar, работающие для обозначения одного и того же;

Да, но я считаю, что выбор был сделан больше с точки зрения симметрии/согласованности. Аргументы общего типа строго более мощные, чем аргументы универсального типа ( impl Trait ). Но мы ввели impl Trait в позицию возврата, имело смысл ввести ее в позицию аргумента.

Учитывая, что мы уже сделали выбор «мы не можем решить» и «сторона двойственности синтаксически не имеет значения», мне кажется последовательным применять «мы не можем решить» везде, чтобы пользователь не получил в "но я мог бы написать это так там, почему бы и нет?" ;)

Я не уверен, что сейчас мы должны поднять руки вверх и сказать: «Давайте все реализуем». Здесь нет такого ясного аргумента, как выигрыш.

PS:

Ре. impl Iterator<Item = impl Baz> не работает как граница в предложении where; так что вам придется смешать это как Iter: Iterator<Item = impl Baz> . Вам нужно разрешить: Iter = impl Iterator<Item = impl Baz> чтобы он работал равномерно (может быть, мы должны?).

Я бы сказал, что мы либо просто поддерживаем where Iter: Iterator<Item = T>, T: Baz (как сейчас), либо идем до конца с Iter = impl Iterator<Item = impl Baz> (как вы предложили). Только разрешить дом на полпути кажется чем-то вроде отговорки.

Использование : Bound также вместо = impl Bound также является явным, просто короче ^,-
Я думаю, что разница в интервалах между X = Ty и X: Ty делает синтаксис разборчивым.

Это разборчиво, но я не думаю, что это так же ясно/явно, что используется экзистенциальный тип. Это усугубляется, когда определение должно быть разбито на несколько строк из-за ограничений этого синтаксиса.

Проигнорировав мой собственный совет, продолжим этот разговор на RFC ;)

Подождите, вы имеете в виду ваш RFC? Я думаю, что это относится и к тому, и к этому, насколько я могу судить. :-)

Подождите, вы имеете в виду ваш RFC? Я думаю, что это относится и к тому, и к этому, насколько я могу судить. :-)

В ПОРЯДКЕ; Тогда давайте продолжим здесь;

Да, но я считаю, что выбор был сделан больше с точки зрения симметрии/согласованности. Аргументы общего типа строго более мощные, чем аргументы универсального типа ( impl Trait ). Но мы ввели impl Trait в позицию возврата, имело смысл ввести ее в позицию аргумента.

Вся моя мысль о последовательности и симметрии. =П
Если вам разрешено писать impl Trait как для экзистенциальной, так и для универсальной квантификации, для меня имеет смысл, что вам также должно быть разрешено использовать Type: Trait как для универсальной, так и для экзистенциальной квантификации.

Что касается выразительной силы, первое, как вы говорите, более мощное, чем второе, но это не обязательно должно быть так; Они могли бы быть одинаково мощными, если бы мы хотели, чтобы они были AFAIK (хотя я абсолютно не говорю, что мы должны это делать ..).

fn foo(bar: impl Trait, baz: typeof bar) { // eww... but possible!
    ...
}

Я не уверен, что сейчас мы должны поднять руки вверх и сказать: «Давайте все реализуем». Здесь нет такого ясного аргумента, как выигрыш.

Мой аргумент заключается в том, что удивление пользователей фразой «Этот синтаксис можно использовать в другом месте, и его смысл ясен здесь, но вы не можете написать его здесь» стоит больше, чем наличие двух способов сделать это (с которыми вы оба должны ознакомиться в любом случае). ). Мы сделали то же самое с https://github.com/rust-lang/rfcs/pull/2300 (объединено), https://github.com/rust-lang/rfcs/pull/2302 (PFCP), https ://github.com/rust-lang/rfcs/pull/2175 (merged), где мы заполняем дыры в согласованности, хотя раньше можно было написать по-другому.

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

На мой взгляд, разборчивости достаточно; Я не думаю, что Rust приписывает «прежде всего явный» и слишком многословный (как я считаю синтаксис, когда он используется слишком часто) также стоит, препятствуя использованию.
(Если вы хотите, чтобы что-то использовалось часто, дайте ему более лаконичный синтаксис... сравните ? в качестве взятки против .unwrap() ).

Это усугубляется, когда определение должно быть разбито на несколько строк из-за ограничений этого синтаксиса.

Этого я не понимаю; Мне кажется, что Assoc = impl Trait должно вызывать разбиение строк даже больше, чем Assoc: Trait просто потому, что первый длиннее.

Я бы сказал, что мы либо просто поддерживаем where Iter: Iterator<Item = T>, T: Baz (как сейчас), либо идем до конца с Iter = impl Iterator<Item = impl Baz> (как вы предложили).
Только разрешить дом на полпути кажется чем-то вроде отговорки.

Вот именно!, давайте не будем идти на полпути/отговорки и реализуем where Iter: Iterator<Item: Baz> ;)

@Centril Хорошо, вы меня

Отредактирую это с моим полным ответом завтра.

Редактировать

Как указывает @Centril , мы уже поддерживаем универсальные типы, используя синтаксис : Trait (bound). например

fn foo<T: Trait>(x: T) { ... }

наряду с «правильными» или «овеществленными» универсальными типами, например

fn foo(x: impl Trait) { ... }

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

Теперь у нас уже есть impl Trait в позиции возврата функции, которая представляет экзистенциальный тип. Связанные типы трейтов являются экзистенциальными по форме и уже используют синтаксис : Trait .

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

type A: Iterator<Item: Foo + Bar>;
type B = (impl Baz, impl Debug, String);

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

Я по-прежнему считаю, что объединение универсальной и экзистенциальной квантификации под одним ключевым словом было ошибкой, и поэтому аргумент непротиворечивости для меня не работает. Единственная причина, по которой одно ключевое слово работает в сигнатурах функций, заключается в том, что контекст обязательно ограничивает вас использованием только одной формы количественного определения в каждой позиции. Есть потенциальные сахара, которые я мог бы видеть, когда у вас нет таких же ограничений

struct Foo {
    pub foo: impl Display,
}

Является ли это сокращением для экзистенциальной или универсальной квантификации? Исходя из интуиции, полученной при использовании impl Trait в сигнатурах функций, я не понимаю, как вы могли решить. Если вы на самом деле попытаетесь использовать его как оба, вы быстро поймете, что анонимная универсальная квантификация в этой позиции бесполезна, поэтому это должна быть экзистенциальная квантификация, но это несовместимо с impl Trait в аргументах функции.

Это две принципиально разные операции, да, они обе используют границы типажа, но я не вижу никаких причин, по которым наличие двух способов объявления экзистенциального типа уменьшило бы путаницу для новичков. Если попытка использовать type Name: Trait на самом деле вероятна для новичков, то это можно решить с помощью lint:

    type Foo: Display;
    ^^^^^^^^^^^^^^^^^^
note: were you attempting to create an existential type?
note: suggested replacement `type Foo = impl Display`

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

Я чувствую, что у меня недостаточно опыта работы с Rust, чтобы комментировать RFC. Тем не менее, я заинтересован в том, чтобы эта фича была объединена в ночной и стабильный Rust, чтобы использовать её с Rust libp2p для построения протокола шардинга Drops of Diamond . Подписался на выпуск, но не успеваю следить за всеми комментариями! Как я могу оставаться в курсе этого вопроса на высоком уровне, не просматривая комментарии?

Я чувствую, что у меня недостаточно опыта работы с Rust, чтобы комментировать RFC. Тем не менее, я заинтересован в том, чтобы эта фича была объединена в ночной и стабильный Rust, чтобы использовать её с Rust libp2p для построения протокола шардинга Drops of Diamond . Подписался на выпуск, но не успеваю следить за всеми комментариями! Как я могу оставаться в курсе этого вопроса на высоком уровне, не просматривая комментарии? На данный момент похоже, что мне, возможно, просто придется делать это, проверяя время от времени, а не подписываясь на выпуск. Было бы хорошо, если бы я мог подписаться по электронной почте, чтобы получать новости высокого уровня по этому вопросу.

Я по-прежнему считаю, что объединение универсальной и экзистенциальной квантификации под одним ключевым словом было ошибкой, и поэтому аргумент непротиворечивости для меня не работает.

В качестве общего принципа, независимого от этой особенности, я нахожу эту линию рассуждений проблематичной.

Я считаю, что мы должны подходить к проектированию языка исходя из того, каков язык, а не как мы хотели бы, чтобы он был при каком-то альтернативном развитии истории. Синтаксис impl Trait как универсальная квантификация в позиции аргумента стабилизирован, поэтому вы не можете отказаться от него. Даже если вы считаете, что X, Y и Z были ошибками (и я мог бы найти много вещей, которые я лично считаю ошибками в дизайне Rust, но я принимаю их и предполагаю...), мы должны жить с ними сейчас, и я думаю о том, как мы можем совместить все вместе, учитывая новую функцию (сделать вещи согласованными).

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


Вы могли бы доказать (но я бы не стал), что:

struct Foo {
    pub foo: impl Display,
}

семантически эквивалентен:

struct Foo<T: Display> {
    pub foo: T,
}

в соответствии с аргументом функция-аргумент.

По сути, учитывая impl Trait , вы должны подумать: «Это похоже на возвращаемый тип или на аргумент?» , что может быть сложно.


Если попытка использовать type Name: Trait на самом деле вероятна для новичков, то это можно решить с помощью lint:

Я бы тоже линтил, но в другую сторону; Я думаю, что следующие способы должны быть идиоматическими:

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

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

Существует только _один способ_ объявить экзистенциально квантифицированные типы: existential type Name: Bound; (используя existential поскольку это указано в RFC, я не совсем против исключения ключевого слова из этой формулировки).

Существует также дополнительный сахар для неявного объявления безымянного экзистенциально квантифицируемого типа в текущей области видимости: impl Bound (на данный момент игнорируя универсальный квантифицирующий сахар в аргументах функции).

Итак, текущее использование возвращаемого типа — это простое удаление шумов:

fn foo() -> impl Iterator<Item = impl Display> { ... }
existential type _0: Display;
existential type _1: Iterator<Item = _0>;
fn foo() -> _1 { ... }

расширение до const , static и let также тривиально.

Одно расширение, не упомянутое в RFC: поддержка этого сахара в синтаксисе type Alias = Concrete; , поэтому, когда вы пишете

type Foo = impl Iterator<Item = impl Display>;

на самом деле это сахар для

existential type _0: Display;
existential type _1: Iterator<Item = _0>;
type Foo = _1;

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

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

Я в основном согласен с комментарием arg: impl Trait , в основном из-за риска поощрения серьезных изменений в библиотеках, поскольку impl Trait не работает с турбофишом (прямо сейчас, и вам понадобится частичный турбофиш, чтобы он работал хорошо). Таким образом, линтинг в clippy кажется менее простым, чем в случае с псевдонимами типов (где нет Turbofish, вызывающего какие-либо проблемы).

Я в основном согласен с комментарием

@Centril только что поднял это со мной в IRC, и я согласен, что это справедливое замечание относительно обратной совместимости (слишком легко сломать). Когда/если приземлится частичный турбофиш, я думаю, что следует добавить ворс компилятора, но не раньше.

Итак... у нас было довольно много дискуссий о синтаксисе именованных экзистенциальных типов. Попробуем прийти к какому-то заключению и написать его в RFC/PR-пост, чтобы кто-то мог приступить к фактической реализации? :-)

Лично я, как только мы назвали экзистенциалы, предпочёл бы убрать линт (если таковой имеется) от любого использования impl Trait любом месте .

@rpjohnst Ну, вы, конечно, согласны со мной и @Centril в отношении именованных экзистенциалов ... что касается исключения их в аргументах функций, это возможно, но, вероятно, обсуждение для другого места. Это зависит от того, желает ли кто-то отдать предпочтение простоте или общности в этом контексте.

Актуален ли RFC на impl Trait в позиции аргумента? Если да, то можно ли утверждать, что его семантика _универсальна_? Если да: я хочу плакать. Глубоко.

@phaazon : Примечания к выпуску Rust 1.26 для impl Trait :

Дополнительное замечание для теоретиков типов: это не экзистенциальное, а универсальное. Другими словами, impl Trait является универсальным во входной позиции, но экзистенциальным в выходной позиции.

Просто выскажу свои мысли по этому поводу:

  • У нас уже был синтаксис для переменных типа, и на самом деле есть несколько вариантов использования переменных произвольного типа (т. е. очень часто вы хотите использовать переменную типа в нескольких местах вместо того, чтобы просто поместить ее в одном месте).
  • Ковариантные экзистенциалы открыли бы нам двери к функциям ранга n, что сейчас трудно сделать без трейта (см. это ), и это фича, которой действительно не хватает в Rust.
  • impl Trait легко ссылаться как на «тип, выбранный вызываемым пользователем», потому что… потому что на данный момент это единственная языковая конструкция, которая позволяет нам это делать! Выбор типа вызывающей стороной уже доступен через несколько конструкций.

Я действительно думаю, что текущее решение impl Trait в позиции аргумента очень жаль. :плакать:

Я действительно думаю, что импл Черта в позиции аргумента текущее решение вызывает сожаление. 😢

Хотя я немного расстроен этим, я, безусловно, думаю, что лучше потратить время на реализацию let x: impl Trait прямо сейчас!

Ковариантные экзистенциалы открыли бы нам двери к функциям ранга n.

У нас уже есть синтаксис для него ( fn foo(f: impl for<T: Trait> Fn(T)) ) (также известный как «тип HRTB»), но он еще не реализован. fn foo(f: impl Fn(impl Trait)) выдает ошибку, что «вложенные impl Trait не разрешены», и я ожидаю, что мы захотим, чтобы это означало версию с более высоким рангом, когда мы получим тип HRTB.

Это похоже на то, как Fn(&'_ T) означает for<'a> Fn(&'a T) , поэтому я не ожидаю, что это вызовет споры.

Глядя на текущий черновик, impl Trait в позиции аргумента является _универсальным_, но вы говорите, что impl for<_> Trait превращает его в _экзистенциальный_?! Насколько это безумно?

Почему мы решили, что нам нужно представить еще один способ создания универсальной? Я имею в виду:

fn foo(x: impl MyTrait)

Интересен только тем, что переменная анонимного типа появляется в типе только один раз . Если вам нужно вернуть тот же тип:

fn foo(x: impl Trait) -> impl Trait

Явно не получится. Мы советуем людям перейти от более общей идиомы к более ограничительной, вместо того, чтобы просто выучить одну и использовать ее повсюду. Вот и вся моя болтовня. Добавление функции, которая не имеет добавленной стоимости - предположение об обучении, которое я прочитал в RFC, является странным аргументом, когда мы думаем вместо новичков - им все равно нужно будет что-то изучать, так почему мы делаем синтаксис более двусмысленным, чтобы в значительной степени каждый, чтобы понизить кривую обучения здесь? Когда люди привыкнут к универсальному и экзистенциальному (а придет один, принцип очень прост с правильными словами), люди начнут думать, почему у нас есть одно и то же ключевое слово / шаблоны для выражения обоих, а также where и параметры шаблона на месте.

Argh Я думаю, все это уже было принято, и я зря разглагольствую. Я просто думаю, что это очень жаль. Я почти уверен, что я не единственный, кто разочарован решением RFC.

(Вероятно, нет особого смысла продолжать обсуждать это после того, как функция была стабилизирована, но см. здесь убедительный аргумент (с которым я согласен), почему impl Trait в позиции аргумента, имеющей семантику, которую она делает, разумна и Это по той же причине, по которой fn foo(arg: Box<Trait>) работает примерно так же, как fn foo<T: Trait>(arg: Box<T>) , хотя dyn Trait является экзистенциальным; теперь замените dyn с impl .)

Глядя на текущий проект, impl Trait в позиции аргумента является универсальным, но вы говорите, что impl for<_> Trait превращает его в экзистенциальное?!

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

fn foo<F: for<G: Fn(X) -> Y> Fn(G) -> Z>(f: F) {...}

который мог бы одновременно с добавлением (т.е. без изменений в impl Trait ) быть записан как:

fn foo(f: impl for<G: Fn(X) -> Y> Fn(G) -> Z) {...}

Это универсальный impl Trait , просто Trait является HRTB (аналогично impl for<'a> Fn(&'a T) ).
Если мы решим (что, как я полагаю, вероятно), что impl Trait внутри аргументов Fn(...) также является универсальным, вы можете написать это для достижения того же эффекта:

fn foo(f: impl Fn(impl Fn(X) -> Y) -> Z) {...}

Это то, что я думал, что вы имели в виду под «высшим рангом», если вы этого не сделали, пожалуйста, дайте мне знать.

Еще более интересным решением могло бы быть применение той же обработки в экзистенциальной позиции, т.е. разрешение этого (что означало бы «вернуть какое-то закрытие, которое принимает любое другое закрытие»):

fn foo() -> impl for<G: Fn(X) -> Y> Fn(G) -> Z {...}

быть записано так:

fn foo() -> impl Fn(impl Fn(X) -> Y) -> Z {...}

Это будет экзистенциальная impl Trait содержащая универсальную impl Trait (связанную с экзистенциальной, а не с объемлющей функцией).

@eddyb Не было бы более разумно иметь два отдельных ключевых слова для экзистенциальной и универсальной количественной оценки в целом, для согласованности и чтобы не сбивать с толку новичков?
Разве ключевое слово для экзистенциальной квантификации не может быть повторно использовано для экзистенциальных типов?
Почему мы используем impl для экзистенциальной ( и универсальной) квантификации, но existential для экзистенциальных типов?

Я хотел бы отметить три момента:

  • Нет особого смысла обсуждать, является ли impl Trait экзистенциальным или универсальным. Большинство программистов, вероятно, не читали достаточного количества руководств по теории типов. Вопрос должен заключаться в том, нравится ли это людям или они находят это запутанным. Чтобы ответить на этот вопрос, некоторые формы обратной связи можно увидеть здесь, в этой ветке, на Reddit или на форуме . Если что-то нуждается в дальнейшем объяснении, оно не проходит обязательный тест на интуитивную или неудивительную функцию. Таким образом, мы должны смотреть на то, сколько людей и насколько они запутались, и есть ли у них больше вопросов, чем с другими функциями. Действительно печально, что такая обратная связь приходит после стабилизации и с этим явлением надо что-то делать, но это тема отдельного разговора.
  • Технически, даже после стабилизации в этом случае был бы способ избавиться от функции (отложив решение, если это необходимо). Можно было бы протестировать написание функций, которые используют это, и удалить эту возможность в следующей редакции (сохранив при этом возможность вызывать их, если они происходят из ящиков другой редакции). Это удовлетворило бы гарантии устойчивости к ржавчине.
  • Нет, добавив еще два ключевых слов , чтобы указать экзистенциальные и универсальные типы не улучшить путаницы, было бы только сделать вещи еще хуже.

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

Были возражения против impl Trait в позиции аргумента, пока это была идея. Подобная обратная связь _не нова_, она широко обсуждалась даже в соответствующей ветке RFC. Было много дискуссий не только об универсальных/экзистенциальных типах с точки зрения теории типов, но и о том, как это может запутать новых пользователей.

Правда, реальных новых пользовательских ракурсов мы не получили, но это не возникло из ниоткуда.

@Boscop any и some были предложены в качестве пары ключевых слов для выполнения этой работы, но было решено против (хотя я не знаю, было ли где-либо записано обоснование).

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

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

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

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

Да? Я имею в виду, что я не спорю, было ли то, что произошло, хорошей или плохой идеей. Я просто хочу отметить, что ветка RFC действительно получила отзывы по этому поводу, и решение было принято в любом случае.

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

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

Худший? Как это так? Я предпочитаю больше помнить, чем двусмысленность/замешательство.

Да? Я имею в виду, что я не спорю, было ли то, что произошло, хорошей или плохой идеей. Я просто хочу отметить, что ветка RFC действительно получила отзывы по этому поводу, и решение было принято в любом случае.

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

Теперь это было представлено, и у нас есть способ получить фактические достоверные данные или максимально достоверные данные о том, насколько люди запутались по шкале от 0 до 10.

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

Например здесь, я уже начал такое обсуждение и есть некоторые реальные шаги, которые можно предпринять, пусть и небольшие: https://internals.rust-lang.org/t/idea-mandate-n-independent-uses -перед-стабилизацией-функции/7522/14. У меня не было времени писать RFC, поэтому, если кто-то опередит меня в этом или захочет помочь, я не буду возражать.

Худший? Как это так?

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

impl Trait как при выборе вызываемого абонента, было бы достаточно. Если вы попытаетесь использовать его в качестве аргумента, вы внесете путаницу. HRTB устранили бы эту путаницу.

@vorner Ранее я утверждал, что мы должны проводить настоящее A/B-тестирование с новичками в Rust, чтобы увидеть, что им на самом деле легче и труднее в освоении, потому что это трудно догадаться, как тому, кто хорошо разбирается в Rust.
FWIW, я помню, когда я изучал Rust (исходя из C++, D, Java и т. д.), универсальные количественные дженерики типов (включая их синтаксис) было легко понять (время жизни в дженериках было немного сложнее).
Я думаю , что impl Trait для типов ARG приведет к много путаницы с новичками вниз по линии , и многие вопросы , как это .
В отсутствие каких-либо доказательств того, какие изменения облегчат изучение Rust, мы должны воздерживаться от внесения таких изменений и вместо этого вносить изменения, которые делают/сохраняют Rust более согласованным, потому что согласованность делает его, по крайней мере, легким для запоминания. Новичкам в Rust в любом случае придется прочитать книгу пару раз, поэтому введение impl Trait для аргументов, позволяющее откладывать дженерики в книге на потом, на самом деле не избавляет от какой-либо сложности.

@eddyb Кстати, зачем нам нужно еще одно ключевое слово existential для типов в дополнение к impl ? (Я бы хотел, чтобы мы использовали some для обоих..)

FWIW, я помню, когда я изучал Rust (исходя из C++, D, Java и т. д.), универсальные количественные дженерики типов (включая их синтаксис) было легко понять (время жизни в дженериках было немного сложнее).

Сам я тоже не считаю это проблемой. В моей нынешней компании я веду занятия по Rust — пока мы встречаемся раз в неделю, и я стараюсь преподавать на практике. Это опытные программисты, в основном имеющие опыт работы с Java и Scala. Несмотря на то, что были некоторые препятствия, дженерики (по крайней мере, их чтение — они немного осторожны при их написании) в позиции аргумента не были проблемой. Был небольшой сюрприз по поводу дженериков в возвращаемой позиции (например, вызывающая сторона выбирает, что возвращает функция), особенно то, что это часто можно опустить, но объяснение заняло около 2 минут, прежде чем оно щелкнуло. Но я боюсь даже упоминать о существовании impl Trait в позиции аргумента, потому что теперь мне придется отвечать на вопрос, почему он существует, а у меня нет реального ответа на него. Это плохо для мотивации, а наличие мотивации имеет решающее значение для процесса обучения.

Итак, вопрос в том, есть ли у сообщества достаточно голоса, чтобы вновь открыть дебаты с некоторыми данными, подтверждающими аргументы?

@eddyb Кстати, зачем нам еще одно экзистенциальное ключевое слово для типов в дополнение к impl? (Хотелось бы, чтобы мы использовали некоторые из них для обоих ..)

Почему бы не forall … /me медленно ускользает

@phaazon У нас есть forall (то есть «универсальный»), и это for , например, в HRTB ( for<'a> Trait<'a> ).

@eddyb Да, тогда используйте его и для экзистенциального , например, как Haskell с forall .

Вся дискуссия очень самоуверенна, я немного удивлен, что идея аргумента стабилизировалась. Я надеюсь, что позже появится способ отправить еще один RFC, чтобы отменить это (я полностью готов написать его, потому что мне действительно очень не нравится вся путаница, которую это вызовет).

Я действительно не понимаю. Какой смысл иметь их в позиции аргумента? Я не так много пишу на Rust, но мне очень понравилось, что я могу делать -> impl Trait . Когда бы я использовал его в качестве аргумента?

Насколько я понял, это было в основном для последовательности. Если я могу написать тип impl Trait в позиции аргумента в сигнатуре fn, почему я не могу написать его в другом месте?

Тем не менее, лично я бы предпочел просто сказать людям: «Просто используйте параметры типа»…

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

Кроме того, тогда возникает проблема, за что или против!

Учитывая, что вы вообще не можете выразить несколько вещей с помощью impl Trait , функция с impl Trait качестве одного из аргументов не может выполнять турбофиш и, следовательно, вы не можете взять его адрес (я что-то забыл? другой недостаток?), я думаю, что нет особого смысла анализировать параметры типа, потому что вам все равно нужно их использовать.

поэтому вы не можете взять его адрес

Можно, если сделать вывод из подписи.

Какой смысл иметь их в позиции аргумента?

Их нет, потому что это то же самое, что и использование привязки трейта.

fn foo(x: impl Debug)

Это то же самое, что и

fn foo<A>(x: A) where A: Debug
fn foo<A: Debug>(x: A)

Также учтите это:

fn foo<A>(x: A) -> A where A: Debug

impl Trait в позиции аргумента не позволяет вам сделать это, потому что это анонимно . Тогда это довольно бесполезная функция, потому что у нас уже есть все необходимое для решения таких ситуаций. Людям нелегко освоить эту новую функцию, потому что почти все знают переменные типов/параметры шаблонов, а Rust — единственный язык, использующий этот синтаксис impl Trait . Вот почему многие люди разглагольствуют о том, что нужно было оставить привязки возвращаемого значения/let, потому что это ввело новую, необходимую семантику (т.е. тип, выбранный вызываемым пользователем).

Вкратце, @iopq : вам это не понадобится, и в этом нет смысла, кроме как «Давайте добавим еще одну конструкцию синтаксического сахара, которая на самом деле никому не понадобится, потому что она справляется с очень специфическим использованием — т.е. с переменными анонимного типа» .

Кроме того, кое-что, что я забыл сказать: становится сложнее увидеть, как ваша функция параметризована/мономорфизирована.

@Verner С частичной турбофишей имеет смысл протестировать ее ради простоты, удобочитаемости и ясности. Хотя я не очень люблю функцию в позиции arg.

Как это согласовано, когда -> impl Trait вызываемый выбирает тип, а в x: impl Trait вызывающий выбирает тип?

Я понимаю, что по-другому это не работает, но это не кажется «последовательным», это кажется противоположностью последовательного

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

У нас было два RFC, которые получили в общей сложности около 600 комментариев между ними, начиная с более чем 2 лет назад, для решения вопросов, повторно рассматриваемых в этой ветке:

  • rust-lang/rfcs#1522 ("Минимум impl Trait ")
  • rust-lang/rfcs#1951 («Завершить синтаксис и область видимости параметров для impl Trait , расширив их до аргументов»)

(Если вы прочтете эти обсуждения, то увидите, что изначально я был сильным сторонником подхода с двумя ключевыми словами. Теперь я думаю, что использование одного ключевого слова — правильный подход.)

Спустя 2 года и сотни комментариев было принято решение, и теперь функция стабилизирована. Это проблема с отслеживанием функции, которая открыта для отслеживания все еще нестабильных вариантов использования impl Trait . Повторное рассмотрение урегулированных аспектов impl Trait относится к теме этой проблемы с отслеживанием. Вы можете продолжить обсуждение этого вопроса, но, пожалуйста, не в трекере.

Как это было стабилизировано, когда impl Trait даже не получил поддержки в позиции аргумента для фнс в чертах??

@daboross Тогда нужно поставить галочку в исходном посте!

(Просто обнаружил, что https://play.rust-lang.org/?gist=47b1c3a3bf61f33d4acb3634e5a68388&version=stable в настоящее время работает)

Я думаю, что странно, что https://play.rust-lang.org/?gist=c29e80715ac161c6dc95f96a7f91aa8c&version=stable&mode=debug не работает (пока), более того, это сообщение об ошибке. Я один так думаю? Возможно, нужно было бы добавить флажок для impl Trait в возвращаемой позиции в типажах, или это было сознательное решение разрешить только impl Trait в аргумент-позиции для функций типажей, вынуждая использовать existential type для возвращаемых типов? (что... показалось бы мне непоследовательным, но, может быть, я что-то упускаю?)

@Эклеог

было ли это сознательным решением разрешить только impl Trait в аргумент-позиции для функций типажей, вынуждая использовать экзистенциальный тип для возвращаемых типов?

Да -- позиция возврата impl Trait в типажах была отложена до тех пор, пока у нас не появится более практический опыт использования экзистенциальных типов в типажах.

@cramertj Мы еще не на том этапе, когда у нас достаточно практического опыта, чтобы реализовать это?

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

@mark-im Лично я не вижу ничего спорного в return-position impl Trait для методов типажей ... может быть, я что-то упускаю.

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

Я понимаю. Думаю, я просто считаю это недостающей частью существующей функции, а не новой функцией.

Я думаю, что @alexreg прав, очень заманчиво использовать экзистенциальные impl Trait в методах трейтов . На самом деле это не новая функция, но я думаю, что есть несколько вещей, которые нужно решить, прежде чем пытаться ее реализовать?

@phaazon Возможно, да ... Я действительно не знаю, насколько детали реализации будут отличаться от того, что у нас уже есть сегодня, но, возможно, кто-то может это прокомментировать. Я бы хотел увидеть экзистенциальные типы и для привязок let/const, но я определенно могу принять это как функцию, выходящую за рамки этой, таким образом, ожидая еще один цикл или около того, прежде чем начать с этого.

Интересно, сможем ли мы сдержать универсальную импл-черту в трейтах...

Но да, я думаю, я понимаю вашу точку зрения.

@mark-im Нет, мы не можем, они уже стабильны .

Они находятся в функциях, но как насчет объявлений Trait?

@mark-im, как показывает фрагмент, они стабильны как в объявлениях импликаций, так и в объявлениях признаков.

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

// GOOD:
type Foo: Iterator<Item: Display>;

type Bar = (impl Display, impl Debug);

// BAD
type Foo = impl Iterator<Item = impl Display>;

type Bar0: Display;
type Bar1: Debug;
type Bar = (Bar0, Bar1);

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

// Concrete type with a generic body
struct Data<TBody> {
    ts: Timestamp,
    body: TBody,
}


// A name for an inferred iterator
type IterData = Data<impl Read>;
type Iter: Iterator<Item = IterData>;


// A function that gives us an iterator. Also takes some arbitrary range
fn iter(&self, range: impl RangeBounds<Timestamp>) -> Result<Iter, Error> { ... }


// A struct that holds on to that iterator
struct HoldsIter {
    iter: Iter,
}

Для меня не имеет смысла, что type Bar = (impl Display,); это хорошо, а type Bar = impl Display; — плохо.

Если мы выбираем другой альтернативный синтаксис экзистенциального типа (все отличные от rfc 2071 ?), будет ли ветка форума на https://users.rust-lang.org/ подходящим местом для этого?

У меня недостаточно знаний об альтернативах, чтобы начать такую ​​тему сейчас, но поскольку экзистенциальные типы все еще не реализованы, я думаю, что обсуждение на форумах, а затем новый RFC, вероятно, было бы лучше, чем говорить об этом в проблеме отслеживания. .

Что не так с type Foo = impl Trait ?

@daboross Вместо этого, вероятно, внутренний форум. Я подумываю написать об этом RFC, чтобы доработать синтаксис.

@daboross В этой теме уже было более чем достаточно обсуждений синтаксиса. Я думаю, что если @Centril сможет написать RFC для этого на данный момент, то отлично.

Есть ли какой-нибудь выпуск, под которым я могу подписаться для обсуждения экзистенциалов в трейтах?

Есть ли аргумент, связанный с макросами, для того или иного синтаксиса?

@tomaka в первом случае type Foo = (impl Display,) - это действительно единственный синтаксис, который у вас есть. Я предпочитаю type Foo: Trait type Foo = impl Trait просто потому, что мы привязываем тип, который можем назвать, например, <TFoo: Trait> или where TFoo: Trait , тогда как с impl Trait мы не можем назвать тип.

Чтобы уточнить, я не говорю, что type Foo = impl Bar это плохо, я говорю, что type Foo: Bar лучше в простых случаях, отчасти из-за мотивации @KodrAus .

Последнее я читаю как: «тип Foo удовлетворяет Bar», а первое как: «тип Foo равен некоторому типу, удовлетворяющему Bar». Таким образом, первое, на мой взгляд, более прямое и естественное с экстенсиональной точки зрения («что я могу сделать с Foo»). Чтобы понять последнее, вам необходимо глубже понять экзистенциальную квантификацию типов.

type Foo: Bar также довольно удобен, потому что если этот синтаксис используется в качестве привязки к связанному типу в трейте, то вы можете просто скопировать объявление в трейте в импл, и он просто будет работать (если это всю информацию, которую вы хотите раскрыть ..).

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

@KodrAus

Вот как я читаю определения этих типов:

  • type Foo: Trait означает, что " Foo - это тип, реализующий Trait "
  • type Foo = impl Trait означает, что " Foo является псевдонимом некоторого типа, реализующего Trait "

Для меня Foo: Trait просто объявляет ограничение на реализацию Foo Trait . В каком-то смысле type Foo: Trait кажется неполным. Похоже, у нас есть ограничение, но фактическое определение Foo отсутствует.

С другой стороны, impl Trait напоминает о том, что «это единственный тип, но компилятор вычисляет его имя». Следовательно, type Foo = impl Trait подразумевает, что у нас уже есть конкретный тип (который реализует Trait ), из которых Foo является просто псевдонимом.

Я считаю, что type Foo = impl Trait более четко передает правильное значение: Foo — это псевдоним некоторого типа, реализующий Trait .

@stjepang

type Foo: Trait означает, что «Foo — это тип, реализующий Trait».
[..]
В некотором смысле type Foo: Trait кажется неполным.

Я тоже так это прочитал (по модулю формулировки...), и это экстенсионально правильная интерпретация. Это говорит все о том, что вы можете делать с Foo (морфизмы, которые предоставляет тип). Следовательно, он является экстенсионально полным. С точки зрения читателей и особенно начинающих, я думаю, что экстенсиональность важнее.

С другой стороны, impl Trait напоминает «это один тип, но компилятор заполняет пробел». Таким образом, type Foo = impl Trait подразумевает, что у нас уже есть конкретный тип (который реализует Trait ), псевдонимом которого является Foo , но компилятор выяснит, какой это тип на самом деле.

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

@Центрил

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

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

Интересна дихотомия экстенсионала и интенсионала — я никогда раньше не думал о impl Trait таким образом.

Тем не менее, я позволю себе не согласиться с выводом. FWIW, мне никогда не удавалось разобраться с экзистенциальными типами в Haskell и Scala, так что считайте меня новичком. :) impl Trait в Rust с первого дня кажется очень интуитивным, что, вероятно, связано с тем, что я думаю об этом как о ограниченном псевдониме, а не о том, что можно сделать с этим типом. Так между зная , что Foo и что можно сделать с этим, я выбираю первое.

Только мой 2c, хотя. У других могут быть другие ментальные модели impl Trait .

Я полностью согласен с этим комментарием : type Foo: Trait кажется неполным. А type Foo = impl Trait кажется более похожим на использование impl Trait другом месте, что помогает языку казаться более последовательным и запоминающимся.

@joshtriplett См. https://github.com/rust-lang/rust/issues/34511#issuecomment -387238653 для начала обсуждения согласованности; Я считаю, что разрешать формы форм — это на самом деле последовательная вещь. И только разрешение одной из форм (в зависимости от того...) непоследовательно. Разрешение type Foo: Trait также особенно хорошо сочетается с https://github.com/rust-lang/rfcs/pull/2289, с помощью которого вы можете указать: type Foo: Iterator<Item: Display>; что делает вещи аккуратно однородными.

@stjepang Экстенсиональная перспектива type Foo: Bar; не требует от вас понимания экзистенциальной квантификации в теории типов. Все, что вам действительно нужно понять, это то, что Foo позволяет вам выполнять все операции, предоставляемые Bar , вот и все. С точки зрения пользователя Foo , это также все, что интересно.

@Центрил

Кажется, теперь я понимаю, откуда вы взялись, и привлекательность использования синтаксиса Type: Trait в как можно большем количестве мест.

Существует сильная коннотация вокруг : , используемого для границ типовых реализаций, и = , используемых для определений типов и границ типов, равных другому типу.

Я думаю, что это очевидно и в вашем RFC. Например, возьмем эти два ограничения типов:

  • Foo: Iterator<Item: Bar>
  • Foo: Iterator<Item = impl Bar>

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

Позвольте мне попытаться проиллюстрировать эту мысль на другом примере:

trait Person {
    type Name: Into<String>; // Just a type bound, not a definition!
    // ...
}

struct Alice;

impl Person for Alice {
    type Name = impl Into<String>; // A concrete type definition.
    // ...
}

Как тогда нам определить экзистенциальный тип, реализующий Person ?

  • type Someone: Person , что выглядит как привязка типа.
  • type Someone = impl Person , что выглядит как определение типа.

@stjepang Выглядеть так, будто привязанный тип — это неплохо :) Мы можем реализовать Person for Alice вот так:

struct Alice;
trait Person          { type Name: Into<String>; ... }
impl Person for Alice { type Name: Into<String>; ... }

Смотри, м'а! Содержимое внутри { .. } как для типажа, так и для реализации идентично, это означает, что вы можете скопировать текст из типажа нетронутым, насколько это касается Name .

Поскольку связанный тип является функцией уровня типа (где первый аргумент Self ), мы можем видеть псевдоним типа как ассоциированный тип с нулевой арностью, так что ничего странного не происходит.

Эти две границы, в конце концов, имеют одинаковый эффект, но (я думаю) немного отличаются. Первый говорит: «Элемент должен реализовывать свойство Bar», а второй говорит: «Элемент должен быть равен некоторому типу, реализующему Bar».

Ага; Я нахожу первую формулировку более точной и естественной. :)

@Центрил Хех. Означает ли это, что одного type Thing; достаточно для введения абстрактного типа?

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { type Output; fn neg(self) -> Self::Output { self } }

@kennytm Я думаю, что это технически возможно; но вы можете спросить, желательно это или нет, в зависимости от того, что вы думаете о неявном/явном. В этом конкретном случае я думаю, что было бы технически достаточно написать:

trait Neg           { type Output; fn neg(self) -> Self::Output; }
impl Neg for MyType { fn neg(self) -> Self::Output { self } }

и компилятор может просто вывести type Output: Sized; для вас (что является совершенно неинтересной границей, которая не дает вам никакой информации). Это то, что следует учитывать для более интересных границ, но этого не будет в моем первоначальном предложении, потому что я думаю, что это может стимулировать API с низкой доступностью, даже когда конкретный тип очень прост, из-за лени программиста:) Также не будет type Output; быть изначально по той же причине.

Я думаю, что после прочтения всего этого я больше согласен с @Centril. Когда я вижу type Foo = impl Bar я склонен думать, что Foo — это особый тип, как и с другими псевдонимами. Но это не так. Рассмотрим этот пример:

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

ИМХО, немного странно видеть = в объявлении Displayable, но при этом не иметь равных возвращаемых типов foo и bar (т.е. это = не является транзитивным, в отличие от всего остального). Проблема в том, что Foo _не_ является псевдонимом для определенного типа, который, как оказалось, подразумевает некоторый Trait. Иными словами, это один тип в любом контексте, в котором он используется, но этот тип может быть разным для разных целей, как в примере.

Несколько человек упомянули, что type Foo: Bar кажется «неполным». Для меня это хорошо. В некотором смысле Foo является неполным; мы не знаем, что это такое, но мы знаем, что оно удовлетворяет Bar .

@mark-им

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

Вау, это правда? Меня бы это, конечно, очень смутило.

Есть ли причина, по которой Displayable будет сокращением для impl Display а не отдельным конкретным типом? Полезно ли такое поведение, учитывая, что псевдонимы трейтов (отслеживание проблемы: https://github.com/rust-lang/rust/issues/41517) можно использовать аналогичным образом? Пример:

trait Displayable = Display;

fn foo() -> impl Displayable { "hi" }
fn bar() -> impl Displayable { 42 }

@mark-им

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Это не годный пример. Из справочного раздела по экзистенциальным типам в RFC 2071 :

existential type Foo = impl Debug;

Foo можно использовать как i32 в нескольких местах модуля. Однако каждая функция, использующая Foo качестве i32 должна независимо накладывать ограничения на Foo , чтобы она была равна i32

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

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

Я предполагаю, что ваш пример даст что-то вроде expected type `&'static str` but found type `i32` при возврате bar , поскольку foo уже установил бы конкретный тип Displayable в &'static str .

РЕДАКТИРОВАТЬ: Если вы не придете к этому из интуиции, которая

type Displayable = impl Display;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

эквивалентно

fn foo() -> impl Display { "hi" }
fn bar() -> impl Display { 42 }

а не мое ожидание

existential type _0 = impl Display;
type Displayable = _0;

fn foo() -> Displayable { "hi" }
fn bar() -> Displayable { 42 }

Я предполагаю, какая из этих двух интерпретаций верна, может зависеть от RFC, который может писать @Centril .

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

Я предполагаю, какая из этих двух интерпретаций верна, может зависеть от RFC, который может писать @Centril .

Причина, по которой type Displayable = impl Display; существует, заключается в том, что это псевдоним для определенного типа.
См. https://github.com/rust-lang/rfcs/issues/1738 , проблему, которую решает эта функция.

@ Nemo157 Ваши ожидания верны. :)

Следующее:

type Foo = (impl Bar, impl Bar);
type Baz = impl Bar;

будет обессахарен до:

/* existential */ type _0: Bar;
/* existential */ type _1: Bar;
type Foo = (_0, _1);

/* existential */ type _2: Bar;
type Baz = _2;

где _0 , _1 и _2 — номинально разные типы, поэтому Id<_0, _1> , Id<_0, _2> , Id<_1, _2> (и симметричные экземпляры) все необитаемы, где Id определено в refl .

Отказ от ответственности: я (охотно) не читал RFC (но знаю, о чем он), поэтому я мог прокомментировать то, что кажется «интуитивным» с синтаксисом.

Для синтаксиса type Foo: Trait я бы вполне ожидал, что что-то подобное будет возможно:

trait Trait {
    type Foo: Display;
    type Foo: Debug;
}

Точно так же, как в настоящее время возможно where Foo: Display, Foo: Debug .

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

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

@Эклеог

Для синтаксиса type Foo: Trait я бы вполне ожидал, что что-то подобное будет возможно:

Это возможно. Эти «псевдонимы типов» объявляют связанные типы (псевдонимы типов могут быть интерпретированы как функции уровня 0-арного типа, в то время как связанные типы являются функциями уровня 1+-арного типа). Конечно, вы не можете иметь несколько связанных типов с одним и тем же именем в одном трейте, это было бы похоже на попытку определить два псевдонима типа с одним и тем же именем в модуле. В impl type Foo: Bar также соответствует экзистенциальной квантификации.

О, и я думаю, что чем больше синтаксиса в Rust, тем сложнее его выучить.

Оба синтаксиса уже используются. type Foo: Bar; уже допустимо в типах, а также для универсальной количественной оценки как Foo: Bar где Foo — переменная типа. impl Trait используется для экзистенциальной квантификации в позиции возврата и для универсальной квантификации в позиции аргумента. Разрешение пробелов согласованности обоих плагинов в языке. Кроме того, они оптимальны для различных сценариев, поэтому оба варианта обеспечивают глобальный оптимум.

Кроме того, новичку вряд ли понадобится type Foo = (impl Bar, impl Baz); . Большинство вариантов использования, скорее всего, будут type Foo: Bar; .

В исходном запросе на включение для RFC 2071 упоминается ключевое слово typeof которое, похоже, было полностью исключено из обсуждения. Я считаю, что предлагаемый в настоящее время синтаксис довольно неявный, поскольку и компилятор, и любой человек, который читает код, ищет конкретный тип.

Я бы предпочел, чтобы это было сделано явно. Итак, вместо

type Foo = impl SomeTrait;
fn foo_func() -> Foo { ... }

мы бы написали

fn foo_func() -> impl SomeTrait { ... }
type Foo = return_type_of(foo_func);

(с именем return_type_of для велосипедного навеса) или даже

fn foo_func() -> impl SomeTrait as Foo { ... }

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

trait Bar
{
    type Assoc: SomeTrait;
    fn func() -> Assoc;
}

impl Bar for SomeType
{
    type Assoc = return_type_of(Self::func);
    fn func() -> Assoc { ... }
}

или даже

impl Bar for SomeType
{
    fn func() -> impl SomeTrait as Self::Assoc { ... }
}

Извините, если это уже обсуждалось и было отклонено, но я не смог найти.

@Центрил

Это возможно. Эти «псевдонимы типов» объявляют связанные типы (псевдонимы типов могут быть интерпретированы как функции уровня 0-арного типа, в то время как связанные типы являются функциями уровня 1+-арного типа). Конечно, вы не можете иметь несколько связанных типов с одним и тем же именем в одном трейте, это было бы похоже на попытку определить два псевдонима типа с одним и тем же именем в модуле. В реализации тип Foo: Bar также соответствует экзистенциальной квантификации.

(извините, я хотел поместить это в impl Trait for Struct , а не в trait Trait )

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

impl Trait for Struct {
    type Type: Debug;
    type Type: Display;

    fn foo() -> Self::Type { 42 }
}

(ссылка на игровую площадку для полной версии)
вроде должно работать.

Потому что это просто положить две оценки на Type , таким же образом , как where Type: Debug, Type: Display работа .

Если это не разрешено (что я, кажется, понимаю под «Конечно, вы не можете иметь несколько связанных типов с одним и тем же именем в одном признаке»? но учитывая мою ошибку в написании trait Trait вместо impl Trait for Struct я не уверен), то я думаю, что это проблема с синтаксисом type Type: Trait .

Затем внутри объявления trait синтаксис уже type Type: Trait и не допускает множественных определений. Так что я думаю, может быть, эта лодка уже давно уплыла…

Однако, как указано выше @stjepang и @joshtriplett , type Type: Trait кажется неполным. И хотя это может иметь смысл в объявлениях trait (на самом деле оно спроектировано так, чтобы быть неполным, хотя и странно, что не допускает множественных определений), это не имеет смысла в блоке impl Trait , где тип должен быть известен точно (и в настоящее время может быть записан только как type Type = RealType )

impl Trait используется для экзистенциальной квантификации в позиции возврата и для универсальной квантификации в позиции аргумента.

Да, я также думал о impl Trait в позиции аргумента, когда писал это, и задавался вопросом, должен ли я сказать, что поддержал бы тот же аргумент для impl Trait в позиции аргумента, если бы знал, что он подвергается стабилизации. . Тем не менее, я думаю, что было бы лучше не разжигать эту дискуссию :)

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

Оптимальность и простота

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

impl Trait и :

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

Представьте себе новичка, который всегда видел, как type Type: Trait приближается к его первым type Type = impl Trait . Они, вероятно, могут догадаться, что происходит, но я почти уверен, что будет момент «Что за хрень? Я использую Rust уже много лет, и до сих пор есть синтаксис, которого я никогда не видел?». Это более или менее ловушка, в которую попал C++.

Раздувание функций

Я думаю, что чем больше у него функций, тем сложнее его изучать. И я не вижу огромного преимущества использования type Type: Trait сравнению с type Type = impl Trait : это, например, 6 сохраненных символов?

Если rustc выводит ошибку при виде type Type: Trait которой говорится, что человек, пишущий это, использует type Type = impl Trait будет иметь для меня гораздо больше смысла: по крайней мере, есть единственный способ писать вещи , это имеет смысл для всех ( impl Trait уже ясно распознано как экзистенциальная позиция в возврате) и охватывает все варианты использования. И если люди пытаются использовать то, что они считают интуитивным (хотя я бы с этим не согласился, для меня = impl Trait более интуитивен по сравнению с нынешним = i32 ), они по праву перенаправляются на условно-правильный способ его написания.

В первоначальном запросе на включение для RFC 2071 упоминается ключевое слово typeof, которое, кажется, было полностью исключено из этого обсуждения. Я считаю, что предлагаемый в настоящее время синтаксис довольно неявный, поскольку и компилятор, и любой человек, который читает код, ищет конкретный тип.

typeof кратко обсуждался в вопросе, который я открыл 1,5 года назад: https://github.com/rust-lang/rfcs/issues/1738#issuecomment -258353755

Говоря как новичок, я нахожу синтаксис type Foo: Bar запутанным. Это синтаксис связанного типа, но он должен быть в трейтах, а не в структурах. Если вы видите impl Trait один раз, вы можете понять, что это такое, или же вы можете найти его. С другим синтаксисом это сделать сложнее, и я не уверен, в чем преимущество.

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

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

Вау, это совсем не то, что я понял. Спасибо @Nemo157 за то, что

В этом случае я действительно предпочел бы синтаксис =.

@Эклеог

тогда я думаю, что это проблема с синтаксисом type Type: Trait .

Это может быть разрешено, и это будет совершенно точно определено, но обычно вы пишете where Type: Foo + Bar вместо where Type: Foo, Type: Bar , так что это не кажется очень хорошей идеей. Вы также можете легко запустить хорошее сообщение об ошибке для этого случая, предлагая вместо этого написать Foo + Bar в случае связанного типа.

type Foo = impl Bar; также имеет проблемы с понятностью, поскольку вы видите = impl Bar и делаете вывод, что вы можете просто заменить его в каждом случае, когда он используется как -> impl Bar ; но это не сработает. @mark-im сделал эту интерпретацию, которая кажется гораздо более вероятной ошибкой. Поэтому я делаю вывод, что type Foo: Bar; — лучший выбор для обучения.

Однако, как указано выше @stjepang и @joshtriplett , тип Type: Trait кажется неполным.

Это не является неполным из расширенного POV. Вы получаете столько же информации от type Foo: Bar; сколько и от type Foo = impl Bar; . Итак, с точки зрения того, что вы можете сделать с type Foo: Bar; , все готово. На самом деле последний обессахаривается как type _0: Bar; type Foo = _0; .

РЕДАКТИРОВАТЬ: я имел в виду, что, хотя для некоторых это может показаться неполным, это не с технической точки зрения.

Тем не менее, я думаю, что было бы лучше не разжигать эту дискуссию :)

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

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

Если бы мы стремились к простоте, я бы отказался от type Foo = impl Bar; .
Следует отметить, что предполагаемая простота C (предполагаемая, потому что Haskell Core и подобные вещи, вероятно, проще, но все еще звучат...) дорого обходится, когда речь идет о выразительности и надежности. C не является моей путеводной звездой в языковом дизайне; отнюдь не.

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

Но они ничем не будут отличаться по своей семантике. Один обесахаривает другой.
Я думаю, что путаница при попытке написать type Foo: Bar; или type Foo = impl Bar только для того, чтобы один из них не работал, даже если оба имеют совершенно четко определенную семантику, только на пути пользователя. Если пользователь пытается написать type Foo = impl Bar; , то срабатывает lint и предлагает type Foo: Bar; . Линт обучает пользователя другому синтаксису.
Для меня важно, чтобы язык был однородным и последовательным; Если мы решили где-то использовать оба синтаксиса, мы должны применять это решение последовательно.

Представьте себе новичка, который всегда видел, как type Type: Trait приближается к его первым type Type = impl Trait .

В этом конкретном случае сработает анализатор и порекомендует прежний синтаксис. Когда дело доходит до type Foo = (impl Bar, impl Baz); , новичку в любом случае придется выучить -> impl Trait , поэтому он должен быть в состоянии вывести из этого значение.

Это более или менее ловушка, в которую попал C++.

Проблема C++ в основном в том, что он довольно старый, имеет багаж C и множество функций, поддерживающих слишком много парадигм. Это не отдельные функции, просто другой синтаксис.

Я думаю, что чем больше у него функций, тем сложнее его изучать.

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

И я не вижу огромного преимущества в использовании type Type: Trait сравнению с type Type = impl Trait: это примерно 6 сохраненных символов?

Да, только 6 символов сохранено. Но если мы рассмотрим type Foo: Iterator<Item: Iterator<Item: Display>>; , то вместо этого мы получим: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>>; котором намного больше шума. type Foo: Bar; также является более прямым по сравнению с последним, менее подвержен неправильному толкованию (повторная замена..) и лучше работает для связанных типов (копирует тип из признака..).
Кроме того, type Foo: Bar можно было бы естественным образом расширить до type Foo: Bar = ConcreteType; что открыло бы конкретный тип, но также гарантировало бы, что он удовлетворяет Bar . Ничего подобного нельзя сделать за type Foo = impl Trait; .

Вывод rustc ошибки при виде type Type: Trait которой говорится, что человек, пишущий его, использует type Type = impl Trait имел бы для меня гораздо больше смысла: по крайней мере, существует единственный способ записи,

они по праву перенаправляются на условно-правильный способ написания.

Я предлагаю, чтобы существовал один общепринятый способ записи вещей; type Foo: Bar; .

@lnicola

Говоря как новичок, я нахожу синтаксис type Foo: Bar запутанным. Это синтаксис связанного типа, но он должен быть в трейтах, а не в структурах.

Я повторю, что псевдонимы типов действительно можно рассматривать как связанные типы. Вы сможете сказать:

trait Foo        { type Baz: Quux; }
// User of `Bar::Baz` can conclude `Quux` but nothing more!
impl Foo for Bar { type Baz: Quux; }

// User of `Wibble` can conclude `Quux` but nothing more!
type Wibble: Quux;

Мы видим, что он работает точно так же в ассоциированных типах и псевдонимах типов.

Да, только 6 символов сохранено. Но если мы рассмотрим type Foo: Iterator<Item: Iterator<Item: Display>>; , то вместо этого мы получим: type Foo = impl Iterator<Item = impl Iterator<Item = impl Display>> ; который имеет гораздо больше шума.

Это кажется ортогональным синтаксису объявления именованного экзистенциала. Четыре предложенных синтаксиса, насколько я помню, потенциально позволяют это сделать.

type Foo: Iterator<Item: Iterator<Item: Display>>;
type Foo = impl Iterator<Item: Iterator<Item: Display>>;
existential type Foo: Iterator<Item: Iterator<Item: Display>>;
existential type Foo = impl Iterator<Item: Iterator<Item: Display>>;

Возможность использовать предложенное вами сокращение Trait<AssociatedType: Bound> вместо синтаксиса Trait<AssociatedType = impl Bound> для объявления анонимных экзистенциальных типов для связанных типов экзистенциального типа (либо именованного, либо анонимного) является независимой функцией (но, вероятно, уместной в условия сохранения согласованности всего набора признаков экзистенциального типа).

@ Nemo157 Nemo157 Это разные функции, да; но я думаю, что естественно рассматривать их вместе ради последовательности.

@Центрил

Я извиняюсь, но они ошибаются. Это не является неполным из расширенного POV.

Я никогда не предполагал, что в предлагаемом вами синтаксисе отсутствует информация; Я предполагал, что это кажется неполным; это кажется неправильным для меня и для других. Я понимаю, что вы не согласны с этим, с той точки зрения, с которой вы исходите, но это не делает ваше ощущение неправильным.

Также обратите внимание, что в этой теме люди продемонстрировали проблему интерпретации именно с этой разницей в синтаксисе. type Foo = impl Trait делает более ясным, что Foo — это конкретный, но безымянный конкретный тип, независимо от того, сколько раз вы его используете, а не псевдоним для признака, который может принимать другой конкретный тип. каждый раз, когда вы его используете.

Я думаю, полезно сказать людям, что они могут взять все, что знают о -> impl Trait и применить их к type Foo = impl Trait ; существует обобщенная концепция impl Trait которую они могут использовать в качестве строительного блока в обоих случаях. Такой синтаксис, как type Foo: Trait скрывает этот обобщенный строительный блок.

@джоштриплетт

Я предполагал, что это кажется неполным; это кажется неправильным для меня и для других.

Хорошо; Я предлагаю использовать здесь термин, отличный от incomplete потому что для меня он предполагает отсутствие информации.

Также обратите внимание, что в этой теме люди продемонстрировали проблему интерпретации именно с этой разницей в синтаксисе.

То, что я заметил, было ошибкой интерпретации, сделанной в треде, о том, что означает type Foo = impl Bar; . Один человек интерпретировал различное использование Foo как номинально не один и тот же тип, а скорее разные типы. То есть, именно: «псевдоним для признака, который может принимать другой конкретный тип каждый раз, когда вы его используете» .

Некоторые утверждают, что type Foo: Bar; сбивает с толку, но я не уверен, что альтернативная интерпретация type Foo: Bar; отличается от предполагаемого значения. Мне было бы интересно услышать об альтернативных интерпретациях.

@Центрил

Я повторю, что псевдонимы типов действительно можно рассматривать как связанные типы.

Они могут, но сейчас связанные типы связаны с чертами. impl Trait работает везде или почти везде. Если вы хотите представить impl Trait как ассоциированный тип, вам придется ввести сразу два понятия. То есть, вы видите impl Trait как возвращаемый тип функции, догадываетесь или читаете, что это такое, а затем, когда вы видите impl Trait в псевдониме типа, вы можете повторно использовать это знание.

Сравните это с просмотром связанных типов в определении трейта. В этом случае вы полагаете, что это то, что должны определять или реализовывать другие структуры. Но если вы столкнетесь с type Foo: Debug вне черты, вы не узнаете, что это такое. Его некому реализовать, так что это какая-то предварительная декларация? Имеет ли это какое-то отношение к наследованию, как в C++? Это похоже на модуль ML, где кто-то другой выбирает тип? И если вы уже видели impl Trait , между ними нет никакой связи. Мы пишем fn foo() -> impl ToString , а не fn foo(): ToString .

тип Foo = импл Бар; также имеет проблемы с понятностью в том, что вы видите = impl Bar и делаете вывод, что вы можете просто заменить его в каждом случае, когда он используется как -> impl Bar

Я говорил это здесь раньше, но это все равно, что думать, что let x = foo(); означает, что вы можете использовать x вместо foo() . В любом случае, это деталь, которую кто-то может быстро найти, когда это необходимо, но принципиально не меняет концепцию.

То есть легко понять, о чем идет речь (выведенный тип, как в -> impl Trait ), даже если вы не знаете точно, как это работает (что происходит, когда у вас есть конфликтующие определения для него). С другим синтаксисом трудно даже понять, что это такое.

@Центрил

Хорошо; Я предлагаю использовать здесь термин, отличный от неполного, потому что для меня он предполагает отсутствие информации.

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

type Foo: Trait; не выглядит как полное объявление. Похоже, чего-то не хватает. И кажется необоснованно отличным от type Foo = SomeType<X, Y, Z>; .

Возможно, мы приближаемся к тому моменту, когда наши однострочники сами по себе не могут преодолеть этот консенсусный разрыв между type Inferred: Trait и type Inferred = impl Trait .

Как вы думаете, стоит ли собрать экспериментальную реализацию этой функции с любым синтаксисом (даже указанным в RFC), чтобы мы могли начать экспериментировать с ней в более крупных программах и посмотреть, как она вписывается в контекст?

@lnicola

[..] impl Trait работает везде или почти

Ну и Foo: Bound тоже работает почти везде ;)

Но если вы столкнетесь с type Foo: Debug вне черты, вы не узнаете, что это такое.

Я думаю, что использование его в: trait -> impl -> type alias помогает в обучении.
Кроме того, я думаю, что вывод о том, что «тип Foo реализует Debug», вероятно, основан на
видеть type Foo: Debug в признаках и из общих границ, и это также правильно.

Имеет ли это какое-то отношение к наследованию, как в C++?

Я думаю, что об отсутствии наследования в Rust нужно узнать на гораздо более раннем этапе, чем при изучении обсуждаемой нами функции, поскольку это очень важно для Rust.

Это похоже на модуль ML, где кто-то другой выбирает тип?

Этот вывод также можно сделать для type Foo = impl Bar; из-за arg: impl Bar когда вызывающий (пользователь) выбирает тип. Мне вывод о том, что пользователь выбирает тип, кажется менее вероятным для type Foo: Bar; .

Я уже говорил это здесь раньше, но это все равно, что думать, что let x = foo(); означает, что вы можете использовать x вместо foo() .

Если язык ссылочно прозрачен, вы можете заменить x на foo() на type Foo = impl Foo; , псевдонимы типов фактически прозрачны с точки зрения ссылок. И наоборот, если привязка x = foo() доступна, другие foo() можно заменить на x .

@джоштриплетт

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

Справедливо; но что в нем должно быть такого, чего нет?

type Foo: Trait; не выглядит полным объявлением.

Мне кажется полным. Это похоже на суждение о том, что Foo удовлетворяет Trait что является точно предполагаемым значением.

@Centril для меня «чего-то не хватает» - это фактический тип, для которого это псевдоним. Это немного связано с моим замешательством ранее. Дело не в том, что такого типа нет, а в том, что этот тип анонимен... Использование = неявно подразумевает, что тип есть, и это всегда один и тот же тип, но мы не можем назвать его.

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

@mark-им

@Centril для меня «чего-то не хватает» - это фактический тип, для которого это псевдоним. Это немного связано с моим замешательством ранее. Дело не в том, что такого типа нет, а в том, что этот тип анонимен... Использование = неявно подразумевает, что тип есть, и это всегда один и тот же тип, но мы не можем назвать его.

Мне тоже так кажется.

Есть ли шанс решить два отложенных вопроса в ближайшее время, а также вопрос о пожизненном элизии? Сам бы сделал, но не знаю как!

По-прежнему много путаницы в отношении того, что именно означает impl Trait , и это совсем не очевидно. Я думаю, что отложенные элементы определенно должны подождать, пока у нас не будет четкого представления о точной семантике impl Trait (что должно произойти в ближайшее время).

@varkor Какая семантика неясна? Насколько я знаю, ничего в семантике impl Trait не изменилось со времен RFC 1951 и расширено в 2071 году.

@alexreg У меня не было никаких планов, но вот грубый план: после того, как синтаксический анализ был добавлен, вам нужно понизить типы static s и const s внутри экзистенциального внедрения. контекст типажей , как это сделано DefId в ImplTraitContext::Existential необязательным, поскольку вы не хотите, чтобы ваш impl Trait подбирал дженерики из определения родительской функции. Это должно дать вам приличный кусок пути. Возможно, вам будет легче, если вы будете опираться на экзистенциальный тип PR @oli-obk.

@cramertj : семантика impl Trait в языке полностью ограничена его использованием в сигнатурах функций, и неверно, что его расширение на другие позиции имеет очевидное значение. Я скажу об этом кое-что более подробное в ближайшее время, где, кажется, идет большая часть разговора.

@варкор

семантика impl Trait в языке полностью ограничена его использованием в сигнатурах функций, и неверно, что его расширение на другие позиции имеет очевидное значение.

Значение было указано в RFC 2071 .

@cramertj : значение в RFC 2071 неоднозначно и допускает несколько интерпретаций того, что означает фраза «экзистенциальный тип».

TL;DR — я попытался указать точное значение impl Trait , которое, как я думаю, проясняет детали, которые были, по крайней мере, интуитивно неясны; вместе с предложением нового синтаксиса псевдонима типа.

Экзистенциальные типы в Rust (пост)


В чате Discord rust-lang было много дискуссий о точной (т.е. формальной, теоретической) семантике impl Trait за последние пару дней. Я думаю, что было полезно прояснить множество деталей об этой функции и о том, что она собой представляет, а что нет. Это также проливает свет на то, какой синтаксис правдоподобен для псевдонимов типов.

Я написал небольшое резюме некоторых наших выводов. Это обеспечивает интерпретацию impl Trait которую я считаю довольно ясной, и точно описывает различия между позицией аргумента impl Trait и позицией возврата impl Trait (что не является «универсально количественное» против «экзистенциально-количественное»). Есть и практические выводы.

В нем я предлагаю новый синтаксис, отвечающий общеизвестным требованиям «псевдоним экзистенциального типа»:
type Foo: Bar = _;

Поскольку это такая сложная тема, нужно сначала прояснить довольно многое, поэтому я написал ее в виде отдельного поста. Обратная связь очень ценится!

Экзистенциальные типы в Rust (пост)

@варкор

RFC 2071 неоднозначен и допускает несколько интерпретаций того, что означает фраза «экзистенциальный тип».

Как это двусмысленно? Я прочитал ваш пост - я все еще знаю только одно значение нединамического экзистенциального в статике и константах. Он ведет себя так же, как и return position impl Trait , вводя новое определение экзистенциального типа для каждого элемента.

type Foo: Bar = _;

Мы обсуждали этот синтаксис в RFC 2071. Как я уже сказал, мне нравится, что он ясно демонстрирует, что Foo является единственным выводимым типом и что он оставляет место для невыводимых типов, которые остаются экзистенциальными за пределами текущего модуля ( например type Foo: Bar = u32; ). Мне не понравились два его аспекта: (1) у него нет ключевого слова, и поэтому его труднее искать, и (б) он имеет ту же проблему многословия по сравнению с type Foo = impl Trait что и синтаксис abstract type Foo: Bar; имеет: type Foo = impl Iterator<Item = impl Display>; становится type Foo: Iterator<Item = MyDisplay> = _; type MyDisplay: Display = _; . Я не думаю, что любой из них является нарушителем условий сделки, но это не явная победа, так или иначе, ИМО.

@cramertj Здесь возникает двусмысленность:

type Foo = impl Bar;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Если бы Foo действительно был псевдонимом для экзистенциального типа, то f и g поддерживали бы разные конкретные возвращаемые типы. Несколько люди инстинктивно прочитали , что синтаксис таким образом, и на самом деле некоторые участники обсуждения синтаксиса RFC 2071 только поняли , что это не так, как предложение работы в рамках недавнего обсуждения раздора.

Проблема в том, что, особенно перед лицом аргумента-позиции impl Trait , совсем не ясно, куда должен идти квантор существования. Для аргументов это узко ограничено; для ответной позиции он кажется узким, но оказывается шире; для type Foo = impl Bar правдоподобны обе позиции. Синтаксис на основе _ подталкивает к интерпретации, которая даже не включает «экзистенциальный», ловко обходя эту проблему.

Если Foo действительно типа псевдоним для экзистенциального типа

(выделено мной). Я прочитал, что 'an' как 'конкретный', что означает, что f и g _не_ поддерживают разные конкретные возвращаемые типы, поскольку они относятся к одному и тому же экзистенциальному типу. Я всегда видел, что type Foo = impl Bar; использует то же значение, что и let foo: impl Bar; , то есть представляет новый анонимный экзистенциальный тип; делая ваш пример эквивалентным

existential type _0: Bar;
type Foo = _0;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

который, я надеюсь, относительно однозначен.


Одна из проблем заключается в том, что значение « impl Trait в псевдонимах типов» никогда не указывалось в RFC. Это кратко упоминается в разделе «Альтернативы» RFC 2071 , но явно игнорируется из-за этой присущей двусмысленности обучения.

Мне также кажется, что я видел некоторые упоминания о том, что псевдонимы типов уже непрозрачны с точки зрения ссылок. Я думаю, что это было на url.o, но я не смог найти обсуждение после некоторых поисков.

@cramertj
Продолжая точку impl Trait , которые все согласуются с текущим использованием в подписях, но имеют разные последствия при расширении impl Trait на другие локаций (я знаю еще 2, кроме той, что описана в посте, но которые не совсем готовы к обсуждению). И я не думаю, что интерпретация в посте обязательно является самой очевидной (лично я не видел подобного объяснения APIT и RTIP с этой точки зрения).

Что касается type Foo: Bar = _; , я думаю, что его следует обсудить снова — нет ничего плохого в том, чтобы взглянуть на старые идеи свежим взглядом. Что касается ваших проблем с ним:
(1) У него нет ключевого слова, но везде используется тот же синтаксис, что и при выводе типа. Поиск документации для «подчеркивания»/«подчеркивания типа»/и т. д. может легко предоставить страницу с выводом типа.
(2) Да, это правда. Мы думали о решении этой проблемы, которое, как мне кажется, хорошо сочетается с нотацией подчеркивания, и, надеюсь, скоро будет готово предложить ее.

Как и @cramertj, я не вижу здесь аргумента.

Я просто не вижу фундаментальной двусмысленности, которую описывает пост говорится в сообщении impl Trait всегда была неясной и нуждалась в переосмыслении.

Неоднозначность области действия, которую описывает type Foo: Bar = _; похоже, решает проблему type Foo: Bar; связанную с необходимостью взрыва нескольких операторов для объявления любого слегка нетривиального экзистенциального объекта, но я не думаю, что этого достаточно, чтобы действительно изменить "бесконечная велосипедная" ситуация.

В чем я убежден, так это в том, что любой синтаксис, который мы получим, должен иметь ключевое слово, отличное от type , потому что все синтаксисы «просто type » слишком вводят в заблуждение. На самом деле, возможно, не используйте type в синтаксисе _вообще_, чтобы никто не мог предположить, что он смотрит на «псевдоним типа, но как-то более экзистенциальный».

existential Foo = impl Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }
existential Foo: Trait;
fn f() -> Foo { .. }
fn g() -> Foo { .. }



md5-b59626c5715ed89e0a93d9158c9c2535



existential Foo: Trait = _;
fn f() -> Foo { .. }
fn g() -> Foo { .. }

Для меня не очевидно, что любой из них полностью _предотвращает_ неверное истолкование того, что f и g могут возвращать два разных типа, реализующих Trait , но я подозреваю, что это так же близко к предотвращению, как и мы могли бы получить.

@Иксрек
Фраза «экзистенциальный тип» проблематична именно из-за неоднозначности области видимости. Я не видел, чтобы кто-то еще указывал на то, что область применения APIT и RPIT совершенно различна. Это означает, что такой синтаксис, как type Foo = impl Bar , где impl Bar является «экзистенциальным типом», по своей сути неоднозначен.

Да, терминология теории типов часто использовалась неправильно. Но в RFC это использовалось неправильно (или, по крайней мере, не объяснено), так что двусмысленность исходит из самого RFC.

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

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

В чем я убежден, так это в том, что любой синтаксис, который мы получим, должен иметь ключевое слово, отличное от type

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

В стороне: кстати, я думаю, что можно полностью отказаться от «экзистенциальных типов», прояснив всю ситуацию, что я или кто-то другой вскоре проследит.

Я ловлю себя на мысли, что синтаксис, отличный от type , также помог бы, именно потому, что многие люди интерпретируют type как простой заменяемый псевдоним, что подразумевало бы интерпретацию «потенциально другой тип каждый раз».

Я не видел, чтобы кто-то еще указывал на то, что область применения APIT и RPIT совершенно различна.

Я думал, что область видимости всегда была явной частью предложений impl Trait, поэтому на нее не нужно было «указывать». Все, что вы сказали об области видимости, похоже, просто повторяет то, что мы уже приняли в прошлых RFC. Я понимаю, что из синтаксиса это не очевидно для всех, и это проблема, но не то чтобы никто не понимал этого раньше. На самом деле, я думал, что большая часть обсуждения RFC 2701 была посвящена тому, какой должна быть область видимости type Foo = impl Trait; , в том смысле, какой вывод типа можно и на что нельзя смотреть.

Можно придумать согласованный синтаксис, в котором нет этой путаницы.

Вы пытаетесь сказать, что type Foo: Bar = _; — это синтаксис, или вы думаете, что мы его еще не нашли?

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

вы изобрели совершенно новую нотацию

Я думал, что просто заменил одно ключевое слово другим ключевым словом. Вы видите какие-то дополнительные изменения, которых я не планировал?

Если подумать, поскольку мы все это время неправильно использовали «экзистенциальный», это означает, что existential Foo: Trait / = impl Trait вероятно, больше не являются законными синтаксисами.

Итак, нам нужно новое ключевое слово, которое нужно поставить перед именами, которые относятся к какому-то типу, неизвестному внешнему коду... и я ничего не понимаю в этом. alias , secret , internal и т. д. кажутся довольно ужасными и вряд ли имеют меньше «путаницы с уникальностью», чем type .

Если подумать, поскольку мы все это время неправильно использовали «экзистенциальный», это означает, что existential Foo: Trait / = impl Trait вероятно, больше не являются законными синтаксисами.

Да, я полностью согласен — я думаю, что нам нужно полностью отказаться от термина «экзистенциальный»* (были некоторые предварительные идеи, как это сделать, при этом хорошо объясняя impl Trait ).

*(возможно резервирование срока только для dyn Trait )

@joshtriplett , @Ixrec : я согласен с тем, что нотация _ означает, что вы больше не можете заменять в той же степени, что и раньше, и если это является приоритетом, нам понадобится другой синтаксис.

Имейте в виду, что _ в любом случае уже является особым случаем в отношении подстановки — это влияет не только на псевдонимы типов: везде, где вы в настоящее время можете использовать _ , вы предотвращаете полную ссылочную прозрачность.

Имейте в виду, что _ в любом случае уже является особым случаем в отношении подстановки — это влияет не только на псевдонимы типов: везде, где вы в настоящее время можете использовать _, вы предотвращаете полную ссылочную прозрачность.

Не могли бы вы рассказать нам, что это означает? Я не знал о понятии «ссылочной прозрачности», на которое влияет _ .

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

Я не уверен, что это _приоритет_. Для меня это был единственный объективный аргумент, который мы когда-либо находили, который, казалось, предпочитал один синтаксис другому. Но все это может измениться в зависимости от того, какие ключевые слова мы сможем заменить type .

Не могли бы вы рассказать нам, что это означает? Я не знал о понятии «ссылочной прозрачности», на которое влияет _ .

Да уж, извините, бросаюсь словами, не объясняя их. Позвольте мне собраться с мыслями, и я сформулирую более связное объяснение. Это хорошо сочетается с альтернативным (и потенциально более полезным) способом взглянуть на impl Trait .

Под ссылочной прозрачностью понимается возможность замены ссылки на ее определение и наоборот без изменения семантики. В Rust это явно не выполняется на уровне терминов для fn . Например:

fn foo() -> usize {
    println!("ey!");
    42
}

fn main() {
    let bar = foo();
    let baz = bar + bar;
}

если мы заменим каждое вхождение bar на foo() (определение bar ), то мы явно получим другой результат.

Однако для псевдонимов типов в настоящее время сохраняется ссылочная прозрачность (AFAIK). Если у вас есть псевдоним:

type Foo = Definition;

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

Представляем:

type Foo = impl Bar;

означает, что каждое вхождение Foo относится к одному и тому же типу, означает, что если вы напишете:

fn stuff() -> Foo { .. }
fn other_stuff() -> Foo { .. }

вы не можете заменить вхождения Foo на impl Bar и наоборот. То есть, если вы пишете:

fn stuff() -> impl Bar { .. }
fn other_stuff() -> impl Bar { .. }

возвращаемые типы не будут унифицироваться с Foo . Таким образом, ссылочная прозрачность для псевдонимов типов нарушается введением impl Trait с семантикой RFC 2071 внутри них.

О ссылочной прозрачности и type Foo = _; , продолжение следует... (автор @varkor)

Я ловлю себя на мысли, что синтаксис, отличный от типа, также поможет, именно потому, что многие люди интерпретируют тип как простой заменяемый псевдоним, что подразумевало бы интерпретацию «потенциально другой тип каждый раз».

Хорошая точка зрения. Но разве бит присваивания = _ подразумевает, что это только один тип?

Я уже писал это раньше, но...

Что касается ссылочной прозрачности: я думаю, что полезнее рассматривать type как привязку (например, let ) вместо подстановки, подобной препроцессору C. Как только вы посмотрите на это таким образом, type Foo = impl Trait означает именно то, что кажется.

Я предполагаю, что новички с меньшей вероятностью будут думать о impl Trait как об экзистенциальных типах, а не как об универсальных типах, но как о «вещи, которая impl sa Trait . If they want to know more, they can read the включает документацию по Trait`ам. измените синтаксис, вы потеряете связь между ним и существующей функцией без особых преимуществ. _Вы всего лишь замените один потенциально вводящий в заблуждение синтаксис другим._

Что касается type Foo = _ , это перегружает _ совершенно не относящимся к делу значением. Это также может показаться сложным найти в документации и/или Google.

@lnicola С тем же успехом вы могли бы использовать привязки const вместо привязок let , где первая ссылочно прозрачна. Выбор let (который не является ссылочно прозрачным внутри fn ) является произвольным выбором, который я не считаю особенно интуитивным. Я думаю, что интуитивное представление о псевдонимах типов состоит в том, что они ссылочно прозрачны (даже если это слово не используется), потому что они псевдонимы .

Я также не рассматриваю type как замену препроцессора C, потому что он должен избегать захвата и уважать дженерики (без SFINAE). Вместо этого я думаю о type точно так же, как о привязке на таком языке, как Idris или Agda, где все привязки являются чистыми.

Я предполагаю, что новички с меньшей вероятностью будут думать о impl Trait как об экзистенциальных, а не об универсальных типах, а как о «вещи, которая включает в себя Черту».

Это кажется мне различием без разницы. Жаргон «экзистенциальный» не используется, но я полагаю, что пользователь интуитивно связывает его с той же концепцией, что и экзистенциальный тип (который является не чем иным, как «каким-то типом Foo, который подразумевает Bar» в контексте Rust).

Что касается type Foo = _ , это перегружает _ совершенно не относящимся к делу значением.

Как же так? type Foo = _; здесь совпадает с использованием _ в других контекстах, где ожидается тип.
Это означает «вывести реальный тип», как если бы вы написали .collect::<Vec<_>>() .

Это также может показаться сложным найти в документации и/или Google.

Не должно быть так сложно? Мы надеемся, что «тип подчеркивания псевдонима» приведет к желаемому результату ...?
Похоже, это не отличается от поиска «типа псевдонима импл черта».

Google не индексирует специальные символы. Если в моем вопросе StackOverflow есть символ подчеркивания, Google не будет автоматически индексировать его для запросов, содержащих слово подчеркивания.

@Центрил

Как же так? type Foo = _; здесь соответствует использованию _ в других контекстах, где ожидается тип.
Это означает «определить реальный тип», как если бы вы написали .collect::>().

Но эта функция не выводит тип и не дает вам псевдоним типа для него, она создает экзистенциальный тип, который (за пределами некоторой ограниченной области, такой как модуль или ящик) не унифицируется с «настоящим типом».

Google не индексирует специальные символы.

Это больше не так (хотя, возможно, зависит от пробелов..?).

Но эта функция не выводит тип и не дает вам псевдоним типа для него, она создает экзистенциальный тип, который (за пределами некоторой ограниченной области, такой как модуль или ящик) не унифицируется с «настоящим типом».

Предлагаемая семантика type Foo = _; является альтернативой псевдониму экзистенциального типа, полностью основанному на выводе. Если это было не совсем ясно, я собираюсь вскоре добавить что-то, что должно немного лучше объяснить намерения.

@iopq В дополнение к примечанию @varkor о недавних изменениях, я также хотел бы добавить, что для других поисковых систем всегда возможно, что официальная документация и тому подобное явно используют буквальное слово «подчеркивание» в сочетании с type , чтобы он стал доступным для поиска.

Вы все равно не получите хороших результатов с _ в своем запросе по какой-либо причине. Если вы ищете подчеркивание, вы получаете вещи со словом подчеркивание в них. Если вы ищете _, вы получаете все, что имеет подчеркивание, поэтому я даже не знаю, актуально ли это

@Центрил

Выбор let (который оказывается непрозрачным с точки зрения ссылок внутри fn) — это произвольный выбор, который я не считаю особенно интуитивным. Я думаю, что интуитивное представление о псевдонимах типов состоит в том, что они ссылочно прозрачны (даже если это слово не используется), потому что они псевдонимы.

Извините, я все еще не могу понять это, потому что моя интуиция полностью отстала.

Например, если у нас есть type Foo = Bar , моя интуиция говорит:
«Мы объявляем Foo , который становится того же типа, что и Bar ».

Затем, если мы напишем type Foo = impl Bar , моя интуиция говорит:
«Мы объявляем Foo , который становится типом, реализующим Bar ».

Если Foo - это просто текстовый псевдоним для impl Bar , то это было бы для меня очень неинтуитивно. Мне нравится думать об этом как о текстовых и семантических псевдонимах.

Таким образом, если Foo можно заменить на impl Bar любом месте, это текстовый псевдоним, который мне больше всего напоминает макросы и метапрограммирование. Но если Foo было присвоено значение в момент объявления и может использоваться в нескольких местах с этим исходным значением (не контекстуальным значением!), это семантический псевдоним.

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

Возможно, я нахожу ссылочную прозрачность неинтуитивной из-за моего опыта работы не с Haskell, кто знает... :) Но в любом случае, это определенно не то поведение, которое я ожидал бы от Rust.

@ Nemo157

Если Foo действительно типа псевдоним для экзистенциального типа

(выделено мной). Я прочитал, что «an» как «конкретный», что означает, что f и g не будут поддерживать разные конкретные типы возврата, поскольку они относятся к одному и тому же экзистенциальному типу.

Это неправильное использование термина «экзистенциальный тип» или, по крайней мере, способ, который противоречит сообщению @varkor . type Foo = impl Bar может показаться, что Foo является псевдонимом для типа ∃ T. T: Trait и если вы замените ∃ T. T: Trait везде, где вы используете Foo , даже не -textually , вы можете получить другой конкретный тип в каждой позиции.

Область действия этого квантификатора ∃ T (выраженного в вашем примере как existential type _0 ) является рассматриваемой вещью. В APIT все обстоит так: вызывающая сторона может передать любое значение, удовлетворяющее ∃ T. T: Trait . Но этого нет ни в RPIT, ни в объявлениях existential type RFC 2071, ни в вашем примере с дешугаризацией — там квантификатор находится дальше, на уровне всей функции или всего модуля, и вы имеете дело с одни и те же T везде.

Таким образом, двусмысленность - у нас уже есть impl Trait размещающий свой квантификатор в разных местах в зависимости от его положения, так что же нам ожидать для type T = impl Trait ? Некоторые неофициальные опросы, а также некоторые осознания постфактум участников ветки RFC 2071 доказывают, что так или иначе неясно.

Вот почему мы хотим отойти от интерпретации impl Trait как чего-то общего с экзистенциалами и вместо этого описать его семантику в терминах вывода типов. type T = _ не имеет такой двусмысленности — все еще поверхностный уровень «невозможно скопировать и вставить _ вместо T », но больше нет « один тип , псевдонимом которого является T может означать несколько конкретных типов». (Непрозрачное/не унифицирующее поведение — это то, о чем говорит @varkor .)

ссылочная прозрачность

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

Например, элемент const ссылочно прозрачен (упоминается в https://github.com/rust-lang/rust/issues/34511#issuecomment-402520768), и это фактически вызвало путаницу между новыми и старыми пользователей (rust-lang-nursery/rust-clippy#1560).

Поэтому я думаю, что для программиста на Rust ссылочная прозрачность — это не первое, о чем он может подумать.

@stjepang @kennytm Я не говорю, что все будут ожидать, что псевдонимы типов с type Foo = impl Trait; будут действовать ссылочно прозрачным образом. Но я думаю, что нетривиальное количество пользователей будет, о чем свидетельствует путаница в этой теме и в других местах (на что ссылается @rpjohnst ...). Это проблема, но, возможно, не непреодолимая. Это то, что нужно иметь в виду, когда мы движемся вперед.

Мои текущие мысли о том, что следует делать в этом вопросе, изменились в соответствии с

re: ссылочная прозрачность

type Foo<T> = (T, T);

type Bar = Foo<impl Copy>;   // not equivalent to (impl Copy, impl Copy)

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

@centril Я поднимаю руку, когда дело доходит до ожидания ссылочной прозрачности для Foo в type Foo = impl Bar; . Однако с type Foo: Bar = _; я бы не ожидал ссылочной прозрачности.

Также возможно, что мы могли бы расширить return-position impl Trait для поддержки нескольких типов без какого-либо механизма, подобного enum impl Trait , путем мономорфизации (частей) вызывающего объекта . Это усиливает интерпретацию « impl Trait всегда экзистенциально», приближает ее к dyn Trait и предлагает синтаксис abstract type , который не использует impl Trait вообще.

Я написал это здесь: https://internals.rust-lang.org/t/extending-impl-trait-to-allow-multiple-return-types/7921.

Просто примечание, когда мы стабилизируем новые экзистенциальные типы — «экзистенциальный» всегда предназначался как временное ключевое слово (согласно RFC) и (IMO) ужасно. Мы должны придумать что-то получше, прежде чем стабилизироваться.

Разговоры об «экзистенциальных» типах, похоже, ничего не проясняют. Я бы сказал, что impl Trait обозначает конкретный предполагаемый тип, реализующий Trait. Описанное таким образом, type Foo = impl Bar явно является специфическим, всегда одним и тем же типом — и это также единственная действительно полезная интерпретация: поэтому его можно использовать в других контекстах, помимо того, из которого он был выведен, как в структурах.

В этом смысле имеет смысл также писать impl Trait как _ : Trait .

@rpjohnst ,

Также возможно, что мы могли бы расширить return-position impl Trait для поддержки нескольких типов.

Это сделало бы его строго менее полезным ИМО. Смысл псевдонимов для типов impl заключается в том, что функция может быть определена как возвращающая impl Foo , но конкретный тип все еще распространяется через программу в другие структуры и прочее. Это сработало бы, если бы компилятор неявно сгенерировал подходящий enum , но не с мономорфизацией.

@jan-hudec Эти идеи обсуждались в Discord, и есть некоторые проблемы, в основном связанные с тем фактом, что текущая интерпретация позиции возврата и позиции аргумента impl Trait несовместима.

Сделать impl Trait обозначением конкретного выводимого типа — хороший вариант, но для исправления этого несоответствия необходимо, чтобы это был другой вид вывода типа, чем сегодня в Rust — он должен выводить полиморфные типы, чтобы он мог сохранять текущее поведение аргумента-позиции impl Trait . Это, наверное, самый простой способ, но он не так прост, как вы говорите.

Например, когда impl Trait означает «использовать этот новый тип вывода, чтобы найти полиморфный тип, который реализует Trait », type Foo = impl Bar начинает подразумевать что-то о модулях. Правила RFC 2071 о том, как вывести abstract type говорят, что все использования должны независимо выводить один и тот же тип, но этот полиморфный вывод, по крайней мере, подразумевал бы чуть больше жизни, гораздо более правдоподобной идею), был бы вопросы вокруг этого взаимодействия.

Есть также тот факт, что некоторые люди всегда будут интерпретировать синтаксис type Foo = impl Bar как псевдоним для экзистенциального, независимо от того, понимают ли они слово «экзистенциальный» и независимо от того, как мы его преподаем. Таким образом, выбор альтернативного синтаксиса, даже если он работает с интерпретацией, основанной на выводах, вероятно, все же является хорошей идеей.

Кроме того, хотя синтаксис _: Trait самом деле вдохновил дискуссию вокруг интерпретации на основе логического вывода, он не делает того, что нам нужно. Во-первых, вывод, подразумеваемый _ , не является полиморфным, так что это плохая аналогия с остальной частью языка. Во-вторых, _ подразумевает, что фактический тип виден где-то еще, в то время как impl Trait специально разработан для того, чтобы скрыть фактический тип.

Наконец, причина, по которой я написал это предложение мономорфизации, заключалась в том, чтобы найти другой способ унифицировать значение аргумента и возвращаемой позиции impl Trait . И хотя да, это означает, что -> impl Trait больше не гарантирует один конкретный тип, но в любом случае в настоящее время у нас нет способа воспользоваться этим. И все предлагаемые решения являются раздражающими обходными путями — дополнительные шаблонные приемы abstract type , typeof и т. д. Заставляя всех, кто хочет полагаться на поведение одного типа, также называть этот единственный тип через abstract type Синтаксис

Эти идеи обсуждались в Discord, и есть некоторые проблемы, в основном связанные с тем фактом, что текущая интерпретация позиции возврата и позиции аргумента impl Trait несовместима.

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

У меня есть функция, в которой вызывающая сторона определяет тип возвращаемого значения. Конечно, я не могу использовать там impl Trait. Это не так интуитивно, как вы предполагаете, пока не поймете разницу.

Лично я не считаю это несоответствие проблемой на практике.

Верно. Это говорит мне не о том, что мы должны игнорировать несоответствие, а о том, что мы должны заново объяснить дизайн, чтобы он был непротиворечивым (например, объяснив его как вывод полиморфного типа). Таким образом, расширение в будущем (RFC 2071 и т.д.) может быть проверено на новую, последовательную интерпретацию , чтобы предотвратить вещи стать запутанным.

@rpjohnst

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

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

@mikeyhew Вы меня неправильно поняли - он отлично работает для замыканий или других безымянных типов, потому что я говорю о придумывании имени с помощью синтаксиса RFC 2071 abstract type . Вы должны придумать имя независимо от того, собираетесь ли вы использовать один тип где-либо еще.

@rpjohnst о, понятно, спасибо за уточнение

Ждем let x: impl Trait с нетерпением.

В качестве еще одного голоса за let x: impl Trait он упростит некоторые из примеров futures , вот пример примера , в настоящее время он использует функцию только для того, чтобы получить возможность использовать impl Trait :

fn make_sink_async() -> impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> { // ... }

вместо этого это можно было бы записать как обычную привязку let:

let future_sink: impl Future<Output = Result<
    impl Sink<SinkItem = T, SinkError = E>,
    E,
>> = // ...;

Я могу наставлять кого-то через реализацию let x: impl Trait если это необходимо. Это не невозможно трудно сделать, но определенно и не легко. Точка входа:

Аналогично тому, как мы посещаем тип возвращаемого значения impl Trait в https://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159, нам нужно посетить тип местных жителей в https. ://github.com/rust-lang/rust/blob/master/src/librustc/hir/lowering.rs#L3159 и убедитесь, что их вновь созданные экзистенциальные элементы возвращаются вместе с локальными.

Затем при посещении типа местных жителей обязательно установите для ExistentialContext значение Return чтобы включить его.

Это уже должно завести нас очень далеко. Не уверен, что полностью, это не на 100% похоже на черту реализации позиции возврата, но в основном должно вести себя так.

@rpjohnst ,

Эти идеи обсуждались в Discord, и есть некоторые проблемы, в основном связанные с тем фактом, что текущая интерпретация атрибута return-position и arguments-position impl несовместима.

Возвращает нас к областям, о которых вы говорили в своей статье. И я думаю, что они на самом деле соответствуют закрывающим «круглым скобкам»: для позиции аргумента это список аргументов, для позиции возврата — это функция, а для псевдонима — область действия, в которой псевдоним определен.

Я открыл RFC, предлагающий решение конкретного синтаксиса existential type , основанное на обсуждении в этой ветке, исходном RFC и синхронных обсуждениях: https://github.com/rust-lang/rfcs/pull /2515.

Текущая реализация экзистенциального типа не может использоваться для представления всех определений текущей позиции возврата impl Trait , поскольку impl Trait фиксирует каждый аргумент универсального типа, даже если он не используется, то же самое должно быть возможно сделать с existential type , но вы получаете предупреждения о неиспользуемом параметре типа: (игровая площадка)

fn foo<T>(_: T) -> impl ::std::fmt::Display {
    5
}

existential type Bar<T>: ::std::fmt::Display;
fn bar<T>(_: T) -> Bar<T> {
    5
}

Это может иметь значение, поскольку параметры типа могут иметь внутреннее время жизни, которое ограничивает время жизни возвращенного impl Trait даже если само значение не используется, удалите <T> из Bar на игровой площадке. выше, чтобы увидеть, что вызов foo не работает, но bar работает.

Текущая реализация экзистенциального типа не может использоваться для представления всех определений трейтов реализации текущей позиции возврата.

можно, просто очень неудобно. Вы можете вернуть новый тип с полем PhantomData + фактическим полем данных и реализовать черту как пересылку в фактическое поле данных.

@oli-obk Спасибо за дополнительный совет. С вашим предыдущим советом и некоторыми советами от @cramertj я, вероятно, мог бы попробовать это в ближайшее время.

@fasihrana @Nemo157 См. выше. Может быть, через несколько недель! :-)

Может ли кто-нибудь пояснить, что поведение existential type без неявного захвата параметров типа (упомянутое @Nemo157 ) является преднамеренным и останется таким, как есть? Мне это нравится, потому что решает #42940

Я реализовал это так специально

@Arnavion Да, это

Взаимодействие между existential_type и never_type уже обсуждалось?

Может быть, ! должен быть в состоянии заполнить любой экзистенциальный тип независимо от задействованных признаков.

existential type Mystery : TraitThatIsHardToEvenStartImplementing;

fn hack_to_make_it_compile() -> Mystery { unimplemented!() }

Или должен быть какой-то особый неприкасаемый тип, выступающий в качестве уровня типов unimplemented!() , способный автоматически удовлетворять любому экзистенциальному типу?

@vi Я думаю, что это подпадает под общее правило «никогда тип не должен реализовывать все черты без каких-либо нестандартных несамостоятельных методов или связанных типов». Хотя я не знаю, где это будет отслеживаться.

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

existential type уже работает для типовых методов. Что касается impl Trait , это вообще входит в RFC?

@alexreg Я считаю, что для этого требуется, чтобы GAT могли преобразовать сахар в анонимный связанный тип, когда у вас есть что-то вроде fn foo<T>(..) -> impl Bar<T> (становится примерно -> Self::AnonBar0<T> ).

@Centril , ты хотел сделать там <T> на impl Bar ? Неявное поведение захвата типа impl Trait означает, что вы получаете ту же потребность в GAT даже с чем-то вроде fn foo<T>(self, t: T) -> impl Bar; .

@ Nemo157, нет, извини, я этого не сделал. Но ваш пример еще лучше иллюстрирует проблему. Спасибо :)

@alexreg Я считаю, что для этого требуется, чтобы GAT могли преобразовать сахар в анонимный связанный тип, когда у вас есть что-то вроде fn foo(..) -> импл Бар(становится примерно -> Self::AnonBar0).

Ах я вижу. Честно говоря, это не кажется строго необходимым, но это, безусловно, один из способов его реализации. Однако отсутствие движения на GAT меня немного беспокоит ... давно ничего не было слышно.

Triage: https://github.com/rust-lang/rust/pull/53542 был объединен, поэтому, я думаю, можно поставить галочки для {let,const,static} foo: impl Trait .

Смогу ли я когда-нибудь написать:

trait Foo {
    fn GetABar() -> impl Bar;
}

??

Возможно нет. Но есть текущие планы по подготовке всего, чтобы мы могли получить

trait Foo {
    type Assoc: Bar;
    fn get_a_bar() -> Assoc;
}

impl Foo for SomeType {
    fn get_a_bar() -> impl Bar {
        SomeThingImplingBar
    }
}

Вы можете поэкспериментировать с этой функцией по ночам в виде

impl Foo for SomeType {
    existential type Assoc;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBar
    }
}

Хорошим началом для получения дополнительной информации об этом является https://github.com/rust-lang/rfcs/pull/2071 (и все, что связано с ним)

@oli-obk на rustc 1.32.0-nightly (00e03ee57 2018-11-22) , мне также нужно указать границы черты для existential type чтобы он работал в таком блоке impl . Это ожидается?

@jonhoo возможность указать черты полезна, потому что вы можете предоставить больше, чем просто необходимые черты

impl Foo for SomeDebuggableType {
    existential type Assoc: Bar + Debug;
    fn get_a_bar() -> Assoc {
        SomeThingImplingBarAndDebug
    }
}

fn use_debuggable_foo<F>(f: F) where F: Foo, F::Assoc: Debug {
    println!("bar is: {:?}", f.get_a_bar())
}

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

@ Nemo157 Nemo157 Ах, извините, я имел в виду, что в настоящее время у вас _должны_ быть ограничения. То есть это не скомпилируется:

impl A for B {
    existential type Assoc;
    // ...
}

тогда как это будет:

impl A for B {
    existential type Assoc: Debug;
    // ...
}

О, так что даже в случае, когда трейт не требует границ связанного типа, вы все равно должны дать границу экзистенциальному типу (который может быть пустым) ( игровая площадка ):

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Мне это кажется граничным случаем: наличие безграничного экзистенциального типа означает, что он не предоставляет пользователям _никаких_ операций (кроме авто-черт), так для чего же его можно использовать?

Также следует отметить, что нет способа сделать то же самое с -> impl Trait , -> impl () является синтаксической ошибкой, а -> impl по себе дает error: at least one trait must be specified ; если синтаксис экзистенциального типа становится type Assoc = impl Debug; или подобным, то кажется, что не было бы способа указать связанный тип без хотя бы одной привязки типажа.

@Nemo157 Nemo157 да, я понял только потому, что попробовал буквально код, который вы предложили выше, и он не сработал: p Я как бы предполагал, что он выведет границы из признака. Например:

trait Foo {
    type Assoc: Future<Output = u32>;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc;
}

Казалось разумным не указывать Future<Output = u32> во второй раз, но это не работает. Я предполагаю, что existential type Assoc: ; (который также кажется супер странным синтаксисом) тоже не сделает такой вывод?

trait Foo {
    type Assoc;
    fn foo() -> Self::Assoc;
}

struct Bar;
impl Foo for Bar {
    existential type Assoc: ;
    fn foo() -> Self::Assoc { Bar }
}

Мне это кажется граничным случаем: наличие безграничного экзистенциального типа означает, что он не предоставляет пользователям _никаких_ операций (кроме авто-черт), так для чего же его можно использовать?

Нельзя ли их использовать для потребления в той же реализации трейта? Что-то вроде этого:

trait Foo {
    type Assoc;
    fn create_constructor() -> Self::Assoc;
    fn consume(marker: Self::Assoc) -> Self;
    fn consume_box(marker: Self::Assoc) -> Box<Foo>;
}

Это немного надумано, но может быть полезно — я мог представить ситуацию, когда какая-то предварительная часть должна быть построена перед реальной структурой по причинам времени жизни. Или это может быть что-то вроде:

trait MarkupSystem {
    type Cache;
    fn create_cache() -> Cache;
    fn translate(cache: &mut Self::Cache, input: &str) -> String;
}

В обоих этих случаях existential type Assoc; было бы полезно.

Каков правильный способ определения связанных типов для impl Trait?

Например, если у меня есть трейт Action и я хочу убедиться, что реализация ассоциированного типа трейта доступна для отправки, могу ли я сделать что-то вроде этого:

pub trait Action {
    type Result;
    fn call(&self) -> Self::Result;
}

impl MyStruct {
    pub fn new(name: String) -> impl Action 
    where 
        Return::Result: Send //This Return should be the `impl Action`
    {
        ActionImplementation::new()
    }
}

Что-то это в настоящее время невозможно?

@acyclozebra Я думаю, что синтаксис для этого -> impl Action<Result = impl Send> - это тот же синтаксис, что и, например, -> impl Iterator<Item = u32> только с использованием другого анонимного типа impl Trait .

Было ли обсуждение расширения синтаксиса impl Trait на такие вещи, как поля структуры? Например, если я реализую оболочку для определенного типа итератора для моего открытого интерфейса:

struct Iter<'a> {
    inner: std::collections::hash_map::Iter<'a, i32, i32>,
}

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

Однако IIRC, я не думаю, что на данный момент есть способ указать несколько границ с impl Trait , поэтому я потерял бы некоторые полезные вещи, такие как Clone .

@AGausmann Последнее обсуждение этой темы находится на https://github.com/rust-lang/rfcs/pull/2515. Это позволит вам сказать type Foo = impl Bar; struct Baz { field: Foo } ... . Я думаю, мы можем рассмотреть field: impl Trait как сахар для этого после стабилизации type Foo = impl Bar; . Это действительно похоже на разумное удобное расширение для макросов.

@Центрил ,

Я думаю, мы можем рассматривать field: impl Trait как сахар.

Я не думаю, что это было бы разумно. Поле структуры по-прежнему должно иметь конкретный тип, поэтому вы должны сообщить компилятору, к какому возврату функции оно привязано. Он может сделать вывод, но если у вас есть несколько функций, то будет не так просто найти, какая именно — и обычная политика Rust в таких случаях заключается в том, чтобы быть явным.

Он мог бы вывести это, но если у вас есть несколько функций, будет не так просто найти, какая из них

Вы бы подняли требование об определении использования родительского типа. Тогда это будут все те функции в одном модуле, которые возвращают родительский тип. Мне не кажется, что это так сложно найти. Однако я думаю, что мы хотим урегулировать историю на type Foo = impl Bar; прежде чем переходить к расширениям.

Я думаю, что нашел ошибку в текущей реализации existential type .


Код

trait Collection {
    type Element;
}
impl<T> Collection for Vec<T> {
    type Element = T;
}

existential type Existential<T>: Collection<Element = T>;

fn return_existential<I>(iter: I) -> Existential<I::Item>
where
    I: IntoIterator,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}


Ошибка

error: type parameter `I` is part of concrete type but not used in parameter list for existential type
  --> src/lib.rs:16:1
   |
16 | / {
17 | |     let item = iter.into_iter().next().unwrap();
18 | |     vec![item]
19 | | }
   | |_^

error: defining existential type use does not fully define existential type
  --> src/lib.rs:12:1
   |
12 | / fn return_existential<I>(iter: I) -> Existential<I::Item>
13 | | where
14 | |     I: IntoIterator,
15 | |     I::Item: Collection,
...  |
18 | |     vec![item]
19 | | }
   | |_^

error: could not find defining uses
  --> src/lib.rs:10:1
   |
10 | existential type Existential<T>: Collection<Element = T>;
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

игровая площадка

Вы также можете найти это на stackoverflow .

Я не уверен на 100%, что мы можем поддерживать этот случай из коробки, но вы можете переписать функцию, чтобы она имела два общих параметра:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b4e53972e35af8fb40ffa9a735c6f6b1

fn return_existential<I, J>(iter: I) -> Existential<J>
where
    I: IntoIterator<Item = J>,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Спасибо!
Да, это то, что я сделал, как указано в сообщении stackoverflow:

fn return_existential<I, T>(iter: I) -> Existential<T>
where
    I: IntoIterator<Item = T>,
    I::Item: Collection,
{
    let item = iter.into_iter().next().unwrap();
    vec![item]
}

Планируется ли доступность impl Trait в контексте трейта?
Не только как связанный тип, но и как возвращаемое значение в методах.

impl в типажах — это функция, отдельная от тех, которые отслеживаются здесь, и в настоящее время не имеет RFC. Существует довольно длинная история разработок в этой области, и дальнейшая итерация откладывается до тех пор, пока не будет стабилизирована реализация 2071 (экзистенциальный тип), которая заблокирована из-за проблем с реализацией, а также нерешенного синтаксиса (для которого есть отдельный RFC).

@cramertj Синтаксис почти решен. Я считаю, что сейчас основным блокировщиком является GAT.

@alexreg : https://github.com/rust-lang/rfcs/pull/2515 все еще ждет @withoutboats.

@varkor Да, я просто настроен оптимистично, они скоро увидят свет с этим RFC. ;-)

Возможно ли что-то вроде следующего?

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{
    let mut s = MyStruct {};
    cb(&mut s)
}

Вы можете сделать это сейчас, но только с помощью функции hint чтобы указать конкретный тип Interface

#![feature(existential_type)]

trait MyTrait {}

existential type Interface: MyTrait;

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: FnOnce(&mut Interface) -> U
{

    fn hint(x: &mut MyStruct) -> &mut Interface { x }

    let mut s = MyStruct {};
    cb(hint(&mut s))
}

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

@CryZe То, что вы ищете, не имеет отношения к impl Trait . См. https://github.com/rust-lang/rfcs/issues/2413, чтобы узнать все, что я об этом знаю.

Потенциально это будет выглядеть примерно так:

trait MyTrait {}

struct MyStruct {}
impl MyTrait for MyStruct {}

fn with<F, U>(cb: F) -> U
where
    F: for<I: Interface> FnOnce(&mut I) -> U
{
    let mut s = MyStruct {};
    cb(hint(&mut s))
}

@KrishnaSannasi Ах, интересно. Спасибо!

Это должно работать?

#![feature(existential_type)]

trait MyTrait {
    type AssocType: Send;
    fn ret(&self) -> Self::AssocType;
}

impl MyTrait for () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

impl<'a> MyTrait for &'a () {
    existential type AssocType: Send;
    fn ret(&self) -> Self::AssocType {
        ()
    }
}

trait MyLifetimeTrait<'a> {
    type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType;
}

impl<'a> MyLifetimeTrait<'a> for &'a () {
    existential type AssocType: Send + 'a;
    fn ret(&self) -> Self::AssocType {
        *self
    }
}

Должны ли мы сохранять ключевое слово existential в языке для функции existential_type ?

@jethrogb Да. Тот факт, что в настоящее время это не так, является ошибкой.

@cramertj Хорошо. Нужно ли мне создать отдельную тему для этого или достаточно моего поста здесь?

Сообщить о проблеме было бы здорово, спасибо! :)

Должны ли мы сохранять ключевое слово existential в языке для функции existential_type ?

Я думаю, что намерение состоит в том, чтобы немедленно объявить это устаревшим, когда будет реализована функция type-alias-impl-trait (т. е. добавить lint) и в конечном итоге удалить ее из синтаксиса.

Хотя может кто прояснит.

Закрытие этого в пользу мета-вопроса, который более широко отслеживает impl Trait : https://github.com/rust-lang/rust/issues/63066

нигде нет ни одного хорошего примера того, как использовать impl Trait, очень грустно

Была ли эта страница полезной?
0 / 5 - 0 рейтинги