Две, казалось бы, идентичные семантики: одна неявно связывается, другая нет
Здравствуйте: я недавно изучал Scala (мой опыт работы в основном с C++ шаблонами), и я столкнулся с тем, чего в настоящее время я не понимаю в Scala, и это сводит меня с ума.:(
(Кроме того, это мой первый пост в Stackru, где я заметил, что большинство действительно потрясающих людей из Scala зависают, поэтому мне очень жаль, если я сделаю что-то ужасно глупое с механизмом.)
Моя конкретная путаница связана с неявным связыванием аргументов: я выдвинул конкретный случай, когда неявный аргумент отказывается связываться, но функция с, казалось бы, идентичной семантикой делает.
Конечно, это может быть ошибка компилятора, но, учитывая, что я только начал работать со Scala, вероятность того, что я уже натолкнулся на какую-то серьезную ошибку, достаточно мала, и я ожидаю, что кто-то объяснит, что я сделал неправильно.;П
Я просмотрел код и довольно немного урезал его, чтобы привести единственный пример, который не работает. К сожалению, этот пример все еще достаточно сложен, так как проблема, кажется, возникает только в обобщении.:(
1) упрощенный код, который не работает так, как я ожидал
import HList.::
trait HApplyOps {
implicit def runNil
(input :HNil)
(context :Object)
:HNil
= {
HNil()
}
implicit def runAll[Input <:HList, Output <:HList]
(input :Int::Input)
(context :Object)
(implicit run :Input=>Object=>Output)
:Int::Output
= {
HCons(0, run(input.tail)(context))
}
def runAny[Input <:HList, Output <:HList]
(input :Input)
(context :Object)
(implicit run :Input=>Object=>Output)
:Output
= {
run(input)(context)
}
}
sealed trait HList
final case class HCons[Head, Tail <:HList]
(head :Head, tail :Tail)
extends HList
{
def ::[Value](value :Value) = HCons(value, this)
}
final case class HNil()
extends HList
{
def ::[Value](value :Value) = HCons(value, this)
}
object HList extends HApplyOps {
type ::[Head, Tail <:HList] = HCons[Head, Tail]
}
class Test {
def main(args :Array[String]) {
HList.runAny( HNil())(null) // yay! ;P
HList.runAny(0::HNil())(null) // fail :(
}
}
Этот код, скомпилированный с Scala 2.9.0.1, возвращает следующую ошибку:
broken1.scala:53: error: No implicit view available from HCons[Int,HNil] => (java.lang.Object) => Output.
HList.runAny(0::HNil())(null)
Я ожидаю, что в этом случае runAll
будет связан с неявным run
аргумент runAny
,
Теперь, если я изменю runAll
так что вместо того, чтобы принимать два своих аргумента напрямую, он вместо этого возвращает функцию, которая в свою очередь принимает эти два аргумента (трюк, который я подумал попробовать, как я видел в чужом коде), он работает:
2) измененный код, который имеет такое же поведение во время выполнения и фактически работает
implicit def runAll[Input <:HList, Output <:HList]
(implicit run :Input=>Object=>Output)
:Int::Input=>Object=>Int::Output
= {
input =>
context =>
HCons(0, run(input.tail)(context))
}
По сути, мой вопрос: почему это работает?;(Я ожидаю, что эти две функции имеют одинаковую общую сигнатуру типа:
1: [Input <:HList, Output <:HList] (Int::Input)(Object):Int::Output
2: [Input <:Hlist, Output <:HList] :Int::Input=>Object=>Int::Output
Если это помогает понять проблему, некоторые другие изменения также "работают" (хотя они изменяют семантику функции и, следовательно, не являются пригодными для использования решениями):
3) жесткое кодирование runAll
только для второго уровня, заменив Output с HNil
implicit def runAll[Input <:HList, Output <:HList]
(input :Int::Input)
(context :Object)
(implicit run :Input=>Object=>HNil)
:Int::HNil
= {
HCons(0, run(input.tail)(context))
}
4) удаление аргумента контекста из неявных функций
trait HApplyOps {
implicit def runNil
(input :HNil)
:HNil
= {
HNil()
}
implicit def runAll[Input <:HList, Output <:HList]
(input :Int::Input)
(implicit run :Input=>Output)
:Int::Output
= {
HCons(0, run(input.tail))
}
def runAny[Input <:HList, Output <:HList]
(input :Input)
(context :Object)
(implicit run :Input=>Output)
:Output
= {
run(input)
}
}
Любое объяснение, которое кто-либо может иметь для этого, будет высоко ценится:(
(В настоящее время мое лучшее предположение состоит в том, что порядок неявного аргумента по отношению к другим аргументам является ключевым фактором, который я упускаю, но который меня смущает: runAny
имеет неявный аргумент в конце, так что очевидное implicit def
плохо работает с трейлингом implicit
"не имеет смысла для меня.)
2 ответа
(Примечание: это краткое изложение обсуждения, которое, возможно, проходило более подробно в разделе комментариев другого ответа на этот вопрос.)
Оказывается, проблема здесь в том, что implicit
параметр не первый в runAny
, но не потому, что механизм неявного связывания игнорирует его: вместо этого проблема заключается в том, что параметр типа Output
не связан ни с чем, и должен быть косвенно выведен из типа run
неявный параметр, который происходит "слишком поздно".
По сути, код для "неопределенных параметров типа" (что к чему Output
в этом случае) используется только в ситуациях, когда рассматриваемый метод считается "неявным", что определяется его прямым списком параметров: в этом случае runAny
Список параметров на самом деле просто (input :Input)
и не является "неявным".
Итак, параметр типа для Input
умудряется работать Int::HNil
), но Output
просто установлен на Nothing
, который "прилипает" и вызывает тип run
аргумент быть Int::HNil=>Object=>Nothing
, который не удовлетворяется runNil
, вызывая runAny
вывод типа, чтобы потерпеть неудачу, дисквалифицировать его для использования в качестве неявного аргумента runAll
,
Путем реорганизации параметров, как это сделано в моем модифицированном примере кода № 2, мы делаем runAny
сам по себе "неявный", что позволяет ему сначала полностью определить параметры своего типа перед применением оставшихся аргументов: это происходит потому, что его неявный аргумент сначала будет привязан к runNil
(или же runAny
снова для более чем двух уровней), чей тип возврата будет взят / связан.
Чтобы связать свободные концы: причина того, что пример кода № 3 работал в этой ситуации, заключается в том, что Output
параметр даже не требовался: тот факт, что он был связан с Nothing
не повлияло на любые последующие попытки связать его с чем-либо или использовать его для чего-либо, и runNil
был легко выбран, чтобы привязать к своей версии run
неявный параметр.
Наконец, пример кода № 4 был на самом деле вырожденным, и его даже не следовало считать "работающим" (я только проверил, что он скомпилирован, а не генерировал соответствующий вывод): типы данных его неявных параметров были настолько упрощенными (Input=>Output
, где Input
а также Output
были на самом деле предназначены для того же типа), что это будет просто связано с conforms:<:<[Input,Output]
: функция, которая, в свою очередь, действовала как личность в этих обстоятельствах.
(Для получения дополнительной информации по случаю № 4, посмотрите на этот явно мертвый вопрос: проблема с неявной неоднозначностью между моим методом и соответствиями в Predef.)
Когда вы объявляете implicit def
как это:
implicit def makeStr(i: Int): String = i.toString
тогда компилятор может автоматически создать неявный Function
объект из этого определения для вас, и вставит его там, где неявный тип Int => String
ожидается. Это то, что происходит в вашей линии HList.runAny(HNil())(null)
,
Но когда вы определяете implicit def
ы, которые сами принимают неявные параметры (например, ваши runAll
метод), он больше не работает, так как компилятор не может создать Function
объект которого apply
Метод потребует неявного - гораздо меньше гарантий, что такой неявный будет доступен на сайте вызова.
Решение этой проблемы состоит в том, чтобы определить что-то вроде этого, а не runAll
:
implicit def runAllFct[Input <: HList, Output <: HList]
(implicit run: Input => Object => Output):
Int :: Input => Object => Int :: Output =
{ input: Int :: Input =>
context: Object =>
HCons(0, run(input.tail)(context))
}
Это определение немного более явное, так как компилятору теперь не нужно пытаться создать Function
объект из вашего def
, но вместо этого позвоню вашему def
непосредственно, чтобы получить необходимый объект функции. И, вызывая его, автоматически вставит необходимый неявный параметр, который он может разрешить сразу.
На мой взгляд, всякий раз, когда вы ожидаете неявные функции этого типа, вы должны предоставить implicit def
это действительно возвращает Function
объект. (Другие пользователи могут не согласиться... кто-нибудь?) Тот факт, что компилятор может создавать Function
обертки вокруг implicit def
в основном, я полагаю, для поддержки неявных преобразований с более естественным синтаксисом, например, использование границ вида вместе с простым implicit def
как мой первый Int
в String
преобразование.