-
[코틀린 완벽 가이드] 5.1 코틀린을 활용한 함수형 프로그래밍코틀린 공부/코틀린 2024. 1. 3. 08:14
차례 : 5.1.1 고차함수 5.1.2 함수 타입 5.1.3 람다와 익명 함수
함수형 프로그래밍은 프로그램 코드를 불변 값을 반환하는 함수의 합성으로 구성할 수 있다는 아이디어를 바탕으로 한다.
함수형 언어는 함수를 일급 시민(first class) 값으로 취급한다.
이 말은 함수를 다른 일반적인 타입의 값과 똑같이 취급한다는 뜻이다. 특히 일급 시민이라는 말에는 변수에 값을 대입하거나 변수에서 값을 읽을 수 있고 함수에 값을 전달하거나 함수가 값을 반환할 수 있다는 뜻이 들어있다.
이런 성질은 함수인 값을 데이터와 마찬가지로 조작할 수 있는 고차 함수라는 함수를 정의할 수 있게 해주며, 코드 추상화와 합성(Composition)이 더 쉽게 가능한 유연성을 제공해준다.
5.1.1 고차함수
앞에서 이미 람다를 사용해 계산을 수행하는 몇가지 예제를 살펴봤다.
val squares = IntArray(5) {idx -> idx*idx} // 0, 1, 4, 9, 16
이 절에서는 람다와 고차함수를 더 자세히 살펴본다.
어떤 정수 배열의 원소의 합계를 계산하는 함수를 정의하고 싶다고 하자.
fun sum(numbers: IntArray): Int { var result = numbers.firstOrNull() ?: throw IllegalArgumentException("Empty array") for(i in 1..numbers.lastIndex) result += numbers[i] return result }
이 함수를 더 일반화해서 곱셈이나 최댓값/최솟값처럼 다양한 집계 함수를 사용하게 하려면 어떻게 해야 할까?
fun aggregate(numbers: IntArray, op:(Int, Int) -> Int): Int { var result = numbers.firstOrNull() ?: throw IllegalArgumentException("Empty array") for(i in 1..numbers.lastIndex) result = op(result, numbers[i]) return result } fun sum(numbers: IntArray) = aggregate(numbers, {result, op -> result + op}) fun max(numbers: IntArray) = aggregate(numbers, {result, op -> if(op > result) op else result})
op 파라미터가 다른 파라미터와 다른 점은 이 파라미터를 표현하는 타입이 함수 타입인 (Int, Int) -> Int라는 점뿐이다.
이 말은 op를 함수처럼 호출할 수 있다는 뜻이다.
sum()과 max() 함수를 보면 aggregate를 호출하는 쪽에서 함숫값을 표현하는 람다식을 인자로 넘긴다는 사실을 알 수 있다. 람다식은 기본적으로 단순한 형태의 문법을 사용해 정의하는 이름이 없는 지역 함수다.
{result, op -> result + op}
result와 op는 함수 파라미터 역할을 하며 -> 다음에 오는 식은 결과를 계산하는 식이다. 이 경우 명시적인 return이 불필요하다. 그리고 컴파일러는 파라미터의 타입을 문맥으로부터 추론해준다.
5.1.2 함수 타입
함수 타입은 함수처럼 쓰일 수 있는 값들의 표시하는 타입이다. 함수 타입은 다음과 같이 두가지 부분으로 구성된다.
- 괄호로 둘러싸인 파라미터 타입 목록은 함숫값에 전달될 데이터의 종류와 수를 정의한다.
- 반환 타입은 함수 타입의 함숫값을 호출하면 돌려받게 되는 값의 타입을 정의한다.
반환값이 없는 함수라도 함수 타입에서는 반호나 타입을 반드시 명시해야 한다. 따라서 이런 경우 Unit을 반환 타입으로 사용한다.
예를 들어,
(Int, Int) -> Boolean
이라는 타입은 인자로 정수를 한 쌍 받아서 결과로 Boolean 값을 계산하는 함수를 뜻한다. 함수 정의에서와 달리 함수 타입 표기에서는 인자 타입 목록과 반환 타입 사이를 : 이 아닌 -> 로 구분한다.
함수 타입인 값도 op(result, numbers[i])와 같이 마치 일반 함수처럼 호출할 수 있다. 함숫값을 호출하는 다른 방법은 invoke() 메서드를 사용하는 것이다. invoke() 메서드도 함수 타입의 메서드도 함수 타입의 파라미터 목록과 똑같은 개수와 타입의 인자를 받는다.
op(result, numbers[i])
op.invoke(result, numbers[i])
함수가 인자를 받지 않는 경우에는 함수 타입의 파라미터 목록에 빈 괄호를 사용한다.
fun measureTime(action: () -> Unit): Long { val start = System.nanoTime() action() return System.nanoTime() - start }
파라미터 타입을 둘러싼 괄호는 필수이므로 함수 타입이 파라미터를 하나만 받거나 전혀 받지 않는 경우에도 괄호를 꼭 쳐야 한다.
val inc: (Int) -> Int = {n -> n+1 } // OK val dec: Int -> Int = {n -> n-1 } // Error
함수 타입의 값을 파라미터에만 사용할 수 있는 것도 아니다. 실제로는 이런 함수 타입을 다른 타입이 쓰일 수 있는 모든 장소에 사용할 수 있다. 예를 들어 함숫값을 변수에 저장할 수도 있다.
fun main(){ val lessThan: (Int, Int) -> Boolean = {a,b -> a < b} println(lessThan(1,2)) // true val moreThan = {a:Int, b:Int -> a > b} println(moreThan(2,1)) // true }
널타입 지정 - 함수 타입 전체를 괄호로 둘러싼 다음에 물음표를 붙인다.
fun measureTime(action: (() -> Unit)?): Long { val start = System.nanoTime() action?.invoke() return System.nanoTime() - start }
5.1.3 람다와 익명 함수
함수형 타입의 구체적인 값은 함수를 묘사하되 이름을 지정하지 않는 람다식을 사용하는 것이다.
{result, op -> result + op}
위와 같은 식을 람다식이라고 부른다. 람다식 정의는 함수 정의와 비슷하게 다음과 같은 요소로 이뤄진다.
- 파라미터 목록 : result, op
- 식이나 문의 목록 : result + op
함수의 정의와 달리 반환 타입을 지정할 필요가 없으며, 람다의 본문으로부터 반환 타입이 자동으로 추론된다. 그리고 람다 본문에서 맨 마지막에 있는 식이 람다의 결과값이 된다.
람다에 인자가 없으면 화살표 기호를 생략할 수 있다.
fun measureTime(action: () -> Unit): Long{ val start = System.nanoTime() action() return System.nanoTime() - start } val time = measureTime{1 + 2}
코틀린은 인자가 하나밖에 없는 람다를 단순화해 사용할 수 있는 문법을 제공한다.람다 인자가 하나인 경우에는 목록과 화살표 기호를 생략하고, 유일한 파라미터를 미리 정해진 'it' 이라는 이름을 사용해 가리킬 수 있다.
fun check(s: String, condition: (Char) -> Boolean): Boolean{ for(c in s){ if(!condition(c)) return false } return true }
println(check("Hello") {c-> c.isLetter()}) // true println(check("Hello") {it.isLowerCase()}) // false
코틀린 1.1 부터는 람다의 파라미터 목록에서 사용하지 않는 람다 파라미터를 밑줄 기호 (_)로 지정 할 수 있다.
fun check(s: String, condition: (Int, Char) -> Boolean): Boolean{ for(i in s.indices){ if(!condition(i, s[i])) return false } return true }
println(check("Hello") {_, c-> c.isLetter()}) // true
익명 함수의 문법은 일반 함수의 문법과 거의 똑같다. 몇 가지 차이점을 정리하면 다음과 같다.
- 익명 함수에는 이름을 지정하지 않는다. 따라서 fun 키워드 다음에 바로 파라미터 목록이 온다.
- 람다와 마찬가지로 문맥에서 파라미터 타입을 추론할 수 있으면 파라미터 타입을 지정하지 않아도 된다.
- 함수 정의와 달리, 익명함수는 식이기 때문에 인자로 함수를 넘기거나 변수에 대입하는 등 일반 값처럼 쓸 수 있다.
fun sum(numbers: IntArray) = aggregate(numbers, fun(result, op): Int {return result + op})
람다와 달리 익명함수를 인자 목록의 밖으로 내보낼 수는 없다.
지역 함수와 마찬가지로 람다나 익명 함수도 클로저, 또는 자신을 포함하는 외부 선언에 정의도니 변수에 접근할 수 있다. 특히 람다나 익명 함수도 외부 영역에 가변 변수 값을 변경할 수 있다.
fun forEach(a: IntArray, action: (Int) -> Unit){ for (n in a){ action(n) } }
fun main(){ var sum = 0 forEach(intArrayOf(1,2,3,4)){ sum += it } println(sum) // 10 }
'코틀린 공부 > 코틀린' 카테고리의 다른 글
[코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.4 객체) (1) 2023.12.30 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.3 단순한 변수 이상 인 프로퍼티) (0) 2023.12.30 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.2 널 가능성) (0) 2023.12.29 [코틀린 완벽 가이드] 4장 클래스와 객체 다루기 (4.1 클래스 정의하기) (0) 2023.12.29 [코틀린 완벽 가이드] 3장 함수 정의하기 (3.5 예외 처리) (0) 2023.12.29