본문 바로가기
Programming/Scala

[10강] 함수와 클로저

by 유주원 2015. 4. 22.

스칼라에서 함수 사용함에 있어 기본 C나 자바와 다른 몇가지 특징이 있어 이번 포스팅에서 설명하려 한다.

첫번째로 스칼라에서는 함수 내에서 함수를 선언하고 사용할 수가 있다.


def processData(data: List, width: Int){

    def processLine(line: String){

        if (line.length > width)

             .....

        }

   

    for (d <- data)

        processLine(d)

}


위의 코드를 보면 processData란 함수 안에 processLine이란 함수를 정의해서 내부적으로만 사용할 수 있도록 했다.

또한 processLine에서는 processData에서 정의한 변수들도 사용이 가능하다.


이런 식으로 코딩을 하면 함수의 지역화가 가능하다. 물론 private이라는 키워드가 있지만 위와 같이 중첩 함수를 사용함으로써 변수도 좀 더 깔끔하게 쓸 수가 있다.(보기에..)


두번째는 스칼라에서는 함수를 값으로 저장할 수가 있다.


스칼라에서는 아래의 형태와 같이 함수 리터럴이 존재한다.


(x :Int) => x + 1


위의 함수 리터럴은 x를 입력으로 받아 1을 더해주겠다는 뜻이다. 스칼라에서는 이러한 함수 리터럴을 값으로 저장할 수 있다.


var function = (x : Int) => x + 1


위와 같이 선언한 경우 함수 리터럴은 아직 소스코드인데 반해, 아래와 같이 값을 실행시키게 되면 객체로 변환이 된다.


function(10)


위와 같이 x+1이란 함수 식을 function이란 변수에 저장을 하고 변수에 파라미터 10을 주고 호출하면 함수 호출과 동일하게 사용할 수가 있다.


위의 식은 아래 식처럼 더욱 간략하게 사용할 수가 있다. 이는 스칼라에서 해당 입력 타입에 대한 추론이 가능하기 때문에 이러한 식 축약이 가능해진다.


var function = x => x + 1


스칼라에서는 많은 함수 리터럴이 사용되고 있다.


var someNumbers = List(-11, -10, -9, -8, -7)

someNumbers.foreach((x:Int) => println(x))

someNumbers.filter((x:Int)=> x> 0)


타입 정보가 확실한 경우에는 아래와 같이 type을 생략할 수도 있다.


someNumbers.foreach(x => println(x))

someNumbers.filter(x => x>0)


세번째로 스칼라에는 _라는 특별한 키워드가 존재한다. 이 키워드에 대한 쓰임 방법은 아래의 예를 들어 설명하겠다.



방금 위에서 설명하기로는 함수를 쓰기 위해서는 (x => x > 5) 이런 형태로 써야 한다고 배웠다.

하지만 _를 사용해서 더 축약할 수도 있다. 해당 _은 number에 속하는 모든 원소들이 들어와야 된다는 것을 함축하고 있다.


_에 인자 타입을 정해줘야 할 경우에는 아래와 같이 사용한다.



위의 예에서는 _ + _ 를 통해 스칼라 인터프리터에서는 어떤 인자 타입이 들어오는지 추론을 하지 못해서 에러를 리턴하고 있다. 그래서 (_:Int) 형태로 인자 타입을 명시해줬다.




_를 일부 매개변수로도 지정할 수가 있다. 아래의 예를 보자.



3개의 정수 타입을 받는 sum이란 함수를 정의한다. 정의된 함수에 대해 sum _ 와 같이 쓰고 변수에 할당하면, _는 기본적으로 모든 파라미터 변수를 함축하는 의미가 된다. 그래서 sum _ = sum(int a, int b, int c)와 같은 의미가 된다.


이번에는 sum(1, _:Int, 3)이라고 정의한 구문을 보자. 이 구문은 첫번째 파라미터와 세번째 파라미터 값은 고정으로 하고 두번째 파라미터 값만 받아서 계산하겠다는 뜻이다.

b(3)을 하게 되면, 1+3+3이 되어서 결과적으로 7을 출력한다.


스칼라에는 클로저란 개념이 존재한다.

만약에 아래와 같은 식이 있다면 어떨까?? 올바르게 동작할까??


var more = 1

val addMore = x => x + more


분명 C나 자바에서는 함수 스코프 영역을 벗어났기 때문에 more에 접근할 수가 없다. 하지만 스칼라에서는 가능하다.

이런 형태의 함수 리터럴 값을 클로저라고 부른다.


만약에 지금 상태에서 more 값을 변경하면 어떻게 될까?? 



스칼라의 클로저는 변수가 참조하는 값이 아니라 변수 자체를 가져오기 때문에 해당 변수의 변화를 감지할 수가 있다. 그래서 변경된 값이 적용이 된다. 

이와 유사하게 클로저 안에서 변경된 값은 밖에서도 동일하게 적용이 된다.


만약에 아래와 같은 클로저가 선언되었고 해당 클로저를 여러 번 선언하면 어떻게 될까?


def makeIncreaser(more:Int) = (x:Int) => x+ more


val inc1 = makeIncreaser(1)

val int9999 = makeIncreaser(9999)


아래는 그 결과 값이다.


inc1(10)

=> 11

inc9999(10)

=> 10009


스칼라 컴파일러에서 클로저에 존재하는 변수에 대해 위와 같은 경우에는 힙에 재배치를 한다. 그렇기 때문에 매서드가 종료되었어도 계속 해당 값으로 살아 있는 것이다.


또한 스칼라에서는 반복 파라미터 사용이 가능하고 인자에 이름을 명시할 수 있다.

아래와 같이 *를 추가하면 반복적인 타입으로 사용할 수가 있다.



결국에는 Array[String] 타입이 넘어간다고 볼 수가 있는데, 그렇다고 Array[String]을 인자로 넘기면 에러가 발생한다.

Array[String]을 인자로 넘기고 싶다면 아래와 같이 해야 한다.


val arr = Array("What", "are", "you")

printString(arr: _*)


_* 을 타입으로 명시해 주면 배열 원소 각각을 인자로 넘기게 된다.


또한 아래와 같이 이름을 붙일수가 있다. 이름을 붙일 경우 파라미터 순서에 상관 없이 이름에 매칭하여 값이 배정된다.



특별히 값을 입력하지 않아도 파라미터에 default로 값을 지정할 수 있다.





마지막으로 스칼라에서는 tail-recursion이 가능하다.

스칼라에서는 var 사용을 줄이기 위해 대부분의 경우 재귀 호출을 이용한 로직을 많이 쓴다. 하지만 C나 자바의 경우 재귀를 호출하게 되면 계속적으로 stack frame이 쌓이게 되고 재귀가 끝나야만 쌓인 stack frame이 하나씩 빠져나가게 된다. 메모리 비효율적이고 성능적으로도 그리 좋지 못하다.


그런데 스칼라에서는 이를 tail-recursion이란 방식으로 해결하고 있다. tail-recursion에서는 추가적인 stack frame을 만들지 않고 같은 stack frame을 재사용하기 때문에 추가적인 메모리를 필요로 하지 않는다.


tail-recursion을 하기 위해서는 return하는 함수가 자기 자신이어야만 한다. 자기 자신 이외의 연산 등이 추가되어 있으면 스칼라에서는 이를 tail-recursion으로 받아들이지 않는다.


tail-recursion이 적용되지 않는 경우


def count(x: Int): Int = {

    if(x == 0) 1

    else count(x-1) + 1

}


def isEven(x: Int): Boolean = if (x == 0) true else isOdd(x-1)

def isOdd(x:Int): Boolean = if(x == 0) false else isEven(x-1)


val funValue = nestedFun _

def nestedFun(x: Int) = {

    if(x!=0){

        funValue(x-1)

}


첫번째는 count 호출 뒤에 연산이 들어가 있기 때문에 tail-recursion이 호출되지 않는다.

두번째의 경우 자기자신을 호출한게 아니기 때문에 tail-recursion이 호출되지 않는다.

세번째는 논리적으로는 동일한 함수를 호출했어도 자기자신을 직접 호출한게 아니라 nestedFun이란 함수의 값을 호출하였기 때문에 tail-recursion이 호출되지 않는다.