본문 바로가기
대외활동/Naver Boostcourse

[부스트코스] 코틀린 프로그래밍 기본 1/2(함수편) - 람다식과 고차 함수

by 드인 2021. 2. 1.

코틀린 프로그래밍 기본 1/2 (함수편)

 


 

0. 오리엔테이션

1. 코틀린의 기본의 기본을 읽혀요!

- 코틀린이란 무엇일까?

 - 변수와 자료형, 연산자

2. 함수형 프로그래밍 이란?

 - 마법의 요술상자, 함수의 기본

 - 요술상자, 함수 가지고 놀기

3. 프로그램 흐름의 제어

 - 프로그램의 흐름을 제어해보자!

4. 코틀린의 표준함수 활용하기

 - 코틀린과 표준함수

5. 강좌 마무리 프로젝트

▶ 깃허브 코드 : github.com/0525hhgus/Kotlin-study

 

0525hhgus/Kotlin-study

Kotlin-study. Contribute to 0525hhgus/Kotlin-study development by creating an account on GitHub.

github.com


[ 함수의 활용 ]


1. 이름없는 함수의 또 다른 형태, 람다

1) 실습 : 람다식을 변수에 넣어보기

package chap04.section1

fun main() {
    val result: Int

    // 람다 함수에 사용할 매개변수 -> 반환할 식
    // val multi = { a: Int, b: Int -> a + b }

    // 일반 변수에 람다식 할당
    val multi: (a: Int, b: Int) -> Int = { a, b -> a + b }

    // 람다식이 할당된 변수는 함수처럼 사용 가능
    result = multi(10,20)
    println(result)
}

 

2) 람다식의 구성 분석하기

▶ 변수에 지정된 람다식

     람다식의 선언 자료형   람다식의 매개변수   람다식의 처리내용

val multi: (Int, Int) -> Int = {x: Int, y: Int -> x * y}

변수를 함수처럼 사용

 (1) 람다식의 선언 자료형은 람다식 매개변수에 자료형이 명시된 경우 생략 가능

 (2) 람다식의 매개변수의 자료형은 선언 자료형이 명시되어 있으면 생략 가능

 (3) 함수의 내용과 결과 반환, 표현식이 여러 줄인 경우 마지막 표현식이 반환

 

3) 람다식의 다양한 표현 방법

▶ 표현식이 2줄 이상일 때

val multi2: (Int, Int) -> Int = { x: Int, y: Int ->
	println("x * y")
	x * y // 마지막 표현식이 반환됨
}

 

▶ 자료형의 생략

val multi: (Int, Int) -> Int = {x: Int, y: Int -> x * y} // 생략되지 않은 전체 표현
val multi = {x: Int, y: Int -> x * y} // 선언 자료형 생략
val multi: (Int, Int) -> Int = {x, y -> x * y} // 람다식 매개변수 자료형의 생략
val multi = {x, y -> x * y} // 에러! 추론 가능하지 않음

 

▶ 실습 : 표현식이 2줄 이상일 때

package chap04.section1

fun main() {
    val result: Int

    // 일반 변수에 람다식 할당
    val multi: (a: Int, b: Int) -> Int = { a, b ->
        println("a: $a, b: $b")
        a * b
    }

    // 람다식이 할당된 변수는 함수처럼 사용 가능
    result = multi(10,20)
    println(result)
}

 

4) 반환 자료형이 없는 표현

반환 자료형이 없거나 매개변수가 하나 있을 때

val greet: ()->Unit = { println("Hello World!") }
val square: (Int)->Int = { x -> x * x }

 

▶람다식 안에 람다식이 있는 경우

val nestedLambda: ()->()->Unit = { { println("nested") } }

 

 

5) 실습 : 람다식 안에 람다식

package chap04.section1

fun main() {

    val nestedLambda: ()->()->Unit = { { println("nestedLambda") } }
}

 

6) 자료형 생략

▶ 선언부의 자료형 생략

val greet = { println("Hello World!") } // 추론 가능
val square = { x: Int -> x * x } // 선언 부분을 생략하려면 x의 자료형을 명시해야 함
val nestedLambda = { { println("nested") } } // 추론 가능

 

🤔 생각해보세요.

 람다(lambda)라는 말의 유래를 한번 찾아보세요. 그리고 왜 이런 익명 함수 기법이 편리한 것인지 생각해 봅시다.

Q1. 람다(lambda)의 유래

A1. 람다 계산은 추상화와 함수 적용 등의 논리 연산을 다루는 형식 체계로,

      추상화 기호인 그리스 문자 람다(λ)를 사용하고 있습니다. 

 

Q2. 익명 함수 기법이 편리한 이유

A2. 함수를 간결하게 표현하여 편의성을 향상되었기 때문입니다.

 

2. 고차함수와 람다식의 이해

1) 실습: 함수의 인자로 함수 사용해보기

package chap04.section2

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

fun mul(a: Int, b: Int): Int {
    return a * b
}

fun main() {

    val result = sum(10,10)
    val result2 = mul(sum(10,5),10)

    println("result: $result, result2: $result2")
}

 

2) 실습: 함수의 반환값으로 함수 사용하기

package chap04.section2

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

fun mul(a: Int, b: Int): Int {
    return a * b
}

fun funFunc(a: Int, b: Int) = sum(a, b)

fun main() {

    val result = sum(10,10)
    val result2 = mul(sum(10,5),10)
    val result3 = funFunc(2,3)

    println("result: $result, result2: $result2, result3: $result3")
}

 

3) 고차함수 - 람다식 함수를 매개변수와 인자로 사용하기

▶ HighOrderTest2.kt

fun main() {

    var result: Int
	
    result = highOrder({ x, y -> x + y }, 10, 20)
    println(result)
}

fun highOrder(sum: (Int, Int) -> Int, a: Int, b: Int): Int {
	return sum(a, b)
}

 

▶ 호출 동작

result = highOrder({ x, y -> x + y }, 10, 20)

fun highOrder(sum: (Int, Int) -> Int, a: Int, b: Int): Int {
         return sum(a, b)
}

 

4) 값에 의한 호출 방법 - 단순 람다식 함수의 인자 전달

▶ 함수가 인자로 전달될 경우

- 람다식 함수는 값으로 처리되어 그 즉시 함수가 수행된 후 값을 전달

 

▶ CallByValue.kt

fun main() {

    val result = callByValue(lambda()) // 람다식 함수를 호출
    println(result)
}

fun callByValue(b: Boolean): Boolean { // 일반 변수 자료형으로 선언된 매개변수
	println("callByValue function")
	return b
}

val lambda: () -> Boolean = { // 람다 표현식이 두 줄
	println("lambda function")
	true // 마지막 표현식 문장의 결과를 반환
}

 

5) 이름에 의한 호출 - 람다식 선언 및 이름의 호출

▶ CallByName.kt

fun main() {

    val result = callByName(otherLambda()) // 람다식 함수를 호출
    println(result)
}

fun callByName(b: () -> Boolean): Boolean { // 일반 변수 자료형으로 선언된 매개변수
	println("callByValue function")
	return b()
}

val otherLambda: () -> Boolean = { // 람다 표현식이 두 줄
	println("lambda function")
	true // 마지막 표현식 문장의 결과를 반환
}

 

6) 다른 함수의 참조에 의한 호출

fun sum(x: Int, y: Int) = x + y
funcParam(3, 2, sum) // 오류! sum은 람다식이 아님
...
fun funcParam(a: Int, b: Int, c: (Int, Int) -> Int): Int {
	return c(a, b)
}
funcParam(3, 2, ::sum)

 

🤔 생각해보세요.

 코틀린은 다양한 단축된 표현법을 사용하고 있습니다. 이것은 언어를 유연하게 하고 읽기 쉽게 고쳐주기도 합니다. 지금까지 사용했던 각종 단축 표현을 생각해 보고 정리해보세요!

Q. 코틀린의 단축된 표현법

A1. 코틀린의 단축된 표현법으로 크게 자료형 생략과 람다식이 존재합니다.

 

 

7) 람다식 함수의 매개변수

▶ 매개변수가 없는 경우

fun main() {
    // 매개변수 없는 람다식 함수
    noParam({ "Hello World!" })
    noParam { "Hello World!" } // 위와 동일 결과, 소괄호 생략 가능
}

// 매개변수가 없는 람다식 함수가 noParam 함수의 매개변수 out으로 지정됨
fun noParam(out: () -> String) = println(out())

 

▶ 매개변수가 한 개인 경우

fun main() {
    // 매개변수 없는 람다식 함수
...
    // 매개변수가 하나 있는 람다식 함수
    oneParam({ a -> "Hello World! $a" })
    oneParam { a -> "Hello World! $a" } // 위와 동일 결과, 소괄호 생략 가능
    oneParam { "Hello World! $it" }  // 위와 동일 결과, it으로 대체 가능
}
...
// 매개변수가 하나 있는 람다식 함수가 oneParam함수의 매개변수 out으로 지정됨
fun oneParam(out: (String) -> String) {
    println(out("OneParam"))
}

 

▶ 매개변수가 두 개 이상인 경우

fun main() {
...
    // 매개변수가 두 개 있는 람다식 함수
    moreParam { a, b -> "Hello World! $a $b"} // 매개변수명 생략 불가
...
}
// 매개변수가 두 개 있는 람다식 함수가 moreParam 함수의 매개변수로 지정됨
fun moreParam(out: (String, String) -> String) {
    println(out("OneParam", "TwoParam"))
}

 

▶ 매개변수를 생략하는 경우

moreParam { _, b -> "Hello world! $b"} // 첫 번째 문자열은 사용하지 않고 생략

 

8) 일반 매개변수와 람다식 함수의 매개변수의 사용

...
fun main() {
...
    // 인자와 함꼐 사용하는 경우
    withArgs("Arg1", "Arg2", { a, b -> "Hello World! $a $b" }) // (1)
    // withArgs()의 마지막 인자가 람다식인 경우 소괄호 바깥으로 분리 가능
    withArgs("Arg1", "Arg2") { a, b -> "Hello World! $a $b" } // (2)
}
...
// withArgs 함수는 일반 매개변수 2개를 포함, 람다식 함수를 마지막 매개변수로 가짐
fun withArgs(a: String, b: String, out: (String, String) -> String) {
    println(out(a, b))
}

 

9) 두 개의 람다식 매개변수를 가진 함수

▶ TwoLambdaParam.kt

fun main() {
    twoLambda({ a, b -> "First $a $b" }, {"Second $it"})
    twoLambda({ a, b -> "First $a $b" }) {"Second $it"} // 위와 동일
}

fun twoLambda(first: (String, String) -> String, second: (String) -> String) {
    println(first("OneParam", "TwoParam"))
    println(second("OneParam"))
}
({첫 번째}, {두 번째})
({첫 번째}) {두 번째}
({첫 번째}, {두 번째}) {세 번째}

 

3. 다양한 함수 (익명함수, 인라인 함수, 확장 함수, 중위함수, 꼬리재귀 함수)

1) 익명 함수

▶ 익명 함수(anonymous functions)

- 함수가 이름이 없는 것

fun (x: Int, y: Int): Int = x + y // 함수 이름이 생략된 익명 함수
val add: (Int, Int) -> Int = fun(x, y) = x + y // 익명함수를 이용한 add 선언
val result = add(10, 2) // add의 사용
val add = func(x: Int, y: Int) = x + y
val add = {x: Int, y: Int -> x + y } // 람다식과 매우 흡사

일반 익명 함수에서는 return, break continue가 사용 가능하지만 람다식에서는 사용하기 어렵다.

  (라벨 표기법과 같이 사용해야 함)

 

2) 인라인 함수

▶ 인라인(inline) 함수

- 함수가 호출되는 곳에 내용을 모두 복사

- 함수의 분기 없이 처리 → 성능 증가

 

 실습: 인라인 함수와 람다식 사용해보기

package chap04.section3

inline fun shortFunc(a: Int, out: (Int) -> Unit) {
    println("Hello")
    out(a)
}

fun main() {
    
    shortFunc(3) {
        println("a: $it")
    }
}

 

3) 인라인 함수의 제한과 금지

인라인 함수의 단점

- 코드가 복사되므로 내용이 많은 함수에 사용하면 코드가 늘어남

 

▶noinline 키워드

- 일부 람다식 함수를 인라인 되지 않게 하기 위해

inline fun sub(out1: () -> Unit, noinline out2: () -> Unit {

 

 실습: 인라인 함수의 제한과 금지

package chap04.section3

inline fun shortFunc(a: Int, noinline out: (Int) -> Unit) {
    println("Hello")
    out(a)
}

fun main() {

    shortFunc(3) {
        println("a: $it")
    }
}

 

 실습: 비지역 반환의 금지

package chap04.section3

inline fun shortFunc(a: Int, crossinline out: (Int) -> Unit) {
    println("Hello")
    out(a)
    println("Goodbye")
}

fun main() {

    shortFunc(3) {
        println("a: $it")
        // return
    }
}

 

4) 확장 함수

▶ 확장 함수(extension function)

- 클래스의 멤버 함수를 외부에서 더 추가할 수 있다.

fun 확장대상.함수명(매개변수, ...): 반환값 {
	...
    return 값
}

 

▶ 확장 함수의 간단한 예

fun main() {
    val source = "Hello World!"
    val target = "Kotlin"
    println(source.getLongString(target))
}

// String을 확장해 getLongString 추가
fun String.getLongString(target: String): String =
        if (this.length > target.length) this  else target

- this는 확장 대상에 있던 자리의 문자열인 source 객체를 나타냄

- 기존의 표준 라이브러리를 수정하지 않고도 확장

 

 

5) 중위 함수

▶ 중위 표기법

- 클래스의 멤버 호출 시 사용하는 점(.)을 생략하고 함수 이름 뒤에 소괄호를 생략해 직관적인 이름을 사용할 수 있는 표현법

- 중위 함수의 조건

  • 멤버 메서드 또는 확장 함수여야 함

  • 하나의 매개변수를 가져야 함

  • infix 키워드를 사용하여 정의

 

▶ 중위 함수의 예

fun main() {
    // 일반 표현법
    // val multi = 3.multiply(10)
    
    // 중위 표현법
    val mult = 3 multply 10
    println("multi: $multi")
}

// Int을 확장해 multiply() 함수가 하나 더 추가되었음
infix fun Int.multiply(x: Int): Int { // infix로 선언되므로 중위 함수
	return this * x
}

 

▶ 실습: 중위 함수 직접 구현해 보기

package chap04.section4

fun main() {
    val num = 3
    // val str = num.strPlus("Kotlin")
    val str = num strPlus "Kotlin" // 중위 표현법

    println(str)

}

infix fun Int.strPlus(x: String): String {
    return "$x version $this"
}

 

6) 재귀

재귀란

- 자기 자신을 다시 참조

- 재귀 함수는 자기 자신을 계속 호출하는 특징

- 재귀 함수의 필수 조건

  • 무한 호출에 빠지지 않도록 탈출 조건을 만들어 둠

  • 스택 영역을 이용하므로 호출 횟수를 무리하게 많이 지정해 연산하지 않음

  • 코드를 복잡하지 않게 함

 

▶ 꼬리 재귀 함수(tail recursive function)

- 스택에 계속 쌓이는 방식이 함수가 계속 씌워지는 꼬리를 무는 형태

- 코틀린 고유의 tailrec 키워드를 사용해 선언

 

▶ 일반 재귀 함수의 예

fun main() {
    val number = 4
    val result: Long
    
    result = factorial(number)
    println("Factorial: $number -> $result")
}

fun factorial(n: Int): Long {
    return if (n == 1) n.toLong() else n * factorial(n-1)
}

 

▶ 꼬리 재귀 함수의 예

- 스택을 사용하지 않음

fun main() {
    val number = 5
    println("Factorial: $number -> ${factorial(number)}")
}

tailrec fun factorial(n: Int, run: Int = 1): Long {
    return if (n == 1) run.toLong() else factorial(n-1, run*n)
}

 

 

🤔 생각해보세요.

 피보나치 수열을  연산하기 위해 재귀 함수로 구현할 수 있는 방법을 생각해 보고 이것을 다시 꼬리 재귀함수로 구현하는 방법에 대해 생각해 봅시다.

Q. 피보나치 수열  연산 코드

A1. 재귀 함수

package chap04.section4

fun main() {
    val number = 4
    val result: Long

    result = fibo(number)
    println("Fibo: $number -> $result")
}

fun fibo(n: Int): Long {
    return if (n == 1 || n == 2) 1 else fibo(n-1) + fibo(n-2)
}

 

A2. 꼬리 재귀 함수

package chap04.section4

fun main() {
    val number = 4
    val result: Long

    result = fibo(number)
    println("Fibo: $number -> $result")
}

tailrec fun fibo(n: Int, run: Int = 1): Long {
    return if (n == 1 || n == 2) 1 else fibo(n-1) + fibo(n-2)
}

 

4. 함수와 변수의 범위(Scope)

1) 함수와 변수의 실행 범위

▶ 함수의 블록({ })

- 블록내에서 사용하는 변수 - 지역 변수(Local variable) (↔전역 변수)

 

▶ 함수의 범위

- 최상위 함수와 지역 함수

fun main() { // 최상위 레벨의 함수
    ...
    fun secondFunc(a: Int) { // 지역 함수 선언
        ... 
    }
    userFunc(4) // 사용자 함수 사용 - 선언부의 위치에 상관 없이 사용
    secondFunc(2) // 지역 함수 사용 - 선언부가 먼저 나와야 사용 가능
}

fun userFunc(counts: Int) { // 사용자가 만든 최상위 레벨의 함수 선언
    ...
}

 

▶ 최상위 및 지역 함수의 사용 범위

- 최상위 함수는 main() 함수 전, 후 어디든 선언하고 코드 내에서 호출 가능

- 지역 함수는 먼저 선언되어야 그 함수를 호출할 수 있음

 

2) 함수의 실행범위 예

▶ LocalFunctionRange.kt

fun a() = b() // 최상위 함수이므로 b()함수 선언 위치에 상관 없이 사용 가능
fub b() = println("b") // b() 함수의 선언

fun c() {
	fun d() = e() // 오류! d()는 지역 함수이며 e()이 이름을 모름
    fun e() = println("e")
}

fun main() { 
	a() // 최상위 레벨의 함수는 어디서든 호출될 수 있다.
    e() // 오류! c() 함수에 정의된 e()는 블록({ })을 벗어난 곳에서 사용할 수 없음
}

 

3) 실습: 함수와 변수의 실행 범위

package chap04.section4

var global = 10

fun main() {

    fun localFunc1() {
        println("localFunc1")
    }

    localFunc1()

    global = 15
    val local1 = 15

    println("global: $global")
    userFunc()
    println("global: $global, local1: $local1")
}

fun userFunc() {
    global = 20
    val local1 = 20
    println("userFunc-global: $global, local1: $local1")
}

 

4) 요약 정리

▶ 전역 변수

- 최상위에 있는 변수로 프로그램이 실행되는 동안 삭제되지 않고 메모리에 유지

- 여러 요소가 동시에 접근하는 경우 잘못된 동작을 유발할 수 있음

- 자주 사용되지 않은 전역 변수는 메모리 자원 낭비

 

▶ 지역 변수

- 특정 코드 블록 내에서만 사용

- 블록 시작 시 임시로 사용되며 주로 스택 메모리를 사용

 

🤔 생각해보세요.

 우리가 어떤 요소를 사용할 때는 사용 범위가 있습니다. 최상위, 전역, 지역이라는 말을 다시 한번 의미를 곰곰히 생각해보고 이러한 범위와 프로그램이 사용하는 메모리와는 무슨 관계가 있을까요?

Q. 최상위, 전역, 지역 범위와 프로그램이 사용하는 메모리와의 관계

A1. 지역 범위에 존재하는 지역 변수, 지역 함수는 스택 메모리에 임시로 쌓이고 전역 범위에서 사용할 수 없습니다. 최상위, 전역 범위에 존재하는 전역 변수, 최상위 함수는 프로그램이 실행되는 동안 메모리에 계속 쌓여 있고 전역 범위에서 사용할 수 있습니다. 따라서 사용 빈도가 적은 전역 변수와 최상위 함수는 메모리 자원을 낭비할 수 있으므로, 지역 변수와 지역 함수로 사용할 필요가 있습니다. 

 


서포터즈 네임택


감사합니다!