Как использовать макросы Scala для создания новых частичных функций или их преобразования?

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

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

Я могу сделать это с простым типом PartialFunction[Int,Any] но не с типами, которые включают в себя case-классы, PartialFunction[MyCaseClass,Any],

Вот код, который работает, и код, который не работает.

Рабочий код: возьмите частичную функцию, деструктурируйте ее с помощью квази-кавычек, снова соберите ту же функцию и верните ее.

package sample

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

object MacroTest {
  type Simple = PartialFunction[Int, Any]

  def no_problem(pf: Simple): Simple = macro no_problemImpl
  def no_problemImpl(c: blackbox.Context)(pf: c.Expr[Simple]) = {
    import c.universe._

    val q"{ case $binder  => $body }" = pf.tree
    q"{ case $binder  => $body }"
  }
}

Этот макрос компилируется и проходит тестирование:

import MacroTest._

val testPf: Simple = { case x => x + 1 }
testPf(2) shouldEqual 3 // passes

  // now do the same with macro:
val result = no_problem({ case x => x + 1 })
result(2) shouldEqual 3 // passes

Неработающий код: точно такой же макрос за исключением использования класса case вместо Int в качестве аргумента частичной функции.

case class Blob(left: Int, right: Int)

type Complicated = PartialFunction[Blob, Any]

def problem(pf: Complicated): Complicated = macro problemImpl
def problemImpl(c: blackbox.Context)(pf: c.Expr[Complicated]) = {
    import c.universe._

    val q"{ case $binder  => $body }" = pf.tree
    q"{ case $binder  => $body }"
}

Код точно такой же, отличается только тип (Complicated вместо Simple).

Макрос-код компилируется, но тест не компилируется (происходит сбой при расширении макроса):

val blob = Blob(1,2)
val testPf: Complicated = { case Blob(x, y) => x + y }
testPf(blob) shouldEqual 3 // passes

  // now try the same with macro:
val result = problem({ case Blob(x, y) => x + y })
  // compile error when compiling the test code: 
Could not find proxy for case val x1: sample.Blob in List(value x1, method applyOrElse, <$anon: Function1>, value result, method apply, <$anon: Function0>, value <local MacroTestSpec>, class MacroTestSpec, package sample, package <root>) (currentOwner= value y )

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

Я не могу решить эту проблему, несмотря на то, что попробовал много альтернативных способов преобразования частичной функции:

  • Использовать контекст макроса whitebox
  • Используйте простые квазицитаты, как в приведенных выше примерах
  • Используйте специальные квазицитаты для случаев и моделей cq"...", pq"..." а также q"{case ..$cases}" как показано в документации для квазицитат
  • Соответствие с охраной: q"{case $binder if $guard => $body }"также с cq а также pq quasiquotes
  • Добавление c.typecheck или же c.untypecheck в разных местах (это раньше называли resetAllAttrs, который сейчас устарел)
  • Не используйте квазицитаты, но делайте все в необработанных синтаксических деревьях: используйте Traverser с сопоставлением необработанного дерева, такого как case UnApply(Apply(Select(t@Ident(TermName(_)), TermName("unapply")), List(Ident(TermName("<unapply-selector>")))), List(binder)) if t.tpe <:< typeOf[Blob] и так далее
  • Попробуй заменить Identв шаблоне соответствия Identвзято из состояния охраны и наоборот (это приводит к странным ошибкам, "утверждение не выполнено" из-за неудачной проверки типов)
  • использование Any вместо конкретных типов, возвращая PartialFunction[Any,Any]или общая функция Function1[Blob,Any] и так далее
  • Используйте параметр типа T вместо Blob, параметризация макроса и принятие PartialFunction[T,Any]

Буду признателен за любую помощь! Я использую Scala 2.11.8 с прямыми макросами (без "макро-рая").

1 ответ

Решение

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

Проблема с макросами def и проверкой типов

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

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

обходные

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

  1. Хакерское решение - сделать круговое представление через строковое представление конечного результата:

    c.parse(showCode(q"{ case $binder  => $body }"))
    

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

  2. Трудноерешение - вручную проверить тип кода с помощью внутренних API компилятора. Я не могу объяснить, как это сделать, в одном посте, но вам придется узнать все о типах, символах и их владельцах. Хуже всего то, что деревья являются изменяемой информацией типа. Если вы идете по этому пути, я рекомендую просмотреть исходный код scala / async.

Лучше всего избегать написания макросов или ждать, пока семантический API scala.meta выпущен, и вы можете использовать его для макросов def.

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