코드로 보는 함수형 프로그래밍
앞선 포스트에서 함수형 프로그래밍에 개념에 대해서 알아 보았습니다.
추상적인 이야기가 많아 포스트를 작성하는 저 또한 이해가 어려웠습니다 :(
포스트 작성 후에도 뭔가 찜찜해 인터넷을 뒤적거리다 좋은 예제를 찾았습니다. 이번 포스트에서는 함수형 프로그래밍언어 스칼라 예제를 참고하여 이해가 쉽게 정리해 보았습니다.
함수형 프로그래밍에 대한 기본적인 특징은 이번 포스트에서도 설명하고 있으며, 자세한 사항은 이전 포스트를 참고 하시길 바랍니다.
함수형 프로그래밍 맛보기
함수형 프로그래밍에는 두 가지 특징
- 함수의 순수성(purity of functions)
- 고차(high-order)함수
지난 포스트에서 설명한 순수함수와 1급함수를 말합니다.
중요한 요소이기 때문에 이 포스트에서 다시 설명 하겠습니다.
함수의 순수성(purity of functions)
순수성이란 함수가 부작용(side effect)가 없다는 것이며 순수 함수를 말합니다.
순수 함수는 두가지 이점이 있습니다.
- 이해하기 쉽다
- 함수의 정확성을 증명하기 쉽다. (정확성 : 함수나 알고리즘이 주어진 명세에 맞는가)
이 외에도 순수 함수는 참조 투명성을 보장합니다. 입력값이 같다면 결과값도 항상 같습니다.
순수 함수는 다중 쓰레드에서 실행되기 위해 쉽게 재배치 되며 순서를 변경할 수 있습니다.
그로 인해 멀티코어 프로세서에서 동시성 프로그래밍에 사용하기 쉽습니다.
고차(high-order)함수
일등 함수(first-class function)을 이야기 합니다.
우리는 함수에 함수를 넘길 수 있고, 함수 내에서 함수를 만들 수도 있고, 함수에서 함수를 반환할수 있습니다.
일등함수 이기 때문입니다.
아래 iterator를 봅시다. 주어진 주식 가격 목록에서 각각의 가격을 별도의 줄에 프린트 하려고합니다.
자바라면 아래와 같이 for를 사용할 것입니다.
for(int i = 0 ; i < prices.size(); i++){
가격출력...
}
이를 for-each를 사용해봅시다
for( double price : prices ){
가격출력...
}
이 스타일을 스칼라로 작성해보았습니다.
val prices = List(211.10, 310.12, 510.45, 645.60, 832.33)
for(price <- prices){
println(price)
}
스칼라는 prices 리스트가 List[Double]라는 결정을 내리고 price의 타입을 Double로 합니다.
위와 같은 스타일의 이터레이터는 외부 이터레이터(external iterator)라고 합니다.
스칼라는 내부 이터레이터도 지원하며 아래와 같이 foreach함수를 사용해서 작성할 수 있습니다.
prices.foreach{ e=> println(e) }
이 foreach 함수는 고차함수(1급 함수)입니다. 이 함수는 매개변수로 다른 함수를 받습니다.
함수는 네가지 부분으로 구성되어 있습니다.
- 이름
- 매개변수 목록
- 반환 타입
- 몸체
앞의 코드에서 몸체가 함수 ‘foreach’에 전달되었습니다.
몸체는 => 와 } 사이에 있습니다.
매개변수 목록은 e입니다. 스칼라는 타입을 추론할 수 있기 때문에 e : Double라고 쓸 필요가 없습니다)
스칼라는 이미 foreach가 어떤 타입의 함수를 원하는지 알기 때문에 이 함수의 리턴타입도 알고 있습니다.
이 함수는 이름없는 함수(anonymous function) 이므로 이름 부분이 없습니다.
위에서 넘긴 이름없는 함수를 스칼라에서 함수값이라고 부릅니다.
스칼라는 이 코드를 더 줄일수 있습니다
위의 예와 같이 함수값의 인자로 전달된 값을 다시 함수 몸체안의 다른 함수로 전달만 할 분이라면 스칼라가 알아서 그렇게 하도록 만들수 있습니다.
prices.foreach { println }
매개변수는 함수값에 전달되고 println에 넘겨지게됩니다.
스칼라는 다음과같은 파이프 스타일도 지원합니다. 결과 셋을 다음으로 넘겨서 계속 처리하는 방식입니다.
아래예제는 위 예제와 동일하게 동작합니다.
prices foreach println
위 예제를 통해서 함수형 스타일을 사용하는 법을 알수 있었습니다.
그것은 단지 한 원소에 대해 동작하는 함수값을 내부 이터레이터에 전닳는 것이며, 내부 이터레이터는 리스트 안의 각각의 원소를 해당 함수에 매개변수로 전달해 호출하는 일을 알아서 할 것입니다.
스칼라의 컬렉션에는 다른 취향의 내부 이터레이터도 있습니다. 그중 몇가지의 활용법을 살펴보겠습니다.
find 함수
prices리스트에서 500보다 큰 최초의 값을 찾고자 합니다. 이럴 때 별도의 변수 없이도 찾을 수 있습니다.
prices find { e => e > 500 }
filter 함수
500보다 큰 모든 값을 찾고싶을 경우는 find를 filter로 바꿔줍시다
prices filter { e => e > 500 }
map 함수
prices 리스트 각각의 값을 10% 계산하고자 할때는 map함수를 활용해 우아하게 해낼 수 있습니다.
println(prices map { e => e * 0.1 })
결과
List(21.11, 31.012, 51.045, 64.56, 83.233)
map함수는 주어진 함수값을 리스트의 모든 원소에 한번씩 적용하고 함수가 리턴하는 값을 모아서 리스트로 만든 후 마지막으로 이 리스트를 반환합니다.
reduce 함수
우리는 이번에 주어진 모든 값(prices 리스트 값)의 합을 내야 합니다.
기존 java라면 아래와 같이 구할 수 있습니다
var total = 0.0;
for(price <- prices){
total += price;
}
println("Total is " + total);
결과
Total is 2509.6
하나의 변수에 값이 계속 바뀌면서 최종값을 도출하게됩니다. 함수형으로 바꾸면 그러지 않아도 됩니다
println("Total is " + prices.reduce { (e1, e2) => e1 + e2})
결과
Total is 2509.6
reduce함수는 두 값을 인자로 받는 함수값을 취합니다.
함수를 처음 호출 할때 e1은 리스트의 첫 원소 e2는 리스트의 두번째 원소로 지정됩니다.
그 이후 함수값을 호출할 때마다 e1에는 이전에 함수값을 호출했던 결과값(e1 + e2)이 지정됩니다.
e2에는 리스트의 다음번 원소가 지정됩니다.
reduce에 관하여 첨언하자면
val x = [a1, a2, a3, a4]에 대해서 x.reduce(f)는 f(f(f(a1, a2), a3), a4) 처럼 계산됩니다
허나 f(f(f(a1, a2), a3), a4) 처럼 될지 f(a1, f(a2, f(a3, a4)))가 될지 보장이 되지 않습니다.
그럴 경우 reduceleft나 reduceRight를 사용하면 됩니다.
객체지향 프로그래밍과의 차이
객체지향 프로그래밍에서는 객체 합성을 잘 하기 위해서 노력합니다.
함수 프로그래밍에서는 함수 합성으로 설계를 합니다.
함수 프로그래밍은 상태를 저장한 변수나 객체를 그 자리에서 변경하지 않고, 함수 호출 흐름을 통해서 전환합니다.
실용적으로 어떻게 사용하는가
위에서 몇가지 함수를 살펴보았으니 실용적으로 어떻게 사용하는지에 대해서 알아봅시다.
아래 예제를 통해 절차중심 스타일과 함수형 스타일의 차이를 볼 수 있습니다.
목표
$500를 넘지 않는 가장 비싼 주식을 찾아야 합니다.
예제에서는 야후 금융 정보를 이용합니다. yahoo finance api
함수 준비
우선 간단한 티커 기호로 시작해봅시다
(티커란 : 증권 시세표시시 종목번호:가격과 같은 정보가 흘러 가는 띠)
val tickers = List("AAPL", "AMD", "CSCO", "GOOG", "HPQ", "INTC", "MSFT", "ORCL", "QCOM", "XRX")
편의상 주식 종목을 출력해주는 케이스 클래스를 만들어봅시다
케이스 클래스는 스칼라에서 쉽게 패턴매칭을 할 수 있는 등의 편리함을 제공하는 변경 불가능한 인스턴스를 만들 때 유용합니다.
case class StockPrice(ticker : String, price : Double) {
def print = println("Top stock is " + ticker + " at price &" + price)
}
Yahoo Finance API를 이용하여 주어진 티커 기호에 대한 가격을 얻어오는 함수를 만들어봅시다
def getPrice(ticker : String) = {
val url = "http://ichart.finance.yahoo.com/table.csv?s=" + ticker
val data = io.Source.fromURL(url).mkString
val price = data.split("\n")(1).split(",")(4).toDouble
StockPrice(ticker, price)
}
위 함수는 야후 URL에서 마지막 주식 가격을 읽어와서 결과값을 파싱합니다.
그 후 티커 기호와 가격을 설정한 StockPrice인스턴트를 반환합니다.
이제 500$를 넘지않는 최대 주식 가격을 찾아 봅시다.
두가지 함수를 추가 하여야 합니다.
- 두 주식 가격 비교
- $500 넘지 않는지 검사
def pickHighPriced(stockPrice1 : StockPrice, stockPrice2 : StockPrice) =
if(stockPrice1.price > stockPrice2.price) sotckPrice1
else stockPrice2
def isNotOver500(stockPrice : StockPrice) = stockPrice.price < 500
두 StockPrice인스턴스가 있다면 pickHighPriced함수는 더 높은 가격의 종목을 반환합니다.
isNotOver500은 가격이 $500이하이면 true를 반환하며, 그렇지 않으면 false를 반환합니다.
절차적 스타일로 구현
위에서 생성한 함수들을 이용하여 우리에게 친숙한 절차적 스타일로 다음과 같이 문제를 풀 수 있습니다.
import scala.collection.mutable.ArrayBuffer[StockPrice]
val stockPrices = new ArrayBuffer[StockPrice]
for(ticker <- tickers) {
stockPrices += getPrice(ticker)
}
for(stockPrice <- stockPrices.reverse){
if(!isNotOver500(stockPrice)) stockPrices -= stockPrice
}
var highestPricedStock = StockPrice("", 0.0)
for(stockPrice <- stockPrices){
highestPricedStock = pickHighPriced(highestPricedStock, stockPrice)
}
highestPricedStock print
//Top stock is AAPL at price $377.41
위에서 어떻게 진행되는지 코드를 하나하나 살펴 봅시다.
우선 ArrayBuffer의 인스턴스를 만듭니다. 이는 변경가능한 컬렉션입니다.(Mutable)
getPrice 함수를 각각의 티커로 호출하여 stockPrices를 각각의 StockPrice인스턴스로 채워넣습니다.
stockPrices를 이터레이션 하면서 $500을 초과하는 모든 주식을 컬렉션에서 제거합니다. (-=이용)
마지막으로 stockPrices를 한번 더 돌면서 현재 리스트인 $500을 넘지 않는 주식 중에서 가장 큰 값을 찾습니다.
highestPricedStock을 StockPrice(“”,0.0)으로 초기화 하였으나 pickHighPriced함수를 거쳐서 highestPricedStock 변수가 변화되게 됩니다.
그리고 결과 출력!
절차 중심의 스타일로 작업하였습니다.
이 코드를 더 개선하거나 할 수 있겠으나 기본 방향은 바뀌지 않을 것입니다. 주식 종목과 가격을 모은 컬렉션의 상태는 몇번 변경을 거치게 됩니다.
함수형 스타일로 구현
tickers map getPrice filter isNotOver500 reduce pickHighpriced print
음?
네 맞습니다. 이게 전부입니다.
tickers map getPrice
는 티커 컬렉션을 StockPrice
인스턴스의 컬렉션으로 변화합니다.filter
함수가 인스턴스 컬렉션을 받은 이후 적용 되며, isNotOver500
을 사용해 컬렉션에서 $500이하의 가격인 StockPrices 인스턴스만을 가지는 더 작아진 컬렉션을 반환합니다.reduce
함수는 더 작아진 컬렉션에서 pickHighPriced
함수를 사용하여 가장 비싼 주식을 찾아 냅니다.
마지막으로 그 결과로 반환된 StockPrice
의 print
메소드가 호출됩니다.
간결할 뿐 아니라, 이 코드는 상태를 변경하지 않습니다. 상태는 합성된 함수의 흐름에 따라 전달되면서 변환될 뿐입니다.
함수형 프로그래밍의 이점
변화되는 상태가 없기 때문에 몇몇 변화되는 부분이 있는 경우보다 코드에서 오류가 날 가능성이 줄어듭니다.
각 중간 단계에서 훨씬 간단하게 별도의 유닛 테스트를 작성 할 수 있습니다.
즉 코드 각각을 하나하나 쫓아가 보거나 전체적으로 쫓을수 있습니다.
함수형 프로그래밍의 단점
컬렉션이 크다면 복사에 따른 부가 비용이 발생할 수 있습니다.
마치며
우선 스칼라 함수 프로그래밍 맛보기를 이해하기 쉽게 번역해주신 Scala-Korea 오현석님께 감사의 말씀을 드립니다.
처음엔 그저 함수형 프로그래밍이 뭔지에 대해서 설명하려 했는데 어쩌다 보니 내용이 이렇게 길어졌습니다.
함수형 프로그래밍언어는 관심있지만 아직 심도있게 파고들지는 않을 예정이라 아마 스칼라 관련 포스트는 추후에 진행 되지않을까 싶습니다.
보시느라 수고하셨습니다.
참고
Scala-Korea
스칼라문법요약 - JDM’s Blog