티스토리 뷰

Programming/Language

스칼라(scala) - 함수

글그리 2017. 9. 12. 16:33

개요


scala는 함수형 언어이며 모든 것이 함수로 되어있다. 예를들면 1 + 2라는 연산은 1이 +라는 함수를 호출해서 2를 파라미터로 넘겨준다는 의미를 가지고 있는 것이다.

// 1.+(2) 정도로 표현할 수 있겠다.

scala에 어떤 종류의 함수들이 있고, 그 함수를 어떻게 사용하는지 알아보자.






기본 함수 정의


기본적으로 사용자 함수를 만들기 위한 형식은 다음과 같다.


def [func id]([param id]: [type], …): [type] = { [function] }


위 형식에서 함수 내용이 한줄 정도로 짧다면 {}를 생략할 수 있고, 반환 자료형을 명시하지 않아도 scala 컴파일러가 반환되는 값으로 자료형을 추론하기 때문에 문제가 되지 않는다.

scala는 함수를 정의할 때 그 함수가 하나의 값을 계산하는 하나의 식처럼 동작하게 정의하는 것을 권장한다. 따라서 다음과 같은 모습의 함수를 작성할 수 있다.


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


함수를 호출하는 방법은 다른 언어와 같다.


val result = sum(1, 2)
// result: Int = 3






내부 함수


scala는 함수 안에 또 함수를 정의할 수 있도록 해준다. 이는 특정 값을 도출하기 위한 연산을 따로 정의하여 함수 내에서 사용할 수 있게 하는데, for루프나 while루프를 잘 사용하지 않는 scala에서 재귀함수로 반복 연산을 할 때 주로 사용하는 문법이다.


재귀함수로 List의 모든 요소를 더하는 함수


def sumList(l: List[Int]): Int = {
def do(ret: Int, l:List[Int]) = {
l match { case Nil => ret
case _ => do(ret+l.head, l.tail)}
}
do(0, l)
}


위 예제에서 볼 수 있듯이 scala에서는 재귀함수로 반복 연산을 하기 때문에 재귀함수를 구현하는 것이 중요하다. 위 예제의 경우 do함수를 꼬리재귀 형태로 최적화 할 수 있는데 이 최적화 코드를 scala에서 제공해준다.

꼬리재귀로 최적화시키고 싶은 함수 앞에 @tailrec 키워드를 붙이는 것으로 최적화시킬 수 있다.


import scala.annotation.tailrec

def sumList(l: List[Int]): Int = {
@tailrec
def do(s: Int, l: List[Int]): Int = {
l match { case Nil => s
case _ => do(s + l.head, l.tail) }
}
do(0, l)
}


l.head / l.tail

List에서 지원하는 함수로 head는 List의 맨 처음 노드를 말하고, tail은 head를 제외한 나머지 List를 말한다.



Nil

아무것도 없는 List라는 의미.



match

switch구문과 같은 역할을 하는 scala 문법.

참조 : 스칼라(scala) - 패턴 매칭






익명 함수 (Anonymous Function)(=Lambda)


scala는 1급 함수를 지원한다. 여기서 말하는 1급 함수(first class function)이라는 말은 최상위 객체로서의 함수라는 뜻으로 scala의 함수는 가장 추상적인 자료형이라는 의미이다. 따라서 함수를 변수에 할당할 수도 있고, 함수의 파라미터로 넘겨줄 수도 있다.

함수를 파라미터로 전달하기 위해 다음처럼 작성할 수 있다. 함수를 파라미터로 넘겨줄 때 이름으로 넘겨줄 수도 있지만 람다식 즉, 익명 함수를 작성해서 넘겨줄 수도 있다.


기본적인 람다 표현식은 다음과 같다. 일반 함수와 다른점은 이름이 없다는 것과 등호(=)대신 화살표 기호(=>)를 사용한다는 점이다.


([type], [type], …): [type] => [function]


전달받은 파라미터 함수에 따라 다르게 동작하는 calc함수를 작성해보자.


def calc(a: Int, b: Int, f: (Int, Int) => Int)): Int = {
f(a, b)
}

val sum = calc(1, 2, (a: Int, b: Int): Int => a + b)
// sum: Int = 3
val mul = calc(3, 4, (a: Int, b: Int): Int => a * b)
// mul: Int = 12


물론 scala는 자료형 추론 기능이 있기 때문에 모든 자료형을 생략하고 sum을 호출하는 코드를 이렇게 작성할 수도 있다.


val sum = calc(1, 2, (a, b) => a + b)
// sum: Int = 3
val mul = calc(3, 4, (a, b) => a * b)
// mul: Int = 12


calc함수를 정의하는 부분에서 파라미터에 적힌 람다식 (Int, Int) => Int 마저도 추상화시키고 싶다면 아래에 나오는 개념을 사용해서 작성할 수 있다.






함수 제네릭 (Function Generic)


scala에서도 제네릭 함수를 정의할 수 있다. 제네릭 함수는 호출하는 시점에 파라미터 값의 자료형이 정해지는 함수로, 추상적인 개념만 들어맞는다면 어떤 자료형이든 처리할 수 있도록 작성된 함수를 말한다. C++의 template으로 설계하는 것과 같은 개념이라고 볼 수 있다.


제네릭 함수의 기본 정의 형식


def [func id][generic type, generic type, …]([param id]: [generic type], …): [generic type] = { [function] }


// 처음에 generic type을 지정할 때 실제로 대괄호([])를 써줘야 한다.



제네릭 함수를 써서 위 예제를 다시 설계하면 calc함수를 더 다채롭게 사용할 수 있다.


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

val intSum = calc[Int](1, 2, (a, b) => a + b)
// intSum: Int = 3
val doubleMul = calc[Double](3.5, 4.6, (a, b) => a * b)
// doubleMul: Double = 8.1
val addString = calc[String]("Hello,", " world", (a, b) => a + b)
// addString: String = "Hello, world"






마치며


1급 함수, 함수 제네릭을 설명하려고 calc함수를 설계하면서 이 두 기능을 이런식으로 사용하는 것이 맞는것인가에 대한 의문이 계속 들었다. 결과적으로 calc는 한번의 구현으로 호출할 때마다 입맛에 맞게 동작하는 함수가 되었다.

calc는 실수이든 정수이든 상관없이, 덧셈이든 뺄셈이든 상관없이 동작한다. 심지어 문자열을 합치는 동작도 할 수 있다.

하지만 할 수 있는 기능과 확장될 수 있는 가능성을 모두 내포할 수 있는 이름을 정할 수 있을까? 그런 이름이 있다고 해도 이렇게 설계가 과연 좋을까 하는 의문이 계속 남는다.

물론 억지로 두 기능을 사용하기 위해 작성한 예제라서 이런 의문을 가지게 되는 것일 수도 있다.

실제로 List의 foreach같은 메소드들은 정말 마법처럼 동작하고 코드를 이해하는 것도 쉽다.



'Programming > Language' 카테고리의 다른 글

C# Tutorial | 기본 문법  (0) 2018.10.29
Python Tutorial | 기본 문법  (0) 2018.09.17
스칼라(scala) - 패턴 매칭(match)  (0) 2017.09.13
스칼라(scala) - 변수(var) 또는 값(val)  (0) 2017.09.11
스칼라(scala) - List  (0) 2017.09.10
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함