본문 바로가기
프로그래밍/코틀린

K016. 코틀린 함수(Functions) 깊게 살펴보기 - Functions 파트7, 람다(Lambdas) 2

by K-인사이터 2024. 3. 5.
반응형

안녕하세요

K-IN 입니다.

 

코틀린에 대해서 알아보겠습니다. 

전체 강의 목록은 아래의 링크를 클릭해주세요.

 

K000. 코틀린 시리즈 (연재물)

안녕하세요 K-IN 입니다. 요즘 코틀린을 이용한 개발 프로젝트가 늘어나고 있습니다. 이에, 코틀린에 대해서 상세하게 정리하는 간행물을 제작하고자 합니다. 여기에 있는 링크들은 모두 코틀린

k-in.tistory.com

 

 

코틀린 함수(Functions) 

코틀린에서 함수에 대해서 이해하려면 아래의 개념들을 숙지해야합니다. "☆" 표기는 중요도를 나타냅니다.

분량이 많아 이글에서는 12번 항목까지만 다룹니다. 

 

이전 내용 코틀린 함수를 자세히 배우려면 아래의 링크들을 참고해주세요.

  1. 기본적인 함수 구조 ☆
  2. 함수 사용 방법 ☆
  3. 기본 인자(Default Arguments) ☆☆
  4. 이름지정 인자(Named Arguments) ☆☆
  5. 단일 표현식 함수(Single-expression Functions) ☆☆☆
  6. 명시적 리턴 타입(Explicit Return Types)
  7. 가변인자 (varargs) ☆☆☆
  8. 중위 표기법 (Infix Notation) ☆☆☆
  9. 함수의 범위 (Function Scope) ☆☆☆
  10. 제너릭 함수 (Generic Functions) ☆☆☆
  11. 꼬리 재귀 함수 (Tail Recursive Functions) ☆☆
  12. 람다 (Lambdas) ☆☆
  13. 인라인 함수 (Inline Functions) ☆☆☆
  14. 연산자 오버로딩 (Operator overloading) 
  15. 빌더 (Builders)

람다, 함수 타입의 인스턴스 생성

우리는 람다(lambdas)와 함수 리터럴(literal)을 통해 함수를 정의할 수 있으며 코틀린은 First Class Functions 이므로 함수를 변수에 저장할 수 있습니다. 

 

즉, 변수에 저장하려면 인스턴스화(=인스턴스를 얻다)를 해야합니다. 

 

그렇다면, 코틀린이 함수 타입의 인스턴스 생성을 위해 제공하는 언어적인 특성에 대해 이해해야합니다. 

크게 특징을 나누면 다음과 같습니다. 

 

  • 람다 표현식 및 익명함수를 사용한 인스턴스 생성
  • 함수 타입을 갖는 함수 리터럴
  • 기존 선언에 대한 호출 가능한 참조
  • 커스텀 클래스를 이용한 함수 타입 구현
  • 컴파일러 타입 추론
  • 함수 타입 간의 상호 교환성
  • 확장 함수에 대한 변수 초기화 

위에서 중요한 특징들을 볼드처리하였습니다.

볼드 처리된 특징들을 상세하게 살펴보겠습니다. 

 

람다 표현식 및 익명함수를 사용한 인스턴스 생성

함수 타입의 변수를 지정하고 이를 초기화(=인스턴스화)하는 방법으로 람다 표현식을 사용하거나 익명함수를 사용할 수 있습니다. 

 

우선 람다 표현식(lambda expression)과 임명함수(anonymous function)을 기본 형태를 살펴보겠습니다. 

{ a, b -> a + b } // 람다 표현식 예시

fun(s: String): Int { // 익명함수 예시
    return s.toIntOrNull() ?: 0
}

 

즉, 아래의 문법적인 구조를 갖춥니다. 

  • 람다표현식: { 매개변수 -> 함수 본문 }
  • 익명함수: fun(매개변수): 반환타입 { 함수 본문 }

람다표현식과 익명함수로 초기화되는 함수타입의 변수의 예시를 살펴보겠습니다. 

각각의 방법으로 인스턴스화된 함수타입 변수는 호출(call)이 가능하며 정상적으로 동작을 수행합니다. 

// 람다표현식을 함수타입 변수로 인스턴스화
val lambdaExample: (Int, Int) -> Int = { a, b -> a + b }

// 익명함수를 함수타입 변수로 인스턴스화 
val anonymousFunction: (String) -> Int = fun(s: String): Int {
    return s.toIntOrNull() ?: 0
}

println(lambdaExample(1, 2)) // 출력 -> 3
println(anonymousFunction("11")) // 출력 -> 11

 

함수 타입을 갖는 함수 리터럴

여기서 잠시 리터럴(literal)의 의미를 이해해 보겠습니다. 

 

정수, 실수, 문자, 문자열, 불리언 등의 타입은 소스 코드에서 값을 표현하는 고정된 표기법이 있습니다. 

즉, 문자는 싱글쿼터(single quote, ')으로 문자를 둘러싸는 방식입니다. 문자열의 경우 더블쿼터(double quote, ")를 사용합니다. 

 

만약 이 표기법을 지키지 않는다면 컴파일러는 표기법이 지키지 않아 에러가 발생합니다. 

이를 리터럴이라고 부릅니다. 아래의 예시들을 살펴보면서 개념을 이해할 수 있습니다. 

// 정수를 소스코드에서 표기할 때, 순수하게 숫자만을 입력합니다. 
val number: Int = 42
// 실수를 소스코드에서 표기할 때, 소수점이하를 점(.)으로 구분하여 입력합니다. 
val pi: Double = 3.14
// 문자를 소스코드에서 표기할 때, 싱글쿼터를 사용하여 입력합니다. 
val letter: Char = 'A'
// 문자열을 소스코드에서 표기할 때, 더블쿼러를 사용하여 입력합니다. 
val greeting: String = "Hello, World!"
// 불리언을 소스코드에서 표기할 때, 소문자로 표기합니다. 
val isTrue: Boolean = true


그렇다면 다시 돌아와 함수 리터럴(function literal)이란 무엇일까요? 

함수를 소스코드에 표기하는 고정된 표기법이라고 해석할 수 있습니다. 

이제 이 단락의 부제목을 다음과 같이 풀어 쓸 수 있습니다. 

 

"함수 타입을 갖는 함수 표기법"입니다. 즉, 함수에서 타입이 기술되는 곳은 인자와 리턴입니다. 

다시말해 함수타입을 인자로 받을 수 있다는 의미입니다. 

 

우리는 앞서 리시버라는 개념을 배웠습니다. 이미 존재하는 타입(클래스)에 대해 확장함수를 정의하는 것을 말합니다. 

그렇다면, 리시버를 갖는 함수를 인자로 사용할 수 있을까요? 

 

정답부터 말하자면 "그렇다"입니다.

아래의 예시는 매우 잘 동작합니다. 

// String 리시버를 갖는 람다 표현식을 인스턴스화함. 
val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }

// (String, Int) -> String 함수타입의 인자를 받는 함수를 정의
fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}

// repeatFun 함수타입 변수를 인자로 제공
val result = runTransformation(repeatFun)
println(result)

 

즉, 정리하면 리시버를 갖는 함수 리터럴 또한 함수타입 변수로 지정할 수 있으며 이때는 아래의 주의사항을 따라야함

또한, 리시버를 갖는 함수에 대한 함수타입을 정의할 시에 인자의 맨앞에는 리시버의 타입을 지정해야 함을 알 수있습니다. 

이는 파이썬 언어의 cls 인자와도 비슷해보입니다. 

// 아래의 함수타입은 String 리시버를 갖는 함수타입
String.(Int) -> String
// 위 함수타입을 인자로하는 함수가 있다면 아래와 같이 표현함. 
fun 함수이름(f: (String, Int) -> String): 리턴타입 { /* 구현 내용 */}

 

위와 같이 리시버와 첫번째 매개변수를 서로 바꾸어서 사용할 수 있는 특징을 코틀린 공식 문서에서는 함수 타입 간의 상호 교환성이라고 불립니다. 

 

간단한 예시를 통해서 이해할 수 있습니다. 

val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun

기존 선언에 대한 호출 가능한 참조

기존에 선언된 함수나 속성에 대한 참조를 간단하게 표현할수 있습니다. 

코틀린은 아래의 유형에 대해서 참조를 간결하게 표현할 수 있습니다. 

 

  • 함수: ::isOdd, String::toInt
  • 속성: List<Int>::size
  • 생성자: ::Regex
fun isOdd(n: Int): Boolean = n % 2 != 0

val oddCheck: (Int) -> Boolean = ::isOdd

val stringToInt: (String) -> Int = String::toInt

val listSize: List<Int>.() -> Int = List<Int>::size

val regexConstructor: Regex = Regex("pattern")
val regexInstance: () -> Regex = ::Regex

 

예시를 보면 기존 참조를 가져와서 쓸수 있는 정도의 의미로 보입니다.

이 개념을 어떻게 쓰면 좋을까요? 

 

우선 간결성에 초점을 맞추어보겠습니다. 

100 * 100 을 x * x 라고 표현하는 대신 Int::times 라고 쓸수 있습니다. 코드 가독성 향상 및 간결하게 표현이 가능합니다. 

 

또한, 함수타입 변수로 인스턴화해서 스레드에 인자(argument)로 전달해야만 하는 번거로움 대신 단순 참조만으로 인자 전달이 가능합니다. 즉, 함수타입 변수를 굳이 선언안해도 된다는 의미입니다. 

 

아래의 예시를 통해 쉽게 이해할 수 있습니다. 

// 기존 선언에 대한 호출 가능한 참조의 쓰임 (1)

// 람다를 사용한 스레드 작업 예시 
val thread1 = Thread { println("Task in thread 1") }
fun printTask() {
    println("Task in thread 2")
}
// 호출 가능한 참조를 사용하여 스레드 작업 간결하게 표현
val thread2 = Thread(::printTask)


// 기존 선언에 대한 호출 가능한 참조의 쓰임 (2)
val numbers = listOf(1, 2, 3, 4, 5)

// 기본적인 람다 사용
val squared1 = numbers.map { it * it }

// 호출 가능한 참조를 사용하여 간결하게 표현
val squared2 = numbers.map(Int::times)

 

커스텀 클래스를 이용한 함수 타입 구현

코틀린은 커스텀 클래스를 통해 함수 타입을 구현할 수 있습니다. 

함수 기반의 프로그래밍은 여러 함수들이 복잡하게 사용되므로 대규모 프로젝트에서 이를 관리하기란 쉽지 않습니다.

 

여기서 객체지향적인 관점을 대입하면 커스텀 클래스를 마치 함수 타입처럼 구현해서 사용할 수 있고 상속을 통해 유연하게 확장을 할 수 있습니다. 

 

아래의 예시처럼 커스텀 클래스를 통해 함수 타입을 정의하는 과정을 통해 DelFunction 도 정의가 가능하며 코드를 모듈화하고 재사용에 유리하도록 설계할 수 있습니다. 

// 함수 타입을 나타내는 인터페이스
interface MyFunction {
    // invoke 함수를 선언하여 함수를 호출할 때 사용되는 메서드 정의
    operator fun invoke(x: Int, y: Int): Int
}


// MyFunction 인터페이스를 구현하는 커스텀 클래스
class AddFunction : MyFunction {
    override fun invoke(x: Int, y: Int): Int {
        return x + y
    }
}

// AddFunction을 함수 타입처럼 사용
val addFunction: MyFunction = AddFunction()

// invoke 메서드 호출을 통해 함수처럼 사용
val result = addFunction(3, 5)

println("Result: $result")

 

 

람다, 함수 타입의 인스턴스 생성, 지금까지 배운 내용을 실습 

람다의 함수 타입의 인스턴스 생성에 대해서 배웠습니다.

아래의 플레이그라운드에서 코드를 변경해가면서 실습하면 이해를 확장할 수 있습니다. 

 

 

이상입니다.

K-IN 올림. 

반응형