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

144

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

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

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

мотивация

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

вступление

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

HLists vs. Tuples

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

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

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

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

Малте Шверхофф
источник

Ответы:

144

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

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было бы невозможно иметь единственную реализацию, которая могла бы принимать аргументы этих двух очень разных форм и преобразовывать их безопасным для типов способом.

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

// 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, которую вы отображаете через список, настолько универсальна, что она принимает все элементы ... почему бы не использовать ее через 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)

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

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())

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

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

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 будет что сказать по этому поводу, учитывая его мудрые комментарии о зависимо типизированных языках программирования ;-)

Майлз Сабин
источник
2
Я немного озадачен последней частью вашего ответа - но также очень заинтригован! Спасибо за ваш великолепный ответ и за многочисленные ссылки. Похоже, мне нужно много читать :-)
Malte Schwerhoff
1
Абстрагирование над арностью чрезвычайно полезно. ScalaMock, к сожалению, страдает от значительного дублирования , потому что различные FunctionNчерты не знаю , как абстрагироваться над арностью: github.com/paulbutcher/ScalaMock/blob/develop/core/src/main/... github.com/paulbutcher/ScalaMock/blob / разработки / ядро / SRC / главная / ... к сожалению , я не знаю ни одного способа , который можно использовать бесформенные , чтобы избежать этого, учитывая , что мне нужно иметь дело с «реальным» FunctionNs
Paul Butcher
1
Я составил (довольно искусственный) пример - ideone.com/sxIw1 - который соответствует первому вопросу. Может ли это извлечь пользу из списков, возможно, в сочетании с «статической типизацией, выполняемой во время выполнения в ответ на динамические данные»? (Я до сих пор не уверен, о чем именно последний)
Malte Schwerhoff
18

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

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

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

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

Дэн Бертон
источник
в любом случае кортежи могут отображаться в списки и обратно, так что есть четкий изоморфизм.
Эрик Каплун
10

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

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

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

Ким Стебель
источник
8

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

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

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

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

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

глина
источник