진짜 함수형 초보자들을 위한 트레이닝(1) in Scala 


검색해보면 많은 초보자들을 위한 함수형 글들이 있지만, 꽤 어렵지 않나 생각이 드는데요. 

예) 초보자친화적 함수형 프로그래밍 투어 <-  이분 블로그에 좋은 글 많네요.

저도 1년 넘게 회사에서 계속 파이썬만 주력으로 사용해서 감이 많이 떨어져서요. (애초에 감이 있었던적도 없었던..) 이번 글에서는 진짜 쉽게 쉽게 간단한 함수를 사용하는 예제를 통해 감을 익히는 시간을 갖도록 하겠습니다. 덤으로 제네릭을 버무려서 말이죠. 스칼라(Scala) 언어로 할 건데 중간 중간 설명해 드릴테니 몰라도 상관없지 않나 합니다. 그래도 자바,C++,파이썬 같은 언어 경험은 있어야 하긴 할 겁니다.

함수를 매개변수로 넣는것은 C에서도 가능하며 대부분의 언어에서 문제는 없습니다만, 함수형에서는 왜 특별 할까요? 이유는 순수함수이기 때문입니다. 즉 매개변수로 들어가는 함수가 내부에서 어떤 부작용을 일으킬지 걱정을 전혀 안해도 된다는 겁니다. 그 함수가 어떤 값을 리턴하는지만 관심을 집중하면 되거든요. 

즉 함수를 통한 구성을 하는데 있어서 많은 지식과 불안요소가 삭제되며, 아주 큰 시스템이라도 부분 부분을 명확히 이해 가능하기 때문에, 코딩을 하고, 유지보수를 하는데 있어서 심플해 집니다.

자 시작해보죠!!

예1)

def add (a : Int, b : Int): Int = {
a + b
}

스칼라에서 함수는 def 로 시작합니다. 
a : Int 는 매개변수 a 는 Int 타입이라는 의미이구요. 자바와는 순서가 다릅니다. 
리턴 값은 Int 형이라는 뜻이며 
= { } 에 함수의 본문을 넣습니다. 
a+b 를 리턴하네요. 스칼라에서는 return 을 보통 생략합니다.

add(1,2)

이렇게 호출해주면 3이 나올 겁니다.

def add (a : Int, b : Int): Int = a + b

참고로 이렇게 {} 를 생략 할 수도 있습니다. 

def abs(n: Int): Int =
if (n < 0) -n
else n

절대값을 리턴해주는 함수입니다. return 문은 역시 없죠. if 문을 통해 통째로 평가되어진 값이 리턴되니까요.


예2)

예1에서 add 함수는 Int 형 밖에 사용을 못했는데요. Double 형도 같이 사용하고 싶을 때 어떻하죠? 
네 이때 제네릭(타입 파라미터) 이 사용됩니다. Int 라고 못박지 말고, 그냥 A 라고 해 두는거죠. (사실 aaaa 라고 해도 됩니다만 제네릭에서는 보통 T,A,B,C 이렇게 짧게 사용합니다.) 

def add[T](x: T, y: T): T = x + y

자 이렇게 해주는것이 가장 먼저 떠올랐습니다. 그냥 T 라는 타입으로 대표해서, T 에는 Int, Double 아무거나 포함한다는 것 이겠네요? 뭐 이렇게 해서 add(1,2) or add(5.5, 4,3) 해 보면? 

삑!!!! 이 형태는 오류이며  컴파일이 안됩니다. 저 T 라는 미지의 타입이 도대체 + 라는 메소드 (스칼라에서는 모든것이 메소드이며 1,2,3 같은 숫자가 모두 객체입니다. 즉 1.+(2) 처럼 1 객체는 + 메소드를 호출하는 것) 즉 저 T 타입은 + 라는 메소드를 가지고 있는 숫자처럼 행동하는 무엇인가로 한정지어져야 하는데 그렇지 않다는 건데요. 이것은 스칼라의 특성이므로 (이것을 해결 하는 방법은 글 마지막에 추가 해 놓았습니다만지금 단계에서는 알 필요 없구요. ) 일단은 그냥 저렇게 해도 된다고 생각하고 넘어갑니다. 

def square(x:Int):Int = x * x

def square[T <: Number](x : T):T = { x * x }

이렇게 해도 마찬가지로 에러가 납니다. 


예3) 

def doSomthing (a :Int, f : SomeFunction ) : Int = {
...
}

이번에는 두번째 매개변수로 어떤 함수가 들어왔습니다. (슈도코드입니다) 함수 내부에는 어떻게 구현 될 까요?

def doSomthing (a :Int, f : () => Int ) : Int = {
a + f()
} doSomthing(1, ()=>3) // 이렇게 호출 !! 결과는 4

f 함수가 매개변수 없이 작동하는 함수라면 이렇게 구현 될 수도 있을테고~ 뭘 지지고 볶고 하든 리턴으로 Int 형만 내보내면 됩니다. 
여기서 () => 3 는 스칼라에서 사용하는 함수리터럴(람다식)입니다.  매개변수가 없고  => 오른쪽은 함수 본문으로 그냥 3을 리턴한다는 뜻입니다.

def doSomthing (a :Int, f : Int => Int ) : Int = {
f(a)
} doSomthing(3, (x)=>x*x) // 이렇게 호출 !! 결과는 9

f 함수가 매개변수를 하나 넣어야 작동하는 함수라면 이렇게 구현 될 수도 있을겁니다. 역시 Int 형으로 리턴하기만 하면 됩니다. 여기서 (x) => x*x 매개변수 하나를 받아서 제곱해서 리턴한다는 함수리터럴이죠.  

def doSomthing (a :Int, f : Int => Int ) : Int = {
a + f(10)
} doSomthing(3, (x)=>x*x) // 이렇게 호출 !! 결과는 103


물론 이렇게 구현 될 수도 있습니다. 

다시 반복해 말하지만 중요한것은 함수의 파라미터로 어떤 함수가 들어 갈 수도 있다는 것과, 그 함수가 어떤 타입을 매개변수로 받고 리턴받는지에 대해서만 좀 신경 쓰면 된다는 겁니다. 그 함수는 항상 동일한 파라미터를 넣어주면 동일한 값을 내뱉어주며, 내부에서 어떤 변경을 가해서 멀티쓰레딩에 위험요소를 가져올수 있는 어떤 부작용도 일으키지 않는다고 생각하면 함수를 엮어서 사용하는데에 좀 더 자신감과 명백함을 가질 수 있게 되겠지요. 


예4) 

def doSomthing (a :Int, f : Int => Int ) : Int => Int = { x => 
f(x * a * 100)
}

함수객체는 매개변수로만 들어오는것이 아니라 리턴으로도 사용됩니다.
여기서는 Int 를 매개변수로 받고 Int 를 리턴해주는 함수를 리턴해 주고 있습니다. 

(Int => Int) 에 해당하는 x => f(x * a * 100) //매개변수 하나를 받는 함수리터럴을 리턴해줍니다.

매개변수로 받은 f 함수가 Int 를 리턴해주는 함수라는것에 집중해 주세요. 


val func1 = doSomthing(1, (x)=>x*x) //매개변수 하나를 입력받고 Int 형을 리턴하는 함수를 얻음
println(func1(3)) // 답은 90000

이렇게 사용 할 수 있겠네요.


예5) 

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)
} factorial(3) // 답은 6 (3 * 2 * 1)

꼬리재귀함수입니다. 잘보면 go 함수를 내부에서 재귀 호출해주는데 가장 마지막에 호출합니다. 그래서 꼬리재귀입니다. somthing * go (...) 이런식으로 되어있다면 꼬리재귀가 아닌데요. 여기서 마지막에 호출되는것은 * 곱하기이니까요. 

def factorial2(n: Int): Int = {
var acc = 1
var i = n
while (i > 0) { acc *= i; i -= 1 }
acc
}

재귀 함수를 안쓰고 while 문을 쓴다면 위와 같을 겁니다. 이렇게 하면 상태를 변화시키는 var 즉 변수가 사용되네요.

예6)

def findFirst[A](as: Array[A], p: A => Boolean): Int = {

def loop(n: Int): Int =
if (n >= as.length) -1
else if (p(as(n))) n
else loop(n + 1)

loop(0)
}
findFirst(Array(1,10,100), (x: Int) => x == 10)  // 답은 1 

배열에서 10 과 같은 요소의 인덱스를 리턴해 주는 함수입니다. 역시 꼬리재귀를 사용했구요. 순수함수입니다. 특히 눈여겨 볼것은 2가지 인데요.

첫째) 

두번째 매개변수의 시그니처가 p: A =>Boolean 이죠? 즉 A 타입을 인자로 받아서 Boolean 타입을 넘겨주는 함수를 인자로 받겠다는 겁니다. 이 조건에 충족한 함수리터럴(익명함수) 을 넘겼습니다. 
(x: Int) => x == 10  말이죠.  x == 10 은 Boolean 타입으로 평가됩니다. 

둘째) 

배열의 타입을 강제 하지 않았네요. 타입파라미터 A 를 사용했습니다. 따라서 

findFirst(Array("john","park","carry"), (x: String) => x == "park")

문자열의 배열에도 사용 할 수 있게 됩니다.


예7)

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

마지막 예제입니다. 조금 복잡해 보입니다. 타입파라미터가 3종류이고 (A,B,C) 
매개변수는 2개입니다. 하나는 A 타입이고, 하나는 C 타입을 리턴해주는 함수타입 입니다) 
리턴되는 것 역시 함수타입입니다. B 를 받아서 C 를 리턴해주는거네요. 

함수 본문에서 

 (b: B) => f(a, b)

다음과 같이 B타입을 매개변수로 넣고 f(a,b) 를 통해 C 타입이 리턴되므로, 

: B => C 

이렇게 리턴되는것에 충족됩니다. 잠시 머리속으로 굴려보는 시간을 갖겠습니다. 생각해보세요. 
새로운 파라다임을 공부하기 위해서는 반드시 익숙해 져야하므로 반복,반복 밖에 답이 없습니다. 

여기서 partial 이라는 함수는 a 값이 이미 내장된 또다른 함수를 제공해 주는 것이 목적일 겁니다.


부록 :

해당내용을 시작하는 시점에 연구할 필요는 없을거 같습니다. (C++개발자들의 대부분이 템플릿메타프로그래밍이나 템플릿 그 자체도 깊이있게 아는사람이 거의 없듯이.. 뭐 그래도 C++ 개발자잖아요?) 

def square[T <: Number](x : T):T = { x * x }

def add[A](x: A, y: A): A = x + y

자 스칼라에서 이렇게는 컴파일이 안되며 (클래스가 아닌 자바의 primitive로 스칼라의 기본 numeric 타입이 매핑되는데, 즉 자바와의 호환성을 유지시켜주기 위해 어쩔 수 없다고도 하는데, 키 포인트는 스칼라에서는 모든 수가 공유하는 공통의 부모타입이 없으며, 대신 수학 라이브러리에서 지원하는 타입 T 에 대한 목시적인 Numeric[T] 가 있다.) 이것을 해결하기 위한 방법은 아래와 같습니다. 타입클래스 패턴이라고도 하는데요.

Numeric[T] 를 사용하면 아래와 같이 간단히 해결되며,  

def square[T](x: T)(implicit num: Numeric[T]): T = {
import num._
x * x
}

def add[A](x: A, y: A)(implicit num: Numeric[A]): A = {
import num._
x + y
}

직접 구현 해서 해결하는 방법은 아래와 같다.

trait Addable[A] {
def plus(x: A, y: A): A
}

implicit class AddableOps[A](lhs: A)(implicit ev: Addable[A]) {
def +(rhs: A): A = ev.plus(lhs, rhs)
}


implicit object IntIsAddable extends Addable[Int] {
def plus(x: Int, y: Int): Int = x + y
}


implicit object DoubleIsAddable extends Addable[Double] {
def plus(x: Double, y: Double): Double = x + y
}
def add[A: Addable](x: A, y: A): A = x + y

add(5.4,4.3) or add(2,3)




레퍼런스:

Functional Programming in Scala 
https://typelevel.org/blog/2013/07/07/generic-numeric-programming.html
https://twitter.github.io/scala_school/advanced-types.html



저작자 표시 비영리 동일 조건 변경 허락
신고
Posted by [前草] 이승현 (wowlsh93@gmail.com)