관리 메뉴

HAMA 블로그

스칼라 강좌 (29) - for comprehensions 본문

Scala

스칼라 강좌 (29) - for comprehensions

[하마] 이승현 (wowlsh93@gmail.com) 2016. 11. 12. 13:42

스칼라에서의 for - comprehensions


1. 개념 

스칼라 Doc 에서는 이렇게 말합니다. (http://docs.scala-lang.org/tutorials/FAQ/yield.html)

파이썬,루비등에 있는 yield 처럼 스칼라도 yield 를 가지고 있지만 좀 다릅니다. 스칼라의 yield 는 for comprehensions 의 일부분으로 사용되며, 다른 언어의 list-comprehensions 의 일반화 입니다. 스칼라의 "for comprehensions" 는 하스켈의 "do" 와 동등하며  멀티플모나딕 연산을 위한 사용편의 정도일 뿐입니다.

사실 for - comprehensions 같은것들은  syntactic sugar 라고 합니다. 번역하면 사용자가 편하게 사용하기 위한 사탕발림 쯤 되는데요. 일단 외우는세요.  자바나 C++에서 스칼라로 넘어 왔을때 사실 모든게 syntatic sugar 로 보이긴하죠 ㅎㅎ

스칼라 for - comprehensions 를 다양한곳에서 가져온 예제를 통해서 살펴보겠습니다. (개발자는 코드로 말합니다. 이해안가면  코드만 계속 반복해서 보세요. 각성 하실 겁니다. ) 

혹시 python 의 yield 를 사용했던 분이라면 그것은 순회하면서 하나씩 바깥으로 던지는데 반해, scala yield 는 모아뒀다가 컬렉션으로 던저 주는 느낌으로 받아 드리면 될 거 같습니다.


예제 0) flatMap  이란?

scala> val nestedNumbers = List(List(1, 2), List(3, 4)) scala> nestedNumbers.flatMap(x => x.map(_ * 2)) res0: List[Int] = List(2, 4, 6, 8)

하나의 리스트로 '합쳐' 준다는 것을 유념하세요. 그냥 map 을 적용시키면 List(2,4) List(6,8) 이렇게 나오겠죠.
flatMap 이 스칼라나 함수형 프로그래밍에서 굉장히 중요합니다.

F[A]에 대한 Monad는 아래 함수를 가진다

  • (F[A], A => F[B]) => F[B] 타입을 가진 flatMap 메소드
    (즉 A를 감싼F타입에서 A를 빼내어 B로 만든후 F 타입으로 다시 감싼다) 
  • A => F[A] 타입을 가진 pure 메소드 (A 를 감싼 타입으로 변경) 

모나드는 3개의 법칙을 따라야 한다.

  1. Left identity: (pure(a) flatMap f) == f(a)
  2. Right identity: (m flatMap pure) == m
  3. Associativity: (m flatMap f flatMap g) == (m flatMap (x => f(x) flatMap g))

모나드는 pure flatMap을 가지며 결합법칙 항등법칙 만족하도록 만든 구현이다. 즉 이 두 함수들을 이용해서 변수의 도입과 결속 그리고 변수 치환수행을 위한 문맥을 제공한다고 볼수 있으며 이 두 함수를 이용해서 많은 함수를 파생 시킬 수 있으며 코드를 줄이는 것도 가능하다


http://www.bench87.com/content/24 발췌 (COMET)

이런것들이 이해 안가면 예제만 반복해서 봅니다. 그럼 이해갑니다. 인간의 능력이란 반복에서 나옵니다.

예제 1)

  1. for(x <- c1; y <- c2; z <-c3) {...}

위의 문장은 아래 처럼 사용 될 수 있습니다.

  1. c1.foreach(x => c2.foreach(y => c3.foreach(z => {...})))

c1의 요소가 3개 , c2 가 3개,  c3 가 3개라면 27번의 행위가 발생되겠군요.


예제 2)

  1. for(x <- c1; y <- c2; z <- c3) yield {...}

위의 문장은 아래 처럼 사용 될 수 있습니다. 

  1. c1.flatMap(x => c2.flatMap(y => c3.map(z => {...})))

예제 3)

  1. for(x <- c; if cond) yield {...}

위의 문장은 아래 처럼 사용 될 수 있습니다.

  1. c.withFilter(x => cond).map(x => {...})

cond 에 합당하는 x 들을 이용하여 어떤 행위를 한 후에 다시 c 타입을 갖게 합니다.


예제 4)

  1. for(x <- c; y = ...) yield {...}

위의 문장은 아래 처럼 사용 될 수 있습니다.

  1. c.map(x => (x, ...)).map((x,y) => {...})

map 을 대신해서 사용 할 수 있지만 아래 예를 보시면 느껴지다 시피 무엇이 더 이해하기 쉽습니까?
괄호가 연속된 map 보다는 for yield 를 사용한게 훨씬 명료하네요.


  1. l.flatMap(sl => sl.filter(el => el > 0).map(el => el.toString.length))

or

  1. for{
  2. sl <- l
  3. el <- sl
  4. if el > 0
  5. } yield el.toString.length


예제 5) 

scala> val capNames = for (e <- names) yield e.capitalize
capNames: Array[String] = Array(Chris, Ed, Maurice)

for 문을 통해서 하나씩 실행한 후에 yield 를 통해서 names 의 capitalize 만 따로 추출해서 새로운 배열로 만들어집니다.

scala> val lengths = for (e <- names) yield {
     |   // imagine that this required multiple lines of code
     |   e.length
     | }
lengths: Array[Int] = Array(5, 2, 7)

이번에는 숫자를 추출해서 그것으로 배열이 만들어 졌습니다.


예제 6) 

scala> val s = for {
     |  x <- 1 to Integer.MAX_VALUE
     |  if(x <= 10)
     | } yield 2*x

s: (2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

모든 숫자중에서 10보다 작은 숫자를 2배해서 set s 에 담아줬습니다.


예제 7)

  1. def eitherExample(num: Some(Int)): Either[String, Int] = num match {
  2.   case Some(n) => Right(n)
  3.   case None => Left("Error! Number is missing!")
  4. }

입력이 합당하면 Either 의 오른쪽에 값(n) 을 넣고 아니면 왼쪽에 에러를 출력 합니다.
match case 를 이용하였습니다.

  1. val x: Either[String, Long] = Right(7)
  2. x.right.map(num => println(num))
  3. x.left.map(msg => println(msg))


Either 의 오른쪽에 값이 있는 x 를 정의하고 오른쪽 왼쪽을 사용합니다. 위에서 map 은 값이 없는 경우라면 무시할 것입니다. 안전하게 사용 할 수 있겠네요.

  1. val x: Either[String, Int] = Right(7)
  2. val result1 = for( res <- x.right ) { res }
  3. val result2 = for( res <- x.left) { res }


이렇게 for 를 사용할 수도 있습니다. 값이 있는곳 만 처리 됩니다.

  1. for {
  2.   id <- x.right
  3.   name <- y.right
  4. } yield {
  5.   val level = lookupLevel(id)
  6.   Member(id, name, level)
  7. }.fold(
  8.   err => NotAuthorized(err),
  9.   member => member
  10. )

마지막으로 for - yield 문을 보시면 Either 의 x, y 값을 id , name 으로 담습니다.
해당 값들에 문제가 없을 경우에 yield 가 실현됩니다. 여기선
 Member 객체를 만들죠. 그 후에 fold 를 통해서
문제가 있다면 첫번째 함수를 따르고 문제가 없다면 두번째 member => member 를 실행해서 결국 member 가 리턴됩니다. 


예제 8)

case class Postcard(msg: String)

def sendPostcards: List[Postcard] = {
    val states = List("Arizona", "New Mexico",
                          "Texas", "Louisiana",
                          "Mississippi", "Virginia",
                          "New York", "Ohio",
                          "Illinois")
    val relatives = List("Grandma", "Grandpa", "Aunt Dottie", "Dad")
    val travellers = List("Kelsey", "DJ")

    var postcardList: List[Postcard] = List()

    for (h <- 0 until travellers.length) {
        val sender = travellers(h)

        for (i <- 0 until relatives.length) {
            val recipient = relatives(i)

            for (j <- 0 until states.length) {
                val theState = states(j)
                postcardList ::=
                    new Postcard("Dear " + recipient + ", " +
                                  "Wish you were here in " +
                                  theState + "! " +
                                  "Love, " + sender)
            }
        }
    }

    postcardList
}


위의 예제를 아래처럼 for - yield 를 사용해서 바꿔 줄 수 있습니다. (정확히 동일하지 않습니다) 

def sendPostcards3: List[Postcard] = {
    val states = List("Arizona", "New Mexico",
                          "Texas", "Louisiana",
                          "Mississippi", "Virginia",
                          "New York", "Ohio",
                          "Illinois")
    val relatives = List("Grandma", "Grandpa", "Aunt Dottie", "Dad")
    val travellers = List("Kelsey", "DJ")

    for {
        traveller <- travellers
        sender = traveller + " (your favorite)"
        relative <- relatives
        theState <- states
        if (relative.startsWith("G"))
    } yield {
        new Postcard("Dear " + relative + ", " +
                        "Wish you were here in " +
                         theState + "! " +
                         "Love, " + sender)
    }
}

각각의 리스트들을 for 문 안에서 하나를 가져와서 if 문을 통해 데이터를 솎아내고, 그 데이터들을 이용하여   yield 를 통해서 하나의 객체를 만든 후에 다시 순회하면서 객체의 List 를 리턴해 주고 있습니다.

각각의 리스트가 5,5,5 개라면 최대 5*5*5 만큼의 객체가 생성될것입니다.

Comments