: map, flatMap, fliter 같은 고차 함수가 리스트를 처리하는 강력한 구성 요소이지만 때로 프로그램을 이해하기 힘들 수 있다.

Ex.

scala> case class Person(name: String,
                         isMale: Boolean,
                         children: Person*)

 

val lara = Person("Lara", false)
val bob = Person("Bob", true)
val julie = Person("Julie", false, lara, bob)
val persons = List(lara, bob, julie)

 

" 모든 엄마와 자식의 쌍을 찾고 싶다. "

scala> persons filter (p => !p.isMale) flatMap (p =>
  |     (p.children map (c => (p.name, c.name))))
res0: List[(String, String)] = List((Julie,Lara), (Julie,Bob))

 

" filter 대신 withFilter를 사용하면 중간 데이터 구조를 만들 필요가 없다. "

scala> persons withFilter (p => !p.isMale) flatMap (p =>
  |     (p.children map (c => (p.name, c.name))))
res1: List[(String, String)] = List((Julie,Lara), (Julie,Bob))

 

" for 표현식이 이해가 쉽고 명확하다. "

scala> for (p <- persons; if !p.isMale; c <- p.children)
  | yield (p.name, c.name)
res2: List[(String, String)] = List((Julie,Lara), (Julie,Bob))

: 스칼라 컴파일러는 결과를 yield하는 모든 for 표현식을 map, flatMap, withFilter 등의 고차 메소드 호출을 조합한 표현식으로 반환한다. yield가 없는 for 표현식은 withFilter와 foreach만을 사용한다.

 

23.1 for 표현식

" 일반적인 for 표현식 "

for ( seq ) yield expr

: seq는 제너레이터, 정의, 필터를 나열한 것으로 연속된 구성요소 사이에는 세미콜론을 넣는다. 괄호 대신 중괄호를 사용하면 세미콜론을 생략해도 된다.

for {
  p <- persons              // a generator
  n = p.name                // a definition
  if (n startsWith "To")    // a filter
} yield n

" 제너레이터의 형태 "

pat <- expr

: expr 표현식은 보통 리스트를 반환한다

: pat 패턴은 리스트의 모든 원소를 하나씩 매치해 얻는다. 매치에 성공하면 패턴 안의 변수에는 리스트 원소의 해당 부분에 있는 값이 들어간다. 매치에 실패하도 MatchError를 발생시키지 않고 무시하고 다음 이터레이션을 진행한다.

: x<-expr 처럼 pat가 변수 하나로만 되어 있는 경우가 가장 일반적이고 변수 x는 expr이 반환하는 모든 원소를 단지 이터레이션

 

" 정의의 형태 "

pat = expr

: pat 패턴을 expr의 값에 바인딩하고 val이 된다.

: x = expr 처럼 변수 하나로 되어 있는 경우가 가장 일반적

 

" 필터의 형태 "

if expr

: expr은 Boolean 타입의 값이다. expr이 false를 반환하는 값을 이터레이션에서 제외하도록 만든다.

 

23.2 n 여왕 문제

: N*N 체스판에서 n개의 여왕 말을 서로 잡지 못하게 놓는 문제 (여왕 말은 같은 열, 행, 또는 대각선 상에 있으면 서로를 잡을 수 있다.)

=> N*N 체스판의 왼쪽 위 구석 칸은 (1,1), 오른쪽 아래 칸은 (N,N)

=> 같은 행에 있으면 안 되기 때문에 모든 행마다 여왕을 배치 => 각 행에 차례로 여왕말 배치, 이 때 새로운 행에 놓은 여왕이 이미 체스판에 위치한 다른 여왕들을 잡을 수 있는 위치인지 검사

=> 1 부터 k-1번째 행까지의 여왕말을 잡을 수 없는 k번째 행 여왕말을 어디에 배치할 지 결정하는 문제

=> 문제의 해를 각 여왕의 체스판 위치를 표시하는 좌표 리스트로 표현

=> k번째 행에 있는 여왕의 좌표를 리스트의 맨 앞에, 그 뒤에 k-1번째 행에 있는 여왕을 놓는 식으로 해를 도출


def queens(n: Int): List[List[(Int, Int)]] = {
  def placeQueens(k: Int): List[List[(Int, Int)]] =
    if (k == 0)
      List(List())
    else
      for {
        queens <- placeQueens(k - 1)     // 체스판에 k-1 여왕을 놓는 모든 해에 대해 이터레이션
        column <- 1 to n                             // k번째 여왕이 들어가야 할 모든 열의 좌표에 대해 이터레이션
        queen = (k, column)
        if isSafe(queen, queens)              // 새 여왕의 위치가 기존 모든 여왕을 고려했을 때 안전한지 결정
      } yield queen :: queens

  placeQueens(n)
}

def isSafe(queen: (Int, Int), queens: List[(Int, Int)]) =
  queens forall (q => !inCheck(queen, q))

def inCheck(q1: (Int, Int), q2: (Int, Int)) =
  q1._1 == q2._1 ||  // same row
    q1._2 == q2._2 ||  // same column
    (q1._1 - q2._1).abs == (q1._2 - q2._2).abs // on diagonal

 

23.3 for 식으로 질의하기

: for 표기법은 기본적으로 데이터베이스 질의 언어의 공통 연산들과 동등하다.

case class Book(title: String, authors: String*)


val books: List[Book] =
  List(
    Book(
      "Structure and Interpretation of Computer Programs",
      "Abelson, Harold", "Sussman, Gerald J."
    ),
    Book(
      "Principles of Compiler Design",
      "Aho, Alfred", "Ullman, Jeffrey"
    ),
    Book(
      "Programming in Modula-2",
      "Wirth, Niklaus"
    ),
    Book(
      "Elements of ML Programming",
      "Ullman, Jeffrey"
    ),
    Book(
      "The Java Language Specification", "Gosling, James",
      "Joy, Bill", "Steele, Guy", "Bracha, Gilad"
    )
  )

 

" 작사의 성이 Gosling인 모든 책의 제목을 찾는다. "

scala> for (b <- books; a <- b.authors
  |      if a startsWith "Gosling")
  | yield b.title
res4: List[String] = List(The Java Language Specification)

" 제목에 Program이라는 문자열이 들어간 모든 책의 제목을 찾는다. "
scala> for (b <- books if (b.title indexOf "Program") >= 0)
  | yield b.title
res5: List[String] = List(Structure and Interpretation of
  Computer Programs, Programming in Modula-2, Elements
  of ML Programming)

" 최소한 두 권 이상의 책을 쓴 작가를 모두 찾는다. "
scala> for (b1 <- books; b2 <- books if b1 != b2;
            |     a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
  | yield a1
res6: List[String] = List(Ullman, Jeffrey, Ullman, Jeffrey)

" 리스트의 중복을 제거한다. "
scala> def removeDuplicates[A](xs: List[A]): List[A] = {
  |   if (xs.isEmpty) xs
  |   else
  |     xs.head :: removeDuplicates(
  |       xs.tail filter (x => x != xs.head)      // for (x <- xs.tail if x != xs.head) yield x
  |     )
  | }
removeDuplicates: [A](xs: List[A])List[A]

scala> removeDuplicates(res6)
res7: List[String] = List(Ullman, Jeffrey)

 

23.4 for 표현식 변환

: 모든 for 표현식은 map, flatMap, withFilter 고차 함수로 표현가능하고 스칼라 컴파일러는 for 표현식을 고차 함수로 변환한다.

 

< 제너레이터가 하나 밖에 없는 for 표현식의 변환 >

for ( x <- expr1 ) yield expr2

=>

expr1. map( x => expr2 )

 

< 제너레이터로 시작하고 필터가 하나 있는 for 표현식의 변환 >

for ( x <- expr1 if expr2 ) yield expr3

=>

for ( x <- expr1 withFilter (x => expr2 ) ) yield expr3

=>

expr1 withFilter ( x=>expr2 ) map ( x => expr3 )

 

" 필터 다음에 다른 원소가 있더라도 마찬가지로 변환 "

for ( x <- expr1 if expr2; seq ) yield expr3    // seq : 제너레이터, 정의, 필터

=>

for ( x <- expr1 withFilter (x => expr2 ) ; seq ) yield expr3

 

< 제너레이터 2개로 시작하는 for 표현식의 변환 >

for ( x <- expr1 ; y <- expr2; seq ) yield expr3

=>

expr1.flatMap( x => for ( y <- expr2; seq ) yield expr3 ) 

: 위 3가지 변환을 통해 제너레이터와 필터로 구성되고 제너레이터가 단순한 변수에 값을 바인딩하는 모든 for 표현식을 변환할 수 있다.

Ex.

for (b1 <- books; b2 <- books if b1 != b2;
     a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
  yield a1


books flatMap (b1 =>
  books withFilter (b2 => b1 != b2) flatMap (b2 =>
    b1.authors flatMap (a1 =>
      b2.authors withFilter (a2 => a1 == a2) map (a2 =>
        a1))))

 

< 제너레이터에 있는 패턴의 변환 >

: 제너레이터의 좌항이 단순한 변수가 아니고 패턴 pat라면 변환 방법이 복잡

Ex. 변수의 튜플에 바인딩하는 제너레이터

for ( ( x1,  ...  ,  xn)  <- expr1 ) yield expr2

=>

expr1.map { case ( x1, ... , xn) => expr2 }

Ex. 임의의 패턴 pat라면

for ( pat <- expr1 ) yield expr2

=>

expr1 withFilter {

  case pat => true

  case _ => false

} map {

   case pat => expr2

}

: 제너레이터가 만들어내는 원소들 중에서 pat와 일치하는 것만 걸러내서 map에 넘긴다.

: 결코 MatchError가 발생하지 않는다.

 

< 정의 변환 >

for ( x <- expr1; y = expr2; seq) yield expr3

=>

for ( ( x,y ) <- for ( x<- expr1)  yield (x, expr2); seq)   yield expr3

: expr2가 계산되는 시점은 새로운 x 값을 제너레이터에서 가져올 때 => expr2 안에서 x를 사용한다면 각 이터레이션마다 가져온 값을 반영한다.

: for 표현식에 어떤 정의를 넣을 때 그 정의보다 앞에 있는 제너레이터가 바인딩한 변수를 사용하지 않는 정의를 넣는 것은 바람직하지 않다.

for (x <- 1 to 1000; y = expensiveComputationNotInvolvingX)
  yield x * y

" 권장 "
val y = expensiveComputationNotInvolvingX
for (x <- 1 to 1000) yield x * y

 

< for 루프 변환 >

: 아무 값도 돌려주지 않고(yield를 사용하지 않고) 부수 효과만 사용하는 for 루프는 foreach만 사용한다.

for ( x <- expr1 ) body

=>

expr1 foreach ( x => body )

 

for ( x<-expr1; if expr2; y <- expr3 ) body

=>

expr1 withFilter ( x => expr2 ) foreach ( x =>

     expr3 foreach ( y => body ))

 

23.5 역방향 적용

: 모든 map, flatMap, filter 호출은 for 표현식으로 표현할 수 있다.

object Demo {
  def map[A, B](xs: List[A], f: A => B): List[B] =
    for (x <- xs) yield f(x)

  def flatMap[A, B](xs: List[A], f: A => List[B]): List[B] =
    for (x <- xs; y <- f(x)) yield y

  def filter[A](xs: List[A], p: A => Boolean): List[A] =
    for (x <- xs if p(x)) yield x
}

 

23.6 for 일반화

: 스칼라 컴파일러는 for 표현식을 오직 map, flatMap, withFilter 메소드만을 사용해 변환한다.(for 루프는 foreach) 사용자가 정의한 데이터 타입 안에 map, flatMap, withFilter, foreach 메소드를 정의하면 for 표현식을 완벽히 지원할 수 있다.

: 리스트나 배열, 스칼라 표준 라이브러리에는 네 가지 메소드를 정의했기 때문에 for 표현식/루프 사용이 가능하다.

: 네 가지 메소드 중 일부만 정의했다면 for 표현식/루프 기능 중 일부만 사용 가능하다.

 

- 정확한 규칙

1. map만 정의했다면 제너레이터가 1개만 있는 for 표현식을 사용할 수 있다.

2. map과 flatMap을 함께 정의했다면 제너레이터가 여럿 있는 for 표현식도 사용 가능하다.

3. foreach를 정의했다면 for 루프(제너레이터 개수는 관계없음)를 사용할 수 있다.

4. withFilter를 정의하면 for 표현식 안에서 if로 시작하는 for 필터 표현식을 사용할 수 있다.

 

- 컴파일러는 for 표현식에서 고차 함수로의 변환을 타입 검사 이전에 수행한다. 그렇기 때문에 최대한의 유연성을 제공할 수 있다. for 표현식을 변환해 나온 결과만을 타입 검사하면 되기 때문이다. 스칼라에는 for 표현식 자체에 대한 타입 검사 규칙은 없다. 또한 map, flatMap, withFilter, foreach가 특정 타입 시그니처를 만족하도록 요구하지도 않는다.

 

- for 표현식을 변환할 때 사용하는 고차 메소드들의 가장 일반적인 의도를 담는 전형적인 설정이 있다.

abstract class C[A] {                     // 어떤 종류의 컬렉션을 나타내는 파라미터화된 클래스
  def map[B](f: A => B): C[B]
  def flatMap[B](f: A => C[B]): C[B]
  def withFilter(p: A => Boolean): C[A]
  def foreach(b: A => Unit): Unit
}

: withFilter 호출 뒤에는 항상 map, flatMap, foreach 중 하나가 호출되어 withFilter가 만든 객체는 그 즉시 다른 메소드로 인해 사용된다. => withFilter와 map/flatMap/foreach 사이에 withFilter로 생긴 새로운 컬렉션에 대한 작업이 없다. => 새로운 컬렉션을 만들지 말고 withFilter로 반환된 원소를 즉시 map/flatMap/foreach에 적용한다.

=> 이를 위한 표준적인 방법은 withFilter가 C를 반환하는 대신 나중에 처리할 수 있는 필터링된 원소를 기억하는 래퍼 객체를 만들어 반환하는 것이다.

 

* 모나드

- 타입을 인자로 받는 타입이다. (M[T])

- 임의 타입의 값을 받아서 그 타입을 인자로 받은 모나드 타입의 값을 반환하는 함수가 있어야 한다. ( T 타입의 값을 받아서 M[T] 타입의 값을 반환하는 함수 - unit operator )

- T 타입의 변수를 받아 M[U] 타입의 모나드를 반환하는 함수를 인자로 받아 M[U] 타입의 값을 반환하는 함수가 있어야 한다.( bind operator ) => 모나드에서 다른 모나드로 진행할 수 있다.

: 타입을 포함하는 컬렉션이 대표적이고 map, flatMap, withFilter을 통해 상태나 I/O, 백트래킹, 트랙잭션을 수행한다. => for 표현식의 개념이 단순히 컬렉션에 대해 이터레이션하는 것보다 더 일반적인 개념

: map, flatMap, withFilter에다가 원소 값을 하나 받아 모나드를 만들어 주는 unit operator을 추가하면 모든 모나드를 표현할 수 있다. => unit operator은 인스턴스 생성자/ 팩토리 메소드 => map, flatMap, withFilter는 모나드 개념을 객체지향에 적용한 것

 

+ Recent posts