오대산/한주영

함수형 프로그래밍 소개

Short introduction to Functional Programming and Scala

Jooyung Han (한주영)
11 min readOct 20, 2013

--

Coursera 에서 수강중인 과정(Scala로 배우는 함수형 프로그래밍의 원칙)을 소개하면서 더불어 ‘함수형 프로그래밍'을 소개할 기회가 생겼다. 설명하다보니 나조차 아직 잘 모르고 있는 내용이 많지만, 이 또한 또다른 초보에게는 도움이 될지도 모르겠다는 생각에서 여기에 다시 옮겨본다.

Scala의 특징

Scala라는 언어는 참 독특한 언어같다. 아직 모르는 것 투성이지만 지금까지 알게된 것만으로도 정말 특이한 언어다. “JVM언어이면서 객체지향과 함수형 프로그래밍을 결합시킨 언어"라는 소개는 다소 식상하겠지만 괜찮은 요약같다.

몇가지 재미난 특징.

함수를 호출할 때 인자를 미리 계산할 지 나중에 계산할 지도 지정할 수 있다. Call by Value 와 Call by Name 이라는 두가지를 모두 지원한다는 얘기. 대개는 언어마다 한 가지 방식을 지원하기 마련일텐데, 인자 리스트에서 이 규약을 지정할 수 있다는 점이 놀라웠다. 지금의 Java에서는 Runnable이나 Callable 같은 인터페이스를 이용하여 thunk를 만들어 넘기는 방식을 취해서 흉내를 낼 수야 있겠지만 언어적으로 지원하는 것에 비하여 간결성의 차이가 크다.

C++에서는 아직 class 라는 키워드 하나로 다양한 개념을 표현했다면 Java에서는 class/interface 라는 키워드로 분리되었다. Scala에서는 trait/class/object/case class 등으로 더 세분화되었다. object 키워드로 단 하나뿐인 객체의 정의를 표현하는 것도 신기했고, trait를 이용하여 다중 구현 상속을 하는 것도 신기했지만, case class를 이용하여 패턴 매칭을 위한 구조를 기존의 클래스 개념에 녹여낸 것이 놀라웠다.

Java Generics에서 아직도 도저히 잘 써먹기 힘든 와일드카드(?)와 관련하여 Scala에서는 클래스를 선언하는 곳에서 covariance/contravariance/invariance를 지정할 수 있다는 점도 특이했다.

물론 아주 기본적인 것으로, var/val 키워드도 처음에는 신기했다. Java처럼 for문을 돌면서 sum을 구할 수도 있고, Haskell이나 Lisp계열 언어처럼 재귀적 표현식으로도 구현할 수 있다. Eclipse에 기반한 IDE에서 제공하는 Worksheet라는 것도 신기했다. (object를 구성하는 한 줄 한 줄을 저장할 때마다 계산하여 바로 옆에 출력해줘서 Smalltalk의 Workspace와 같은 느낌을 준다.)

함수형 프로그래밍의 특징

지금 수준에서 내가 생각하는 함수형 프로그래밍의 큰 특징 2가지는 immutable data와 first-class function이다.

Immutable

함수형 프로그래밍을 가능하게 하는 요소 중 하나가 바로 immutable data이다. 한번 정해진 값은 바뀌지 않는다는 것. primitive type 뿐만 아니라 Java의 Collection에 해당하는 자료 구조들에도 똑같이 적용된다. List를 하나 만들어서 추가/삭제하는 대신 추가가 필요하면 추가된 새로운 List를 만들고, 삭제가 필요하면 삭제된 새로운 List를 만드는 식이다. 이러한 자료 구조를 Persistent Data Structure 라고 부른다.

처리할 데이터가 불변이기 때문에 우리는 수학적 의미의 함수 형태로 함수를 만들게 된다. modifyState(state) 로 state의 내부 상태를 바꾸는 대신 아니라 newState = modifyState(state) 로 새로운 상태의 state를 만든다.

Java의 Map객체가 제공하는 서비스는 put/get/remove가 대표적이다. 즉, 객체를 하나 놓고 값을 넣고 확인하고 지우고 하면서 객체의 상태를 바꾸는 식으로 개발하게 이끈다. 그러나 String을 다룰 때에는 그렇지 않다. String객체는 이미 Java에서도 불변 객체여서 문자열을 다루는 함수들만큼은 좀더 ‘함수형' 스타일에 맞게 짜게 된다.

그런데, 이를 위해서는 자료구조의 ‘효율적' 구현이 필수적이다. 어설프게 함수형 프로그래밍을 공부한 초보 Java 개발자가 ArrayList 객체를 마치 String 객체 다루듯, 즉, 마치 Persistent Data Structure 인양 다루는 코드를 보고 ‘함수형 프로그램은 위험하다'라고 지적한 블로거도 있었다. Java의 ArrayList 같은 컬렉션 객체는 불변 객체처럼 사용하라고 만들어진 객체가 아니다. 그렇게 쓴다고 해서 얻는 이득이 현실적으로 거의 없다. Clojure라고 하는 언어에서는 Java의 Vector와 비슷한 vector 자료 구조를 제공하는데, 심지어 맨 뒤에 요소 하나를 추가하는 경우에도 거의 상수 시간에 가까운 성능을 보여준다고 한다.

First-class ‘function’

함수(혹은 메소드)를 인자로 넘기거나 반환할 수 있고, 리터럴로 바로 정의할 수 있음을 말한다.

Java에서는 ‘객체지향'을 ‘지향'하다보니 가장 기본적인 실행 요소인 함수를 ‘2등 시민'으로 전락시켜버렸다. 함수의 인자로 사용할 수 없게 된 것이다. 그러다보니 수없이 많은 interface가 생겨났고, 이렇게 이름을 붙이기 힘든 경우에 사용하다보니 Runnable 인터페이스는 정말 많이 사용된다. (return값이 붙은 Callable은 사실 Runnable과 마찬가지다.) 늦기는 했지만 Java8에서 lambda가 지원된다고 하니 불편이 다소 해결되지 않을까?

Expression Problem

객체지향 프로그래밍과 비교할만한 사례는 Expression Problem이 좋을 것 같다.

수식(수, 합, 곱 등)을 모델링하되 나중에 새로운 case(변수 등)를 추가하거나 새로운 operation(codegen 등)을 추가할 때 기존 코드를 수정하지 않아야 한다. — Philip Wadler

객체지향적 풀이는 다음과 같다. (예제 코드는 Scala로 작성되었으나 Java를 안다면 코드를 이해하는 데 큰 어려움은 없을 것이다.)

trait Expr {
def eval: Int
}
class Number(value: Int) extends Expr {
override def eval: Int = value
}
class Sum(left: Expr, right: Expr) extends Expr {
override def eval: Int = left.eval + right.eval
}
new Sum(new Number(3), new Number(4)) ==> 7

eval이라는 operation을 각 type이 알아서 처리해주는 polymorphism이 바로 핵심이다.

함수형 풀이는 다음과 같다.

trait Expr {
def eval: Int = this match {
case Number(value) => value
case Sum(left, right) => left.eval + right.eval
}
}
case class Number(value: Int) extends Expr { }
case class Sum(left: Expr, right: Expr) extends Expr { }
Sum(Number(3), Number(4)) ==> 7

operation이 한 군데 정의되어 있다.

얼핏 타입에 따라 case로 분기하는 모양새가 객체지향에서 그렇게 피하라고 하는 switch-case와 같아보인다. 객체지향에서 이 문제를 멋지게 해결한 것이 polymorphism이라고 알고 있으니 말이다.

하지만 Expression Problem은 여기에 새로운 case 혹은 operation을 추가해보라고 한다. 기존 코드 수정없이.

객체지향은 이름에 걸맞게 새로운 객체, 즉 새로운 case를 추가하는데 효과적인 구조다. Product 라는 case를 하나 더 추가해보자. 기존 코드(Number, Sum 및 클라이언트 코드)를 수정하지 않고 쉽게 Product를 추가할 수 있다.

class Product(left: Expr, right Expr) extends Expr {
override def eval: Int = left.eval * right.eval
}

반면 출력하기 위한 print 라는 operation을 추가하는 경우에는 어떤가? 기존의 모든 case들에 operation을 추가해줘야 한다.

trait Expr {
..
def print: String
}
class Number ... {
override def print: String = value.toString
}
class Sum ... {
override def print: String =
left.toString + "+" + right.toString
}

함수형 구현에서는 반대로 나타난다. 이름에 걸맞게 새로운 operation, 즉 함수를 추가하는 것은 쉬운 반면 새로운 case를 추가하려면 기존의 operation들을 모두 수정해줘야 한다.

trait Expr {
def eval: Int = this match {
case Number(value) => value
case Sum(left, right) => left.eval + right.eval
case Product(left, right) => left.eval * right.eval
}
def print: String = this match {
case Number(value) => value.toString
case Sum(left, right) => left.print + "+" + right.print
case Product(left, right) => left.print + "*" + right.print
}
}
case class Number(value: Int) extends Expr { }
case class Sum(left: Expr, right: Expr) extends Expr { }
case class Product(left: Expr, right: Expr) extends Expr { }

어느 하나가 다른 하나보다 낫다기 보다는 마주친 문제가 객체지향적 접근이 유리할 지 함수형 접근이 유리할 지에 따라 달라질 것으로 보인다. (이 부분에서 Scala의 강점이 드러난다. Java의 경우에는 개발자의 선택권이 제한된다. 함수적으로 작성하기 쉽지 않기 때문이다. 반대로 순수한 함수형 언어에서는 객체지향적 접근이 제한된다. 그러나 Scala는 둘 중 하나를 개발자가 문제 상황에 맞게 선택할 수 있다.)

Why Functional Programming (again)?

멀티코어가 기본이 되면서 ‘동시성' 처리에 함수형 프로그래밍이 강점을 보이기 때문이 아닐까.멀티 쓰레드 프로그래밍이 불과 몇년전까지도 가능한 피해야 할 문제였다면 이제는 반드시 고려해야 하는 기본이 되었다. 그러나 아직 보통의 많은 개발자들은 어려워한다. 준비가 덜 되어 있다. 그런데 한편으로는 우리가 사용하는 언어가 문제를 더 복잡하게 만들어서 그런 것은 아닐까?

Side effect 에 기반한 객체지향 프로그래밍에서는 멀티쓰레딩에 효과적으로 대응하기 쉽지 않지만 불변 값을 주로 다루는 함수형 프로그래밍은 본질적으로 동시성 처리가 쉬워진다.

그러나 이 부분은 사실 ‘동시성 지향 프로그래밍(Concurrency oriended programming)'이라는 또다른 주제로 생각해 볼 수도 있을 것 같다. Go라는 언어에서 Concurrency를 풀어내는 방법을 함수형 프로그래밍 방식이라고 볼 수는 없고, OO나 FP와는 또다른 모양새를 띄기 때문이다.

그렇다면, 지금 다시 함수형 프로그래밍이 주목받는 이유는 무엇일까? 개인적으로는 함수형 프로그래밍 ‘언어'의 표현력 때문이 아닐까 생각한다. 파이썬이나 Java 등으로도 함수형 프로그래밍이 가능하다고는 하지만 이렇게 할 것을 권하지 못하는 이유는 언어의 제약으로 인해 표현력의 이득이 거의 없기 때문이다.

여기에 더불어서 앞서 얘기한 동서성 문제를 접근하면, 동시성 프로그래밍을 지원하기 위한 라이브러리나 이디엄을 효과적으로 추상화하여 제공하기 때문에 함수형 언어들이 동시성 문제를 더 잘 지원한다고 볼 수 있을 것 같다.

이 외에도 함수형 프로그래밍 본질적인 장점을 꼽아보면 Side-effect 에 의존한 코드에 비해 유지 보수가 용이하다는 점, 코드를 이해하기 수월하다는 점이 있을 것 같다.

Coursera의 현재 과정에 이어서 “Principles of Reactive Programming”을 들을 예정이다. 함수형 프로그래밍의 연장선 상에서 어떤 이야기가 전개될지 기대된다.

--

--

Jooyung Han (한주영)

가끔 함수형 프로그래밍 관련 글을 쓰거나 번역합니다. “개미 수열을 푸는 10가지 방법"이란 책을 썼습니다. https://leanpub.com/programming-look-and-say