48 votes

forall en Scala

Comme indiqué ci-dessous, en Haskell, il est possible de stocker dans une liste des valeurs de types hétérogènes avec certaines limites contextuelles sur celles-ci :

data ShowBox = forall s. Show s => ShowBox s

heteroList :: [ShowBox]
heteroList = [ShowBox (), ShowBox 5, ShowBox True]

Comment puis-je réaliser la même chose en Scala, de préférence sans sous-typage ?

62voto

Miles Sabin Points 13604

Comme l'a fait remarquer @Michael Kohl, cette utilisation de forall en Haskell est un type existentiel et peut être exactement reproduite en Scala en utilisant soit la construction forSome, soit un joker. Cela signifie que la réponse de @paradigmatic est largement correcte.

Néanmoins, il manque quelque chose par rapport à l'original Haskell, à savoir que les instances de son type ShowBox capturent également les instances de la classe de type Show correspondante d'une manière qui les rend disponibles pour être utilisées sur les éléments de la liste, même lorsque le type sous-jacent exact a été existentiellement quantifié. Votre commentaire sur la réponse de @paradigmatic suggère que vous voulez être capable d'écrire quelque chose d'équivalent au Haskell suivant,

data ShowBox = forall s. Show s => ShowBox s

heteroList :: [ShowBox]
heteroList = [ShowBox (), ShowBox 5, ShowBox True]

useShowBox :: ShowBox -> String
useShowBox (ShowBox s) = show s

-- Then in ghci ...

*Main> map useShowBox heteroList
["()","5","True"]

La réponse de @Kim Stebel montre la manière canonique de faire cela dans un langage orienté objet en exploitant le sous-typage. Toutes choses étant égales par ailleurs, c'est la bonne façon de procéder en Scala. Je suis sûr que vous le savez, et que vous avez de bonnes raisons de vouloir éviter le sous-typage et reproduire l'approche basée sur les classes de types de Haskell en Scala. C'est parti...

Notez que dans le Haskell ci-dessus, les instances de la classe de type Show pour Unit, Int et Bool sont disponibles dans l'implémentation de la fonction useShowBox. Si nous essayons de traduire directement cela en Scala, nous obtiendrons quelque chose comme ,

trait Show[T] { def show(t : T) : String }

// Show instance for Unit
implicit object ShowUnit extends Show[Unit] {
  def show(u : Unit) : String = u.toString
}

// Show instance for Int
implicit object ShowInt extends Show[Int] {
  def show(i : Int) : String = i.toString
}

// Show instance for Boolean
implicit object ShowBoolean extends Show[Boolean] {
  def show(b : Boolean) : String = b.toString
}

case class ShowBox[T: Show](t:T)

def useShowBox[T](sb : ShowBox[T]) = sb match {
  case ShowBox(t) => implicitly[Show[T]].show(t)
  // error here      ^^^^^^^^^^^^^^^^^^^
} 

val heteroList: List[ShowBox[_]] = List(ShowBox(()), ShowBox(5), ShowBox(true))

heteroList map useShowBox

et cela échoue à compiler dans useShowBox comme suit,

<console>:14: error: could not find implicit value for parameter e: Show[T]
         case ShowBox(t) => implicitly[Show[T]].show(t)
                                      ^

Le problème ici est que, contrairement au cas de Haskell, les instances de la classe de type Show ne sont pas propagées de l'argument ShowBox au corps de la fonction useShowBox, et ne sont donc pas disponibles pour être utilisées. Si nous essayons de résoudre ce problème en ajoutant un contexte supplémentaire à la fonction useShowBox,

def useShowBox[T : Show](sb : ShowBox[T]) = sb match {
  case ShowBox(t) => implicitly[Show[T]].show(t) // Now compiles ...
} 

cela corrige le problème dans useShowBox, mais maintenant nous ne pouvons pas l'utiliser en conjonction avec map sur notre liste existentiellement quantifiée,

scala> heteroList map useShowBox
<console>:21: error: could not find implicit value for evidence parameter
                     of type Show[T]
              heteroList map useShowBox
                             ^

En effet, lorsque useShowBox est fourni comme argument à la fonction map, nous devons choisir une instance de Show sur la base des informations de type dont nous disposons à ce moment-là. Il est clair qu'il n'y a pas une seule instance Show qui fera l'affaire pour tous les éléments de cette liste et donc la compilation échoue (si nous avions défini une instance Show pour Any, il y en aurait une, mais ce n'est pas ce que nous recherchons ici ... nous voulons sélectionner une instance de classe de type basée sur le type le plus spécifique de chaque élément de la liste).

Pour que cela fonctionne de la même manière qu'en Haskell, nous devons propager explicitement les instances Show dans le corps de useShowBox. Cela pourrait se passer comme suit,

case class ShowBox[T](t:T)(implicit val showInst : Show[T])

val heteroList: List[ShowBox[_]] = List(ShowBox(()), ShowBox(5), ShowBox(true))

def useShowBox(sb : ShowBox[_]) = sb match {
  case sb@ShowBox(t) => sb.showInst.show(t)
}

puis dans le REPL,

scala> heteroList map useShowBox
res7: List[String] = List((), 5, true)

Notez que nous avons supprimé le contexte lié à ShowBox de sorte que nous avons un nom explicite (showInst) pour l'instance Show pour la valeur contenue. Ensuite, dans le corps de useShowBox, nous pouvons l'appliquer explicitement. Notez également que la correspondance de motif est essentielle pour garantir que nous n'ouvrons le type existentiel qu'une seule fois dans le corps de la fonction.

Comme vous pouvez le constater, cette solution est beaucoup plus compliquée que la solution équivalente en Haskell, et je vous recommande vivement d'utiliser la solution basée sur les sous-types en Scala, à moins que vous n'ayez de très bonnes raisons de faire autrement.

Modifier

Comme indiqué dans les commentaires, la définition Scala de ShowBox ci-dessus comporte un paramètre de type visible qui n'est pas présent dans l'original Haskell. Je pense qu'il est en fait assez instructif de voir comment nous pouvons rectifier cela en utilisant des types abstraits.

Tout d'abord, nous remplaçons le paramètre de type par un membre de type abstrait et nous remplaçons les paramètres du constructeur par des vals abstraits,

trait ShowBox {
  type T
  val t : T
  val showInst : Show[T]
}

Nous devons maintenant ajouter la méthode d'usine que les classes de cas nous fourniraient gratuitement,

object ShowBox {
  def apply[T0 : Show](t0 : T0) = new ShowBox {
    type T = T0
    val t = t0
    val showInst = implicitly[Show[T]]
  } 
}

Nous pouvons maintenant utiliser ShowBox ordinaire là où nous utilisions précédemment ShowBox[_] ... le membre du type abstrait joue le rôle du quantificateur existentiel pour nous maintenant,

val heteroList: List[ShowBox] = List(ShowBox(()), ShowBox(5), ShowBox(true))

def useShowBox(sb : ShowBox) = {
  import sb._
  showInst.show(t)
}

heteroList map useShowBox

(Il est intéressant de noter qu'avant l'introduction d'explict forSome et de wildcards dans Scala, c'était exactement la façon dont on représentait les types existentiels).

Nous avons maintenant l'existentiel exactement au même endroit qu'il se trouve dans le Haskell original. Je pense que c'est le plus proche d'un rendu fidèle que l'on puisse obtenir en Scala.

24voto

Apocalisp Points 22526

El ShowBox L'exemple que vous avez donné concerne un type existentiel . Je renomme le ShowBox pour le constructeur de données SB pour le distinguer du type :

data ShowBox = forall s. Show s => SB s

Nous disons s est "existentielle", mais le forall ici est un quantificateur universel qui se rapporte à la SB constructeur de données. Si nous demandons le type de l'objet SB avec un constructeur explicite forall activé, cela devient beaucoup plus clair :

SB :: forall s. Show s => s -> ShowBox

C'est-à-dire qu'un ShowBox est en fait construit à partir de trois choses :

  1. Un type s
  2. Une valeur de type s
  3. Une instance de Show s .

Parce que le type s devient une partie du construit ShowBox c'est existentiellement quantifié . Si Haskell supportait une syntaxe pour la quantification existentielle, nous pourrions écrire ShowBox comme un alias de type :

type ShowBox = exists s. Show s => s

Scala prend en charge ce type de quantification existentielle et la réponse de Miles donne les détails en utilisant un trait qui consiste exactement en ces trois choses ci-dessus. Mais puisqu'il s'agit d'une question sur "forall en Scala", faisons exactement comme Haskell.

Les constructeurs de données en Scala ne peuvent pas être explicitement quantifiés avec forall. Cependant, chaque méthode d'un module peut l'être. Ainsi, vous pouvez effectivement utiliser le polymorphisme des constructeurs de type comme une quantification universelle. Exemple :

trait Forall[F[_]] {
  def apply[A]: F[A]
}

Un type Scala Forall[F] étant donné un certain F est alors équivalent à un type Haskell forall a. F a .

Nous pouvons utiliser cette technique pour ajouter des contraintes à l'argument du type.

trait SuchThat[F[_], G[_]] {
  def apply[A:G]: F[A]
}

Une valeur de type F SuchThat G est comme une valeur du type Haskell forall a. G a => F a . L'instance de G[A] est implicitement recherchée par Scala si elle existe.

Maintenant, nous pouvons utiliser ceci pour coder votre ShowBox ...

import scalaz._; import Scalaz._ // to get the Show typeclass and instances

type ShowUnbox[A] = ({type f[S] = S => A})#f SuchThat Show

sealed trait ShowBox {
  def apply[B](f: ShowUnbox[B]): B  
}

object ShowBox {
  def apply[S: Show](s: => S): ShowBox = new ShowBox {
    def apply[B](f: ShowUnbox[B]) = f[S].apply(s)
  }
  def unapply(b: ShowBox): Option[String] =
    b(new ShowUnbox[Option[String]] {
      def apply[S:Show] = s => some(s.shows)
  })
}

val heteroList: List[ShowBox] = List(ShowBox(()), ShowBox(5), ShowBox(true))

El ShowBox.apply est le constructeur de données universellement quantifié. Vous pouvez voir qu'il prend un type S , une instance de Show[S] et une valeur de type S tout comme la version Haskell.

Voici un exemple d'utilisation :

scala> heteroList map { case ShowBox(x) => x }
res6: List[String] = List((), 5, true)

Un encodage plus direct en Scala pourrait être d'utiliser une classe de cas :

sealed trait ShowBox
case class SB[S:Show](s: S) extends ShowBox {
  override def toString = Show[S].shows(s)
}

Ensuite :

scala> val heteroList = List(ShowBox(()), ShowBox(5), ShowBox(true))
heteroList: List[ShowBox] = List((), 5, true)

Dans ce cas, un List[ShowBox] est fondamentalement équivalent à un List[String] mais vous pouvez utiliser cette technique avec des traits autres que le Show pour obtenir quelque chose de plus intéressant.

Tout cela se fait en utilisant le Show classe de type de Scalaz .

5voto

Kim Stebel Points 22873

Je ne pense pas qu'une traduction 1 pour 1 de Haskell à Scala soit possible ici. Mais pourquoi ne voulez-vous pas utiliser le sous-typage ? Si les types que vous voulez utiliser (comme Int) n'ont pas de méthode show, vous pouvez toujours l'ajouter via des conversions implicites.

scala> trait Showable { def show:String }
defined trait Showable

scala> implicit def showableInt(i:Int) = new Showable{ def show = i.toString }
showableInt: (i: Int)java.lang.Object with Showable

scala> val l:List[Showable] = 1::Nil
l: List[Showable] = List($anon$1@179c0a7)

scala> l.map(_.show)
res0: List[String] = List(1)

3voto

paradigmatic Points 20871

( Modifier : Ajout de méthodes pour montrer, pour répondre au commentaire. )

Je pense que vous pouvez obtenir la même chose en utilisant des méthodes implicites avec des limites de contexte :

trait Show[T] {
  def apply(t:T): String
}
implicit object ShowInt extends Show[Int] {
  def apply(t:Int) = "Int("+t+")"
}
implicit object ShowBoolean extends Show[Boolean] {
  def apply(t:Boolean) = "Boolean("+t+")"
}

case class ShowBox[T: Show](t:T) {
  def show = implicitly[Show[T]].apply(t)
}

implicit def box[T: Show]( t: T ) =
  new ShowBox(t)

val lst: List[ShowBox[_]] = List( 2, true )

println( lst ) // => List(ShowBox(2), ShowBox(true))

val lst2 = lst.map( _.show )

println( lst2 ) // => List(Int(2), Boolean(true))

0voto

MB_ Points 11

Pourquoi pas :

trait ShowBox {
    def show: String
}

object ShowBox {
    def apply[s](x: s)(implicit i: Show[s]): ShowBox = new ShowBox {
        override def show: String = i.show(x)
    }
}

Comme les réponses des autorités l'ont suggéré, Je suis souvent surpris de voir que Scala peut traduire des "monstres de type Haskell" en des monstres très simples.

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