스칼라 함수형 프로그래밍 - Function literals (함수 리터럴)과 Function value (함수 값)의 이해
View: 175
0
0
작성자: 달빛제이크
카테고리: Scala Language
발행: 2024-06-07
수정
2024-06-29
안녕하세요. 달빛제이크입니다.
계속해서 스칼라 함수형 프로그래밍에 대해서 이야기하겠습니다.
오늘의 주제는 Functions and Closures로 분류된 항목들 중 Function literals (함수 리터럴)와 Function Values (함수 값)에 대해서 알아보려고 합니다.
먼저 literal이라는 사전적 의미는 "문자 그대로의" 라는 뜻을 가지고 있습니다. 프로그래밍 언어에서는 같은 맥락으로 변수나 상수에 할당될 수 있는 값 그 자체를 의미합니다. 리터럴을 식별자에 저장했을 때 변경할 수 있으면 변수가 되고, 변경하지 못할 경우 상수가 됩니다. 변수와 상수는 보통 리터럴이 저장된 식별자를 지칭하는데, 식별자에 리터럴을 할당하는 구문에서 변수와 상수가 결정되기 때문입니다.
val PI = 3.14159
/**
* 식별자 PI가 val로 선언이 되었기 때문에 PI는 값을 변경할 수 없는 상수가 됩니다.
* 3.14159를 리터럴, PI를 일컬어 상수라고 부릅니다. 3.14149 자체도 변경할 수 없기에 상수라고 부를 수 있습니다.
* 스칼라에서 val로 지정된 식별자는 불변 (immutable) 변수이고 상수를 나타내기 위해 대문자를 사용합니다.
* 불변 변수라는 용어가 적합한 표현인지는 의문이 들지만 이해하기 쉬운 용어이기도 합니다.
* 리터럴은 소스 코드에서 고정된 값을 나타내는 구체적인 값입니다.
* 위 예제에서 리터럴은 3.14159 입니다. PI에 할당된 구체적인 값을 의미합니다.
**/
스칼라에서는 함수를 값으로 사용할 수 있도록 Function literals(함수 리터럴)을 제공하는 데 이 때문에 함수를 다른 함수의 인자로 넘길 수 있습니다. 함수를 정의하고 호출할 수 있을 뿐만 아니라, 다른 함수의 인자로 전달할 수 있다는 것은 value의 자격으로 프로그래밍의 모든 기능을 수행할 수 있다는 의미이기 때문에 스칼라에서는 이를 두고 first-class functions라고 부릅니다.
Function literals(함수 리터럴)의 형식은 lambda expression(람다 표현식)을 사용합니다. 람다 표현식은 익명 함수를 정의하기 위한 간결한 문법으로 프로그래밍 언어마다 표현하는 법이 다르지만 대부분 => (Rocket)을 사용하여 표현합니다.
(x: Int) => x + 1 // 첫 번째 예제
val increase = (x: Int) => x + 1 // 두 번째 예제
increase(10) // 11
def decrease = (x: Int) => x - 1 // 세 번째 예제
decrease(11) // 10
첫 번째 예제에서 =>의 왼쪽 (x: Int)는 parameter이고, 오른쪽 x + 1은 표현식입니다.
두 번째 예제에서는 함수 리터럴을 변수에 저장하고, 세 번째 예제에서는 함수 리터럴을 사용해서 함수를 만들었습니다. 형식을 보면 val과 def의 차이만 있을 뿐 내용까지 동일합니다. 함수 리터럴을 변수에 할당하거나 함수 자체로 만들게 되면 일반 함수처럼 호출이 가능합니다. 두 예제의 차이는 변수로 저장했을 경우 runtime에서 함수 리터럴이 함수 값으로 변경되어 메모리에 올라가게 되지만, 함수로 만들어 질 경우에는 일반 함수 동작과 동일하게 호출할 경우에만 함수 값으로 전환되어 실행되고 그 결과 값이 메모리에 남겨집니다.
그렇다면 Function literal(함수 리터럴)과 Function value(함수 값)의 차이는 무엇일까요?
간단하게 이야기 하면 Function literal은 Class, Function value는 Instance화된 object로 보시면 됩니다. Function literal은 함수의 정의 자체, 함수 자체를 표현하는 구문을 의미하고, Function value는 함수 리터럴이 실제로 사용할 수 있는 값을 의미합니다. 스칼라에서는 Function literal을 Function value로 만들 때 scala package에 속해 있는 FunctionN (Function0, Function1, ...) Trait를 Instance화 합니다. 다시 말하면 Function value는 FunctionN Trait의 Instance입니다. 여기서 N은 parameter의 갯수를 의미합니다. 이로서 우리는 Function을 값으로 사용할 수 있게 됩니다.
Function을 값으로 다른 함수에 전달할 수 있기 때문에 매우 편리한 고차 함수들을 이용할 수 있습니다. 이는 사용자가 데이터를 처리하는 데 있어서 꼭 필요한 기능들을 함수가 제공하는 범위 내에서 제어 구조에 대한 고민 없이 유연하게 활용할 수 있게 합니다. 스칼라에서 제공하는 고차 함수에는 foreach, filter, map, flatMap, reduce, fold 등 다양하게 존재하며 모두 List, Set, Map 등 Collection에서 사용할 수 있습니다.
val numbers = List(-3, -2, -1, 0, 1, 2, 3)
numbers.foreach((x: Int) => println(x))
// results
// -3
// -2
// -1
// 0
// 1
// 2
// 3
val moreThanZero = nubmers.filter((x: Int) => x > 0) // List(1, 2, 3)
moreThanZero.foreach((x: Int) => println(x))
// results
// 1
// 2
// 3
앞의 예제는 고차 함수 중 foreach와 filter를 사용해서 정수형 데이터를 처리하는 간단한 예제입니다. 여기서 filter에 주어진 람다 표현식 (x: Int) => x > 0을 좀 더 간단하게 표현할 수 있습니다.
val numbers = List(-3, -2, -1, 0, 1, 2, 3)
// original
numbers.filter((x: Int) => x > 0)
// numbers가 Integer type으로 선언된 변수이기 때문에 x로 넘긴 원소도 당연히 Integer type이고, 굳이 x에 type을 지정할 필요가 없다.
// numbers를 통해 x의 type을 추정하는 것을 target typing이라고 한다.
numbers.filter((x) => x > 0)
// type을 생략한 후 괄호를 제거할 수 있다.
numbers.filter(x => x > 0)
// x: Int에 괄호를 생략하면 Error가 발생한다.
// 이것은 람다 표현식에 대한 문법 제약이라기 보다는 연산자 우선 순위에 따라 컴파일러가 해석을 못하는 경우이다.
numbers.filter(x: Int => x > 0) // Error
// Placeholder syntax를 사용해서 더 간단한 코드 작성이 가능하다.
// Parameter가 몇 개가 사용되던지 Function Literal에 한 번 씩만 사용되면, parameter를 placeholder로 대체 할 수 있다.
numbers.filter(_ > 0)
// Function literal에서 사용하는 Placeholder의 type이 명확하지 않을 경우 type을 지정해 주어야 한다.
val f = _ + _ // Error
val f = (_: Int) + (_: Int) // OK
이번 Function literals와 Function value에 대한 내용은 주제가 주제인 만큼 글이 많이 길어졌습니다.
Function literal과 고차 함수는 스칼라에서 데이터를 다루기 위해 매우 자주 사용하는 문법이기 때문에 사용하다 보면 금방 익숙해질 수 있을 것입니다.
다음 글에서는 Partially applied functions에 대해서 알아 보겠습니다.
감사합니다.
