Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Tags
more
Archives
Today
Total
관리 메뉴

DOing

[kotlin] invoke 함수를 가진 객체, 람다 본문

Java & Kotlin

[kotlin] invoke 함수를 가진 객체, 람다

mangdo 2021. 12. 1. 10:30

  현대 프로그래밍 언어에서 보편적으로 사용되는 람다는 메서드로 전달할 수 있는 익명 함수를 의미한다. 이번 포스팅에서는 코틀린에서 이러한 람다를 어떻게 사용하고 있는지 알아보려고 한다. 람다를 제대로 이해하기 위해 우선 코틀린에서 사용하는 함수의 반환타입과 연산자에 대해서 먼저 설명하고, 람다에 대해 본격적으로 알아보겠다.

 


Unit

자바의 void와 같은 기능을 한다.

Unit은 의미 있는 값을 반환하지 않는 함수의 반환타입으로 사용할 수 있다.

코틀린에서 void를 안쓰고 Unit을 사용하는 이유는 다음과 같다.

  • Unit은 모든 기능을 갖는 일반적인 타입
  • Unit은 타입 인자로 사용 가능.
  • Unit은 단 하나의 인스턴스를 갖는 타입을 의미한다.
    • unit 타입에 속한 값은 Unit 단 하나이다.

반환 타입을 선언하지 않으면 Unit을 반환한다. 두 함수는 동일하다.

fun f(): Unit {...}

fun f(){...}

 


Nothing

코틀린에는 반환 값이라는 개념자체가 의미 없는 함수가 일부 존재한다. Nothing타입은 아무 값도 반환시키지 않으며, 함수가 정상적으로 끝나지 않는다라는 것을 의미한다.

fun fail (message: String) : Nothing {
	throw IllegalStateException(message)
}

val address = company.address ?: fail("No address")
// company.address가 널인 경우, 
// 실행되는 fail()함수에서 예외가 발생한다는 사실을 파악하고 address가 널이 아님을 추론할 수 있다.

 


고차함수

고차함수는 람다나 함수 참조를 인자로 넘기거나 반환하는 함수이다.

  • 함수 타입

(Int, String) -> Unit
// (Int, String)은 파라미터 타입, Unit은 반환 타입

 

  • 인자로 함수를 받는 함수

// 함수 선언
fun twoAndThree(
	operation: (Int, Int) -> Int
){
	val result = operation(2,3) // 함수 타입인 파리미터를 호출
	println("the result is $result")
}

// 함수 호출
>> twoAndThree { a, b -> a+b }
>> the result is 5

>> twoAndThree { a, b -> a+b }
>> the result is 6

 

  • filter 함수 정의

fun String.filter(predicate: (Char)-> Boolean) : String

 : predicate는 문자(Char타입)을 받아서 Boolean타입 결과값을 반환하는 함수 타입 파라미터이다.

 

 

  • filter 함수 구현

fun String.filterMade(predicate: (Char)-> Boolean) : String {
	// 수신 객체는 String (여기서는 "ab1c")
	val sb = StringBuilder()
	for (index in 0 until this.length) {
		val element = this.get(index)
		if (predicate(element)) { // predicate 파라미터로 받은 함수를 호출
			sb.append(element)
		}
	}
	return sb.toString()
}

fun main() {
    println("ab1c".filterMade{ it in 'a'..'z' }) 
    // 여기서 predicate라는 함수타입 파라미터는 it in 'a'..'z' (= 문자형 하나를 받아서 그게 'a'..'z'안에 있는 지 확인하는 함수)다.
}

 

  • 함수 타입의 파라미터에 디폴트 값을 지정

fun <T> Collection<T>.joinToStringMade(
	separator: String = ",",
	prefix: String = "",
	postfix: String = "",
	transform: (T) -> String = { it.toString() } // 디폴트 값 지정
): String{
	val result = StringBuilder(prefix)
	for ((index, element) in this.withIndex()) {
		if (index > 0) result.append(separator)
		result.append(transform(element))
	}
	result.append(postfix)
	return result.toString()
}

fun main() {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToStringMade()) // 디폴트 함수 사용
    // Alpha,Beta
    println(letters.joinToStringMade() { it.toLowerCase() }) // 람다를 인자로 전달
    // alpha,beta 
    println(letters.joinToStringMade(separator = "! ", transform = { it.toLowerCase() })) //  
    // alpha! beta
}

  : 이 함수는 제네릭 함수다. 따라서 컬렉션의 원소 타입을 표현하는 T를 타입 파라미터로 받는다. transform 람다는 그 T 타입의 값을 인자로 받는다.

 

  • 함수 타입의 파라미터에 널이 들어올 수 있게 지정

fun <T> Collection<T>.joinToStringMade(
		separator: String = ",",
		prefix: String = "",
		postfix: String = "",
		transform: ((T) -> String)?= null // 디폴트 값 지정
): String{
		val result = StringBuilder(prefix)
		for ((index, element) in this.withIndex()) {
			if (index > 0) result.append(separator)
			// 힘수타입이 invoke 메소드를 구현하는 인터페이스임을 활용해서 안전 호출하기
			val str = transform?.invoke(element) ?: element.toString()
			result.append(transform(element))
		}
		result.append(postfix)
		return result.toString()
}

fun main() {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToStringMade()) // 디폴트 함수 사용
    // Alpha,Beta
    println(letters.joinToStringMade() { it.toLowerCase() }) // 람다를 인자로 전달
    // alpha,beta 
    println(letters.joinToStringMade(separator = "! ", transform = { it.toLowerCase() })) //  
    // alpha! beta
}

 


invoke

코틀린에는 invoke라는 함수, 정확히는 연산자가 존재한다. invoke 연산자는 이름 없이도 호출할 수 있다.

class MyFunction {
    operator fun invoke(str: String): String {
        return str.uppercase()
    }
}

fun main() {
    val myfunction = MyFunction()
    println(myfunction.invoke("hello")) // HELLO
	println(myfunction("hello")) // HELLO
}

 


연산자

  이렇듯 분명히 invoke와 같이 이름을 부여한 함수임에도 불구하고 실행을 간편하게 할 수 있게 하는 것들을 연산자라고 부른다. 그런 연산자들 몇 개를 코틀린에서 미리 정해놓았다. 대표적으로 + 연산자가 있다.

data class Point(val x: Int, val y: Int){
	operator fun plus(other: Point): Point{
		return Point(x + other.x, y + other.y) 
	}
}

fun main() {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    println(p1 + p2) //Point(x=40, y=60)
}

다음과 같이 연산자를 오버로딩해서 사용하면 된다. 연산자를 오버로딩하는 함수 앞에는 꼭 operator가 있어야 한다. operator 변경자를 추가해 plus 함수를 선언하고 나면, + 연산자는 plus 함수 호출로 컴파일된다.

 

 

 

 


람다는 invoke 함수를 가진 객체다.

  예를 들어 아래와 같은 람다 함수가 있다고 하자. 이미 코틀린이 기본으로 제공하는 함수를 한 번 감싸는 의미 없는 코드이지만 이해를 위해 작성해보았다.

val toUpperCase = { str: String -> str.toUpperCase() }

 

모든 변수에는 타입이 있다. 그렇다면 toUpperCase는 타입이 무엇일까?

String을 받고, 다시 String을 반환하는 (String) -> String 타입이다.

 

  (String) -> String 타입이 아직까진 어색하게 느껴질 수 있다. 정확히 어떤 의미 일까? 사실 이런 함수 타입은 컴파일된 코드안에서 일반 인터페이스로 변한다. 즉, 함수 타입의 변수는 FunctionN 인터페이스를 구현하는 객체를 저장한다. 코틀린은 함수 인자의 개수 인자의 갯수에 따라 Function0<R>, Function1<P1, R> 등의 인터페이스를 제공한다. Function0은 인자가 없는 함수, Function1는 인자가 하나인 함수에 해당되는 인터페이스이다. 각 인터페이스는 invoke 메소드 정의가 하나 들어있다.

  invoke를 호출하면 해당 함수를 실행할 수 있다. 함수 타입인 변수는 인자의 개수에 따라 적당한 FunctionN 인터페이스를 구현하는 클래스의 인스턴스를 저장하며, 그 클래스의 invoke메소드 본문에는 람다의 본문이 들어간다.

결국 위에서 작성한 toUpperCase()는 아래 코드와 같다. toUpperCase()는 현재 invoke()라는 연산자 하나를 가진 객체라고 할 수 있다.

val toUpperCase = object : Function1<String, String> {
    override fun invoke(p1: String): String {
        return p1.toUpperCase()
    }
}

 

  실제로 코틀린에서 작성한 람다는 위의 코드와 같기 때문에 결국 람다도 컴파일 시간에 람다가 할당된 객체로 변환한다. 개발자는 그것의 invoke()를 호출하는 것이다. 이제 toUpperCase.invoke("hello")가 아닌 toUpperCase("hello")와 같이 호출할 수 있음을 기억한 채로 toUpperCase를 사용해 보자.

fun main() {
    val strList = listOf("a","b","c")
    println(strList.map(toUpperCase)) 
    // map함수는 strList의 요소들을 순회하면서 각각의 요소마다 toUpperCase(요소)를 실행한다.
    // toUpperCase가 invoke연산자를 가지고 있기 때문에 이렇게 편하게 사용 가능한 것이다.
}

// 다음과 같이 사용할 수 있다.
fun main() {
    val strList = listOf("a","b","c")
    println(strList.map {str: String -> str.toUpperCase()})
		// {str: String -> str.toUpperCase()} 이 부분이 결국 런타임 시 invoke를 하나 가지는 오브젝트로 변환된다.
}

 

 

 


함수를 반환하는 함수

enum class Delivery { STANDARD, EXPEDITED }
class Order(val itemCount: Int)

fun getCostCalculator(delivery: Devlivery): (Order) -> Double {
		if (delivery == Delivery.EXPEDITED){
			return { order -> order.itemCount + 3000 }
		}
		return { order -> order.itemCount}
}

getCostCalculator()는 배송 수단에 따라서 다른 배송비를 계산해주는 함수를 반환하는 함수이다.

getCostCalculator()의 반환 타입은 (Order) -> Double , Order를 받아서 double을 반환하는 타입이다.