Comment puis-je faire « if..else » dans une for-compréhension?
-
10-10-2019 - |
Question
Je pose une question fondamentale qui m'a confondu récemment. Je veux écrire un Scala Pour l'expression de faire quelque chose comme ce qui suit:
for (i <- expr1) {
if (i.method) {
for (j <- i) {
if (j.method) {
doSomething()
} else {
doSomethingElseA()
}
}
} else {
doSomethingElseB()
}
}
Le problème est que, dans les multiples générateurs d'expression, je ne sais pas où je peux mettre chacun pour le corps d'expression.
for {i <- expr1
if(i.method) // where can I write the else logic ?
j <- i
if (j.method)
} doSomething()
Comment puis-je réécrire le code dans Scala style?
La solution
Le premier code que vous avez écrit est tout à fait valide, il n'y a donc pas besoin de le réécrire. Par ailleurs, vous avez dit que vous vouliez savoir comment le faire style Scala. Il n'y a pas vraiment une « Scala de style », mais je suppose un style plus fonctionnel et harnachement.
for (i <- expr1) {
if (i.method) {
for (j <- i) {
if (j.method) {
doSomething()
} else {
doSomethingElseA()
}
}
} else {
doSomethingElseB()
}
}
La première préoccupation est que ce ne renvoie aucune valeur. Tout ce qu'il fait est des effets secondaires, qui doivent être évités aussi bien. Donc, le premier changement serait comme ceci:
val result = for (i <- expr1) yield {
if (i.method) {
for (j <- i) yield {
if (j.method) {
returnSomething()
// etc
Maintenant, il y a une grande différence entre
for (i <- expr1; j <- i) yield ...
et
for (i <- expr1) yield for (j <- i) yield ...
Ils retour des choses différentes, et il y a des moments que vous voulez le plus tard, pas le premier. Je suppose que vous voulez que l'ancienne, cependant. Maintenant, avant d'aller plus loin, nous allons fixer le code. Il est laid, difficile à suivre et uninformative. Le refactoring Let par extraction des méthodes.
def resultOrB(j) = if (j.method) returnSomething else returnSomethingElseB
def nonCResults(i) = for (j <- i) yield resultOrB(j)
def resultOrC(i) = if (i.method) nonCResults(i) else returnSomethingC
val result = for (i <- expr1) yield resultOrC(i)
Il est déjà beaucoup plus propre, mais il ne revient pas tout à fait ce que nous attendons. Regardons la différence:
trait Element
object Unrecognized extends Element
case class Letter(c: Char) extends Element
case class Punct(c: Char) extends Element
val expr1 = "This is a silly example." split "\\b"
def wordOrPunct(j: Char) = if (j.isLetter) Letter(j.toLower) else Punct(j)
def validElements(i: String) = for (j <- i) yield wordOrPunct(j)
def classifyElements(i: String) = if (i.nonEmpty) validElements(i) else Unrecognized
val result = for (i <- expr1) yield classifyElements(i)
Le type de result
il y a Array[AnyRef]
, tout en utilisant de multiples générateurs donnerait Array[Element]
. La partie la plus facile du correctif est la suivante:
val result = for {
i <- expr1
element <- classifyElements(i)
} yield element
Mais cela seul ne fonctionnera pas, parce que classifyElements se retourne AnyRef
, et nous voulons le retourner une collection. Maintenant, validElements
revenir une collection, donc ce n'est pas un problème. Nous avons seulement besoin de fixer la partie else
. Depuis validElements
retourne un IndexedSeq
, Retournons que de la part de else
ainsi. Le résultat final est:
trait Element
object Unrecognized extends Element
case class Letter(c: Char) extends Element
case class Punct(c: Char) extends Element
val expr1 = "This is a silly example." split "\\b"
def wordOrPunct(j: Char) = if (j.isLetter) Letter(j.toLower) else Punct(j)
def validElements(i: String) = for (j <- i) yield wordOrPunct(j)
def classifyElements(i: String) = if (i.nonEmpty) validElements(i) else IndexedSeq(Unrecognized)
val result = for {
i <- expr1
element <- classifyElements(i)
} yield element
Ce fait exactement la même combinaison de boucles et les conditions que vous avez livré, mais il est beaucoup plus facile à lire et facile à changer.
A propos de rendement
Je pense qu'il est important de noter une chose au sujet du problème présenté. Simplifions il:
for (i <- expr1) {
for (j <- i) {
doSomething
}
}
, qui est mis en œuvre avec foreach
(voir , ou d'autres questions similaires et répond). Cela signifie que le code ci-dessus fait exactement la même chose que ce code:
for {
i <- expr1
j <- i
} doSomething
Exactement la même chose. Ce n'est pas vrai du tout quand on utilise yield
. Les expressions suivantes ne donnent pas le même résultat:
for (i <- expr1) yield for (j <- i) yield j
for (i <- expr1; j <- i) yield j
Le premier extrait sera mis en œuvre à travers deux appels map
, tandis que le second extrait utilisera un flatMap
et un map
.
Ainsi, il est seulement dans le contexte de yield
qu'il fait même aucun sens à se soucier de l'imbrication des boucles de for
ou en utilisant plusieurs générateurs. Et, en fait, générateurs représente le fait que quelque chose est généré , qui est seulement vrai de vrai pour-compréhensions (ceux yield
ing quelque chose).
Autres conseils
La partie
for (j <- i) {
if (j.method) {
doSomething(j)
} else {
doSomethingElse(j)
}
}
peut être réécrite comme
for(j <- i; e = Either.cond(j.method, j, j)) {
e.fold(doSomething _, doSomethingElse _)
}
(bien sûr, vous pouvez utiliser un rendement plutôt que si votre do .. méthodes renvoient quelque chose)
Ici, il est pas si terrible utile, mais si vous avez une structure plus profonde imbriquée, il pourrait ...
import scalaz._; import Scalaz._
val lhs = (_ : List[X]) collect { case j if j.methodJ => doSomething(j) }
val rhs = (_ : List[X]) map doSomethingElse
lhs <-: (expr1 partition methodI) :-> rhs
Vous ne pouvez pas. Le for (expr, si). Construction du filtre juste l'élément qui doit être manipulé dans la boucle
Si l'ordre n'a pas d'importance pour les appels à doSomething () et doSomethingElse (), vous pouvez réorganiser le code comme celui-ci.
val (tmp, no1) = expr1.partition(_.method)
val (yes, no2) = tmp.partition(_.method)
yes.foreach(doSomething())
no1.foreach(doSomethingElse())
no2.foreach(doSomethingElse())
Pour répondre à votre question initiale, je pense que pour compréhensions peut être très agréable pour les cas d'utilisation spécifiques, et votre exemple ne correspond pas bien.
Les conditions spécifiées dans un acte de Scala de fonctionnement pour filtrer les éléments des générateurs. Les éléments ne satisfaisant pas les conditions sont rejetées et ne sont pas présentés au rendement / bloc de code.
Ce que cela signifie est que si vous souhaitez effectuer d'autres opérations sur la base d'une expression conditionnelle, les besoins de test pour être reportés au bloc rendement / code.
Il faut aussi savoir que le fonctionnement est relativement coûteux à calculer (actuellement) alors peut-être un peut-être quelque chose comme approche itérative plus simple pourrait être plus approprié,:
expr1 foreach {i =>
if (i.method) {
i foreach {j =>
if (j.method)
doSomething()
else
doSomethingElseA()
}
}
else
doSomethingElseB()
}
Mise à jour:
Si vous devez utiliser une pour la compréhension et vous pouvez vivre avec certaines restrictions, ce travail pourrait:
for (i <- expr1; j <- i) {
if (i.method) {if (j.method) doSomething() else doSomethingElseA()} else doSomethingElseB()
}