[Kotlin] 3. 함수와 함수형 프로그래밍

문상훈·2022년 7월 14일
0

Kotlin

목록 보기
4/7


어제 하루종일 쓴 게 날아갔다.. 😂 덕분에 함수형 프로그래밍 제대로 익혔다 .. !! 🤪

🛴 03. 함수와 함수형 프로그래밍

📌 03-1 함수 선언하고 호출하기

함수란 무엇일까?

함수 : 여러 값(인자)을 입력받아 기능을 수행하고 결괏값을 반환하는 코드의 모음이다.

  • 함수를 왜 사용할까?
    📍 코드를 재사용하기 위해 !

함수의 구조 자세히 살펴보기

fun sum(a: Int, b: Int): Int {
    var sum = a + b
    return sum
}

1️⃣ fun 키워드로 함수 선언 시작하기

  • 모든 함수는 fun 키워드로 시작한다.

2️⃣함수 이름 짓기 sum

  • 함수의 이름이 길어지면 카멜 표기법🐫으로 이름을 짓는다.

3️⃣매개변수 정의하기 (a: Int, b: Int)

  • 매개변수 : 함수에서 입력한 값을 받는 변수
  • 쉼표와 함께 여러개 지정한다.
  • 반드시 콜론(:)과 함께 자료형을 명시한다.

4️⃣반환값의 자료형 명시하기 : Int

  • 함수가 반환하는 값이 있으면 반환값의 자료형도 반드시 명시한다.

5️⃣함수의 본문 완성하기 var sum = a + b

  • 중괄호 안에 함수의 기능을 담당하는 함수의 본문코드를 작성한다.

6️⃣값 반환하기 return sum

  • 함수의 본문에서 어떤 기능을 수행하여 값을 반환 할때 return 키워드를 사용한다.
  • 반환값이 없으면 생략 가능

📍 대괄호로 감싼 부분은 생략 가능

fun 함수이름([변수 이름: 자료형, 변수 이름2: 자료형]): [반환값의 자료형]{
   표현식
   ...
   [return 반환값]
}

📍 덧셈 함수 간략하게 만들기

  • 앞의 식을 다음과 같이 간단하게 표현 가능하다.
fun sum(a: Int, b: Int) = a + b

1️⃣ 중괄호 안의 코드가 한 줄이면 대입연산자(=)를 사용하여 간결하게 표현이 가능하다.
2️⃣ 매개변수에 자료형이 반환값의 자료형과 같으면 생략가능하다.

💥 코드의 양을 줄일 수 있고 읽기에도 좋아 실무에서 많이 사용한다.


함수 호출과 프로그램의 실행 순서

fun main() {
    var result1: Int = sum(3, 2)
    var result2: Int = sum(6, 7)

    println(result1)
    println(result2)
}

fun sum(a: Int, b: Int): Int {
    var sum = a + b
    return sum
}

1️⃣ main() 함수 실행 2️⃣ sum에 인자 전달 3️⃣ sum() 함수 호출

📫 인자매개변수 차이
매개변수 : 함수를 선언할 때 fun sum(a: Int, b: Int)
인자 : 함수를 호출할 때 sum(3, 2)


함수 호출과 메모리

  • 프로그램이 실행되면 프로그램을 위한 공간이 메모리에 만들어진다.
fun main() { // 최초의 스택 프레임
    var num1 = 10 // 임시변수 or 지역변수
    var num2 = 3 // 임시변수 or 지역변수
    var result: Int

    result = max(num1, num2) // 두번째 스택 프레임
}

fun max(a: Int, b: Int) = if (a > b) a else b // a,b 는 max()의 임시 변수

함수와 스택 프레임

  • 함수의 각 정보는 프레임(Frame)이라는 정보로 스택(Stack) 메모리의 높은 주소부터 거꾸로 채워진다.
  • 지역변수(Local Variable) : 함수가 종료되면 스택 프레임과 함께 사라지는 임시 변수

스택프레임의 생성과 소멸

  • 스택 프레임 : 함수가 호출될 때마다 해당 정보가 스택 메모리에 쌓이는 것
    • 스택 프레임은 각각 분리되어 있어, 함수에 선언된 변수도 각각 분리하여 생각한다.
  • 지역변수는 프레임으로 분리된 변수들이다.

📍 함수를 호출 ❶ 스택에 스택 프레임 생성 ❷ 생성한 순서의 반대로 소멸


반환값이 없는 함수

Unit : 코틀린에서 다루는 특수한 자료형 중 하나로 반환값이 없을 때 사용한다.

  • 두 인자를 그대로 출력할 때
    • : Unit은 생략가능
      📍 코틀린은 함수에서 반환값과 반환값의 자료형이 없으면 반환값의 자료형을 Unit으로 추론한다.
fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}

📫 Unitvoid 차이 - Unit은 자바의 void와 대응

  • Unit : 특수한 객체를 반환한다.
  • void : 정말로 아무것도 반환하지 않는다.

매개변수 제대로 활용하기

매개변수에 기본값 정하기

fun main() {
    val name = "지묵"
    val email = "hong@example.kr"

    add(name)
    add(name, email)
    add("둘리", "dooly@example.kr")
    defaultArgs() // 인자 없이 호출
    defaultArgs(200) // 인자를 하나만 호출
}

fun add(a: String, b: String = "default") {
    val output = "${a}님의 이메일 : ${b}"
    println(output)
}

fun defaultArgs(x: Int = 100, y: Int = 200) {
    println(x + y)
}

출력 :

지묵님의 이메일 : default
지묵님의 이메일 : hong@example.kr
둘리님의 이메일 : dooly@example.kr
300
400
  • 코틀린은 함수 매개변수의 기본값 기능을 제공한다.
  • 함수를 선언할 때 매개변수에 기본값을 지정할 수 있다.
  • 매개변수의 기본값을 제공하면 인자를 전달하지 않고도 함수를 실행할 수 있다.
  • 매개변수의 이름과 함께 인자를 전달할 수 있다.
fun main() {
    namedParam(x = 200, z = 100)
    namedParam(z = 150)
}

fun namedParam(x: Int = 100, y: Int = 200, z: Int) {
    println(x + y + z)
}

출력 : 500 450

매개변수의 개수가 고정되지 않은 함수 사용하기

  • 가변인자(Variable Argument) : 인자의 개수가 변한다.
    • 함수는 하나만 정의해 놓고 여러 개의 인자를 받을 수 있다.
    • 매개변수 왼쪽에 vararg 키워드 붙이면 된다.
   fun main() {
    normalVararg(1, 2, 3, 4)
    normalVararg(4, 5, 6)
}

fun normalVararg(vararg counts: Int) {
    for (num in counts) {
        print("$num ")
    }
    print("\n")
}

출력 :

1 2 3 4
4 5 6 

📌 03-2 함수형 프로그래밍

  • 코틀린은 함수형 프로그래밍(FP: Functional Programming)과 객체 지향 프로그래밍(OOP : Object-Oriented Programming)을 모두 지원하는 다중 패러다임 언어
    📍 함수형, 객체 지향 프로그래밍의 장점은 코드를 간략하게 만들 수 있다는 것
    -> 대규모 프로그램의 설계에도 적합하다.
  • 함수형 프로그래밍은 코드가 간략화되고 테스트나 재사용성이 좋아지면서 개발 생산성이 늘어난다는 장점이 있다.

    📫 다중 패러다임 언어란?
    다중 패러다임 언어 : 한 가지 구현 규칙에 얽매이지 않고 다양한 문법과 형식을 지원하는 언어

함수형 프로그래밍이란?

함수형 프로그래밍 : 순수 함수를 작성하여 프로그램의 부작용을 줄이는 프로그래밍 기법

순수함수

  • 같은 인자에 대하여 항상 같은 값을 반환한다.
    • 부작용이 없는 함수 ➡️ 값 예측이 가능해진다.
  • 함수 외부의 어떤 상태도 바꾸지 않는다.
fun sum(a: Int, b: Int): Int {
    return a + b // 동일한 인자인 a,b를 입력받아 항상 a+b 를 출력 -> 부작용이 없다.
}

📍 순수함수가 아닌 함수의 예

fun check() {
    val test = User.grade() // check에 없는 외부 함수 사용
    if (test != null) process(test) // test는 User.grade()의 실행 결과에 따라 달라짐
}
  • 실행 결과를 예측하기 어렵다 ➡️ 순수 함수가 될 수 없다.

람다식

{ x, y -> x+y } // 람다식의 예 (이름이 없는 함수 형태)
  • 수학에서 말하는 람다 대수 : 이름이 없는 함수로 2개 이상의 입력을 1개의 출력으로 단순화
  • 함수형 프로그래밍의 람다식 : 다른 함수의 인자로 넘기는 함수, 함수의 결괏값으로 반환하는 함수, 변수에 저장하는 함수

일급객체

  • 일급 객체는 함수의 인자로 전달할 수 있다.
  • 일급 객체는 함수의 반환값에 사용할 수 있다.
  • 일급 객체는 변수에 담을 수 없다.

함수형 프로그래밍에서는 함수를 일급 객체라 생각한다.
람다식은 일급 객체의 특징을 가진다.
함수가 일급 객체면 일급 함수라 부른다.
일급 함수에 이름이 없는 경우 람다식 함수, 람다식 이라고 부를 수 있다.

📫 람다식은 일급 객체의 특징을 가진 이름없는 함수이다.

고차함수

고차함수 : 다른 함수를 인자로 사용하거나 함수를 결괏값으로 반환하는 함수

fun main() {
    println(highFunc({ x, y -> x + y }, 10, 20))
}

fun highFunc(sum: (Int, Int) -> Int, a: Int, b: Int): Int = sum(a, b)
  • 매개변수가 람다식 함수 형식으로 선언되어 있다. sum: (Int, Int) -> Int
    📍 highFun은 람다식 함수를 인자로 받아들일 수 있는 고차함수가 된다.

📫 함수형 프로그래밍의 정의와 특징

  • 순수 함수를 사용해야 한다.
  • 람다식을 사용할 수 있다.
  • 고차 함수를 사용할 수 있다.

📌 03-3 고차 함수와 람다식

고차 함수의 형태

고차함수 : 인자나 반환값으로 함수를 사용한다.

일반 함수를 인자나 반환값으로 사용하는 고차함수

📝 함수의 인자로 함수를 사용 mul(sum(3, 3), 3)

fun main() {
    val result1 = sum(3, 2)
    val result2 = mul(sum(3, 3), 3)

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

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

출력 : result1 : 5, result2 : 18

📝 함수를 반환값으로 사용 return sum(2, 2)

fun main() {
    println("funFun :  ${funFun()}")
}

fun sum(a: Int, b: Int) = a + b
fun funFun(): Int {
    return sum(2, 2)
}

출력 : funFun : 4

람다식을 인자나 반환값으로 사용하는 고차함수

📝 람다식을 변수에 할당하기 val multi = { x: Int, y: Int -> x * y }

fun main() {
    var result: Int
    val multi = { x: Int, y: Int -> x * y } // 일반 변수에 람다식 할당
    result = multi(10, 20) // 람다식이 할당된 변수는 함수처럼 사용 가능
    println(result) 
}

출력 : 200

📫 매개변수에 자료형이 지정되어 있다면 변수의 자료형은 생략할 수 있다.

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 } // 람다식의 매개변수 자료형의 생략

💥 위 세 개 표현은 모두 같은 표현이다 !!

✏️ 람다식에 매개변수와 반환값이 없을 때

fun main() {
    val greet: () -> Unit = { println("Hello World!") }
    val greet = { println("Hello World!") } // 위의 식과 똑같은 표현
    val square: (Int) -> Int = { x -> x * x }
    val square = { x: Int -> x * x } // 위의 식과 똑같은 표현
}
  • greet : 람다식의 매개변수와 반환값이 없음
  • square : 람다식을 보고 매개변수와 반환값을 추론할 수 있으면 람다식의 매개변수의 자료형 생략가능

✏️ 람다식 안에 람다식 넣기

val nestedLambda: () -> () -> Unit = { { println("nested") } }
val nestedLambda = { { println("nested") } } // 위의 식과 똑같은 표현

📝 람다식을 매개변수에 사용하기 result = highOrder({ x, y -> x + y }, 10, 20)

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)
}

출력 : 30

📝 인자와 반환값이 없는 람다식

fun main() {
    val out: () -> Unit = { println("Hello World!") }  // 인자와 반환값이 없는 람다식의 선언

    out() // 함수처럼 사용가능
    val new = out // 람다식이 들어 있는 변수를 다른 변수에 할당
    new()
}

출력 :

Hello World!
Hello World!
  • 람다식은 인자가 없거나 반환값이 없을 수 있다.
    • 람다식 본문에도 Hello World!를 출력한다는 함수의 내용만 있을 뿐 매개변수가 없기 때문에 인자를 전달받지 않고 있다.

📍 이름 없는 함수를 표현하기 위해 등장한 람다식 표현은 함수형 프로그래밍에서 아주 중요한 개념이다.
📍 람다식은 많은 코드들을 간략화하고 함수 자체를 인자나 매개변수로 이용할 수 있어 프로그램의 효율성도 높일 수 있다.


람다식과 고차 함수 호출하기

  • 기본형 변수 : 스택에 할당된 이 있다.
    • 다른 함수에 인자로 전달 시 해당 값이 복사되어 전달
  • 참조형 변수 : 스택에 할당된 객체의 참조 주소가 있고 객체는 에 있다.
    • 함수에 전달할 때는 참조된 주소가 복사되어 전달
  • JVM에서 실행되는 자바나 코틀린은 함수를 호출할 때 인자의 값만 복사하는 값에 의한 호출 (Call by Value) 가 일반적이다.
    • 참조에 의한 호출(Call by Reference)은 자바나 코틀린에서 사용되지 않는다.

값에 의한 호출

  • 값에 의한 호출 : 함수가 또 다른 함수의 인자로 전달될 경우callByValue(lambda()) 람다식 함수는 값으로 처리되어 그 즉시 함수가 수행된 후 값을 전달한다.
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
}

출력 :

lambda function
callByValue function
true

이름에 의한 람다식 호출

fun main() {
    val result = callByName(otherLambda)
    println(result)
}

fun callByName(b: () -> Boolean): Boolean {
    println("callByName function")
    return b()
}

val otherLambda: () -> Boolean = {
    println("otherLambda function")
    true
}

출력 :

callByName function
otherLambda function
true
  • 값에 의한 호출과의 차이는 매개변수 b가 람다식의 자료형으로 선언되었다는 것 !!

    callByValue : val result = callByValue(lambda())
    callByName : val result = callByName(otherLambda)
    💥 함수 형태로 호출해야 비로소 람다식이 실행됨 !!

다른 함수의 참조에 의한 일반 함수 호출

이 부분이 와닿지 않아 앞의 개념을 계속 해서 뒤적 거렸다 😵
어렵게 어렵게 이해한 방식을 그대로 서술하겠다..!

fun main() {
    // 1. 인자와 반환값이 있는 함수
    val res1 = funcParam(3, 2, ::sum)
    println(res1)
    // 2. 인자가 없는 함수
    hello(::text)
    // 3. 일반 변수에 값처럼 할당
    val likeLambda = ::sum
    println(likeLambda(6, 6))
}

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

fun text(a: String, b: String) = "Hi! $a $b"

fun funcParam(a: Int, b: Int, c: (Int, Int) -> Int): Int {
    return c(a, b)
}

fun hello(body: (String, String) -> String): Unit {
    println(body("Hello", "World"))
}

출력 :

5
Hi! Hello World
12
  1. 인자와 반환값이 있는 함수
    1) ::sum 을 통해 sum() 함수를 람다식으로 사용 가능
    2) funcParam(3, 2, ::sum)실행
    3) res1에 sum(3,2)의 결과값이 저장 -> 5 출력

  2. 🚨인자가 없는 함수🚨
    1) hello(::text) 실행
    2) text() 함수 참조(= 람다식)를 인자로 갖는 hello() 함수 실행
    3) body("Hello", "World") 실행 = text("Hello, "World) -> body인즉슨 text!!!✨
    4) 위의 결괏값 Hi! Hello World 반환

  3. 일반 변수에 값처럼 할당
    1) likeLambdasum이라는 함수 대입
    2) likeLambda(6,6)를 실행하면 sum(6,6)과 같이 12 출력

📫 콜론 2개(::)를 이용한 표기법

hello(::text) // 함수 참조 기호
hello({ a, b -> text(a, b) }) // 람다식 표현
hello { a, b -> text(a, b) } // 소괄호 생략

위의 3가지 표현은 모두 동일한 결과를 출력한다.
📍 매개변수와 인자 구조가 동일한 경우 람다식 표현법이 간략화된 참조기호인 ::을 사용하면 좀 더 편리하게 작성할 수 있다.


람다식의 매개변수

  • 매개변수 개수에 따라 람다식을 구성하는 방법

람다식에 매개변수가 없는 경우

fun main() {
    noParam({ "Hello World!" })
    noParam { "Hello World!" }
}

fun noParam(out: () -> String) = println(out())
  • {...} 매개변수가 없는 람다식 표현 (->를 사용 안 함)

람다식의 매개변수가 1개인 경우

fun main() {
    oneParam({ a -> "Hello World $a" })
    oneParam { a -> "Hello World $a" }
    oneParam { "Hello World $it" }
}

fun oneParam(out: (String) -> String) = println(out("OneParam"))
  • 매개변수가 1개인 경우에는 화살표 표기를 생략하고 $it으로 대체할 수 있다.
    • $it은 람다식 매개변수로 지정된 String 형과 매칭되어 "OneParam" 문자열로 바뀌며 최종적으로 Hello World OneParam 출력

람다식의 매개변수가 2개 이상인 경우

fun main() {
    moreParam({ a, b -> "Hello World $a $b" })
}

fun moreParam(out: (String, String) -> String) = println(out("OneParam", "TwoParam"))

✏️ 특정 람다식의 매개변수를 사용하고 싶지 않을 경우

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

일반 매개변수와 람다식 매개변수를 같이 사용하기

fun main() {
    withArgs("Args1", "Args2", { a, b -> "Hello World $a $b" })
    withArgs("Args1", "Args2") { a, b -> "Hello World $a $b" }
}

fun withArgs(a: String, b: String, out: (String, String) -> String) = println(out(a, b))
  • 둘의 출력값은 같다. Hello World Args1 Args2
  • 일반 함수의 마지막 매개변수가 람다식이라면 소괄호 바깥으로 람다식을 뺄 수 있다.
    • 람다식 매개변수가 마지막 인자 위치에 있어야 한다는 규칙이 있다.
      📍 람다식이 길어지면 읽기 어려워진다는 단점을 보완한다.

일반 함수에 람다식 매개변수를 2개 이상 사용하기

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"))
}
  • twoLambda에 람다식이 2개 정의 되어 있으니 중괄호를 생략하진 못하지만, 마지막 인자가 람다식이므로 마지막 인자의 중괄호는 소괄호 바깥으로 뺄 수 있다.
    • ({ex1},{ex2}) = ({ex1}) {ex2}
    • ({ex1}, {ex2}) {ex3}

📌 03-4 고차 함수와 람다식의 사례 알아보기

음 이런 사례들이 있구나!


📌 03-5 코틀린의 다양한 함수 알아보기

익명 함수

익명함수 : 일반 함수이지만 이름이 없는 것

fun(x: Int, y: Int): Int = x + y // 함수 이름이 생략된 익명 함수

val add: (Int, Int) -> Int = fun(x, y) = x + y // 익명 함수를 사용한 add 선언
val add = { x: Int, y: Int -> x + y } // 위와 같은 표현
val result = add(10, 2) // add 사용

📫 람다식으로 표기할 수 있는 굳이 굳이 익명 함수를 쓰는 이유는 ?!

  • 람다식에는 return, break, continue와 같은 제어문을 사용하기 어렵다
  • 함수 본문 조건식에 따라 함수를 중단하고! 반환하는! 경우에 익명 함수를 사용한다.

인라인 함수

인라인 함수 : 함수가 호출되는 곳에 함수 본문의 내용을 모두 복사해 넣어 함수의 분기 없이 처리되기 때문에 코드의 성능을 높일 수 있다.

  • 코드가 복사되어 들어가기 때문에 대개 내용을 짧게 작성한다.
  • 람다식 매개변수를 가지고 있는 함수에서 동작한다.
  • 일반적인 함수 호출은 함수가 호출될 때마다 분기되지만, 인라인 함수는 내용이 복사되어 main() 함수의 블록 코드에 들어가기 때문에 분기 없이! 흐름에 방해하지 않고! 코드가 수행된다.
fun main() {
    shortFunc(3) { println("First call: $it") }
    shortFunc(5) { println("Second call: $it") }
}

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

출력 :

Before calling out()
First call: 3
After calling out()
Before calling out()
Second call: 5
After calling out()

인라인 함수 제한하기

  • 인라인 함수의 본문이 너무 길면 컴파일러에서 성능 경고를 한다.
  • 인라인 함수가 너무 많이 호출되면 오히려 코드 양만 늘어서 성능이 좋지 않다.
    • 코드가 복사되어 들어가기 때문에 매개변수 참조도 불가능하다.

📍 noinline 키워드를 사용해서 일부 람다식을 인라인되지 않게 한다.
-> 인라인으로 처리되지 않고 분기하여 호출된다.

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

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

출력 :

Before calling out()
First call: 3
After calling out()

인라인 함수와 비지역 반환

fun main() {
    shortFunc(3) {
        println("First call: $it")
        return
    }
}

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

출력 :

Before calling out()
First call: 3
  • return으로 람다식을 빠져나올 수 있다.
  • After 까지 실행 안 됨 -> 비지역 반환 (No-local Return)

✏️ 비지역 반환을 금지하는 방법

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

inline fun shortFunc(a: Int, crossinline out: (Int) -> Unit) {
    println("Before calling out()")
    nestedFunc { out(a) }
    println("After calling out()")
}

fun nestedFunc(body: () -> Unit) {
    body()
}
  • crossline 키워드를 사용한다.
    • 비지역 반환을 금지하는 람다식에 사용한다.

확장 함수

확장 함수 : 클래스처럼 필요로 하는 대상에 함수를 더 추가할 수 있다.

  • 기존 멤버 메서드는 아니지만 기존의 클래스에 내가 우너하는 함수를 하나 더 표함시켜 확장하고 싶을 때 사용
fun 확장대상.함수이름(매개변수 ... ): 반환값 {
..
return}

📫 코틀린의 모든 클래스에 내가 만들어 놓은 확장 함수를 추가할 수 있을까?!

  • 최상위 클래스인 Any에 확장함수를 구현하면 가능🙆‍♀️
    • 코틀린의 최상위 요소는 Any이기 때문에 Any에 확장 함수를 추가하면 코틀린의 모든 요소에 상속되기 때문에 가능하다 !!
fun main() {
   val source = "Hello World!"
   val target = "Kotlin"
   println(source.getLongString(target))
}

fun String.getLongString(target: String): String =
   if (this.length > target.length) this else target

출력 : Hello World!

  • 확장대상String.표기로 String.getLongString() 와 같이 선언해 기존에 없는 새로운 메서드를 만든다.
  • if 문 안에 있는 this는 확장 대상 자리에 있던 문자열 (여기서는 source)

📍 확장 함수 기법을 사용하면 기존 클래스의 선언 구현부를 수정하지 않고 외부에서 쉽게 기능을 확장할 수 있다.
📍 확장함수를 만들 때 확장하려는 대상에 동일한 이름의 멤버 함수 or 메서드가 존재할 경우 얘네가 확장함수보다 우선으로 호출된다.


중위 함수

중위함수 : 일종의 연산자를 구현할 수 있는 함수

  • 조건
    • 멤버 메서드 또는 확장 함수여야 한다.
    • 하나의 매개변수를 가져야 한다.
    • infix 키워드를 사용하여 정의한다.

📍 중위 표현법 (Infix Notaion) : 클래스의 멤버를 호출할 때 사용하는 .을 생략하고 함수 이름 뒤에 소괄호를 붙이지 않아 직관적인 이름을 사용할 수 있는 표현법

fun main() {
    val multi = 3 multiply 10 // 중위 함수 표현 
    println("multi : $multi")

    val multi2 = 3.multiply(15) // 일반 표현법
    println("multi2 : $multi2")
}

infix fun Int.multiply(x: Int): Int {
    return this * x
}

꼬리 재귀 함수

  • 재귀 : 자기 자신을 다시 참조하는 방법
    • 자기 자신을 계속 호출한다.
    • 조건
      • 무한 호출에 빠지지 않도록 탈출 조건을 만들어 둔다.
      • 스택 영역을 이용하므로 호출 횟수를 무리하게 많이 지정해 연산하지 않는다.
      • 코드를 복잡하지 않게 한다.

📍 코틀린에서는 꼬리 재귀 함수를 통해 스택 오버플로 현상을 해결할 수 있다.

  • tailrec 키워드를 사용한다.

📝 일반적인 팩토리얼 재귀 함수 vs 꼬리 재귀 함수

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

    val num2 = 5
    println("Factorial : $num2 -> ${tailFactorial(num2)}")
}

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

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

출력 :

Factorial : 4 - > 24
Factorial : 5 -> 120
  • 일반적인 재귀 : 재귀 함수가 먼저 호출되고 계산 n * factorial(n - 1)
  • 꼬리 재귀 : 계산을 먼저하고 재귀 함수가 호출
    📍 함수가 계산을 먼저 할 수 있도록 함수를 수정해야 한다.
    • tailFactorial(n - 1, run * n) : 인자 안에서 팩토리얼의 도중 값을 계산하고 호출
      -> 팩토리얼의 값을 그때그때 계산하기 때문에 스택 메모리를 낭비하지 않아도 된다.

📌 03-6 함수와 변수의 범위

  • 함수는 실행블록 {} 을 가진다.
    • 함수가 종료될 때 가지고 있던 지역 변수를 삭제한다.
  • 블록 내에 또 다른 함수를 정의하면 그 함수는 지역 함수가 된다.
    📍 지역 함수를 먼저 선언하고 사용을 해야한다!

📫 전역 객체는 소스의 전체 범위에서 사용 가능! 지역 객체는 선언된 내부에서 사용 가능!

함수의 범위

  • 최상위 함수 : main()함수나 사용자 함수
  • 지역 함수 : 함수 안에 또 다른 함수가 선언되어 있는 경우
    • 선언 순서에 영향을 받는다.
fun a() = b()
fun b() = println("b")
fun c() {
    fun d() = e()
    fun e() = println("e")
}


fun main() {
    a()
    e()
}
  • a() b() c() 는 최상위 함수이므로 선언 순서에 영향을 받지 않음
  • d() e()는 지역 함수이므로 순서에 영향을 받음 -> c()의 밖에선 호출❌

변수의 범위

  • 지역 변수(Local Variable) : 특정 코드 블록 안에 있는 변수
  • 전역 변수(Global Variable) : 최상위에 있는 변수로 프로그램이 실행되는 동안 삭제되지 않고 메모리에 유지 되는 변수
fun main() {
    val local1 = 20
    val local2 = 21

    fun nestedFunc() {
        global += 1
        val local1 = 30
        println("nestedFunc local1 : $local1")
        println("nestedFunc local2 : $local2")
        println("nestedFunc global : $global")
    }

    nestedFunc()
    outsideFunc()

    println("main global : $global")
    println("main local1 : $local1")
    println("main local2 : $local2")
}

fun outsideFunc() {
    global += 1
    val outVal = "outside"
    println("outsideFunc global : $global")
    println("outsideFunc outVal : $outVal")

}

출력:

nestedFunc local1 : 30
nestedFunc local2 : 21
nestedFunc global : 11
outsideFunc global : 12
outsideFunc outVal : outside
main global : 12
main local1 : 20
main local2 : 21
  • 여러 함수에서 접근하면서 global의 값은 1씩 증가했다.
  • outsideFunc() 안에 존재하는 outVal은 여기 블록에서만 유효하다.
profile
내가 왜 개발잔거지

0개의 댓글