Are HLists nothing more than a convoluted way of writing tuples? Ask Question

Are HLists nothing more than a convoluted way of writing tuples? Ask Question

I am really interested in finding out where the differences are, and more generally, to identify canonical use cases where HLists cannot be used (or rather, don't yield any benefits over regular lists).

(I am aware that there are 22 (I believe) TupleN in Scala, whereas one only needs a single HList, but that is not the kind of conceptual difference I am interested in.)

I've marked a couple of questions in the text below. It might not actually be necessary to answer them, they are more meant to point out things that are unclear to me, and to guide the discussion in certain directions.

Motivation

I've recently seen a couple of answers on SO where people suggested to use HLists (for example, as provided by Shapeless), including a deleted answer to this question. It gave rise to this discussion, which in turn sparked this question.

Intro

It seems to me, that hlists are only useful when you know the number of elements and their precise types statically. The number is actually not crucial, but it seems unlikely that you ever need to generate a list with elements of varying but statically precisely known types, but that you don't statically know their number. Question 1: Could you even write such an example, e.g., in a loop? My intuition is that having a statically precise hlist with a statically unknown number of arbitrary elements (arbitrary relative to a given class hierarchy) just isn't compatible.

HLists vs. Tuples

If this is true, i.e, you statically know number and type - Question 2: why not just use an n-tuple? Sure, you can typesafely map and fold over an HList (which you can also, but not typesafely, do over a tuple with the help of productIterator), but since number and type of the elements are statically known you could probably just access the tuple elements directly and perform the operations.

On the other hand, if the function f you map over an hlist is so generic that it accepts all elements - Question 3:なぜ 経由で使用しないのでしょうかproductIterator.map? 興味深い違いの 1 つは、メソッドのオーバーロードから生じる可能性があります。オーバーロードされた が複数ある場合f、(productIterator とは対照的に) hlist によって提供されるより強力な型情報により、コンパイラーはより具体的な を選択できるようになりますf。ただし、メソッドと関数は同じではないため、これが Scala で実際に機能するかどうかはわかりません。

HLists とユーザー入力

同じ前提、つまり要素の数と型を静的に知る必要があるという前提に基づいて -質問4:hlist は、要素が何らかのユーザー インタラクションに依存する状況で使用できますか? たとえば、ループ内で hlist に要素を設定することを想像してください。要素は、特定の条件が満たされるまで、どこか (UI、構成ファイル、アクター インタラクション、ネットワーク) から読み取られます。hlist のタイプはどのようなものになりますか? 静的に長さが不明なリストで機能し、システム内のコンポーネント A がコンポーネント B から任意の要素のリストを取得できるようにするインターフェイス仕様 getElements: HList[...] の場合も同様です。

ベストアンサー1

質問1から3への回答: の主な用途の1つは、HListsアリティの抽象化です。アリティは通常、抽象化の特定の使用サイトでは静的に既知ですが、サイトごとに異なります。shapelessの次の例を見てください。

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これら 2 つの非常に異なる形状の引数を受け入れ、型安全な方法で変換できる単一の実装を持つことは不可能です。

引数を抽象化する機能は、固定引数が関係するあらゆる場所で興味深いものになるでしょう。また、メソッド/関数のパラメータリストやケースクラスを含むタプルも同様です。ここ任意のケースクラスのアリティを抽象化して、型クラスのインスタンスをほぼ自動的に取得する方法の例については、

// 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を排除できます。もちろん、定型句の繰り返しに対する許容度が高い場合は、関心のある各図形ごとに複数の実装を記述することで同じ結果を得ることができます。

質問 3 では、「... hlist にマップする関数 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)

質問4、ユーザー入力に関して、考慮すべきケースが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())

他のリストと同じ長さであるかどうか以外、リストの実際の長さを気にしない状況もあります。これも、shapeless がサポートしているもので、完全に静的な場合と、上記のように静的/動的が混在するコンテキストの両方でサポートされています。ここ拡張例については、こちらをご覧ください。

ご存知のとおり、これらのメカニズムはすべて、少なくとも条件付きで静的な型情報が必要であるため、外部から提供される型なしデータによって完全に駆動される完全に動的な環境ではこれらの手法を使用できないように思われます。しかし、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")

私は確信しています翻訳者彼を考えると、それについて何か言うことがあるだろう依存型プログラミング言語に関する Sage のコメント;-)

おすすめ記事