Хіба HLists - це не що інше, як перекручений спосіб написання кортежів?


144

Мені дуже цікаво дізнатись, де є відмінності, і в цілому, визначити канонічні випадки використання, коли HList не може бути використаний (а точніше, не приносить жодних переваг за звичайні списки).

(Я знаю, що TupleNу Scala є 22 (я вважаю) , тоді як одному потрібен лише один HList, але це не та концептуальна різниця, яка мене цікавить.)

У тексті нижче я позначив пару питань. На це, можливо, не потрібно відповідати, вони мають на меті вказати на незрозумілі для мене речі та вести дискусію в певних напрямках.

Мотивація

Нещодавно я бачив пару відповідей на SO, де люди пропонували використовувати HLists (наприклад, як це надає Shapeless ), включаючи видалену відповідь на це питання . Це породило цю дискусію , що, в свою чергу, викликало це питання.

Вступ

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

HLists vs. Tuples

Якщо це правда, тобто ви статистично знаєте число і тип - Запитання 2: чому б просто не використовувати n-кортеж? Звичайно, ви можете безпечно набрати карту та скласти HList (що також можна, але не набрати безпечно, зробити над кортежем за допомогою productIterator), але оскільки кількість та тип елементів статично відомі, ви, ймовірно, можете просто отримати доступ до елементів кортежу безпосередньо і виконувати операції.

З іншого боку, якщо функція, fяку ви переглядаєте через список, настільки загальна, що вона приймає всі елементи - питання 3: чому б не використовувати її через productIterator.map? Гаразд, одна цікава відмінність може виникнути від перевантаження методу: якби у нас було декілька перевантажених f, наявність більш сильної інформації про тип, надану списком (на відміну від productIterator), могла б дозволити компілятору вибрати більш конкретний f. Однак я не впевнений, чи справді це діятиме в Scala, оскільки методи та функції не однакові.

Списки HL та введення користувача

Спираючись на те саме припущення, а саме, що вам потрібно статично знати кількість та типи елементів - Питання 4: Чи можна використовувати списки в ситуаціях, коли елементи залежать від будь-якої взаємодії користувача? Наприклад, уявіть, як заповнити список із елементами всередині циклу; елементи зчитуються звідкись (UI, конфігураційний файл, взаємодія актора, мережа), поки не буде виконано певну умову. Яким буде тип списку? Аналогічно специфікації інтерфейсу getElements: HList [...], який повинен працювати зі списками статично невідомої довжини, що дозволяє компоненту А в системі отримувати такий список довільних елементів з компонента 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(або еквівалентних структур) може усунути. Звичайно, якщо ваша толерантність до багаторазових котлів висока, ви можете отримати один і той же результат, написавши кілька реалізацій для кожної форми, яка вас цікавить.

У третьому запитанні ви запитуєте "... якщо функція, яку ви переказуєте на список, настільки загальна, що приймає всі елементи ... чому б не використовувати його через productIterator.map?". Якщо функція, яку ви переглядаєте на HList, справді має форму, Any => Tто відображення productIteratorбуде служити вам ідеально. Але функції форми Any => T, як правило, не такі цікаві (принаймні, вони не є, якщо вони не вводять ролі внутрішньо). Безформний надає форму значення поліморфної функції, яка дозволяє компілятору вибирати конкретні типи саме таким чином, як ви сумніваєтесь. Наприклад,

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

Є й інші ситуації, коли ми можемо не перейматися фактичною довжиною певного списку, окрім того, що це така ж довжина, як і деякі інші списки. Знову ж таки, це щось безформене, як повністю статично, так і в змішаному статичному / динамічному контексті, як зазначено вище. Дивіться тут для розширеного прикладу.

Правда, як ви зауважуєте, всі ці механізми вимагають наявності інформації статичного типу, принаймні умовно, і це, здавалося б, виключає використання цих методів у цілком динамічному середовищі, повністю керованому зовнішніми нетипізованими даними. Але з появою підтримки для компіляції часу виконання як складової відбиття 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 буде щось про це сказати, враховуючи коментарі мудреця щодо залежно набраних мов програмування ;-)


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

17

Просто для того, щоб бути зрозумілим, HList - це по суті не що інше, як пачка Tuple2з трохи іншим цукром зверху.

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

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

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


кортежі в будь-якому випадку можна відобразити до списків і назад, тому очевидний ізоморфізм.
Ерік Каплун

10

Є багато речей, які ви не можете (добре) зробити з кортежами:

  • написати загальну функцію додавання / додавання
  • записати зворотну функцію
  • записати лаконічну функцію
  • ...

Зрозуміло, ви можете все це зробити з кортежами, але не в загальному випадку. Таким чином, використання 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 ints та отримати список, який статично набраний, щоб мати саме (n + 1) ints, не чітко вказуючи n.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.