11 votes

Nil et List en tant qu'expressions de cas en Scala

Ce code compile :

def wtf(arg: Any) = {  
  arg match {  
    case Nil => "Nil a été passé à arg"  
    case List() => "List() a été passé à arg"  
    case _ => "sinon"  
  }  
}

Mais celui-ci ne compile pas :

def wtf(arg: Any) = {  
  arg match {  
    case List() => "List() a été passé à arg"  
    case Nil => "Nil a été passé à arg"  
    case _ => "sinon"  
  }  
}  

La ligne case Nil => ... est marquée comme code inaccessible. Pourquoi, dans le premier cas, la ligne case List() => ... n'est pas marquée de la même erreur ?

10voto

extempore Points 8016

La réponse réelle nécessite la compréhension d'un détail d'implémentation malheureux qui m'a coûté beaucoup de temps à découvrir.

1) Le cas List() invoque un extracteur, pour lequel aucune vérification d'exhaustivité/inatteignabilité n'est possible dans le cas général car les extracteurs invoquent des fonctions arbitraires. Jusqu'ici tout va bien, on ne peut pas s'attendre à résoudre le problème de l'arrêt.

2) Il a été déterminé qu'à l'époque plus "sauvage" du compilateur, le motif de correspondance pouvait être accéléré considérablement (sans perdre la vérification d'exhaustivité) si "case List()" était simplement traduit en "case Nil" lors d'une phase précoce du compilateur pour éviter l'extracteur. C'est toujours le cas et bien que cela puisse être annulé, apparemment beaucoup de gens ont été avertis que "case List() => " est parfaitement correct et nous ne voulons pas pessimiser tout leur code soudainement. Je dois donc simplement trouver une solution.

Vous pouvez voir empiriquement que List est privilégié en l'essayant avec une autre classe. Aucune erreur d'inatteignabilité.

import scala.collection.immutable.IndexedSeq
val Empty: IndexedSeq[Nothing] = IndexedSeq()
def wtf1(arg: Any) = {  
  arg match {  
    case Empty => "Nil a été passé à l'argument"  
    case IndexedSeq() => "IndexedSeq() a été passé à l'argument"  
    case _ => "autrement"  
  }  
}

def wtf2(arg: Any) = {  
  arg match {  
    case IndexedSeq() => "IndexedSeq() a été passé à l'argument"  
    case Empty => "Nil a été passé à l'argument"  
    case _ => "autrement"  
  }  
}

6voto

Travis Brown Points 56342

La divergence est particulièrement étrange car le code pour le cas Nil dans la deuxième version n'est définitivement pas du tout inaccessible, comme nous pouvons le constater si nous cachons un peu de choses au compilateur:

def wtf(arg: Any) = {
  arg match {
    case List() => "List() a été passé à arg"  
    case x => x match {
      case Nil => "Nil a été passé à arg"
      case _ =>"sinon"
    }
  }
}

Maintenant wtf(Vector()) retournera "Nil a été passé à arg". Cela peut sembler contreintuitif, mais c'est parce que les motifs littéraux correspondent à des valeurs qui sont égales en termes de ==, et Vector() == Nil, mais Vector() ne correspond pas au motif extracteur List().

Plus simplement:

scala> (Vector(): Seq[_]) match { case List() => true; case Nil => false }
:8: erreur: code inatteignable

scala> (Vector(): Seq[_]) match { case List() => true; case x => x match { case Nil => false } }
res0: Boolean = false

La réponse du compilateur est donc complètement inversée: dans la version "correcte", le deuxième cas est inatteignable, et dans la version "mauvaise", le deuxième cas est parfaitement valide. J'ai signalé cela comme un bug (SI-5029).

5voto

Don Roby Points 24965

Nil est un objet étendant List[Nothing]. Étant plus spécifique que List(), il n'est pas atteint s'il apparaît après List() dans l'expression du cas.

Bien que je pense que ce qui précède est plus ou moins vrai, ce n'est probablement pas toute l'histoire.

Il y a des indices dans l'article Matching Objects with Patterns, bien que je ne voie pas de réponse définitive là-bas.

Je soupçonne que la détection de l'inaccessibilité est simplement plus complètement implémentée pour les constantes nommées et les littéraux que pour les modèles de constructeur, et que List() est interprété comme un modèle de constructeur (bien que ce soit un trivial), alors que Nil est une constante nommée.

3voto

huynhjl Points 26045

Je ne trouve rien dans la spécification du langage concernant les clauses de correspondance inatteignables. Quelqu'un me corrige si je me trompe.

Je suppose donc que les erreurs de compilation inatteignables sont faites sur la base d'un maximum d'efforts, ce qui pourrait expliquer pourquoi le premier cas ne se plaint pas.

scala -Xprint:typer suggère que Nil est un schéma littéral utilisant immutable.this.Nil.== pour vérifier une correspondance, tandis que List() est un schéma d'extracteur. En regardant la mise en œuvre de l'extracteur, il semble faire quelque chose comme cela:

def unapplySeq[A](x: CC[A]): Some[CC[A]] = Some(x)

Je suis donc devenu curieux et j'ai déployé une implémentation alternative :

object ListAlt {
  def unapplySeq[A](l: List[A]): Some[List[A]] = Some(l)
}

Cela fonctionne comme List():

scala> Nil match { case ListAlt() => 1 }
res0: Int = 1

Mais si j'implémente votre fonction avec ça, ça compile bien (à l'exception de l'avertissement non vérifié) :

def f(a: Any) = a match { case ListAlt() => 1 case Nil => 2 case _ => 0 }

scala> f(List())
res2: Int = 1

scala> f(Nil)
res3: Int = 1

scala> f(4)
res4: Int = 0

Je me demande donc si l'implémentation du moteur de correspondance de schémas traite de manière spéciale Nil et List(). Peut-être qu'il traite List() comme un littéral...

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