본문 바로가기

Programming Project/스칼라로 배우는 함수형 프로그래밍 정리

[02]스칼라로 함수형 프로그래밍 시작하기 - 스칼라로 배우는 함수형 프로그래밍


투명한 기부를 하고싶다면 이 링크로 와보세요! 🥰 (클릭!)

바이낸스(₿) 수수료 평생 20% 할인받는 링크로 가입하기! 🔥 (클릭!)

2020/09/05 - [Programming Project/스칼라로 배우는 함수형 프로그래밍 정리] - [01]함수형 프로그래밍이란 무엇인가? - 스칼라로 배우는 함수형 프로그래밍

에서 이어지는 글입니다.

 

이 글은 스칼라로 배우는 함수형 프로그래밍을 읽고 정리한 글입니다.

 

-

이번 글에서는 꼬리 재귀 함수(tail recursion function)를 이용해서 루프를 작성하는 방법을 알아 보며, 고차 함수(high-order-function)에 대해서도 알아본다. 고차 함수는 다른 함수를 인수로 받는 함수인데, 계산의 결과를 함수로 리턴 할 수도 있다. 또한 다형적(polymorphic) 고차 함수의 예도 몇 가지 소개한다.

 

2.1 스칼라 언어의 소개. 

다음은 이번 장에서 자세히 살펴볼, 스칼라로 작성된 완결된 프로그램이다.

// single line comment
/* multi line comment */
/** doc style comment */

// 싱글톤 객체의 선언. 클래스와 클래스의 유일한 인스턴스를 동시에 선언한다.
object MyModule {
    def abs(n: Int): Int = 
        if(n < 0) -n
        else n
    
    // private method는 오직 MyModule의 다른 멤버들만 호출할 수 있다.
    private def formatAbs(x: Int) = {
        val msg = "The absolute value of %d is %d"
        msg.format(x, abs(x)) // 문자열의 두 %d 자리표를 각각 x와 abs(x)로 치환한다
    }
    
    def main(args: Array[String]): Unit = 
        println(formatAbs(-42))
}

이 프로그램은 MyModule이라는 이름의 객체(object; 모듈[module]이라고도 한다)를 선언한다.

scala 코드는 반드시 object나 class로 선언되는 클래스 안에 들어가야 하고, 여기에서는 그 중 더 간단한 object를 사용했다.

더보기

object 키워드

object키워드는 새로운 singleton(단일체) 형식을 만든다. 싱글톤은 class와 비슷하되, 명명된 인스턴스가 단 하나라는 점이 다르다. 스칼라의 object선언이 Java에서 익명클래스(Anonymous Class)의 새 인스턴스를 생성하는 것과 아주 비슷하다고 생각하면 될 것이다.

스칼라에는 Java의 static 키워드에 해당하는 것이 없으며, Java에서 정적 멤버를 가진 클래스를 사용할 만한 상황일 때 스칼라에서는 object를 사용하곤 한다.

함수(메서드) 블록의 마지막 줄은 함수의 리턴값으로 취급된다. 

main 메서드는 core pure functional를 호출하고 그 결과를 콘솔에 출력하는 외부 계층(shell)이다. side effect가 발생함을 강조하기 위해 이런 메서드를 procedure(절차) 또는 impure function(불순 함수)라고 부르기도 한다.

 

    def main(args: Array[String]): Unit = 
        println(formatAbs(-42))

main 이라는 이름은 특별하다. 프로그램을 실행할 때 스칼라가 main이라는 이름을 가진 메서드를 찾기 떄문이다. 좀 더 구체적으로는, Array[String]을 입력으로, Unit을 출력으로 가져야 한다. (void와 Unit이 비슷한 의미라고 생각할 수 있다.) 일반적으로 리턴 타입이 Unit이라는 것은 이 함수가 side effect가 있음을 암시한다.

 

 

2.2 프로그램의 실행

sbt를 사용하여 command line 에서 바로 실행할 수 있다.

REPL (Read-evaluate-print loop)을 사용하여 직접 실행시켜보면서 결과를 볼 수도 있다.

 

scala> MyModule.abs(-42)

res0: Int = 42

2.3 모듈, 객체, namespace

위의 예제에서 MyModule은 abs가 속한 namespace이다. 몇 가지 세부적인 사항을 제외하고 생각하면, 스칼라의 모든 값은 소위 객체(object)이며, 각각의 객체는 0개 이상의 멤버(member)를 가질 수 있다. 자신의 멤버들에게 namespace를 제공하는 것이 주된 목적인 객체를 흔히 모듈(module)이라고 부른다.

스칼라에선 2 + 1 도 2의  + 메서드에 1을 인수로서 전달하는 표현식 2.+(1)에 대한 syntactic sugar(구문적 겉치레)일 뿐이다. 스칼라에선 연산자(operator)라는 특별한 개념이 존재하지 않는다. 

import를 사용하면 객체의 멤버를 현재 범위로 importing하는 것이 가능하다.

 

scala > import MyModule.abs

 

scala > abs(-42)

res0: 42

 

그리고 다음과 같이 밑줄 표기법을 사용하면 객체의 모든 nonprivate한 멤버를 범위에 import할 수 있다.

 

import MyModule._

 

2.4 고차 함수: 함수를 함수에 전달

함수를 값으로서 함수에 전달할 수 있다. pure functional programming을 할 때, 다른 함수를 인수로 받는 함수를 작성하는 것이 유용할 때가 많다. 그런 함수를 고차 함수(high-order-function)이라고 부른다. 이번 절에서는 factorial 함수 또한 작성 해 보도록 하자.

 

2.4.1 잠깐 곁가지 : 함수적으로 루프 작성하기

factorial 함수를 작성 해 보자.

def factorial(n: Int): Int = {
    def go(n:Int, acc: Int): Int = 
        if (n <= 0) acc
        else go(n-1, n*acc)
    
    go(n, 1)
}

루프를 functional 하게 작성하는 방법은 바로 재귀 함수를 이용하는 것이다. 

go 의 인수들은 루프의 상태에 해당한다. 지금 예에서 go의 인수는 남아 있는 값 n과 현재 누적된 factorial의 acc이다. 다음 반복으로 넘어갈 때에는 새로운 loop를 재귀적으로 호출하면 된다. (여기서는 go(n-1, n*acc)의 형태) 그리고 루프에서 벗어날 때, 본인의 값 acc를 그대로 돌려준다. 스칼라에서는 이러한 종류의 self-recursion(자기 재귀)를 검출해서, 재귀 호출이 tail position(꼬리 위치)에서 일어난다면 while 루프를 사용했을 때와 같은 종류의 바이트코드로 컴파일한다. 핵심은, 재귀 호출의 반환 이후 별달리 하는게 없다면 이런 종류의 최적화(tail call elimination)을 적용한다는 것이다.

 

더보기

스칼라의 꼬리 호출

go(n-1, n*acc)는 꼬리 호출인 반면, 1+go(n-1, n*acc) 는 꼬리 호출이 아니다. 호출 후 반환값에 1을 더해야 하기 때문이다. 재귀 함수에 대해 tail call이 실제로 제거되었는지 확인할 필요가 있다면, tailrec annotaion을 재귀함수에 적용하면 된다. (@tailrec을 위에 달면 된다) 그러면 tail recursion이 아닌 경우 에러를 발생시킨다.

 

2.4.2 첫 번째 고차 함수 작성

factorial 함수를 만들었으니, 이제 이 함수르 사용하도록 예제 프로그램을 수정 해 보자.

object MyModule {
    // ... <- abs 와 factorial의 정의들. 생략.
    private def formatAbs(x: Int) = {
       val msg = "The absolute value of %d is %d."
       msg.format(x, abs(x))
    }
    
    private def formatFactorial(n: Int) = {
        val msg = "The factorial of %d is %d."
        msg.format(n, factorial(n))
    }
    
    def main(args: Array[String]): Unit = {
        println(formatAbs(-42))
        println(formatFactorial(7))
    }
}    

두 함수 formatAbs와 formatFactorial은 거의 동일하다. 이를 다음과 같이 일반화하면 어떨까?

def formatResult(name: String, n: Int, f: Int => Int) = {
    val mag = "The %s of %d is %d."
    msg.format(name, n, f(n))
}

이 formatResult 함수는 f라는 다른 함수를 인수로 받는 고차 함수이다.

 

 

 

 

 

더보기

헷갈릴까봐 단어정리

꼬리 재귀 함수(tail recursion function)

고차 함수(high-order-function)

다형적(polymorphic)

이름공간(namesepace) ㅋㅋㅋㅋㅋ