Списки Scala с экзистенциальными типами: `map {case t => ...}` работает, `map {t => ...}` нет?

Предположим, мы определили экзистенциальный тип:

type T = (X => X, X) forSome { type X }

а затем определил список типа List[T]:

val list = List[T](
  ((x: Int) => x * x, 42),
  ((_: String).toUpperCase, "foo")
)

Хорошо известно [1], [2], что следующая попытка map не работает:

list.map{ x => x._1(x._2) }

Но тогда почему работает следующее ?:

list.map{ case x => x._1(x._2) }

Обратите внимание, что ответы на оба связанных вопроса предполагают, что при сопоставлении с образцом требуется переменная типа, но она также работает без переменной типа. Акцент вопроса больше на Почему работает { case x => ... }?.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
0
126
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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


Во-первых, заметьте, что

list.map{ x => x._1(x._2) }
list.map{ case x => x._1(x._2) }

по сути то же самое, что

list map f1
list map f2

с участием

val f1: T => Any = t => t._1(t._2)
val f2: T => Any = _ match {
  case q => q._1(q._2)
}

Действительно, компиляция f1 завершается неудачно, тогда как f2 завершается успешно.

Мы можем понять, почему компиляция f1 терпит неудачу:

  1. t относится к типу (X => X, X) forSome { type X }.
  2. Следовательно, предполагается, что первый компонент t._1 имеет тип (X => X) forSome { type X }.
  3. Аналогичным образом предполагается, что второй компонент t._2 имеет тип X forSome { type X }, то есть просто Any.
  4. Мы не можем применить (X => X) forSome { type X } к Any, потому что на самом деле он может оказаться (SuperSpecialType => SuperSpecialType) для некоторого SuperSpecialType.

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


Чтобы понять, почему f2 успешно компилируется, можно посмотреть результат проверки типов. Если мы сохраним это как someFile.scala:

class O {
  type T = (X => X, X) forSome { type X }

  def f2: T => Any = t => t match {
    case q => q._1(q._2)
  }

  def f2_explicit_func_arg: T => Any = t => t match {
    case q => {
      val f = q._1
      val x = q._2
      f(x)
    }
  }
}

а затем сгенерируйте вывод проверки типов с помощью

$ scalac -Xprint:typer someFile.scala 

мы получаем по существу (с удалением некоторого шума):

class O extends scala.AnyRef {
  type T = (X => X, X) forSome { type X };
  def f2: O.this.T => Any = ((t: O.this.T) => t match {
    case (q @ _) => q._1.apply(q._2)
  });
  def f2_explicit_func_arg: O.this.T => Any = ((t: O.this.T) => t match {
    case (q @ _) => {
      val f: X => X = q._1;
      val x: X = q._2;
      f.apply(x)
    }
  })
}

Вторая версия f2_explicit_func_arg (эквивалент f2) более информативна, чем более короткая оригинальная версия f2. В коде f2_explicit_func_arg без сахара и с проверкой типа мы видим, что тип X чудесным образом появляется снова, и проверка типов действительно делает вывод:

f: X => X
x: X

так что f(x) действительно действителен.


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

Мы также могли бы написать:

type TypeCons[X] = (X => X, X)
list.map{ case t: TypeCons[x] => t._1(t._2) }

или даже более явно:

list.map{ case t: TypeCons[x] => {
  val func: x => x = t._1
  val arg: x = t._2
  func(arg)
}}

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

Мне действительно жаль, что я не понял Зачем, это то, что сопоставление с образцом делает это так, как не делает вывод обычного типа. Я знаю, что обычная вспомогательная функция может делать то же самое: def helper[S](q: (S => S, S)) = q._1(q._2). Я где-то видел обсуждение этого вопроса, но не могу вспомнить где.

Joe Pallas 09.04.2018 04:48

@JoePallas Я не уверен, как здесь должна помочь вспомогательная функция, по крайней мере, list map helper не работает. Я также не уверен, на каком уровне вы хотели бы получить объяснение «почему»: я надеюсь, что мне удалось прояснить, почему list.map{x => ...} не должен работать на любом вообразимом языке. Аргумент, запрещающий list.map{x => ...}, разваливается, как только вводится дополнительный этап сопоставления с образцом, поэтому нет никаких «теоретических» причин, по которым он не должен работать. Действительно, на практике Scala удается прикрепить больше информации о типе к согласованной переменной.

Andrey Tyukin 09.04.2018 13:38

@JoePallas [... продолжение ...] "Почему Scala удается вывести всевозможные полезные ограничения на типы переменных, используемых при сопоставлении с образцом?" Я не знаю, как на него ответить, потому что 1) я недостаточно знаю о внутреннем устройстве Scala сопоставления с образцом или вывода типа 2) я не знаю, как далеко мне нужно разложить объяснение «почему» до тех пор, пока кто-то другой принимает все крошечные под-аргументы как должное, как аксиомы.

Andrey Tyukin 09.04.2018 14:07

@AndreyTyukin Попробуй list.map(helper(_)) или list.map(t => helper(t)).

Alexey Romanov 09.04.2018 14:17

@AlexeyRomanov Да, все работает, спасибо! Комментарий под ваш ответ здесь говорит именно об этом. Но, похоже, это работает по другой причине, чем сопоставление с образцом: вы применяете универсально количественную функцию к экзистенциально количественно определяемому аргументу, что полностью имеет смысл. Есть ли какая-то общая тема между универсальной вспомогательной функцией и сопоставлением с образцом, которая помогает лучше объяснить «тип X чудесным образом снова появляется»?

Andrey Tyukin 09.04.2018 14:33

Я помню, что была некоторая информация об этом в блоге Typelevel, но я не помню, где именно. typelevel.org/blog/2015/07/13/type-members-parameters.html должен быть отправной точкой.

Alexey Romanov 09.04.2018 15:04

@AlexeyRomanov Я читал статью (по крайней мере, большую ее) несколько раз, боюсь, что она слишком далека от конкретной темы вопроса и, вероятно, не даст ответа для "Почему?" это менее расплывчато, чем «Scala имеет мощную систему типов, она может выводить много всего». Думаю, я найду утешение в цитате «Scala достаточно велика, чтобы очень немногие ее понимали» и приму свои собственные каракули в качестве ответа. Приму повторно, если кто-то предоставит примерно такой же подробный ответ и внесет то, чего я не знал.

Andrey Tyukin 09.04.2018 15:31

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

Alexey Romanov 09.04.2018 15:50

@AlexeyRomanov Да, я об этом и думал. Он находится в второй пост серии. «Когда мы вызываем вариант с параметризацией типа для реализации экзистенциального варианта… мы просто помогаем компилятору… и scalac, и javac умудряются сделать вывод, что тип T должен быть (иначе невыразимым) экзистенциальным».

Joe Pallas 10.04.2018 00:13

Другие вопросы по теме