ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스칼라] 함수와 클로저
    스칼라 2022. 1. 31. 21:51
    (x: Int) => x + 1 // 함수 리터럴의 예
    // 인터프리터의 출력 res1: Int => Int = <function1>

    Programming in scala 4th edition을 읽고 정리한 내용입니다.

     

    1. 메서드

    메서드는 객체 안의 멤버인 함수이다. 스칼라에서 함수는 메서드를 포함한 좀 더 넓은 개념이다. 

    class ClassExample {
      val postFix = "ClassExample`s method"
      
      //메서드
      def printFunction(s: String): Unit = {
        println(getModifiedString(s))
      }
      
      //메서드
      private def getModifiedString(str: String): String = {
        s"$postFix: $str" 
      }
    }

     

    2. First class function

    스칼라는 1급 계층 함수(first class function)을 제공한다. 단순히 함수를 정의하고 호출하는것 뿐 아니라 값처럼 주고받을 수 있다. 스칼라에서는 함수를 다른 함수의 인자로 넘길수 있고 함수에서 함수를 반환할 수도 있다.

    (x: Int) => x + 1 //함수 리터럴의 예
    res1: Int => Int = <function1> //인터프리터의 출력

    위는 함수 리터럴의 예이다. 함수 리터럴은 변수에 할당할 수도 있고 그 변수를 이용해서 호출할 수도 있다.

    val functionLiteral = (x: Int) => x + 1
    functionLiteral(2)
    
    // 출력 결과는 3이다.

    컬렉션의 foreach나 filter를 사용할때 함수 리터럴이 자주 사용될 것이다.

    val list = List(1, 2, 3, 4, 5)
    list.foreach(x => print(s"$x "))  // 결과: 1 2 3 4 5
    list.filter(x => x > 3) // 결과: List(4, 5)

     

    3. 함수 리터럴

    컬렉션에서 사용된 함수 리터럴의 경우 컴파일러가 컬렉션의 타입을 가지고 함수리터럴 인자의 타입을 추론할 수 있다. 이것을 타겟 타이핑(target typing)이라고 한다. 그래서 굳이 함수 리터럴 인자의 타입을 적어주지 않아도 된다.

    list.filter(x => x > 3)

    함수 리터럴의 인자를 위치 표시자 (_) 로 대체할 수도 있다. 예를들어 위의 filter에 사용된 함수 리터럴은 아래 표현으로도 나타낼 수 있다.

    list.filter(_ > 3)

    인자가 2개인 경우는 아래와 같이 표현할 수 있다. 

    val twoArgFunction = (_: Int) + (_: Int)
    println(twoArgFunction(1, 2))  // 결과 3

    위치 표시자는 함수 호출에 사용된 인자가 주어진 순서대로 대체된다고 생각하면 이해하기 쉽다.

     

    4. 부분 적용 함수

    위치 지시자를 활용하면 함수에 다른 인자를 미리 apply 해둘 수 있다. 예를들어 인자가 3개 필요한 함수에서 인자 2개를 미리 어떤 값으로 채워둔 함수를 만들 수 있다.

    def sum(a: Int, b: Int, c: Int) = a + b + c
    sum(1, 2, 3)
    
    val sumTen = sum(5, 5, _: Int) // a, b 가 5로 할당되어있는 함수
    sumTen(10) // 출력 결과: 20

    아예 전체 파라미터를 대체시켜버릴 수도 있다.

    val allReplaced = sum _
    allReplaced(1, 2, 3)

    이렇게 위치 표시자를 사용하면 def 로 선언된 함수를 함수값(변수에 할당 가능한 형태로)으로 변환한다고 생각할 수도 있다. 나중에 중첩 함수에서 내부함수를 반환할때 부분 적용 함수 형태로 바꿔주어야 리턴이 가능하다.

    def outer() = {
      def inner(str: String) = print(s"I'm $str")
      inner
    } // 컴파일 불가
    
    def outer() = {
      def inner(str: String) = print(s"I'm $str")
      inner _
    } // 이렇게 사용해야함

    반드시 함수가 필요하다는게 명확하면 아예 함수 이름만 전달할 수도 있다.

    val list = List(1, 2, 3, 4, 5)
    list.foreach(println)

     

    5. 클로저

    클로저: 주어진 함수 리터럴로부터 실행 시점에 만들어낸 객체인 함숫값. 클로저라는 이름은 리터럴 본문의 자유변수들의 바인딩을 캡처하여 자유변수가 없게 닫는 행위에서 따온 말이다.

    자유변수란 함수 리터럴 내에서 의미 부여하지 않은 변수를 말한다. 바운드 변수는 이와 반대로 함수 문맥 내에서만 의미를 가지는 변수이다.

    val freeFunction = (x: Int) => x + more

    위와 같은 함수가 있을 때 x는 바운드 변수, more는 함수 리터럴 내에서 지정된 것이 없으므로 자유변수이다.

    var more = 10
    val freeFunction = (x: Int) => x + more
    freeFunction(10) // 결과 20
    
    more = 10000
    freeFunction(10)//결과 10010

    위와같이 freeFunction이라는 클로저를 만든뒤 more의 값을 바꾸면 바뀐 값도 반영된다. 클로저는 변수의 값이 아닌 변수 자체를 캡처한다. 

    만약 클로저를 리턴하는 함수를 선언하고 자유변수들을 그 함수에서 지정하는 식으로 구성하면 어떨까?

    def makeCloser(arg: Int) = {
      (x: Int) => x + arg
    }
    
    val plusOne = makeCloser(1)
    val plusTen = makeCloser(10)
    plusOne(10) // 11
    plusTen(10) // 20

     makeCloser는 arg에 값을 할당하고 종료되는데 스칼라 컴파일러는 이 arg가 함수가 종료되더라도 유지되도록 힙에 미리 재배치한다. 이렇게 만들어진 클로저들은 변수해 할당해서 계속 활용할 수 있다.

     

    6. 특별한 형태의 함수 호출

    스칼라는 함수 호출시 반복파라미터, 이름 붙인 인자, 디폴트 인자를 지원한다.

     

    반복 파라미터

    스칼라에서는 마지막 함수 파라미터를 반복 가능하다고 지정할 수 있다. 인자 타입 뒤에 (*)을 추가하면 되는데 이렇게 지정된 인자는 가변인자가되어 길이가 변하는 인자를 담을 수 있다.

    //반복파라미터
    def echo(args: String*) = {
      for(arg <- args) {
        print(s"$arg ")
      }
    }
    
    echo("가") // 출력결과: 가
    echo("A", "B") // 출력결과 A B

    반복 파라미터는 함수 내부에서는 지정된 타입의 Seq로 취급된다. 위의 함수에서 args는 Seq[String]으로 취급된다. 하지만 직접 배열을 전달하려고 하면 에러가 발생하는데, 배열을 반복인자로 전달하려면 : _* 기호를 추가해주어야 한다.

    val list = List("A", "B", "C")
    echo(list: _*)

     

    이름 붙인 인자

    함수 호출시 인자를 넘겨줄 때 파라미터 이름에 할당해주는것처럼 넘겨줄 수도 있다.

    def createPerson(name: String, age: Int) = {
      s"$name $age"
    }
    
    println(createPerson("james", 50))
    println(createPerson(name = "james", age = 50))
    println(createPerson(age = 50, name = "james"))
    
    // 출력 결과
    james 50
    james 50
    james 50

    이름 붙인 인자는 선언 순서대로 넘겨주지 않아도 된다. (age를 먼저 할당해주더라도 결과는 같음)

     

    디폴트 인자값

    스칼라 함수에서는 함수 파라미터의 디폴트 값을 지정해 줄 수 있다.

    def createPerson(name: String = "park", age: Int = 10) = {
      s"$name $age"
    }
    
    println(createPerson()) // park 10
    println(createPerson(age = 30)) // park 30

    디폴트 값을 지정하고 함수 호출시 별 다른 값을 지정해주지않으면 디폴트 값이 함수 내에서 활용된다. 이름 붙인 인자와 같이 활용했을때 활용도가 높다.

     

    7. 꼬리 재귀

    어떤 로직을 구현할때 while루프로도 구현할 수 있고 재귀로 구현할 수 있는 경우도 있다. 어떤 값이 충분히 근사할때까지 반복처리를 하는 함수를 구현한다고 해보자. 다음은 재귀와 while루프 두가지 방법으로 구현한 함수이다.

    def approximate(guess: Double): Double = 
      if (isGoodEnough(guess)) guess
      else approximate(improve(guess))
    
    def approximateLoop(initialGuess: Double): Double = {
      var guess = initialGuess
      while (!isGoodEnough(guess)) {
        guess = improve(guess)
      guess
      }
    }

    일반적으로 함수호출스택을 부르는것보다 반복문을 처리하는것이 성능상 더 유리하기 때문에 위의 두 함수에서도 while로 처리한 구현이 더 좋은 구현이라고 생각할 수 있다. 그렇지만 스칼라 컴파일러에는 꼬리재귀를 최적화 하는 기능이있다. 

    꼬리 재귀: 함수의 마지막 부분에서 재귀호출이 일어나는 함수 (함수 결과값에 어떤 연산이 추가되지않고 자기 자신이 불려지고 결과가 리턴되는 형태여야만 한다)

    스칼라 컴파일러의 최적화를 거치고 나면 위의 두 구현의 성능은 차이가 거의 없다.

    def boom(x: Int): Int = { 
      if(x == 0) throw new Exception("boom!")
      else boom(x - 1) + 1 // 결과값에 추가 연산이 일어나서 꼬리 재귀 최적화가 수행되지 않음
    }
    
    boom(3)
    // 출력 결과 boom이 계속 호출된 것을 볼 수 있음
    java.lang.Exception: boom!
      at .boom(<console>:13)
      at .boom(<console>:14)
      at .boom(<console>:14)
      at .boom(<console>:14)
      ... 40 elided
    def boom(x: Int): Int = {
      if(x == 0) throw new Exception("boom!")
      else boom(x - 1) // 꼬리 재귀 최적화 일어남
    }
    
    boom(3)
    // 출력 결과
    java.lang.Exception: boom!
      at .boom(<console>:13)
      ... 40 elided

    꼬리 재귀는 2 함수가 번갈아 호출되는 형태로 재귀가 구현되거나, 함숫값을 호출하는 형태로 구현되면 최적화가 일어나지 않는다.

    '스칼라' 카테고리의 다른 글

    [스칼라] 상속과 구성  (0) 2022.02.03
    [스칼라] 흐름 제어 추상화  (0) 2022.02.01
    [스칼라] 내장 제어 구문  (0) 2022.01.31
    [스칼라] 함수형 객체  (0) 2022.01.30
    [스칼라] 기본 타입과 연산  (0) 2022.01.30

    댓글

Designed by Tistory.