flatMap / Map 변환에 대한 이해를위한 것과 혼동


87

나는 정말로 Map과 FlatMap을 이해하지 못하는 것 같습니다. 내가 이해하지 못하는 것은 for-comprehension이 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을 사용하는 이유에 대해 밝힐 수 있다면 좋을 것입니다.

답변:


199

TL; DR은 최종 예제로 직접 이동

나는 노력하고 요약 할 것이다.

정의

for이해가 결합 구문 바로 가기입니다 flatMapmap읽기에 대한 이유 쉽다 방법이다.

일을 조금 단순화하고 class앞서 언급 한 두 가지 방법을 모두 제공 하는 모든 것이 a라고 할 수 있다고 가정 하고 내부 유형이있는 a를 의미하는 monad기호 M[A]를 사용할 것 입니다.monadA

일반적으로 볼 수있는 몇 가지 모나드는 다음과 같습니다.

  • 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)

map 및 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. 하나만있는 for-expression 은 인수로 전달 된 표현식이 <-있는 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식 : A는 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
"is the same as"라는 단어는 메타 언어에 속하며 코드 블록에서 제거되어야합니다.

3
모든 FP 초보자는 이것을 읽어야합니다. 이것이 어떻게 달성 될 수 있습니까?
mert inan 2014 년

1
@melston을 사용하여 예제를 만들어 봅시다 Lists. 어떤 값에 map대해 함수 A => List[B](필수 모나드 연산 중 하나) 를 두 번 수행하면 List [List [B]]가됩니다 (유형이 일치하는 것은 당연한 것입니다). for comprehension 내부 루프 flatMap는 List [List [B]] 모양을 간단한 List [B]로 "평탄화" 하는 해당 작업을 사용하여 이러한 함수를 구성합니다. 이것이 명확하길 바랍니다
pagoda_5b

1
당신의 대답을 읽는 것은 순수한 굉장함이었습니다. 스칼라에 관한 책을 쓰셨 으면 좋겠어요. 블로그가 있나요?
Tomer Ben David

1
@coolbreeze 명확하게 표현하지 않았을 수도 있습니다. 내가 의미하는 바는 yield절이 customer.value그 유형 Int이고 따라서 전체 for comprehensionList[Int].
pagoda_5b

7

나는 스칼라 메가 마인드가 아니기 때문에 자유롭게 나를 바로 잡으십시오. 그러나 이것이 내가 flatMap/map/for-comprehension사가를 나 자신에게 설명하는 방법입니다 !

이해하기 for comprehension과에 그것의 번역 scala's map / flatMap우리는 작은 조치를 취할과 구성 부품을 이해해야한다 - mapflatMap. 그러나없는 scala's flatMap단지 mapflatten당신이 너 자신을 물어! 그렇다면 왜 그렇게 많은 개발자들이 그것 또는 for-comprehension / flatMap / map. 음, 스칼라 mapflatMap시그니처를 살펴보면 동일한 반환 유형을 반환 M[B]하고 동일한 입력 인수 A(적어도 사용하는 함수의 첫 번째 부분)에서 작동하는 것을 볼 수 있습니다. 그렇다면 무엇이 차이가나요?

우리의 계획

  1. 스칼라의 map.
  2. 스칼라의 flatMap.
  3. 스칼라의 for comprehension.` 이해

스칼라의지도

스칼라 맵 서명 :

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

그러나 우리가이 서명을 볼 때 누락 된 큰 부분이 있습니다. 이것은 어디 A에서 오는 것일까 요? 컨테이너는 유형 A이므로 컨테이너 의 컨텍스트에서이 함수를 보는 것이 중요합니다 M[A]. 우리의 컨테이너가 될 수있는 List유형의 항목을 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우리는 단지에서 매핑을 우리를 위해이 컨테이너 수송을 주는가 AB그리고의 상자에 넣어 것입니다 M[B]우리를 위해 상자에 넣어 것입니다!

containerize내부 항목을 변환하는 방법을 방금 지정한 항목에 지정하지 않은 것을 알 수 있습니다. 그리고 우리는 M둘 다에 대해 동일한 컨테이너 를 가지고 M[A]있고 M[B]이것은 M[B]동일한 컨테이너라는 것을 의미합니다. 즉, 만약 당신이 가지고 있다면 당신 List[A]은 a를 가질 List[B]것이고 더 중요한 map것은 당신을 위해 그것을하는 것입니다!

이제 우리는 처리했는지 map에에하자 이동 flatMap.

스칼라의 flatMap

그 서명을 보자 :

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

map flatMap에서 flatMap으로 변환 할 A to B뿐만 아니라 컨테이너화하는 함수를 제공하는 flatMap 의 큰 차이를 볼 수 있습니다 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사용 하므로 먼저 다음과 같이 끝납니다.fflatMap

    mkMatcher (pat) flatMap (f // pull out f function give item to next assembly line worker (you see it have access to f, and not package it back i mean let the map to determine the packaging let the next assembly line worker container. mkMatcher (pat2) map (g => f (s) ...)) // 이것이 어셈블리 라인의 마지막 함수이므로 map을 사용하고 컨테이너에서 g를 다시 패키징으로 당깁니다. , 그것 map과이 포장은 끝까지 조절되어 우리의 포장이나 용기가 될 것입니다, yah!


4

그 이유는 이점으로 적절한 "빠른 실패"오류 처리를 제공하는 모나드 연산을 연결하는 것입니다.

사실 꽤 간단합니다. mkMatcher방법은 리턴 Option(모나드이다). mkMatcher모나드 연산 의 결과는 a None또는 a Some(x)입니다.

적용 map또는 flatMapA와 기능을 None항상 것은 반환 None- 함수는 매개 변수로 전달 map하고 flatMap평가되지 않습니다.

따라서 귀하의 예제에서 mkMatcher(pat)None을 반환하면 적용된 flatMap은 a를 반환하고 None(두 번째 모나드 연산 mkMatcher(pat2)은 실행되지 않음) 최종 결과 map는 다시 None. 즉, for comprehension의 작업 중 하나라도 None을 반환하면 Fail Fast 동작이 발생하고 나머지 작업은 실행되지 않습니다.

이것은 오류 처리의 모나 딕 스타일입니다. 명령형 스타일은 기본적으로 점프 (catch 절로) 인 예외를 사용합니다.

마지막 참고 :이 patterns함수는 명령형 오류 처리 ( try... catch)를 다음을 사용하여 모나 딕 스타일 오류 처리 로 "변환"하는 일반적인 방법입니다.Option


의 첫 번째 및 두 번째 호출을 "연결"하는 데 사용되는 이유 flatMap(및 사용되지 않음 map) 를 알고 있습니까? 그러나 두 번째 및 블록을 "연결"하는 데 사용되는 mkMatcher이유 map(및 사용하지 않음 flatMap)를 알고 계십니까? mkMatcheryields
Malte Schwerhoff

1
flatMap모나드에서 "wrapped"/ lifted 결과를 반환하는 함수를 전달하고 map자체적으로 랩핑 / 리프팅을 수행합니다. (가)에서 작업의 체인 통화 중에 for comprehension당신이 필요 flatmap매개 변수로 전달 된 함수는 반환 할 수 있도록 None(당신이 없음에 값을 들어 올릴 수 없습니다). 마지막 작업 호출 인의 하나 yield가 실행 되고 값을 반환 할 것으로 예상됩니다 . map쇄 마지막 동작은 충분하며을 피는 모나드로 함수의 결과를 해제하는 데있다.
Bruno Grieder 2013 년

1

이것은 다음과 같이 traslated 될 수 있습니다.

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인 함수를 반환합니다. 이는 함수에 Pattern.compile(string)표시된대로 방금 실행되는 일반 Java 프로 시저입니다 pattern. 그럼이 줄을 봐

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

map기능의 결과에 적용 pattern하고, Option[Pattern]소위, p에가 p => xxx당신이 컴파일 된 바로 패턴이다. 따라서 pattern이 주어지면 pString을 취하고 패턴과 일치 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),, 유일한 차이점은 , 왜 대신에 mkMatcheruses 의 첫 번째 호출이라는 flatMap것입니다 map. mkMatcher(pat2) map (g => ....)returns 이므로 두 호출 모두에 사용하면 Option[Boolean]중첩 된 결과를 얻을 Option[Option[Boolean]]수 있습니다 map.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.