web-dev-qa-db-fra.com

Les listes HL ne sont-elles rien de plus qu'une façon compliquée d'écrire des tuples?

Je suis vraiment intéressé à découvrir où se trouvent les différences, et plus généralement, à identifier les cas d'utilisation canoniques où les listes H ne peuvent pas être utilisées (ou plutôt, ne produisent aucun avantage par rapport aux listes régulières).

(Je suis conscient qu'il y a 22 (je crois) TupleN dans Scala, alors que l'on n'a besoin que d'une seule HList, mais ce n'est pas le genre de différence conceptuelle qui m'intéresse.)

J'ai marqué quelques questions dans le texte ci-dessous. Il n'est peut-être pas vraiment nécessaire d'y répondre, ils sont plutôt destinés à signaler des choses qui ne me sont pas claires et à guider la discussion dans certaines directions.

Motivation

J'ai récemment vu quelques réponses sur SO où les gens ont suggéré d'utiliser des HLists (par exemple, comme fourni par Shapeless ), y compris une réponse supprimée à - cette question . Elle a donné lieu à cette discussion , qui à son tour a déclenché cette question.

Intro

Il me semble que les hlists ne sont utiles que lorsque vous connaissez statiquement le nombre d'éléments et leurs types précis. Le nombre n'est en fait pas crucial, mais il semble peu probable que vous ayez besoin de générer une liste avec des éléments de types connus mais statiquement précis, mais que vous ne connaissez pas statiquement leur nombre. Question 1: Pourriez-vous même écrire un tel exemple, par exemple, en boucle? Mon intuition est qu'avoir une hlist statiquement précise avec un nombre statistiquement inconnu d'éléments arbitraires (arbitraire par rapport à une hiérarchie de classes donnée) n'est tout simplement pas compatible.

HLists vs Tuples

Si cela est vrai, c'est-à-dire que vous connaissez statiquement le nombre et le type - Question 2: pourquoi ne pas simplement utiliser un n-tuple? Bien sûr, vous pouvez mapper et replier en toute sécurité une HList (ce que vous pouvez aussi, mais pas typiquement, faites sur un tuple à l'aide de productIterator), mais comme le nombre et le type des éléments sont connus statiquement, vous pouvez probablement accéder directement aux éléments Tuple et effectuer les opérations.

D'un autre côté, si la fonction f que vous mappez sur une liste h est si générique qu'elle 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 avions plusieurs f surchargés, avoir les informations de type plus fortes fournies par la hlist (contrairement à productIterator) pourrait permettre au compilateur de choisir un _ plus spécifique f. Cependant, je ne sais pas si cela fonctionnerait réellement dans Scala, car les méthodes et les fonctions ne sont pas les mêmes.

HListes et saisie utilisateur

En partant de la même hypothèse, à savoir que vous devez connaître statiquement le nombre et les types des éléments - Question 4: les listes peuvent-elles être utilisées dans les situations où le les éléments dépendent de tout type d'interaction utilisateur? Par exemple, imaginez remplir une liste h avec des éléments à l'intérieur d'une boucle; les éléments sont lus de quelque part (UI, fichier de configuration, interaction d'acteur, réseau) jusqu'à ce qu'une certaine condition se vérifie. Quel serait le type de la liste h? Similaire pour une spécification d'interface getElements: HList qui [...] devrait fonctionner avec des listes de longueur statiquement inconnue, et qui permet au composant A d'un système d'obtenir une telle liste d'éléments arbitraires du composant B.

141
Malte Schwerhoff

Répondre aux questions 1 à 3: l'une des principales applications de HLists est l'abstraction sur l'arité. L'arité est généralement connue statiquement sur tout site d'utilisation donné d'une abstraction, mais varie d'un site à l'autre. Prenez ceci, de sans forme 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 utiliser HLists (ou quelque chose d'équivalent) pour résumer l'arité des arguments Tuple à flatten, il serait impossible d'avoir une seule implémentation qui pourrait accepter des arguments de ces deux formes très différentes et se transformer les d'une manière sûre de type.

La capacité d'abstraction sur l'arité est susceptible d'être intéressante partout où des arités fixes sont impliquées: ainsi que des tuples, comme ci-dessus, qui incluent des listes de paramètres de méthode/fonction et des classes de cas. Voir ici pour des exemples de la façon dont nous pourrions résumer l'arité des classes de cas arbitraires pour obtenir des instances de classe de type 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'itération d'exécution ici, mais il y a duplication , que l'utilisation de HLists (ou des structures équivalentes) peuvent éliminer. Bien sûr, si votre tolérance aux plaques répétitives est élevée, vous pouvez obtenir le même résultat en écrivant plusieurs implémentations pour chaque forme qui vous tient à cœur.

À la troisième question, vous demandez "... si la fonction f que vous mappez sur une liste h est si générique qu'elle accepte tous les éléments ... pourquoi ne pas l'utiliser via productIterator.map?". Si la fonction que vous mappez sur une HList est vraiment de la forme Any => T puis le mappage sur productIterator vous servira parfaitement. Mais les fonctions de la forme Any => T ne sont généralement pas si intéressants (du moins, ils ne le sont pas à moins de taper cast en interne) shapeless fournit une forme de valeur de fonction polymorphe qui permet au compilateur de sélectionner des cas spécifiques au type exactement de la manière dont vous en doutez. 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)

En ce qui concerne votre quatrième question, sur la saisie des utilisateurs, il y a deux cas à considérer. Le premier est des situations où nous pouvons établir dynamiquement un contexte qui garantit l'obtention d'une condition statique connue. Dans ces types de scénarios, il est parfaitement possible d'appliquer des techniques informes, mais clairement à condition que si la condition statique n'obtient pas au moment de l'exécution, alors nous avons suivre un chemin alternatif. Sans surprise, cela signifie que les méthodes qui sont sensibles aux conditions dynamiques doivent donner des résultats facultatifs. Voici un exemple utilisant 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 de l ne capture pas la longueur de la liste, ni les types précis de ses éléments. Cependant, si nous nous attendons à ce qu'il ait une forme spécifique (c'est-à-dire s'il doit se conformer à un schéma fixe connu), alors nous pouvons essayer d'établir ce fait et 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 y a d'autres situations où nous pouvons ne pas nous soucier de la longueur réelle d'une liste donnée, à part le fait qu'elle est de la même longueur qu'une autre liste. Encore une fois, c'est quelque chose qui prend en charge sans forme, à la fois de manière entièrement statique, et également dans un contexte mixte statique/dynamique comme ci-dessus. Voir ici pour un exemple étendu.

Il est vrai, comme vous le constatez, que tous ces mécanismes nécessitent que des informations de type statique soient disponibles, au moins conditionnellement, et cela semble exclure ces techniques d'être utilisables dans un environnement complètement dynamique, entièrement piloté par des données non typées fournies de l'extérieur. Mais avec l'avènement du support de la compilation d'exécution en tant que composant de Scala réflexion en 2.10, même ce n'est plus un obstacle insurmontable ... nous pouvons utiliser la compilation d'exécution pour fournir une forme de - staging léger et faire effectuer notre typage statique lors de l'exécution en réponse à des données dynamiques: extrait des précédents 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 @ PLT_Borat aura quelque chose à dire à ce sujet, compte tenu de ses sages commentaires sur les langages de programmation typés de manière dépendante ;-)

140
Miles Sabin

Juste pour être clair, une HList n'est rien d'autre qu'une pile de Tuple2 avec du sucre légèrement différent sur le dessus.

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

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

Votre question porte donc essentiellement sur les différences entre l'utilisation de tuples imbriqués et de tuples plats, mais les deux sont isomorphes, donc à la fin il n'y a vraiment aucune différence, sauf la commodité dans laquelle les fonctions de bibliothèque peuvent être utilisées et quelle notation peut être utilisée.

17
Dan Burton

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

  • écrire une fonction générique de pré-ajout/ajout
  • é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. Ainsi, l'utilisation de HLists rend votre code plus SEC.

9
Kim Stebel

Je peux expliquer cela dans un langage super simple:

La dénomination Tuple vs list n'est pas significative. Les HLists peuvent être nommées HTuples. La différence est que dans Scala + Haskell, vous pouvez le faire avec un tuple (en utilisant Scala):

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

pour prendre un Tuple d'entrée d'exactement deux éléments de n'importe quel type, ajoutez un troisième élément et renvoyez un Tuple entièrement typé avec exactement trois éléments. Mais bien que cela soit complètement générique sur les types, il doit spécifier explicitement les longueurs d'entrée/sortie.

Ce qu'une HList de style Haskell vous permet de faire est ce générique sur la longueur, de sorte que vous pouvez ajouter à n'importe quelle longueur de Tuple/list et récupérer un Tuple/list entièrement typé statiquement. Cet avantage s'applique également aux collections typées de manière homogène où vous pouvez ajouter un int à une liste d'exactement n entiers et récupérer une liste qui est typée statiquement pour avoir exactement (n + 1) entiers sans spécifier explicitement n.

6
clay