코틀린 함수형 프로그래밍

짱구·2023년 3월 25일
0

kotlin

목록 보기
5/8
post-thumbnail

스코프 함수

  • 코틀린의 표준 라이브러리에는 객체의 컨텍스트 내에서 코드 블록을 실행하기 위해서만 존재하는 몇가지 함수가 포함되어 있는데 이를 스코프 함수라고 부릅니다.

  • 스코프 함수를 제대로 사용하면 불필요한 변수 선언이 없어지며 코드를 더 간결하고 읽기 쉽게 만들어줍니다.

  • 스코프 함수의 코드 블록 내부에서는 변수명을 사용하지 않고도 객체에 접근할 수 있는데 그 이유는 수신자 객체에 접근할 수 있기 때문입니다.

  • 수신자 객체는 람다식 내부에서 사용할 수 있는 객체의 참조입니다.

  • 스코프 함수를 사용하면 수신자 객체에 대한 참조로 this 또는 it을 사용합니다.

스코프 함수의 종류

코틀린은 총 5개의 유용한 스코프 함수를 제공하며 각 스코프 함수들은 본질적으로 유사한 기능을 제공합니다.

함수명수신자 객체 참조 방법반환 값확장 함수 여부
letit함수의 결과O
runthis함수의 결과O
withthis함수의 결과X
applythis컨텍스트 객체 O
alsoit컨텍스트 객체 O

1. let

  • null이 아닌 경우 사용될 로직을 작성하고 새로운 결과를 반환하고 싶을때 사용
fun main() {
    val str: String? = null

    str?.let { // 실행 조차 되지 않음!!
        println(it) 
        println("hi") 
        // 아무것도 출력되지 않음
    }
}
	
  • null이 아닌 경우 실행
    val str: String? = "안녕"
    str?.let {
        println(it)
        // 안녕
    }
  • 또한 let은 함수의 결과를 반환합니다 (let 함수 내부의 마지막 코드가 결과로 반환)
fun main() {
    val str: String? = "안녕"
    val result = str?.let {
        println(it)
        1234
        // return 1234와 같은 의미이며 return이 생략된 것!!
    }
    println(result)
	// 1234 출력, let 함수 마지막 코드가 결과로 반환
}

let을 쓸때 호출이 중첩되면 코드가 복잡해지므로 이런 경우엔 if를 사용하는 편이 낫습니다.

    val str: String? = "안녕"
    val result = str?.let {
        println(it)
        val abc : String? = "abc"
        abc?.let {
            val def : String? = "def"
            def?.let {
                println("abcdef가 null이 아님")
            }
        }
        1234
    }
    println(result)
  • 위 코드를 if-else 식으로 변경
    val str: String? = "안녕"
    val result = str?.let {
        println(it)
        val abc : String? = "abc"
        val def : String? = "def"
        if (!abc.isNullOrEmpty() && !def.isNullOrEmpty()) {
            println("abcdef가 null이 아님")
        }
        1234
    }
    println(result)

2. run

수신 객체의 프로퍼티를 구성하거나 새로운 결과를 반환하고 싶을때 사용합니다.

  • DatabaseClient 객체가 생성
class DatabaseClient {
    var url: String? = null
    var username: String? = null
    var password: String? = null
    
    // DB에 접속하고 Boolean 결과를 반환
    fun connect(): Boolean {
        println("DB 접속 중 ...")
        Thread.sleep(1000)
        println("DB 접속 완료")
        return true
    }
}
  • run을 사용하지 않고 일반적인 코드로 작성했을 때
fun main() {
    val config = DatabaseClient()
    config.url = "localhost:3306"
    config.username = "mysql"
    config.password = "1234"
    val connected = config.connect()
    
    println(connected)
    // DB 접속 중 ...
	// DB 접속 완료
	// true
}
  • run을 사용
fun main() {
    val connected = DatabaseClient().run {
        url = "localhost:3306" // this.url과 같음!! this. 생략
        username = "mysql"
        password = "1234"
        connect()
    }
    
    println(connected)
    // DB 접속 중 ...
    // DB 접속 완료
    // true
}

위와 같이 run을 사용하면 불필요한 변수 선언을 할 필요가 없고 중복되는 코드의 양이 줄어드는 효과가 있습니다.
확장 함수 형태로 실행을 하다보니 this 키워드로 수신자 객체를 참조 할 수 있고 생략도 가능한 것을 볼 수 있습니다.

  • let을 사용할 순 있으나 it을 사용해야하기 때문에 불편
    val connected = DatabaseClient().let {
        it.url = "localhost:3306"
        it.username = "mysql"
        it.password = "1234"
        it.connect()
    }
    println(connected)

3. with

다른 스코프 함수와 다른 점은 with는 확장 함수가 아닙니다.

  • 결과 반환없이 내부에서 수신 객체를 이용해 다른 함수를 호출하고 싶을때 사용
    val str = "안녕하세요"
    with (str) {
        println("length = $length") // this.length에서 this. 생략!!
    }
  • let이나 run과 같이 함수의 결과가 반환됩니다.
    val str = "안녕하세요"
    val length = with(str) {
        length // this.length에서 this. 생략!!
    }
    
    println(length) // 5
  • run으로 작성한 코드를 with로 변경
fun main() {
    val connected = with(DatabaseClient()) {
        url = "localhost:3306"
        username = "mysql"
        password = "1234"
        connect()
    }
    println(connected)
}

4. apply

수신 객체의 프로퍼티를 구성하고 수신 객체를 그대로 결과로 반환하고 싶을때 사용합니다.

	//반환 타입이 수신 객체
    val client: DatabaseClient = DatabaseClient().apply {
        url = "localhost:3306"
        username = "mysql"
        password = "1234"
        connect()
    }

앞서 공부한 let, run, with는 함수의 결과가 반환타입으로 변환되는데 반해서 apply는 수신 객체 그대로 반환됩니다.

5. also

부수 작업을 수행하고 전달받은 수신 객체를 그대로 결과로 반환하고 싶을때 사용합니다.

  • User 객체 생성
class User(val name: String, val password: String) {
    fun validate() {
        if (name.isNotEmpty() && password.isNotEmpty()) {
            println("검증 성공!")
        } else {
            println("검증 실패!")
        }
    }
}
  • also를 쓰지 않은 경우
fun main() {
    val user: User = User(name = "tony", password = "1234")
    user.validate()
}
  • also를 쓴 경우
fun main() {
    User(name = "tony", password = "1234").also {
        it.validate()
    }
}

스코프 함수 사용시 유의할 점

이와 같은 스코프 함수는 모두 기능이 유사하기 때문에 실무에선 섞어쓰는 경우도 많습니다.
"this는 키워드", 키워드는 사전에 정의된 예약어이기 때문에 다른 의미로 사용할 수 없지만 it은 특정 용도에서만 작동하는 소프트 키워드이기 때문에 다른 용도로 사용할 수 있습니다.

val this: String? = null // this라는 변수명을 가질 수 없음!!, 컴파일 오류
val it: String? = null // it은 변수명을 가질 수 있음!!, 작동

중첩으로 사용하는 경우 this, it에 대해 혼동하기 쉽습니다.

  • 중첩 함수내에서 외부 함수에 대한 접근을 하려면 it은 자기 자신의 참조기 때문에 불가능합니다.
fun main() {
    val hello = "hello"
    val hi = "hi"
    
    hello.let {
        println(it.length)
        hi.let {
            println(it.length)
        }
    }
}
  • it 대신 변수를 선언합니다.
fun main() {
    val hello = "hello"
    val hi = "hi"
    
    hello.let { a ->
        println(a.length)
        hi.let { b ->
            println(a.length)
            println(b.length)
        }
    }
}

함수형 프로그래밍

함수형 프로그래밍,FP(Functional-Programming)는 수학의 함수적 개념을 참고해 만든 패러다임의 하나로 깔끔하고 유지보수가 용이한 소프트웨어를 만들기 위해 사용합니다.

함수형 패러다임은 부수효과가 없고 똑같은 input이 들어오면 항상 동일한 output을 내놓는 순수함수의 개념을 기반으로 람다, 고차함수, 커리, 메모이제이션, 모나드 등의 개념을 포함합니다.

함수를 값으로 사용

함수형 프로그래밍에서 함수는 1급 객체로 분류됩니다.
1급 객체란 함수를 인자에 넘기거나 변수에 대입하거나 함수를 반환하는 개념 등을 통틀어 1급 객체라고 합니다.

  • "함수도 타입이다", 코틀린은 ()-> Unit 화살표 표기법을 사용합니다.
val func: () -> Unit = {}

함수는 값이 될 수 있고 역으로 값은 함수가 될 수 있습니다.
그러므로 함수에 인자로 넘기거나 데이터 구조에 저장할 수 있습니다.

  • 메서드 생성
val printHello: () -> Unit = { println("Hello") }
  • 리스트에 저장 후 사용
fun main() {
    val list = mutableListOf(printHello)
    list[0]() // 함수 호출을 위해 () 사용
    // Hello
}
  • 함수는 값이므로 변수로 받아서 처리
val list = mutableListOf(printHello)
val func = list[0]
func()
// Hello
  • 다른 함수를 인자로 사용
fun call(block: () -> Unit) {
    block()
}

fun main() {
    call(printHello)
    // Hello
}
  • fun으로 선언된 함수는 값으로 다룰 수 없고 변수에 담아져 있어야 사용 가능
fun printNo() {
    println("No!")
}

fun main() {
    val list = mutableListOf(printNo) // 컴파일 오류
}
  • 인자를 받아 결과를 리턴하는 함수 값 만들기
val printMessage : (String) -> Unit = { message : String -> println(message) }

// 타입 생략
val printMessage : (String) -> Unit = { message -> println(message) }

// it 사용
val printMessage : (String) -> Unit = { println(it) }
  • 다수의 인자를 받아 결과를 리턴하는 함수 값 만들기
val plus: (Int, Int) -> Int = { a, b -> a + b }

fun main() {
    val result = plus(1, 3)
    println(result)
    // 4
}

고차 함수 (Higher-Order Function, HOF)

고차 함수는 함수를 인자로 받거나 결과로 돌려주는 함수를 의미합니다.
컬렉션의 filter, map, forEach 등도 함수를 인자로 받아서 결과를 반환하므로 고차함수입니다.

// collection과 function(함수)를 인자로 받음
fun forEachStr(collection: Collection<String>, action: (String) -> Unit) {
    for (item in collection) {
        action(item)
    }
    // a
    // b
    // c
}

fun main() {
    val list = listOf("a", "b", "c")
    val printStr: (String) -> Unit = { println(it) }
    forEachStr(list, printStr)
}

익명함수

함수형 프로그래밍에선 이름 없는 무명 함수를 익명함수라고 합니다.

fun outerFunc(): () -> Unit {
    return fun() {	// 변수명이 없다!!
        println("이것은 익명함수!")
    }
}

fun main() {
    outerFunc()()
    // 이것은 익명함수!
}
  • 익명함수를 람다 표현식으로 변환
fun outerFunc(): () -> Unit {
    return { println("이것은 람다함수!") }
}
  • 조금 더 추상화
fun outerFunc(): () -> Unit = { println("이것은 람다함수!") }

함수의 마지막 인자가 함수인 경우 후행 람다 전달을 사용할 수 있습니다.

fun forEachStr(collection: Collection<String>, action: (String) -> Unit) {
    for (item in collection) {
        action(item)
    }
}

fun main() {
    val list = listOf("a", "b", "c")
    
    // action(funtion, 함수)을 매개볏수로 넘기지 않고 후행 람다 전달을 사용
    forEachStr(list) {
        println(it)
    }
}

람다 함수의 파라미터가 1개인 경우에만 it 을 사용할 수 있습니다.

fun arg1(block: (String) -> Unit) {}
fun arg2(block: (String, String) -> Unit) {}

fun main() {

    arg1 {
        it.length
    }
    
    arg2 {
        it.length // 컴파일 오류
        it.length // 컴파일 오류
    }
    
    arg2 { a, b ->
        a.length
        b.length
    }
}

람다 레퍼런스 사용

람다 레퍼런스를 사용하면 좀 더 가독성 좋게 함수를 인자로 넘길 수 있습니다.
탑-레벨 또는 로컬 함수는 (::함수명)을 사용하고 클래스의 멤버이거나 확장 함수는 클래스 지시자를 사용합니다. (클래스명::함수명)

  • 일반적인 함수 값 스타일
    val callReference: () -> Unit = { printHello() }
    callReference()
	// Hello
  • 람다 레퍼런스 사용
val callReference = ::printHello
callReference()()
// Hello
  • numberList를 만들어서 람다 레퍼런스 사용 전
val numberList = listOf("1", "2", "3")
numberList.map { it.toInt() }.forEach { println(it) }
  • 람다 레퍼런스 사용 후
numberList.map(String::toInt).forEach(::println)

애로우 라이브러리

직접 함수를 제작하는 방법도 있지만 고급 기능은 이미 만들어져 있고 검증된 라이브러리를 사용하는 것이 좋습니다.

애로우는 코틀린에서 함수형 프로그래밍을 위한 라이브러리입니다.

애로우는 함수형 프로그래밍의 다양한 개념 및 기능들을 제공하고 있고 점점 사용사례가 많아지고 있습니다.

애로우 링크

출처 : fastcampus

profile
코드를 거의 아트의 경지로 끌어올려서 내가 코드고 코드가 나인 물아일체의 경지

0개의 댓글