스칼라에서의 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 만큼의 객체가 생성될것입니다.

* 참고로 커링이 무엇인가요에 대한 대답은 너무 쉽다. 하지만  왜 커링이 그렇게 유용하나요? 에 대한 대답은 아직 못얻었다. 뇌 로는 알겠지만 체득하지 못한 상태.  스칼라를 함수형 파라다임으로 사용을 많이 해봐야 알게 되지 않을까..

스칼라에서의 커링 (currying)


1. 개념 

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

메소드에는 파라미터 목록을 여럿 정의할 수 있다. 파라미터 목록의 수 보다 적은 파라미터로 메소드가 호출되면, 해당 함수는 누락된 파라미터 목록을 인수로 받는 새로운 함수를 만든다.

중요한 포인트를 뽑아내어 쉽게 설명해보면 

매우 쉬운 설명 

 - 메소드라는건 파라미터를 가지고 있지요?  예를들어  add( int x , int y)  이렇게 2개를 가지고 있을때
 - add 라는 메소드를 호출 할 때 인자 2개를 넣어주지 않으면 보통 에러 나잖습니까?
 - 근데 하나만 넣어도 에러가 나지 않는데다가 ~~   즉 add (10)  이렇게 해두 되고 
 - 누락된 파라미터 만을 사용하는 새로운 메소드를 만들어 준 답니다.~ 즉 add(int y) 라는 새 메소드말이죠.
 - add (int y) 라는 새 메소드는 나중에 호출 할 수 있는데 그 때 이전에 넣어 둔 10이 암시적으로 작용됩니다.

 슈도코드로 살짝 보면 

 int add (int x, int y) {
   return x + y;
 }

원래 이런 함수인데
 add (10) 이렇게 호출하면 add(int y) 라는 메소드가 만들어지며 , 10은 새 메소드에 저장되어 있게 됩니다.

int add( int y) {
    return 10 + y;
}

즉 이런 함수가 자동으로 만들어 진 다는 말이지요.
이 함수를 add(5) 호출하면 결과로 이전에 입력된 10을 더하여 15가 리턴됩니다.

2. 스칼라 예제

  1. object CurryTest extends App {
  2. def filter(xs: List[Int], p: Int => Boolean): List[Int] =
  3. if (xs.isEmpty) xs
  4. else if (p(xs.head)) xs.head :: filter(xs.tail, p)
  5. else filter(xs.tail, p)
  6. def modN(n: Int)(x: Int) = ((x % n) == 0)
  7. val nums = List(1, 2, 3, 4, 5, 6, 7, 8)
  8. println(filter(nums, modN(2)))
  9. println(filter(nums, modN(3)))
  10. }

위의 스칼라 예제의 modN(n: Int)(x: Int) 가 커링되는 함수입니다.
보시다시피 modN(n: Int)(x: Int) 는 2개의 파라미터를 넣어야하는 함수입죠..

위에서 설명했다시피 이걸 파라미터 하나만 넣어서 호출 할 수 도 있으며 그렇게 하면 새로운 함수가 만들어진답니다..  즉 modN(2) 이렇게 호출 했더니 에러는 안나고 대신 새로운 함수를 만들어서

def filter(xs: List[Int], p: Int => Boolean)  filter 파라미터 p로 넣어주고 있습니다.

위의 p 파라미터 자리에는  modN(2) 를 호출해서 만들어진 새로운 함수가 들어 간다는 말입니다.
새로운 함수는 이렇게 생겼겠죠?  (이미 2를 n 자리에 넣었다는것을 기억하시구요) 

def modN(x: Int) = ((x % 2) == 0)

3. HOW? 

여기에 하나의 함수를 파라미터로 받아서 , 그 함수를 내부에서 사용하고 2개의 Int 형을 받는 함수를 리턴하는 함수가 있습니다.

def sum(f:Int=>Int):(Int,Int)=>Int={
   def sumF(a:Int,b:Int):Int= 
      if(a>b) 0 else f(a)+ sumF(a+1,b)
   sumF
  }

이런 경우 내부에 함수를 선언하지 않고 , 함수 파라미터 한개와 2개의 Int 형 파라미터 , 즉 3개의 파라미터를 받는 커링 함수를 아래처럼 만들수 도 있습니다.

def sum(f:Int=>Int)(a:Int,b:Int):Int=
  if(a>b) 0 else f(a)+sum(f)(a+1,b)

4. {} 의 사용 

스칼라에서는 함수호출시  ()  대신 {} 중괄호를 사용 할 수 있다.  단 !!! 인재가 하나일 경우 !!

println ("Hello World") 대신해서 println { "Hello World" } 를 사용 할 수 있다는 의미이다.

스칼라에서 이렇게 한 이유는 클라이언트 프로그래머가 중괄호 내부에 함수 리터럴을 사용하도록 하기 위해서이다. 이렇게 작성한 메소드는 호출 시 좀 더 제어 추상화 구문과 비슷해진다.

def withPrintWriter(file: File, op: PrintWriter => Unit ) {

......
}

이런 함수를 호출 할 때는 

withPrintWriter ( new File ("date.txt") , 

writer => writer.println(new java.util.Date) ) 


이렇게 할 수 있는데 , 인자가 2개이기 때문에 { } 를 사용할 수 없다.

하지만 !!!!

def withPrintWriter(file: File) (op: PrintWriter => Unit ) {
val writer = new PrintWriter(file)
try {
op (writer)

} finally {
writer.close()
}
}

이러한 함수라면 {} 를 사용가능하다.

val file = new File("date.txt")
withPrintWriter (file) {
writer => writer.println(new java.util.Date)
}

왜 그러냐면 커링을 통해서 file 을 매개변수로 먼저 넣은 새로운 함수가 생겨졌고, 

그 새로운 함수는  매개변수 하나를 필요로 하는 함수이기 때문이다.


보통 (변수,함수) 매개변수 조합일 때 많이 사용된다. 

def foldLeft[B](z: B)(op: (B, A) => B): B = {
  var result = z
  this foreach (x => result = op(result, x))
  result
}
Vector(1, 2, 3).foldLeft(Vector[Int]()) { (is, i) =>
  is :+ i * i
}

5. 추가 읽을거리 : Partial function 과의 차이 

함수 인자를 생략하는 문법들에는 

- 디폴트파라미터
- implicit 
- partial applied function
- currying 

등 이 있는데 그 중에 헤깔리기 쉬운 currying vs partial function 에 대해 살펴보자. 아래 링크를 읽으심이

http://www.vasinov.com/blog/on-currying-and-partial-function-application/


- https://github.com/enshahar/BasicFPinScala/blob/master/Intermediate/Monad.md

- https://github.com/enshahar/BasicFPinScala/blob/master/Intermediate/Monad_Rules.md

Monad Programming with Scala Future

http://www.haruair.com/blog/2986

http://blog.seulgi.kim/2015/07/what-is-monad.html

- 자바언어로 서술한 Functor 와 모나드 

 제 능력이 부족한 관계로 위의 글로써 대신합니다. (모나드 관련 한글 블로그 모음) 

 아참! 위의 글을 읽기 전에 반드시 모나드 괴담!을 읽어보세요. ^^


모나드가 무엇인지 이해함으로써 하스켈 프로그래머가 되겠다는 것은 "악기란 무엇인가?"를 이해하면 모든 악기의 연주자가 될 수 있으리라는 믿음과 같다. 즉 모나드를 이해하는것과 프로그래밍을 잘하는것과는 별개의 문제.
Fallacy (2015)


모나드 간단 정리
(코딩하다 보면 아 대략 이러면 모나드다 정도면 될 거 같습니다. 완벽한 모나드의 법칙을 이해 할 필요까지는 없을듯) 

1.모나드는 다른 타입을 인자로 받는 타입이다. 

case class Boxed[T](value:T);

2.모나드 타입의 값을 생성하는 함수가 있어야 한다.

def initBoxed(x:Int):Boxed[Int] = Boxed(x)
def initLogged(x:Int):Logged[Int] = Logged(x, List())
def initMyOption(x:Int):MyOption[Int] = MySome(x)

3.다른 모나드 타입으로 진행하는 함수가 있어야 한다. (또한 감싼 값에 대해서 꺼낼 수 있는 방법을 제공하며 꺼낸 값을 가지고 원하는 형식으로 변형하고 다시 감싸서 반환)

session => Future[User]
=> flatMap => (User => Future[Order])
=> flatMap => (Order => Future[List[Item]])

위의 3가지 성질을 모두 가지고 있을때 모나드라 칭한다.

* 스칼라 Future 는 모나드가 아니다.=> https://stackoverflow.com/questions/27454798/is-future-in-scala-a-monad


아래의  자바스크립트의 예를 보면 대략 모두 가지고 있음을 알 수 있다. 

// 타입 스크립트로 Monad의 인터페이스를 설계함
interface MStatic<T> {
    // constructor that wraps value
    new(value: T): M<T>;
}

interface M<T> {
    // bind as an instance method
    bind<U>(transform: (value: T) => M<U>): M<U>;
}


 


스칼라에서 JSON 데이터 다루기

* scala 기존제공하는것보다 json4s 나 spray-json 이 더 나은듯하다.


1. 디펜던시 추가 

import scala.util.parsing.json._


2. 문자열에서 Json 객체 (Map 타입) 로 변경  - (1)


def main(arg : Array[String]): Unit ={


val result = JSON.parseFull("""
{"name": "Naoki", "lang": ["Java", "Scala"]}
""")

result match {
case Some(e) => println(e)
case None => println("Failed.")
}

} //print 결과 : Map(name -> Naoki, lang -> List(Java, Scala)


3. 문자열에서 Json 객체 (Map 타입) 로 변경  - (2)

def main(arg : Array[String]): Unit ={


val msg = """

{ "callid" : 1 ,
"gatewayid" : "gatewayid-1" ,
"switchid" : "switch-id" ,
"content" : "hi"
}
"""

val result = JSON.parseFull(msg)

result match {
case Some(e) => val res1 = e.asInstanceOf[Map[String,Any]];
print(res1.get("callid"))
case None => println("Failed.")
}

} //print 결과 : Some(1.0)

asInstanceOf 를 통해서 형변환을 수행 하였다.


case Some(map: Map[String,Any]) => print(map.get("callid"))

Some 내부에서 형변환 할 수 도 있다.


4. Json 객체 (Map 타입) 에서 문자열로 변경 


의존성 추가 ( json4s 를 이용함) 

libraryDependencies += "org.json4s" %% "json4s-native" % "3.2.10"
libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.2.10"

import 

import org.json4s.jackson.Serialization

map 을  String 으로 변경

implicit val formats = org.json4s.DefaultFormats

Serialization.write(mutableMap)


 

Partially applied function 과 Partial function 

먼저 아래 링크 참고 하세요. (아웃사이더님이 번역하신  글이 있습니다)

https://blog.outsider.ne.kr/953  


아마 저 글을 읽어도 Partial function에 관해 조금 헤깔리실텐데요. 간략하게 다시 설명해 드리겠습니다.
함수란 모든 x 에 대해서 y 라고 할때 부분 함수란 일부 x 에 대해서 y 인 함수를 말 합니다. 즉 제한을 둔겁니다.

예를들어

function  A (int x ) {
   return x * x 
}

라는 함수에 대해서 아래 함수는 부분함수입니다.

function A'  (int x) {
  if x > 10 
     return x * x 
}

모든 것을 처리하지 않고 싶을 때 부분 함수를 만들게 됩니다.
그 모든 것 말고 예외인 항목을 입력하면 에러/예외가 발생하게 말이죠.

그 예외 항목이 먼지 알려주는 함수가 isDefinedAt 입니다.
따라서 이 함수를 먼저 이용해서 해당 파라미터가 그 부분함수에서 잘 작동하는지 확인한 후에 
파라미터를 넘긴다는 야그죠. 

아래 예제를 봅시다.

scala> List(41, "cat") map { case i: Int ⇒ i + 1 }
scala.MatchError: cat (of class java.lang.String)

 "cat" 이 map 으로 전달되면 case 에 매칭되지 않기 때문에 에러가 나는데 

scala> List(41, "cat") collect { case i: Int ⇒ i + 1 }
res1: List[Int] = List(42)

Collect 경우는 괜찮네요? 

네 Collect 경우는 isDefinedAt 으로 부분함수에 대한 검증을 합니다. 

마지막으로 PartialFunction 은 아래와 같이 만들며 자동으로 isDefinedAt 이 만들어지는게 특징입니다.

[Int, Int] 는 인자로 Int 리턴값으로 Int 를 말합니다.

val fraction: PartialFunction[Int, Int] =
  { case d: Int if d != 042 / d }

이 경우 isDefindAt 에 0 이 들어가면 false 를 내보내겠지요?

마지막 예제를 보면서 생각해 보는 시간을 가지세요 ^^

scala> val liar: PartialFunction[Any, Int] =
  { case i: Int ⇒ i; case s: String ⇒ s.toInt }

liar: PartialFunction[Any,Int] = <function1>

scala> liar.isDefinedAt(42)
res10: Boolean = true

scala> liar.isDefinedAt("cat")
res11: Boolean = true

scala > liar ("3")
res12: Int = 3 

scala> liar("cat")
java.lang.NumberFormatException: For input string: "cat"

scala> val honest: PartialFunction[Any, Int] = { case i: Int ⇒ i; case s: String if isParsableAsInt(s) ⇒ s.toInt } honest: PartialFunction[Any,Int] = <function1> scala> honest.isDefinedAt("cat") res12: Boolean = false



언더스코어  _  는 스칼라에서 어떻게 사용되나?


1. Import 


import scala.concurrent._

import 에서 _ 는 모든을 뜻한다. 자바에서 * 


2. 디폴트 초기화 

class Foo {
var i : Int = _ // i = 0
var s: String = _ // s = null

}

Int  는 0 으로 초기화 되고 String 을 대입하면 null 로 초기화 됩니다.


3. 고계 함수에서 익명 파라미터


( 1 to 10) map { _ + 1 }


4. 부분 함수에서 익명 파라미터

좀 더 정확히 얘기하면 partially applied function.   partial function 과 구분된다. 바로가기 

def f(i : Int) : String = i.toString
def g = (x: Int) => f(x)
def g2 = f _
def u(i: Int) (d: Double) : String = i.toString + d.toString

def w = u(4) _ // (y: Double) => u(4)(y) 와 동일
println(w(5)) // 45.0 출력


5. 패턴 매칭

expr match {
case List(1,_,_) => "첫번째 요소는 1 이며 3개의 요소를 가진 리스트"
case List(_*) => "0 개나 혹은 더 많은 요소를 가진 리스트"
case Map[_,_] => "아무 타입의 키와 아무 타입의 값을 갖는 맵"
case _ => "나머지 아무거나"
}


6. 제네릭에서 와일드카드 

def size(objs: List[_]) : Int = {
objs.size
}

def loggedIn(req: RequestWithAttributes[_]): User = {

...

}


7. 프로퍼티에서 세터 

class Test {
private var a = 0
def age = a
def age_=(n:Int) = {
require(n>0)
a = n
}
}
val t = new Test
t.age = 5
println(t.age)


참고 :

 http://ananthakumaran.in/2010/03/29/scala-underscore-magic.html

http://www.slideshare.net/normation/scala-dreaded


Null 과 친구들 

Null  –  Trait 이다. 모든 참조 타입(AnyRef를 상속한 모든 클래스) 의 서브클래스이다. 값 타입과는 호환성이 없다. 

null  –  Null 의 인스턴스이고 자바의 null 가 비슷. val i : Int = null; 불가능 (값 타입과 호환성 없다) 

NothingTrait 이며 모든것들의 서브타입이다.기술적으로 예외는 Nothing 타입을 갖는다. 이 타입은 값이 존재하지 않는다. 값에 대입이 가능하다. 즉  리턴타입이 Int 인 메소드에서 리턴을 Nothing 타입인 예외를 던질 수 있다. 

Nil    –  아무것도 없는 List 를 나타낸다.

None – 아무것도 없다는 리턴 값을 표현하기 위해 사용된다. null 포인트 예외를 회피하기 위해 Option[T] 의 자식클래스로 사용된다. 

Unit  – 아무 값도 리턴 하지 않는 메소드의 리턴타입으로 사용. 



Apply

자바를 하다가 스칼라로 넘어오면 희안한거 많이 보는데 apply 라는 것도  분명히 마주치게 되는데 이게 여러군데에서  다른 의미로 사용되서 좀 헤깔리죠. 그래서 이 참에 정리 한번 하고 넘어 갑니다. 


기본 

해당 객체 뒤에 하나 이상의 값을 괄호{}로 둘러싸서 호출하면 그 객체의 apply() 메소드를 자동으로 호출해 줍니다. 스칼라에서는 apply 라는 메소드가 기본적으로 이 객체(타입) 저 객체(타입) 에 각자 정의되어 있습니다.함수타입에서 apply 하면 이렇게 작동하고 , 리스트에서  apply 하면 요렇게 작동하고 그렇습니다. 중구난방이라는 말 그래서 어느정도는 외워야합니다.

예를 보시죠

예1)

class applyTest {
def apply(n : Int): Unit = {
print ("hello world")
}
}

object test {

def main(arg : Array[String]): Unit ={

val at = new applyTest
at{10}
}
}

그냥 at{10} 로 호출하면 hello world 가 찍힙니다. 참고로 매개변수가 아예 없이는 안됩니다.

결국 {..} 사이에는 apply 의 인자가 들어가게 됩니다. 위에는 Int 형이 들어가네요.

예2)

class applyTest{

def apply( f : => Int): Unit ={
val n = f + 3
println("n = %d".format(n))
}
}


object test {

def main(arg : Array[String]): Unit ={
val t = new applyTest()
t{
println("apply test")
10
}
}
}

이게 참 골때리는 겁니다. 편의성을 높힌 Syntactic sugar 라고 하지만 여러언어를 쓰다보면 꽤 헥깔립니다.

먼저 applyTest 클래스에 apply 함수를 만들어 두었습니다. 이 apply 는  => Int 같은 람다식 (입력인자가 없고 리턴이 Int 형인) 함수타입을 인자로 받습니다. 여기까진 OK~

근데 10은 멀까요 -.-a

def apply( f : => Int)

여기에 f 에 대응되는 함수가 

println("apply test") 

return 10 

란 야그죠.

결국 f + 3 은 10 + 3 과 같습니다.


예3) 함수 자체를 넘기기도 합니다.

class applyTest{

def apply( f : String => Unit): Unit ={
f("hello world")
}
}


object test {

def main(arg : Array[String]): Unit ={
val t = new applyTest()
t{
println
}
}
}


예4) 함수의 리턴값을 넘기기도 하고 

object obj1{
def getWord: String = {
return "hello world"
}

}

class applyTest{

def apply( str : String): Unit ={
println(str)
}
}

object test {

def main(arg : Array[String]): Unit ={
val t = new applyTest()
t{
obj1.getWord
}
}
}

예4) 역시 이렇게 함수 자체를 넘길 수도 있습니다.


object obj1{
def getWord: String = {
return "hello world"
}

}

class applyTest{

def apply( block: => String): Unit ={
print(block)
}
}

object test {

def main(arg : Array[String]): Unit ={
val t = new applyTest()
t{
obj1.getWord
}
}
}

Array 에서의 apply 

예 1)  Array 에서의 값 가져오기

val a = new  Array[String](2)  // String 타입이 2개 담길수 있는 배열의 객체를 만들었네요.

a(0)  이렇게 하면 첫번째  값을 가져 올 수 있는데요. 객체에 바로 () 를 붙혔습니다. 어색하죠? 

즉 여기선  a.apply(0) 이렇게 자동으로 변경 됩니다.

그리고 Array 에서의 apply 메소드의 역할은 해당 인덱스에 해당하는 값을 꺼내오는 겁니다.


예 2Array 생성하기 

val a = Array("hello", "world") 


이렇게 만들 수 있습니다. new 가 생략되었는데 이게 가능한 이유는 

암시적으로 apply()  라는 이름의 팩토리 메소드를 호출해서 객체를 만들어줍니다.

Array.apply("hello", "world") 이렇게 되는거죠 여기서는 ..




함수타입에서 사용

val f1 = (x: Int, y: Int) => x + y
f1(2,3) // f1.apply(2,3) 와 같다. 결과 : Int = 5

저렇게 호출하면 알아서 apply 해준다.
참고로 저 함수 리터럴의 타입은 <function2> 이며 스칼라에선 모든게 다 객체이다.



Set 에서의 apply

val fruit = Set("apple", "orange", "peach")
fruit("peach")

위에서 호출되는 apply 는 contains 와 동일하다.




Play2 에서 Action 호출에서의 사용 

스칼라언어 기반 Play 프레임워크 웹개발에서는 

def doSomething = Action{
Ok.apply(views.html.index("Hi there"))
}

이런 모양새로 이루어지는데 여기서 Action 이 무엇일까요? 

위의 예는 다음과 같이 풀어지게 되는데요

def doSomething: Action[AnyContent] = Action.apply(
    Ok.apply(views.html.index("Hi there"))
)

위의 apply 메소드의 정의는  apply(block: => Result) 이라 익명의 함수가 들어가게 됩니다.  

이 말인 즉,  Ok.apply 함수는 Result 를 반환하겠군요.

Action.apply 함수는 Result 를 사용한 후에 Action[AnyContent] 를 반환하구요.



+ Recent posts