HListはタプルを書く複雑な方法にすぎませんか?


144

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

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

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

動機

私は最近、人々が(によって提供されるように、例えばHListsを使用することが提案SO上の回答のカップルを見てきました型崩れを削除答えを含む)、この質問を。それがこの議論を引き起こし、それが今度はこの疑問を引き起こしました。

はじめに

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

HListsとタプル

これが真である場合、つまり、静的に数値と型を知っている場合- 質問2:なぜnタプルを使用しないのですか?確かに、あなたはtypesafelyマッピングすることができますし、(あなたもすることができますが、HList折り重なっていないの助けを借りて、タプルを超える行うtypesafely、 productIterator)が、要素の数と種類は静的にあなたはおそらくタプル要素にアクセスする可能性が知られているので、直接操作を実行します。

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

HListとユーザー入力

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

回答:


144

質問1から3に対処HListsします。主な用途の1つは、アリティの抽象化です。アリティは通常、抽象化の特定の使用サイトで静的に知られていますが、サイトによって異なります。これを、無形のから見てみましょう。

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

アリティを抽象化する機能は、固定されたアリティが関与している場所ならどこでも興味を持つ可能性があります。メソッド/関数のパラメーターリストやケースクラスを含む上記のタプルと同様です。タイプクラスインスタンスをほぼ自動的に取得するために、任意の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(または同等の構造)を排除することができますが。もちろん、繰り返しボイラープレートの許容度が高い場合は、気になるすべての形状に対して複数の実装を記述することで同じ結果を得ることができます。

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

他のリストと同じ長さであるということを除いて、特定のリストの実際の長さを気にしない他の状況があります。繰り返しますが、これは完全に静的に、また上記のように静的/動的混合のコンテキストでも、形のないサポートです。詳細な例については、こちらをご覧ください。

ご覧のとおり、これらのメカニズムはすべて、少なくとも条件付きで静的な型情報が利用可能である必要があり、外部から提供された型指定されていないデータによって完全に駆動される完全に動的な環境でこれらの手法を使用できないように思われます。しかし、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 / develop / core / src / main /… 「本物の」FunctionNs に対処する必要があることを考えると、残念ながらShapelessを使用してこれを回避できる方法は知りません
Paul Butcher

1
私は(かなり人工的な)例-ideone.com/sxIw1-を作成しましたが、これは問題1に沿ったものです。これは、おそらく「動的データに応じて実行時に実行される静的型付け」と組み合わせて、hlistsから利益を得ることができますか?(私は後者が正確に何であるかについてはまだ
わかりません

17

明確にするために、HListは基本的にはTuple2、わずかに異なる砂糖を上に積み重ねたものにすぎません。

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

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

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


タプルはhlistsにマッピングでき、とにかく戻すことができるため、明確な同型性があります。
Erik Kaplun 2014年

10

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

  • 一般的なプリペンド/アペンド関数を作成する
  • 逆関数を書く
  • concat関数を書く
  • ...

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


8

超簡単な言葉でこれを説明できます:

タプルとリストの命名は重要ではありません。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を明示的に指定せずに、正確にnの整数のリストにintを追加し、正確に(n + 1)の整数を持つように静的に型指定されたリストを取得できる同種の型のコレクションにも適用されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.