Являются ли HLists не более чем сложным способом написания кортежей?

Мне действительно интересно узнать, где различия и, в более общем плане, определить канонические варианты использования, когда HLists нельзя использовать (точнее, не приносить никаких преимуществ по сравнению с обычными списками).

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

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

Мотивация

Недавно я увидел пару ответов на SO, где люди предлагали использовать HLists (например, как это предусмотрено Shapeless), включая удаленный ответ на этот вопрос. Это вызвало эту дискуссию, что, в свою очередь, вызвало этот вопрос.

Введение

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

HLists против кортежей

Если это верно, т.е. вы статически знаете число и тип - Вопрос 2:, почему бы просто не использовать n-кортеж? Несомненно, вы можете набросать карты и свернуть их поверх HList (которые вы также можете использовать, но не), сделать по кортежу с помощью productIterator), но поскольку число и тип элементов Статически известно, что вы, вероятно, можете просто напрямую обращаться к элементам кортежа и выполнять операции.

С другой стороны, если функция f, отображаемая по hlist, является настолько общей, что принимает все элементы - Вопрос 3:, почему бы не использовать ее через productIterator.map? Хорошо, одна интересная разница может возникнуть в результате перегрузки метода: если у нас было несколько перегруженных f, то более сильная информация о типе, предоставленная hlist (в отличие от productIterator), могла позволить компилятору выбрать более конкретный f, Однако я не уверен, действительно ли это работает в Scala, поскольку методы и функции не совпадают.

HLists и пользовательский ввод

Основываясь на том же предположении, а именно, что вам нужно знать число и типы элементов статически - Вопрос 4: могут использоваться hlists в ситуациях, когда элементы зависят от любого взаимодействия пользователя? Например, представьте, что вы заполняете hlist элементами внутри цикла; элементы читаются где-то (пользовательский интерфейс, файл конфигурации, взаимодействие с актером, сеть) до тех пор, пока не будет выполнено определенное условие. Каким будет тип hlist? Аналогично для спецификации интерфейса getElements: HList [...], которая должна работать со списками статически неизвестной длины и которая позволяет компоненту A в системе получить такой список произвольных элементов из компонента B.

Ответ 1

Решение вопросов от одного до трех: одно из основных приложений для HLists - абстрагирование по arity. Arity обычно статически известна на любом данном участке использования абстракции, но варьируется от сайта к сайту. Возьмите это из бесформенного examples,

def flatten[T <: Product, L <: HList](t : T)
  (implicit hl : HListerAux[T, L], flatten : Flatten[L]) : flatten.Out =
    flatten(hl(t))

val t1 = (1, ((2, 3), 4))
val f1 = flatten(t1)     // Inferred type is Int :: Int :: Int :: Int :: HNil
val l1 = f1.toList       // Inferred type is List[Int]

val t2 = (23, ((true, 2.0, "foo"), "bar"), (13, false))
val f2 = flatten(t2)
val t2b = f2.tupled
// Inferred type of t2b is (Int, Boolean, Double, String, String, Int, Boolean)

Без использования HLists (или что-то эквивалентного) абстракции над артерией аргументов кортежа flatten невозможно было бы иметь одну реализацию, которая могла бы принимать аргументы этих двух очень разных форм и преобразовывать их в безопасный тип.

Возможность абстрагироваться от arity, вероятно, будет интересна везде, где задействованы фиксированные arities: а также кортежи, как указано выше, включая списки параметров метода/функции и классы case. См. здесь для примеров того, как мы можем абстрагироваться от арности произвольных классов case, чтобы получить экземпляры класса типа почти автоматически,

// A pair of arbitrary case classes
case class Foo(i : Int, s : String)
case class Bar(b : Boolean, s : String, d : Double)

// Publish their `HListIso`'s
implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _)

// And now they're monoids ...

implicitly[Monoid[Foo]]
val f = Foo(13, "foo") |+| Foo(23, "bar")
assert(f == Foo(36, "foobar"))

implicitly[Monoid[Bar]]
val b = Bar(true, "foo", 1.0) |+| Bar(false, "bar", 3.0)
assert(b == Bar(true, "foobar", 4.0))

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

В третьем вопросе вы спрашиваете: "... если функция f, отображаемая над hlist, является настолько общей, что она принимает все элементы... почему бы не использовать ее через productIterator.map?". Если функция, которую вы наводите на HList, действительно имеет вид Any => T, то отображение через productIterator будет служить вам отлично. Но функции вида Any => T обычно не так интересны (по крайней мере, они не относятся, если они не печатают внутри себя). shapeless обеспечивает форму значения полиморфной функции, которая позволяет компилятору выбирать типы конкретных случаев точно так, как вы сомневаетесь. Например,

// size is a function from values of arbitrary type to a 'size' which is
// defined via type specific cases
object size extends Poly1 {
  implicit def default[T] = at[T](t => 1)
  implicit def caseString = at[String](_.length)
  implicit def caseList[T] = at[List[T]](_.length)
}

scala> val l = 23 :: "foo" :: List('a', 'b') :: true :: HNil
l: Int :: String :: List[Char] :: Boolean :: HNil =
  23 :: foo :: List(a, b) :: true :: HNil

scala> (l map size).toList
res1: List[Int] = List(1, 3, 2, 1)

В отношении вашего вопроса четыре, о вводе пользователя, необходимо рассмотреть два случая. Во-первых, это ситуации, когда мы можем динамически установить контекст, который гарантирует получение известного статического условия. В этих сценариях вполне можно применять бесформенные методы, но ясно, что если статическое условие не будет достигнуто во время выполнения, мы должны следовать альтернативному пути. Неудивительно, что это означает, что методы, чувствительные к динамическим условиям, должны давать необязательные результаты. Здесь пример с использованием HList s,

trait Fruit
case class Apple() extends Fruit
case class Pear() extends Fruit

type FFFF = Fruit :: Fruit :: Fruit :: Fruit :: HNil
type APAP = Apple :: Pear :: Apple :: Pear :: HNil

val a : Apple = Apple()
val p : Pear = Pear()

val l = List(a, p, a, p) // Inferred type is List[Fruit]

Тип l не отображает длину списка или точные типы его элементов. Однако, если мы ожидаем, что она будет иметь конкретную форму (т.е. Если она должна соответствовать известной, фиксированной схеме), то мы можем попытаться установить этот факт и действовать соответственно,

scala> import Traversables._
import Traversables._

scala> val apap = l.toHList[Apple :: Pear :: Apple :: Pear :: HNil]
res0: Option[Apple :: Pear :: Apple :: Pear :: HNil] =
  Some(Apple() :: Pear() :: Apple() :: Pear() :: HNil)

scala> apap.map(_.tail.head)
res1: Option[Pear] = Some(Pear())

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

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

val t1 : (Any, Any) = (23, "foo") // Specific element types erased
val t2 : (Any, Any) = (true, 2.0) // Specific element types erased

// Type class instances selected on static type at runtime!
val c1 = stagedConsumeTuple(t1) // Uses intString instance
assert(c1 == "23foo")

val c2 = stagedConsumeTuple(t2) // Uses booleanDouble instance
assert(c2 == "+2.0")

Я уверен, что @PLT_Borat будет иметь что сказать об этом, учитывая его комментарий sage о зависимых языках программирования на английском языке; -)

Ответ 2

Чтобы быть понятным, HList - это не что иное, как стопка Tuple2 с немного другим сахаром сверху.

def hcons[A,B](head : A, tail : B) = (a,b)
def hnil = Unit

hcons("foo", hcons(3, hnil)) : (String, (Int, Unit))

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

Ответ 3

Есть много вещей, которые вы не можете сделать (ну) с кортежами:

  • написать общую функцию prepend/append
  • написать обратную функцию
  • написать функцию concat
  • ...

Вы можете все это сделать с кортежами, конечно, но не в общем случае. Поэтому использование HLists делает ваш код более сухим.

Ответ 4

Я могу объяснить это на простом языке:

Имена наименований в наборе и именах несущественны. HLists можно назвать HTuples. Разница в том, что в Scala + Haskell вы можете сделать это с помощью кортежа (используя синтаксис Scala):

def append2[A,B,C](in: (A,B), v: C) : (A,B,C) = (in._1, in._2, v)

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

То, что HList Haskest позволяет вам делать, это сделать эту общую длину, поэтому вы можете добавить любую длину кортежа/списка и вернуть полностью статически типизированный кортеж/список. Это преимущество также относится к однородно типизированным коллекциям, где вы можете добавить int в список точно n int и вернуть список, который статически типизирован, чтобы иметь точно (n + 1) ints без явного указания n.