for-内包表記からflatMap / Mapへの変換と混同される


87

MapとFlatMapを理解していないようです。私が理解していないのは、for-incomhensionがmapとflatMapへのネストされた呼び出しのシーケンスであるということです。次の例は、Scalaの関数型プログラミングの例です

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
            f <- mkMatcher(pat)
            g <- mkMatcher(pat2)
 } yield f(s) && g(s)

に翻訳する

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = 
         mkMatcher(pat) flatMap (f => 
         mkMatcher(pat2) map (g => f(s) && g(s)))

mkMatcherメソッドは次のように定義されます。

  def mkMatcher(pat:String):Option[String => Boolean] = 
             pattern(pat) map (p => (s:String) => p.matcher(s).matches)

そして、パターンメソッドは次のとおりです:

import java.util.regex._

def pattern(s:String):Option[Pattern] = 
  try {
        Some(Pattern.compile(s))
   }catch{
       case e: PatternSyntaxException => None
   }

ここでmapとflatMapを使用する背後にある理論的根拠を誰かが解明できれば素晴らしいことです。

回答:


197

TL; DRは最後の例に直接進みます

おさらいしてみます。

定義

for理解が結合する構文のショートカットであるflatMapmapおよそ読みやすいとの理由だ道インチ

少し単純化classして、前述の両方のメソッドを提供するものはすべてa monadと呼ぶことができ、シンボルM[A]を使用monadして内部タイプのaを意味するとしAます。

一般的に見られるモナドには次のものがあります。

  • List[String] どこ
    • M[X] = List[X]
    • A = String
  • Option[Int] どこ
    • M[X] = Option[X]
    • A = Int
  • Future[String => Boolean] どこ
    • M[X] = Future[X]
    • A = (String => Boolean)

地図とflatMap

ジェネリックモナドで定義 M[A]

 /* applies a transformation of the monad "content" mantaining the 
  * monad "external shape"  
  * i.e. a List remains a List and an Option remains an Option 
  * but the inner type changes
  */
  def map(f: A => B): M[B] 

 /* applies a transformation of the monad "content" by composing
  * this monad with an operation resulting in another monad instance 
  * of the same type
  */
  def flatMap(f: A => M[B]): M[B]

例えば

  val list = List("neo", "smith", "trinity")

  //converts each character of the string to its corresponding code
  val f: String => List[Int] = s => s.map(_.toInt).toList 

  list map f
  >> List(List(110, 101, 111), List(115, 109, 105, 116, 104), List(116, 114, 105, 110, 105, 116, 121))

  list flatMap f
  >> List(110, 101, 111, 115, 109, 105, 116, 104, 116, 114, 105, 110, 105, 116, 121)

表現のために

  1. <-記号を使用する式の各行はflatMap呼び出しに変換されますが、最後の行は終了map呼び出しに変換されますが、左側の「バインドされた記号」がパラメーターとして引数関数に渡されます(what以前に呼び出しましたf: A => M[B]):

    // The following ...
    for {
      bound <- list
      out <- f(bound)
    } yield out
    
    // ... is translated by the Scala compiler as ...
    list.flatMap { bound =>
      f(bound).map { out =>
        out
      }
    }
    
    // ... which can be simplified as ...
    list.flatMap { bound =>
      f(bound)
    }
    
    // ... which is just another way of writing:
    list flatMap f
  2. 1つだけのfor式<-map、式が引数として渡される呼び出しに変換されます。

    // The following ...
    for {
      bound <- list
    } yield f(bound)
    
    // ... is translated by the Scala compiler as ...
    list.map { bound =>
      f(bound)
    }
    
    // ... which is just another way of writing:
    list map f

さてポイントに

あなたが見ることができるように、map操作は、元の「形状」を保持するmonad同じことが起こるのためので、yield:表現ListのままListでの操作により変換コンテンツを持ちますyield

一方、の各結合線は、for連続したの構成にすぎずmonads、単一の「外部形状」を維持するために「平坦化」する必要があります。

少しの間、各内部バインディングがmap呼び出しに変換されたと仮定しますが、右辺は同じA => M[B]関数M[M[B]]であり、内包表記の各行にa を使用することになります。構文
全体の意図は、for連続するモナド演算(つまり、「モナド形状」の値を「持ち上げる」演算)の連結を簡単に「フラット化」しA => M[B]、最終的な変換を実行する可能性のある最終map演算を追加することです。

これが、機械的な方法で適用される変換の選択の背後にあるロジック、つまりn flatMap、単一のmap呼び出しによって終了するネストされた呼び出しが説明されることを願っています。

構文の
表現力を示すために考案された実例for

case class Customer(value: Int)
case class Consultant(portfolio: List[Customer])
case class Branch(consultants: List[Consultant])
case class Company(branches: List[Branch])

def getCompanyValue(company: Company): Int = {

  val valuesList = for {
    branch     <- company.branches
    consultant <- branch.consultants
    customer   <- consultant.portfolio
  } yield (customer.value)

  valuesList reduce (_ + _)
}

あなたはタイプを推測できますかvaluesList

すでに述べたように、の形はmonad理解によって維持されるため、Listin company.branchesで始まり、で終わる必要がありListます。
代わりに、内部の型が変化し、次のyield式によって決定されます。customer.value: Int

valueList でなければなりません List[Int]


1
「と同じ」という単語はメタ言語に属しており、コードブロックの外に移動する必要があります。

3
すべてのFP初心者はこれを読んでください。どうすればこれを達成できますか?
2014

1
@melstonで例を作ってみましょうLists。ある値mapに対して関数A => List[B](必須のモナディック演算の1つ)を2回実行すると、List [List [B]]になります(型が一致していることは当然です)。for内包の内部ループは、対応するflatMap操作でこれらの関数を構成し、List [List [B]]形状を単純なList [B]に「フラット化」します。これが明確であることを願っています
pagoda_5b

1
それはあなたの答えを読む純粋な素晴らしさです。Scalaについての本を書いてほしいと思いますが、ブログなどはありますか?
Tomer Ben David

1
@coolbreezeはっきりと表現しなかったのかもしれません。私が意味したことは、yield節はcustomer.valueであり、その型はIntであるため、全体はにfor comprehension評価されますList[Int]
pagoda_5b

6

私はスカラメガマインドではないので、自由に修正してください。しかし、これが私がflatMap/map/for-comprehensionサガを私に説明する方法です!

理解しfor comprehension、それを翻訳するscala's map / flatMapには、小さな手順を踏んで構成部分を理解する必要がmapありflatMapます。しかし、あなた自身に尋ねるscala's flatMapだけmapではありませんflatten!もしそうなら、なぜ多くの開発者はそれを理解するのがとても難しいと感じるのでしょうかfor-comprehension / flatMap / map。まあ、スカラmapflatMapシグネチャだけを見ると、同じ戻り値の型M[B]を返し、同じ入力引数A(少なくとも、関数が取る最初の部分)で機能することがわかります。

私たちの計画

  1. Scalaを理解するmap
  2. Scalaを理解するflatMap
  3. for comprehensionScala の.`を理解する

Scalaのマップ

スカラマップ署名:

map[B](f: (A) => B): M[B]

しかし、このシグネチャを見たときに欠けている大きな部分があり、それは-これAはどこから来たのですか?私たちのコンテナはタイプなAので、コンテナのコンテキストでこの関数を見ることが重要です- M[A]。コンテナListはタイプのアイテムのa である可能性がAあり、map関数はタイプの各アイテムをタイプAに変換する関数を受け取りB、次にタイプB(またはM[B])のコンテナを返します

コンテナを考慮してマップの署名を書きましょう:

M[A]: // We are in M[A] context.
    map[B](f: (A) => B): M[B] // map takes a function which knows to transform A to B and then it bundles them in M[B]

マップについて非常に非常に重要な事実に注意してください。それは、制御できない出力コンテナーに自動的にバンドルされM[B]ます。もう一度強調しましょう:

  1. map私たちのために出力コンテナーを選択し、そのコンテナーは作業するソースと同じコンテナーになるので、M[A]コンテナーについては同じMコンテナーのみを取得し、B M[B]他には何もありません!
  2. mapこのコンテナ化を行うのは、マッピングをAtoに与えるだけBで、それをボックスにM[B]入れて、ボックスに入れます!

containerize内部アイテムの変換方法を指定したばかりのアイテムに、どのように指定したのかわかりません。そして、我々は同じコンテナ持っているようにM、両方のためにM[A]M[B]、この手段はM[B]あなたが持っている場合を意味し、同一の容器であるList[A]、あなたが持ってしようとしているList[B]と、もっと重要なのはmapあなたのためにそれをやっています!

これで対処したので、に進みmapましょうflatMap

ScalaのflatMap

その署名を見てみましょう:

flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]

地図との大きな違いflatMapがわかります。flatMapでは、変換するだけでA to Bなくにコンテナ化する関数も提供していますM[B]

なぜコンテナ化を誰が行うのか気にするのですか?

では、なぜmap / flatMapへの入力関数をそれほど注意してコンテナ化するのM[B]か、またはマップ自体がコンテナ化するのでしょうか。

for comprehension何が起こっているのかを見ると、で提供されるアイテムに複数の変換があることがforわかります。そのため、組立ラインの次の作業者にパッケージを決定する機能を提供します。各ワーカーが製品に対して何かを行う組立ラインがあり、最後のワーカーのみがそれをコンテナーにパッケージ化していると想像してください!flatMapこれへの歓迎はそれが目的です、mapアイテムでの作業が終了したときに、各ワーカーはそれをパッケージ化してコンテナよりコンテナを取得します。

理解のための強力な

次に、上で述べたことを考慮して、理解度を調べます。

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
    f <- mkMatcher(pat)   
    g <- mkMatcher(pat2)
} yield f(s) && g(s)

ここには何があります:

  1. mkMatchercontainerコンテナが関数を含むコンテナを返します:String => Boolean
  2. ルールは、最後のルール<-flatMap除いて複数に翻訳する場合です。
  3. 以下のようf <- mkMatcher(pat)に最初であるsequence(と思うassembly line我々はそれが取ることですから出たいすべて)fと組立ラインの次の作業員に渡し、我々は組立ラインで次の作業員(次の関数)であるもの決定する能力を聞かせてこれが私たちのアイテムのバックのパッケージングであり、これが最後の機能である理由ですmap
  4. ラストg <- mkMatcher(pat2)mapこれを使うでしょう、それはその組立ラインの最後だからです!ですから、最終的な操作を実行できmap( g =>ます。引き出しgや使用f済みによって容器から引き出されたがflatMapゆえ、我々は最初で終わります:

    mkMatcher(pat)flatMap(f // f関数を引き出して、次の組立ラインワーカーにアイテムを与える(これにはへのアクセス権がありf、パッケージ化しないでください。つまり、マップがパッケージ化を決定し、次のアセンブリラインワーカーがcontainer。mkMatcher(pat2)map(g => f(s)...))//これはアセンブリーラインの最後の関数なので、mapを使用してgをコンテナーから引き出し、パッケージングに戻しますその、mapそしてこのパッケージングは​​、ずっとスロットルを上げて、私たちのパッケージまたは私たちのコンテナです、そうです!


4

理論的根拠は、モナド演算を連鎖させることです。これは、利点として、適切な「フェイルファースト」エラー処理を提供します。

それは実際にはかなり単純です。mkMatcherこの方法は、返しOption(モナドです)。mkMatcherモナド演算であるの結果は、NoneまたはのいずれかSome(x)です。

適用するmapflatMapに機能することはNone、常に返すNone-関数がパラメータとして渡されたmapflatMap評価されません。

したがって、例でmkMatcher(pat)は、Noneを返す場合、それに適用されたflatMapはaを返しNone(2番目のモナディック演算mkMatcher(pat2)は実行されません)、最終mapは再びを返しますNone。言い換えると、for内のいずれかの操作がNoneを返した場合、フェイルファースト動作が発生し、残りの操作は実行されません。

これは、エラー処理のモナディックスタイルです。命令型スタイルは、基本的に(catch句への)ジャンプである例外を使用します。

最後の注意:patterns関数は、命令スタイルのエラー処理(try... catch)をモナディックスタイルのエラー処理に "変換"する典型的な方法です。Option


なぜあなたは知っていますflatMap(とではないがmap)、「CONCATENATE」の第一および第二の呼び出しに使用されているmkMatcherが、なぜmap(とないflatMap)「CONCATENATE」第二使用されているmkMatcheryieldsブロックを?
Malte Schwerhoff 2013年

1
flatMapモナドで「ラップ」/リフトされた結果を返す関数を渡すことを期待していますmapが、ラップ/リフト自体は行います。操作の連鎖を通話中にfor comprehensionあなたがする必要がflatmapパラメータとして渡された関数が返すことができますようにNone(あなたはなしに値を持ち上げることはできません)。最後の操作呼び出しであるの1つがyield実行され、値返します。map最後の操作は十分とモナドに関数の結果を持ち上げる必要がなくなりあることチェーンへ。
Bruno Grieder 2013年

1

これは次のように解釈できます。

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
    f <- mkMatcher(pat)  // for every element from this [list, array,tuple]
    g <- mkMatcher(pat2) // iterate through every iteration of pat
} yield f(s) && g(s)

これを実行すると、その拡張方法のより良いビューが表示されます

def match items(pat:List[Int] ,pat2:List[Char]):Unit = for {
        f <- pat
        g <- pat2
} println(f +"->"+g)

bothMatch( (1 to 9).toList, ('a' to 'i').toList)

結果は次のとおりです。

1 -> a
1 -> b
1 -> c
...
2 -> a
2 -> b
...

これは同様であるflatMap各要素をループ- patとforeachの要素mapの各要素にpat2


0

まず、mkMatcher関数に示されているように、シグネチャがString => Booleanである関数を返します。これは、実行される通常のJavaプロシージャです。次に、この行を見てくださいPattern.compile(string)pattern

pattern(pat) map (p => (s:String) => p.matcher(s).matches)

map関数が結果に適用されてpatternいる、Option[Pattern]ので、p中には、p => xxxあなたがコンパイルしただけのパターンです。したがって、patternが指定されるとp、Stringを取り、パターンに一致するsかどうかを確認する新しい関数が構築されsます。

(s: String) => p.matcher(s).matches

p変数はコンパイルされたパターンにバインドされていることに注意してください。これで、署名付きの関数がによってどのようにString => Boolean構築されるかは明らかですmkMatcher

次に、にbothMatch基づく関数をチェックアウトしますmkMatcher。どのようにbothMathch機能するかを示すために、まずこの部分を見てみましょう。

mkMatcher(pat2) map (g => f(s) && g(s))

我々は、署名付き機能得たのでString => BooleanからmkMatcherであり、gこの文脈では、g(s)と等価であるPattern.compile(pat2).macher(s).matches文字列の一致パターン場合返し、pat2。それでは、どの程度f(s)それは同じですが、g(s)唯一の違い、それは、最初の呼び出しでmkMatcherの使用flatMap、代わりにmap、なぜ?のでmkMatcher(pat2) map (g => ....)戻ってOption[Boolean]、ネストされた結果を得るでしょうOption[Option[Boolean]]、あなたが使用している場合map、あなたが望むものではありません両方の呼び出し、ため。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.