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

Здравствуйте: я недавно изучал 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 преобразование.

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