149

私は、違いがどこにあるのかを調べ、より一般的には、HListを使用できない(つまり、通常のリストよりもメリットがない)標準的なユースケースを特定することに本当に興味があります。

(Scalaには22個(私は信じています)があることを知っていますがTupleN、必要なHListは1つだけですが、それは私が興味を持っている概念上の違いではありません。)

以下のテキストでいくつかの質問にマークを付けました。実際には答える必要はないかもしれませんが、私には不明確なことを指摘し、特定の方向に議論を導くことを目的としています。

動機

私は最近、この質問への削除された回答を含む、人々がHListsの使用を提案したSOに関するいくつかの回答(たとえば、Shapelessによって提供されたもの)をました。それはこの議論を引き起こし、それが今度はこの質問を引き起こしました。

イントロ

hlistは、要素の数とその正確なタイプを静的に知っている場合にのみ役立つように思われます。数は実際には重要ではありませんが、変化するが静的に正確に知られているタイプの要素を含むリストを生成する必要はないようですが、それらの数は静的にはわかりません。質問1:たとえば、ループでそのような例を書くことさえできますか?私の直感では、静的に未知の数の任意の要素(特定のクラス階層に対して任意)を持つ静的に正確なhlistを持つことは、互換性がありません。

Hリストとタプル

これが当てはまる場合、つまり、数値とタイプを静的に知っている場合-質問2: nタプルを使用しないのはなぜですか?確かに、HListをタイプセーフにマッピングして折りたたむことができます(タイプセーフではありませが、の助けを借りてタプルをオーバーすることもできますproductIterator)が、要素の数とタイプは静的にわかっているため、おそらくタプル要素にアクセスするだけで済みます直接操作を実行します。

一方、fhlistにマップする関数が非常に一般的で、すべての要素を受け入れる場合-質問3:を介してそれを使用しないのはなぜproductIterator.mapですか?わかりました。メソッドのオーバーロードから1つの興味深い違いが生じる可能性があります。オーバーロードされたものが複数ある場合f、(productIteratorとは対照的に)hlistによって提供されるより強力な型情報を使用すると、コンパイラーはより具体的なを選択できますf。ただし、メソッドと関数は同じではないため、Scalaで実際に機能するかどうかはわかりません。

Hリストとユーザー入力

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

4

4 に答える 4

146

質問1から3に対処する:の主なアプリケーションの1つは、HListsアリティを抽象化することです。アリティは通常、抽象化の特定の使用サイトで静的に知られていますが、サイトごとに異なります。形のない例からこれを取りなさい、

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にマップする関数が非常に一般的で、すべての要素を受け入れる場合... 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())

特定のリストの実際の長さを気にしない場合もありますが、それ以外の場合は、他のリストと同じ長さです。繰り返しになりますが、これは、完全に静的に、また上記のように静的/動的の混合コンテキストでも、シェイプレスがサポートするものです。拡張例については、ここを参照してください。

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

于 2012-08-06T10:05:51.103 に答える
18

明確にするために、HListは本質的に、上にTuple2わずかに異なる砂糖が入ったスタックにすぎません。

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

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

つまり、質問は基本的にネストされたタプルとフラットタプルの違いについてですが、2つは同形であるため、ライブラリ関数を使用できる便利さと表記法を使用できるという利便性を除いて、実際には違いはありません。

于 2012-08-06T22:32:05.230 に答える
11

タプルでは(うまく)できないことがたくさんあります:

  • 一般的なprepend/append関数を記述します
  • 逆関数を書く
  • concat関数を書く
  • ..。

もちろん、タプルを使用してこれらすべてを実行できますが、一般的な場合は実行できません。したがって、HListsを使用すると、コードがより乾燥します。

于 2012-08-06T09:48:10.380 に答える
9

私はこれを超簡単な言葉で説明することができます:

タプルとリストの命名は重要ではありません。HListはHTuplesと名付けることができます。違いは、Scala + Haskellでは、タプルを使用してこれを実行できることです(Scala構文を使用)。

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

任意のタイプの正確に2つの要素の入力タプルを取得し、3番目の要素を追加して、正確に3つの要素を持つ完全に型指定されたタプルを返します。ただし、これは型に対して完全に一般的ですが、入力/出力の長さを明示的に指定する必要があります。

HaskellスタイルのHListでできることは、これを長さ全体でジェネリックにすることです。これにより、任意の長さのタプル/リストに追加して、完全に静的に型指定されたタプル/リストを取り戻すことができます。この利点は、正確にn個のintのリストにintを追加し、nを明示的に指定せずに正確に(n + 1)個のintを持つように静的に型指定されたリストを取得できる同種型のコレクションにも適用されます。

于 2015-05-29T16:59:08.577 に答える