142 votes

HLists ne sont qu’une façon alambiquée de la rédaction de n-uplets ?

J'espère que le titre provocateur qui a retenu votre attention :-) Malgré la première impression que cela peut laisser, je suis vraiment intéressé à trouver où les différences sont, et plus généralement, pour identifier canonique cas d'utilisation où les hlist ne peut pas être utilisé (ou plutôt, ne produisent pas tous les avantages régulier sur les listes).

(Afin de ne pas perdre de temps et d'efforts: je suis conscient qu'il y a 22 (je crois) TupleN en Scala, alors que l'on a seulement besoin d'un seul HList, mais ce n'est pas le genre de différence conceptuelle qui m'intéressent.)

J'ai marqué un couple de questions dans le texte ci-dessous. Il pourrait ne pas être nécessaire pour y répondre, ils sont plus à indiquer des choses qui ne sont pas claires pour moi, et pour orienter la discussion dans certaines directions.

La Motivation

J'ai récemment vu un couple de réponses sur la DONC, où les gens ont proposé d'utiliser les hlist (par exemple, tel que prévu par Informes), y compris une réponse à cette question (Remarque: la réponse a malheureusement été supprimé, maintenant). Elle a donné lieu à cette discussion, qui à son tour a déclenché cette question.

Intro

Il me semble, que les hlist ne sont utiles que si vous connaissez le nombre d'éléments et de leur précise les types de manière statique. Le nombre est en fait pas indispensable, mais il semble peu probable que jamais vous avez besoin de générer une liste avec des éléments de la variable, mais de manière statique précisément les types connus, mais que vous n'avez pas de manière statique connaître leur nombre. Question 1: Pourriez-vous même écrire un tel exemple, par exemple, dans une boucle? Mon intuition est que le fait d'avoir une statique précis hlist avec un statiquement nombre inconnu de l'arbitraire des éléments (arbitraire par rapport à une hiérarchie de classe) n'est tout simplement pas compatible.

Les hlist vs n-Uplets

Si cela est vrai, je.e, vous statiquement connaître le nombre et le type - Question 2: pourquoi ne pas simplement utiliser un n-tuple? Bien sûr, vous pouvez typesafely map et fold sur un HList (que vous pouvez également, mais pas typesafely, faire sur un tuple avec l'aide d' productIterator), mais depuis le nombre et le type des éléments de manière statique sait, vous pourriez juste l'accès au n-uplet d'éléments directement et effectuer les opérations.

D'autre part, si la fonction f vous carte sur une hlist est tellement générique qu'il accepte tous les éléments - Question 3: pourquoi ne pas l'utiliser via productIterator.map? Ok, une différence intéressante pourrait venir de la surcharge de méthode: si nous avons eu plusieurs surchargé fs', qui a la plus forte type d'informations fournies par le hlist (contrairement à l'productIterator) pourrait permettre au compilateur de choisir plus spécifiques, f. Cependant, je ne suis pas sûr si cela fonctionne réellement en Scala, puisque les méthodes et les fonctions ne sont pas les mêmes.

Les hlist et la saisie de l'utilisateur

Sur la même hypothèse, à savoir que vous besoin de connaître le nombre et les types des éléments de la statique - Question 4: pouvez les hlist être utilisé dans les situations où les éléments dépendent de tout type d'interaction de l'utilisateur? E. g., imaginez le remplissage d'un hlist avec des éléments à l'intérieur d'une boucle; les éléments sont lues à partir de quelque part (l'INTERFACE utilisateur, le fichier de config, acteur de l'interaction, réseau) jusqu'à ce qu'une certaine condition est vraie. Quel serait le type de la hlist être? Similaire pour une spécification d'interface getElements: HList[...] qui doivent travailler avec des listes de statiquement longueur inconnue, et qui permet à Un composant dans un système pour obtenir une liste de l'arbitraire des éléments de composant B.

141voto

Miles Sabin Points 13604

Aborder les questions une à trois: l'une des principales applications pour HLists est l'abstraction de plus arité. Arité est généralement connu statiquement à tout usage du site d'une abstraction, mais varie d'un site à l'autre. Prenez ce, à partir informe ses exemples,

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)

Sans l'aide d' HLists (ou quelque chose d'équivalent) à l'abstrait sur l'arité de la n-uplet arguments au flatten il serait impossible d'avoir une seule application qui pourrait accepter les arguments de ces deux formes différentes et de les transformer en un type de façon sécuritaire.

La capacité d'abstraction plus arité est susceptible d'intéresser n'importe où qui fixe arities sont impliqués: ainsi que les tuples, comme ci-dessus, qui comprend méthode/fonction de listes de paramètres, et des classes de cas. Voir ici pour des exemples de la façon dont nous pourrions résumé par l'arité de l'arbitraire des classes de cas pour obtenir le type des instances de classe presque automatiquement,

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

Il n'y a pas d'exécution de l'itération ici, mais il y a duplication, l'utilisation de l' HLists (ou à des structures équivalentes) peut éliminer. Bien sûr, si votre tolérance pour la répétition standard est élevé, vous pouvez obtenir le même résultat par l'écriture de plusieurs implémentations pour chaque et chaque forme que vous vous souciez.

En question trois vous demandez: "... si la fonction f vous carte sur une hlist est tellement générique qu'il accepte tous les éléments ... pourquoi ne pas l'utiliser via productIterator.carte?". Si la fonction que vous carte sur une HList est vraiment de la forme Any => T puis de cartographier productIterator vous servira parfaitement bien. Mais les fonctions de la forme Any => T ne sont généralement pas très intéressant (au moins, ils ne sont pas, à moins qu'ils type de cas à l'interne). informes offre une fonction polymorphe de la valeur qui permet de compiler pour sélectionner le type spécifique de cas exactement de la façon dont vous avez des doutes à propos de. Par exemple,

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

Quant à votre question quatre, à propos de la saisie de l'utilisateur, il y a deux cas à considérer. Le premier est des situations où on peut dynamiquement établir un cadre qui garantit qu'un état statique obtient. Dans ces sortes de scénarios, il est parfaitement possible d'appliquer ces techniques, mais clairement avec la condition que si la condition statique ne pas obtenir à l'exécution alors que nous avons à suivre un autre chemin. Sans surprise, cela signifie que les méthodes qui sont sensibles à la dynamique des conditions de rendement résultats facultatifs. Voici un exemple d'utilisation 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]

Le type d' l ne tient pas compte de la longueur de la liste, ou les types exacts de ses éléments. Cependant, si nous nous attendons à avoir un formulaire spécifique (ie. si il doit se conformer à certains connus, schéma fixe), alors on peut tenter d'établir ce fait et d'agir en conséquence,

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

Il existe d'autres situations où l'on pourrait se soucient pas de la longueur réelle d'une liste donnée, d'autres que c'est de la même longueur que certains autres de la liste. Encore une fois, c'est quelque chose qui informe, prend en charge, à la fois complètement statique, et aussi dans un mélange statique/dynamique contexte comme ci-dessus. Voir ici pour un exemple.

Il est vrai, comme vous l'observez, que l'ensemble de ces mécanismes nécessitent de type statique d'une information disponible, au moins sous certaines conditions, et qui semble exclure ces techniques utilisables dans une dynamique de l'environnement, entièrement piloté par externe fourni des données non typées. Mais avec l'avènement de l'appui pour la compilation d'exécution en tant que composante de la Scala de réflexion dans la version 2.10, même ce n'est plus un obstacle insurmontable ... nous pouvons utiliser la compilation d'exécution pour assurer une forme de léger mise en scène et le typage statique effectuée au moment de l'exécution en réponse à la dynamique de données: extrait de la précédente, ci-dessous ... suivez le lien pour l'exemple complet,

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

Je suis sûr que @PLT_Borat avoir quelque chose à dire à ce sujet, compte tenu de son sage commentaires sur dépendante tapé langages de programmation ;-)

17voto

Dan Burton Points 26639

Pour être clair, une HList n’est rien d’autre qu’une pile de Tuple2 avec du sucre légèrement différent.

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

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

Donc, votre question concerne essentiellement les différences entre l'utilisation de n-uplets imbriqués et de n-uplets plats, mais les deux sont isomorphes.

9voto

Kim Stebel Points 22873

Il y a beaucoup de choses que vous ne pouvez pas faire (bien) avec des tuples:

  • écrire une fonction générique prépend / append
  • écrire une fonction inverse
  • écrire une fonction concat
  • ...

Vous pouvez faire tout cela avec des tuples bien sûr, mais pas dans le cas général. L'utilisation de HLists rend votre code plus sec.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X