1. ホーム
  2. scala

[解決済み] HListはタプルの複雑な書き方に過ぎないのか?

2022-05-08 06:16:42

質問

HListが使用できない(というか、通常のリストよりもメリットがない)典型的なユースケースを特定することに、私はとても興味があります。

(が22個(確か)あることは承知しています)。 TupleN が必要なのに対して、Scala では HList が一つで済みますが、私が興味を持っているのはそのような概念の違いではありません)。

以下の文章には、いくつかの質問に印をつけました。これらは、私にとって不明な点を指摘し、議論を特定の方向に導くためのものです。

モチベーション

最近SOの回答で、HListを使うことを提案されているのをいくつか見ました(例えば 形状なし に対する削除された回答も含まれます。 この質問 . を生じさせました。 この議論 という質問がありました。

イントロ

hlistは、要素の数とその正確な型が静的に分かっている場合にのみ有効であるように思います。しかし、様々な型の要素を持つが静的に正確な型が分かっていて、かつその数が静的に分からないリストを生成する必要はなさそうです。 質問1: そのような例を、例えばループで書くこともできるのでしょうか?私の直感では、静的に正確なhlistに、静的に未知の数の任意要素(与えられたクラス階層に対して任意)を持たせることは、相容れないと思うのです。

HListとTupleの比較

もしこれが本当なら、つまり、あなたは静的に数と型を知っている - 。 質問2. なぜnタプルでないのか?もちろん、HListをタイプセイフにマップしたり、フォールドしたりすることはできる(これもできるけど ではなく の助けを借りて、タプルに対して型安全な処理を行うことができます。 productIterator ) が、要素の数と型が静的に分かっているので、おそらくタプル要素に直接アクセスして演算を実行することができます。

一方、関数 f をマッピングする場合、そのhlistが非常に汎用的であるため、すべての要素を受け入れることができます。 質問3. を介して使用するのはいかがでしょうか。 productIterator.map ? さて、1つの興味深い違いは、メソッドのオーバーロードに起因する可能性があります。 f によって提供される強力な型情報を持つことで、コンパイラはより具体的な f . しかし、Scalaではメソッドと関数が違うので、実際にうまくいくかどうかはわかりません。

HLリストとユーザー入力

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

解決方法は?

質問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つの全く異なる形状の引数を受け取り、型安全な方法で変換することができる単一の実装を持つことは不可能でしょう。

アリティを抽象化する機能は、上記のタプルと同様に、メソッド/関数のパラメータリストやケースクラスなど、固定アリティが関係するあらゆる場所で注目される可能性がある。詳細は こちら は、任意の case class のアリティを抽象化して、ほぼ自動的に type class のインスタンスを取得する例である。

// 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つ目は、既知の静的な条件が得られることを保証するコンテキストを動的に確立できる状況です。この種のシナリオでは、シェイプレス・テクノロジーを適用することは完全に可能ですが、明らかに、もし静的条件が はない。 実行時に取得した場合は、別の道を歩まなければならない。当然のことながら、これは動的な条件に敏感なメソッドは、任意の結果を得なければならないことを意味する。以下は HList s,

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

そう PLT_Borat は、それについて何か言うでしょう。 依存型プログラミング言語に関する賢者のコメント ;-)