私は最近私を混乱させた非常に基本的な質問をしています。 Scala式が次のようなことをするために書きたい:
for (i <- expr1) {
if (i.method) {
for (j <- i) {
if (j.method) {
doSomething()
} else {
doSomethingElseA()
}
}
} else {
doSomethingElseB()
}
}
問題は、複数のジェネレーターで式の場合、式本体のそれぞれをどこに配置できるかわからないことです。
for {i <- expr1
if(i.method) // where can I write the else logic ?
j <- i
if (j.method)
} doSomething()
Scalaスタイルでコードを書き直すにはどうすればよいですか?
最初に書いたコードは完全に有効なので、書き直す必要はありません。他の場所では、Scalaスタイルでそれを行う方法を知りたいと言っていました。実際には「Scalaスタイル」というものはありませんが、もっと機能的なスタイルを想定して取り組んでいきます。
for (i <- expr1) {
if (i.method) {
for (j <- i) {
if (j.method) {
doSomething()
} else {
doSomethingElseA()
}
}
} else {
doSomethingElseB()
}
}
最初の懸念は、これが値を返さないことです。それがするのは副作用だけであり、それも避けるべきです。したがって、最初の変更は次のようになります。
val result = for (i <- expr1) yield {
if (i.method) {
for (j <- i) yield {
if (j.method) {
returnSomething()
// etc
今、間に大きな違いがあります
for (i <- expr1; j <- i) yield ...
そして
for (i <- expr1) yield for (j <- i) yield ...
それらは異なるものを返します、そしてあなたが前者ではなく後者を望む時があります。ただし、前者が必要だと思います。さて、先に進む前に、コードを修正しましょう。それは醜く、従うのが難しく、有益ではありません。メソッドを抽出してリファクタリングしましょう。
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)
すでにかなりクリーンになっていますが、期待どおりの結果が得られていません。違いを見てみましょう:
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)
result
のタイプはArray[AnyRef]
、複数のジェネレーターを使用するとArray[Element]
。修正の簡単な部分はこれです:
val result = for {
i <- expr1
element <- classifyElements(i)
} yield element
しかし、classifyElements自体がAnyRef
を返し、コレクションを返すようにしたいので、それだけでは機能しません。さて、validElements
はコレクションを返すので、それは問題ではありません。 else
の部分を修正するだけで済みます。 validElements
はIndexedSeq
を返しているので、それをelse
の部分にも返しましょう。最終結果は次のとおりです。
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
これは、提示したのとまったく同じループと条件の組み合わせを実行しますが、はるかに読みやすく、変更が簡単です。
収量について
提示された問題について1つのことに注意することが重要だと思います。単純化しましょう:
for (i <- expr1) {
for (j <- i) {
doSomething
}
}
現在、これはforeach
で実装されています( ここ 、または他の同様の質問と回答を参照してください)。つまり、上記のコードはこのコードとまったく同じことをします。
for {
i <- expr1
j <- i
} doSomething
まったく同じこと。 yield
を使用している場合、これはまったく当てはまりません。次の式では、同じ結果は得られません。
for (i <- expr1) yield for (j <- i) yield j
for (i <- expr1; j <- i) yield j
最初のスニペットは2つのmap
呼び出しによって実装され、2番目のスニペットは1つのflatMap
と1つのmap
を使用します。
したがって、yield
ループのネストや複数のジェネレーターの使用について心配することは、for
のコンテキストでのみ意味があります。そして、実際には、generatorsは、何かが生成されているという事実を表しています。これは、真の理解にのみ当てはまります( yield
ing何か)。
一部
for (j <- i) {
if (j.method) {
doSomething(j)
} else {
doSomethingElse(j)
}
}
次のように書き直すことができます
for(j <- i; e = Either.cond(j.method, j, j)) {
e.fold(doSomething _, doSomethingElse _)
}
(もちろん、do ..メソッドが何かを返す場合は、代わりにyieldを使用できます)
ここではそれほど便利ではありませんが、より深いネストされた構造がある場合は、...
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
それはいけません。 for(expr; if)構文は、ループで処理する必要のある要素をフィルタリングするだけです。
DoSomething()およびdoSomethingElse()の呼び出しで順序が重要でない場合は、次のようにコードを並べ替えることができます。
val (tmp, no1) = expr1.partition(_.method)
val (yes, no2) = tmp.partition(_.method)
yes.foreach(doSomething())
no1.foreach(doSomethingElse())
no2.foreach(doSomethingElse())
あなたの最初の質問に答えるために、私は、理解のために特定のユースケースにとって非常に良いかもしれないと思います、そしてあなたの例はうまく適合しません。
A Scala操作用に指定された条件は、ジェネレーターから要素をフィルター処理するように機能します。条件を満たさない要素は破棄され、yield/codeブロックに表示されません。
これが意味するのは、条件式に基づいて代替操作を実行する場合は、テストをyield/codeブロックに延期する必要があるということです。
また、for操作は(現在)計算に比較的コストがかかるため、おそらく次のような単純な反復アプローチの方が適切な場合があることにも注意してください。
expr1 foreach {i =>
if (i.method) {
i foreach {j =>
if (j.method)
doSomething()
else
doSomethingElseA()
}
}
else
doSomethingElseB()
}
更新:
理解のためにを使用する必要があり、いくつかの制限付きで生活できる場合、これは機能する可能性があります。
for (i <- expr1; j <- i) {
if (i.method) {if (j.method) doSomething() else doSomethingElseA()} else doSomethingElseB()
}