Сложный вывод типа Scala с лямбда-выражениями

#scala #type-inference #lambda

#scala #вывод типа #лямбда — выражение #лямбда

Вопрос:

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

В моей библиотеке у меня есть признак Effect[-T], который используется для представления временных модификаторов, которые могут быть применены к объекту типа T. У меня есть объект myEffects, у которого есть метод с именем =, который принимает аргумент типа Effect[PlayerCharacter] . Наконец, у меня есть общий метод when[T], который используется для построения условных эффектов путем принятия условного выражения и другого эффекта в качестве аргумента. Подпись выглядит следующим образом:

 def when[T](condition : T => Boolean) (effect : Effect[T]) : Effect[T]
  

Когда я вызываю метод «when» с вышеуказанной сигнатурой, передавая его результат методу =, он не может определить тип аргумента для лямбда-выражения.

 myEffects  = when(_.hpLow()) (applyModifierEffect) //<-- Compiler error
  

Если я объединю аргументы «when» в один список параметров, Scala сможет легко определить тип лямбда-выражения.

 def when[T](condition : T => Boolean, effect : Effect[T]) : Effect[T]

/* Snip */

myEffects  = when(_.hpLow(), applyModifierEffect) //This works fine!
  

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

 def when[T](condition : T => Boolean) : Effect[T]

/* Snip */

myEffects  = when(_.hpLow()) //This works too!
  

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

Насколько я понимаю из раздела 16.10 «Программирование на Scala«, компилятор сначала проверяет, известен ли тип метода, и если да, то он использует это для вывода ожидаемого типа его аргументов. В этом случае самым внешним вызовом метода является =, который принимает аргумент типа Effect[PlayerCharacter]. Поскольку возвращаемый тип when[T] является Effect[T] , а метод, которому передается результат, ожидает аргумент типа Effect[PlayerCharacter] , он может сделать вывод, что T является PlayerCharacter , и, следовательно, тип лямбда-выражения передается в качестве первого аргумента «when»является ли PlayerCharacter => логическим. Похоже, именно так это работает, когда аргументы предоставляются в одном списке параметров, так почему же разбиение аргументов на два списка параметров нарушает его?

Комментарии:

1. pchiusano.blogspot.com/2011/05/…

Ответ №1:

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

Я думаю, разница в том, что компилятору трудно доказать самому себе, что два T s одинаковы в condition : T => Boolean и effect : Effect[T] в версии с двумя параметрами. список.

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

Итак, в этом случае:

 def when[T](condition : T => Boolean, effect : Effect[T]) : Effect[T]

/* Snip */

myEffects  = when(_.hpLow(), applyModifierEffect) //This works fine!
  

Тип applyModifierEffect и требуемый тип параметра myEffects = могут помочь ограничить тип параметра _.hpLow() ; все T s должны быть PlayerCharacter . Но в следующем:

 myEffects  = when(_.hpLow()) (applyModifierEffect)
  

Компилятор должен определить тип when(_.hpLow()) независимо, чтобы он мог проверить, допустимо ли его применять applyModifierEffect . И сам по себе, _.hpLow() не предоставляет достаточно информации для компилятора, чтобы сделать вывод, что это так when[PlayerCharacter](_.hpLow()) , поэтому он не знает, что возвращаемый тип является функцией типа Effect[PlayerCharacter] => Effect[PlayerCharacter] , поэтому он не знает, что допустимо применять эту функцию в этом контексте. Я предполагаю, что вывод типа просто не соединяет точки и не выясняет, что существует ровно один тип, который позволяет избежать ошибки типа.

А что касается другого случая, который работает:

 def when[T](condition : T => Boolean) : Effect[T]

/* Snip */

myEffects  = when(_.hpLow()) //This works too!
  

Здесь возвращаемый тип when и его тип параметра связаны более напрямую, без прохождения через тип параметра и возвращаемый тип дополнительной функции, созданной с помощью currying . Поскольку myEffects = требуется an Effect[PlayerCharacter] , T должно быть PlayerCharacter , у которого есть hpLow метод, и компилятор завершен.

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

Комментарии:

1. Возможно, вы правы в этом. На самом деле я не думаю, что функции с несколькими списками параметров являются действительно каррированными, потому что при вызове вам нужно использовать символ подчеркивания вместо любых отсутствующих списков аргументов, тогда как если у вас есть функция, которая явно каррирована, например «def curried(x : Int) = (y : Int) => x y», вам не нужно использовать символы подчеркивания. Но, возможно, моя ментальная модель того, как Scala обрабатывает эти типы функций, нуждается в некотором пересмотре.

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

Ответ №2:

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

Вывод типа выполняется слева направо от одного списка параметров (не параметров) к следующему. Типичным примером является метод foldLeft в коллекциях (скажем, Seq[A])

 def foldLeft[B] (z: B)(op: (B, A) => B): B
  

Тип z будет делать B известным, поэтому op может быть записан без указания B (ни A, который известен с самого начала, введите параметр this).
Если процедура была записана как

 def foldLeft[B](z: B, op: (B,A) => B): B
  

или как

 def foldLeft[B](op: (B,A) => B)(z: B): B
  

это не сработало бы, и нужно было бы убедиться, что тип операции понятен, или передать B эксплицитность при вызове foldLeft .

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

 Effects  = applyModifierEffect when (_.hpLow())
  

Как вы упомянули, этот эффект контравариантен, when сигнатура не разрешена для метода Effect (из T => Boolean -за того, что функция контравариантна в своем первом параметре типа, а условие отображается как параметр, поэтому в контравариантной позиции два контраварианта образуют ковариант), но это все равно можно сделать с неявным

 object Effect {
  implicit def withWhen[T](e: Effect[T]) 
    = new {def when(condition: T => Boolean) = ...}
}
  

Комментарии:

1. Если foldLeft бы они были записаны как def foldLeft[B](z: B, op: (B,A) => B): B , разве это не сработало бы, потому что передача значения для z будет привязана B к его типу? Или тип всего в списке параметров должен быть выводимым независимо от других объектов в том же списке?

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

3. Вы правы в том, что, когда параметр типа a выводится из его аргументов, используется только первый список аргументов. Я считаю, что причина, по которой он может выводить параметр типа «when», заключается в том, что его результат передается методу, который ожидает аргумент типа Effect[PlayerCharacter], и поэтому ожидаемый тип «when» — Effect [PlayerCharacter], и поэтому тип T долженбыть персонажем игрока (или, возможно, каким-то более широким типом). Если я просто вызываю «когда» сам по себе, ни один из примеров, которые я привел, не работает.

4. Спасибо за разъяснение, я немного волновался. В качестве второстепенной детали для вывода аргумента типа используется не только список параметров: def appOn[A,B](a: A)(f: A => B) = f(a) нормально, вы можете написать appOn(3)(_ 1) иB будет выведен из второго списка. Но тот факт, что A является Int , который выводится из первого списка, не будет доступен до второго списка, где это необходимо, если мы хотим, чтобы выводился тип аргумента функции