Подтвердить что ты не робот

Экстрактор для бесформенного HList, который имитирует конкатенацию парсера

Вопрос

Как-то можно создать экстрактор для бесформенного HList, который выглядит следующим образом.

val a ~ _ ~ b = 4 :: "so" :: 4.5 :: HNil
=> a == 4 && b == 4.5
  • Замените :: на ~, что не должно быть проблемой.
  • Избавьтесь от завершающего HNil. Существуют ли какие-либо проблемы, которые могут возникнуть?

Мотивация

После большого пота и слез мне удалось добраться до точки, где работает следующий код:

for(
  x1 :: _ :: x2 :: HNil <- (expInt ~ "+" ~ expInt).llE
) yield (x1 + x2)

expInt анализирует a Int в некоторой монаде E. Тип (expInt ~ "+" ~ expInt).llE - E[Int :: String :: Int :: HNil].

Я хочу, чтобы шаблон слева от <- каким-то образом напоминал конструкцию анализатора комбинаторов справа.

4b9b3361

Ответ 1

Это можно сделать и имеет пару интересных поворотов.

Во-первых, для сопоставления структуры, построенной с помощью правильного ассоциативного конструктора (т.е. ::), вы должны использовать соответствующий правый ассоциативный экстрактор, иначе вы бы разложили и привязали извлеченные элементы в обратном порядке. К сожалению, правые ассоциативные экстракторы должны, как и правые ассоциативные операторы, заканчивать с : в Scala, и это будет сжимать синтаксис комбинатора синтаксического анализатора, поскольку имя экстрактора должно быть ~: вместо простого ~. Тем не менее, я откладываю это на данный момент и работаю с правильной ассоциативностью.

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

Если мы сопоставляем список из более чем двух элементов, нам нужно разложить список на пару, состоящую из головы и хвоста HList, т.е. учитывая l: H :: T, где T <: HList мы должны дать значение типа (H, T). Если, с другой стороны, мы сопоставляем список ровно двух элементов, т.е. формы E1 :: E2 :: HNil, нам нужно разложить список на пару, состоящую только из двух элементов, т.е. (E1, E2), а не головы и хвоста, которая была бы (E1, E2 :: HNil).

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

import shapeless._

trait UnapplyRight[L <: HList] extends DepFn1[L]

trait LPUnapplyRight {
  type Aux[L <: HList, Out0] = UnapplyRight[L] { type Out = Out0 }
  implicit def unapplyHCons[H, T <: HList]: Aux[H :: T, Option[(H, T)]] =
    new UnapplyRight[H :: T] {
      type Out = Option[(H, T)]
      def apply(l: H :: T): Out = Option((l.head, l.tail))
    }
}

object UnapplyRight extends LPUnapplyRight {
  implicit def unapplyPair[H1, H2]: Aux[H1 :: H2 :: HNil, Option[(H1, H2)]] =
    new UnapplyRight[H1 :: H2 :: HNil] {
      type Out = Option[(H1, H2)]
      def apply(l: H1 :: H2 :: HNil): Out = Option((l.head, l.tail.head))
    }
}

Затем мы определяем наш экстрактор через него так,

object ~: {
  def unapply[L <: HList, Out](l: L)
    (implicit ua: UnapplyRight.Aux[L, Out]): Out = ua(l)
}

И тогда мы будем рады,

val l = 23 :: "foo" :: true :: HNil

val a ~: b ~: c = l
a : Int
b : String
c : Boolean

До сих пор так хорошо. Теперь вернемся к проблеме ассоциативности. Если мы хотим добиться такого же эффекта с помощью левого ассоциативного экстрактора (т.е. ~ вместо ~:), нам нужно будет изменить способ декомпозиции. Сначала позвольте desugar использовать правильный синтаксис ассоциативного экстрактора, который мы только что использовали. Выражение

val a ~: b ~: c = l

эквивалентно,

val ~:(a, ~:(b, c)) = l

В противоположность этому левая ассоциативная версия

val a ~ b ~ c = l

эквивалентно,

val ~(~(a, b), c) = l

Чтобы сделать эту работу экстрактором для HLists, наш класс типа unapply должен очистить элементы от конца, а не от начала списка. Мы можем сделать это с помощью бесформенных классов типов Init и Last,

trait UnapplyLeft[L <: HList] extends DepFn1[L]

trait LPUnapplyLeft {
  import ops.hlist.{ Init, Last }
  type Aux[L <: HList, Out0] = UnapplyLeft[L] { type Out = Out0 }
  implicit def unapplyHCons[L <: HList, I <: HList, F]
    (implicit
      init: Init.Aux[L, I],
      last: Last.Aux[L, F]): Aux[L, Option[(I, F)]] =
    new UnapplyLeft[L] {
      type Out = Option[(I, F)]
      def apply(l: L): Out = Option((l.init, l.last))
    }
}

object UnapplyLeft extends LPUnapplyLeft {
  implicit def unapplyPair[H1, H2]: Aux[H1 :: H2 :: HNil, Option[(H1, H2)]] =
    new UnapplyLeft[H1 :: H2 :: HNil] {
      type Out = Option[(H1, H2)]
      def apply(l: H1 :: H2 :: HNil): Out = Option((l.head, l.tail.head))
    }
}

object ~ {
  def unapply[L <: HList, Out](l: L)
    (implicit ua: UnapplyLeft.Aux[L, Out]): Out = ua(l)
}

И теперь мы закончили,

val a ~ b ~ c = l
a : Int
b : String
c : Boolean