Skip to content

2016 08 09 cbk

baekkyu cho edited this page Aug 9, 2016 · 16 revisions

2장 스칼라로 함수형 프로그래밍 시작하기

<주요내용>
- 꼬리재귀함수(tail recursion)를 이용하여 loop를 작성하는 방법
- 고차함수(higher-order function) 소개
- 다형적(polymorphic) 고차함수의 예

2.1 스칼라 언어의 소개

// 주석
/* 주석 */
/** 주석 */
object MyModule {    //object 키워드는 singleton 형식을 만든다. (java의 static과 비슷)
  def abs(n: Int): Int =    // 정수 하나를 받고, 절댓값을 돌려주는 순수함수
    if (n < 0) -n           // 등호(=) 기준으로 왼쪽을 좌변 또는 서명(signature), 오른쪽을 우변 또는 정의(definition)이라고 부른다.
    else n                  // return 키워드는 없음. 우변의 평가결과가 곧 메서드의 반환값이다.

  private def formatAbs(x: Int) = {    // return형식은 타입추론이 가능하다.
    val msg = "The absolute value of %d is %d"
    msg.format(x, abs(x))
  }

  // java의 main메서드와 동일한 기능, 부수효과가 발생하기 때문에 절차(procedure), 불순함수(impure funtion)라고 부른다.
  def main(args: Array[String]): Unit =    //Unit은 java, c의 void와 같은 목적
    println(formatAbs(-42))
}

2.2 프로그램의 실행

  • 일반적으론 스칼라용 빌드도구인 sbt를 이용하는 것이 좋다.
  • 간단한 방법은 스칼라 컴파일러를 이용하는 것이다.
// 컴파일 -> .class파일이 생성된다 (JVM에서 실행될 수 있는 java byte code로 컴파일됨)
> scalac MyModule

// 컴파일된 파일을 스칼라를 이용하여 실행
> scala MyModule
The absolute value of -42 is 42

// 직접 소스파일을 지정하여 스칼라 해석기(interpreter)로 실행하는것도 가능.
> scala MyModule.scala
The absolute value of -42 is 42

// 대화식모드 REPL(read-evaluate-print loop)을 이용하는 방법.
scala> :load MyModule.scala
Loading MyModule.scala...
defined object MyModule

scala> MyModule.abs(-42)
res0: Int = 42

2.3 모듈, 객체, 이름공간

  • 스칼라의 모든값은 **객체(object)**이다, 객체는 0개 또는 하나이상의 구성원(member)를 가질 수 있다.
  • 2 + 1 같은 표현식도 object의 member를 호출하는 것이다. 해당표현식은 2.+(1)에 대한 구문적 겉치레(syntatic sugar)일 뿐이다.
  • 스칼라에서는 연산자(operator)라는 개념이 존재하지 않는다.
  • 인수가 하나인 메서드(method)는 마침표와 괄호를 생략한 중위(infix)표기법으로 호출이 가능하다.
  • MyModule은 abs가 속한 이름공간(namespace) 이다.
  • 자신의 member들에게 namespace를 제공해주는 것이 주된목적인 object를 흔히 **모듈(module)**이라고 부른다.

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

  • 순수한 함수적 프로그램을 작성할 때에는 다른 함수를 인수로 받는 함수를 작성하는 것이 유용한 경우가 많다. 이런 함수를 고차함수(higher-order function)라고 부른다.

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

  • 계승을 구하는 factorial 함수 작성
  // loop를 함수적으로(loop 변수의 변이없이) 작성하는 방법은 재귀함수(recursive function)를 이용하는 것이다.
  def factorial(n: Int): Int = {
    @annotation.tailrec    // 꼬리호출제거여부를 확인해주는 annotation
    def go(n: Int, acc: Int): Int =    //지역정의(local definition)
      if (n <= 0) acc
      else go(n-1, n*acc)

    go(n, 1)
  }
  • 재귀함수는 stackoverflow를 발생시킬 위험이 있다.
  • 스칼라 컴파일러는 자기재귀(self-recursion)를 검출해서, 재귀호출이 꼬리위치(tail position)에서 일어난다면 바이트코드를 생성할 때, while loop와 같은 형태로 변환시켜 준다.

2.4.2 첫 번째 고차 함수 작성

  • 기존 MyModule object에 방금 만든 formatFactorial 함수를 추가 하였다.
  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))
  }
  • formatAbs, formatFactorial은 거의 동일하다. 이 둘을 인수들에 적용할 함수를 인수로 받는 하나의 함수로 일반화 하였다.
  // 고차함수 f의 형식 "Int => Int" 
  def formatResult(name: String, n: Int, f: Int => Int) = {
    val msg = "The %s of %d is %d."
    msg.format(name, n, f(n))
  }

2.5 다형적 함수: 형식에 대한 추상

  • 단형적 함수(monomorphic function) : 한 형식의 자료에만 작용하는 함수
  • 다형적 함수(polymorphic function) : 임의의 형식에 대해 작동하는 코드

2.5.1 다형적 함수의 예

  • 배열에서 문자열을 찾는 monomorphic function
  def findFirst(ss: Array[String], key: String): Int = {
    @annotation.tailrec
    def loop(n: Int): Int =
      if (n >= ss.length) -1
      else if (ss(n) == key) n
      else loop(n + 1)
    loop(0)
  }
  • 배열에서 한 요소를 찾는 polymorphic function
  def findFirst[A](as: Array[A], p: A => Boolean): Int = {
    @annotation.tailrec
    def loop(n: Int): Int =
      if (n >= as.length) -1
      else if (p(as(n))) n    //각 요소를 검사하는 함수 p
      else loop(n + 1)

    loop(0)
  }
  • 다형적 함수를 하나의 메서드로 작성할 때에는 쉼표로 구분된 **형식 매개변수(type parameter)**를 대괄호로 감싼다. (관례상 한글자 대문자 형식으로 작성한다)
  • type parameter는 형식 서명(signature)의 나머지 부분에서 참고할 수 있는 형식변수(type variable)들을 도입한다. (함수의 매개변수 목록이 함수의 본문에서 참조할 수 있는 변수들을 도입하는 것과 마찬가지이다)

2.5.2 익명 함수로 고차 함수 호출

  • 고차 함수를 호출할 때, 기존의 이름 붙은 함수를 인수로 지정해서 호출하는 것이 아니라, 익명함수(anonymous function) 또는 **함수리터럴(function literal)**을 지정해서 호출하는 것이 편리한 경우가 많다.
// REPL에서 작성한 Int 2개를 인수로 받고, 두 값이 같은지 여부를 되돌려주는 함수리터럴.
scala> (x: Int, y: Int) => x == y
res1: (Int, Int) => Boolean = <function2>
// <function2>라는 표기는 res1이 인수를 2개받는 함수라는 뜻이다.
  • 스칼라에서 함수 리터럴을 정의할 때, 실제로 정의되는 것은 apply라는 메서드를 가진 하나의 객체이다.
// 함수리터럴 
val lessThan = (a, b) => a < b

// 해당객체에 대한 syntactic sugar
val lessThan = new Function2[Int, Int, Boolean] {
  def apply(a: Int, b: Int): Boolean = a < b
}

// 참고-apply (apply메서드를 가진 객체는 그 자체를 메서드인 것처럼 호출이 가능하다)
class Obj {
  def apply() = new Obj()
}
val a = new Obj()
val b = Obj()

2.6 형식에서 도출된 구현

부분적용

  • **부분적용(partial application)**이라고 부르는 작업을 수행하는 고차함수를 살펴보자.
  • 위 함수는 부분적용함수(partially applied function) 라고 불린다.
//abstract
def partial1[A, B, C](a: A, f: (A, B) => C): B => C
  • 위 함수는 값 하나와 함수(2개의 parameter를 받는) 하나를 받고, parameter가 하나인 함수를 돌려준다.
  • "partial"이름은 해당함수가 주어진 인수들 모두가 아니라, 일부에만 적용된다는 사실에서 비롯된 것이다.
def partial1[A, B, C](a: A, f: (A, B) => C): B => C =
  (b: B) => f(a, b)    // C를 얻을 수 있는 유일한 방법
  • parameter가 2개인 함수를 받아서 부분적으로 적용하는 고차함수가 생성되었음.
  • 위와 같은 고차함수는 아래처럼 응용이 가능하다.
//partial1 example
val line = Line(1,2,3)
val contains = (l: Line, p: Point) => l.contains(p)
val containsLine = partial1(line, contains)    // closure
println(containsLine(Point(2, 2))) // true, false

참고 연습문제: 커링

  • 커링(currying) : 수학과 컴퓨터 과학에서 커링이란 다중 인수 (혹은 여러 인수의 튜플)을 갖는 함수를 단일 인수를 갖는 함수들의 함수열로 바꾸는 것을 말한다.

  • currying 구현예제

  • parameter가 2개인 함수 f를 parameter로 받고, f를 부분적용하는 함수로 변환한다.

def curry[A, B, C](f: (A, B) => C): A => (B => C) =
    a => b => f(a, b)

// currying 사용예제
def sum = (x: Int, y: Int) => x + y
val curriedSum = curry(sum)
val x = curriedSum(5)
x(3)  // res: Int = 8
x(5)  // res: Int = 10
  • scala에서의 currying 예제
  • parameter 개수보다 적은 parameter로 함수를 호출할 경우 누락된 parameter를 받는 새로운 함수를 생성한다.
def curriedSum = (x: Int, y: Int) => x + y
val x = curriedSum(5)_    // 위치표시자를 입력해야한다
x(3)  // res: Int = 8
x(5)  // res: Int = 10

함수 합성

  • 함수합성(function composition) : 한 함수의 출력을 다른 함수의 입력으로 공급하는 함수
def compose[A,B,C](f: B => C, g: A => B): A => C = 
  a => f(g(a))

// compose example
val sqrt = (x: Int) => x * x
val plusten = (x: Int) => x + 10
val composed = sqrt compose plusten
scala> composed(5)
res1: Int = 225
  • 함수형 프로그래밍에서는 이런 합성이 아주 일상적으로 쓰이기 때문에, 이를 위해 스칼라 표준라이브러리는 compose, andThen과 같은 메서드를 기본으로 제공한다.
  • f compose g 는 g andThen f 와 같다.
Clone this wiki locally