ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5-5. 고급 함수와 함수형 프로그래밍 활용하기 : 수신 객체가 있는 호출 가능 참조
    Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 10. 10. 21:13

    5장

    5-1. 고급 함수와 함수형 프로그래밍 활용하기 : 코틀린을 활용한 함수형 프로그래밍

    5-2. 고급 함수와 함수형 프로그래밍 활용하기 : 확장

    5-3. 고급 함수와 함수형 프로그래밍 활용하기 : 확장 프로퍼티

    5-4. 고급 함수와 함수형 프로그래밍 활용하기 : 동반 확장

    5-5. 고급 함수와 함수형 프로그래밍 활용하기 : 수신 객체가 있는 호출 가능 참조


    5-5. 수신 객체가 있는 호출 가능 참조

    코틀린에서 수신 객체가 있는 함숫값을 정의하는 호출 가능 참조를 만들 수 있다.

    클래스 멤버를 바탕으로 하거나 확장 선언을 바탕으로 이와 같은 참조를 만들 수 있다.

     

    문법적으로 이들은 바인딩된 호출 가능 참조와 비슷하지만,

    수신 객체를 계산하는 식 대신 수신 객체 타입이 앞에 붙는다는 점이 다르다.

     

    fun intSet(numbers: IntArray, op: Int.(Int) -> Int): Int{
        var res = numbers.firstOrNull()
            ?: throw IllegalArgumentException("Empty Arr")
    
        for(i in 1..numbers.lastIndex) res = op(res, numbers[i])
    
        return res
    }
    
    @JvmName("max1")
    fun Int.max(other: Int) = if(this > other) this else other
    
    fun main() {
        val numbers = intArrayOf(1, 2, 3, 4)
        println(intSet(numbers, Int::plus))
        println(intSet(numbers, Int::max))
    }
    

    위 코드에서 Int::plus는 내장 클래스의 plus() 멤버함수( +연산자와 동일)을 사용한 반면

    Int::max는 같은 파일에 정의된 확장 함수를 사용해 정의했다. 두 경우가 같은 구문을 사용한다.


    1) 영역 함수

    코틀린 라이브러리에는 어떤 식을 계산한 값을 임시로 사용할 수 있도록 해주는 몇 가지 함수가 있다.

    때로는 (식을 계산한 결과를 담을) 지역 변수를 명시적으로 선언하지 않고, 식이 들어있는 암시적인 영역을 정의해

    코드를 단순화할 수 있다. 이 함수를 일반적으로 영역 함수(scope function)이라고 부른다.

     

    scope function은 사용자가 인자로 제공한 람다를 간단하게 실행시켜주는 일을 한다.

    세가지 관점에 따라 사용하는 함수가 다르며, 그 관점은 아래와 같다.

     

      1. 문맥 식을 계산한 값을

       scope function으로 전달할 때 수신 객체로 전달하는지(scope function가 확장 함수인 경우)?

       또는 일반적인 함수 인자로 전달하는지(scope function가 일반 함수인 경우)?

      2. scope function의 람다 파라미터가 수신 객체 지정 람다(확장 람다)인지 아닌지?

      3. scope function가 반환하는 값이 람다의 결과값인지 컨텍스트 식을 계산한 값인지?


    scope function은 run, let, with, apply, also 다섯 가지가 있다. 

    모든 scope function은 인라인 함수이기 때문에 런타임 부가 비용이 없다.


    - run과 with 함수

    run() 함수는 확장 람다를 받는 확장 함수이며 람다의 결과를 돌려준다.

    기본적인 사용 패턴은 객체 상태를 설정한 뒤, 이 객체를 대상으로 어떤 결과를 만들어내는 람다를 호출하는 것 이다.

    class Address{
        var zipCode: Int = 0
        var city: String = ""
        var street: String = ""
        var house: String = ""
    
        fun post(message: String): Boolean{
            "Message for {$zipCode, $city, $street, $house}: $message"
            return readLine() == "OK"
        }
    }
    
    fun main() {
        val isReceived = Address().run{
            zipCode = 314159
            city = "Seoul"
            street = "여의도 사거리"
            house = "자이 아파트"
            post("Hello!") // return type
        }
    
        if(!isReceived){
            println("Message is not delivered") // readLine != OK
        }else{
            println("Message is delivered") // readLine == OK
        }
    }

    run 함수가 없으면 Address 인스턴스를 담을 변수를 추가해야 한다.

    이로 인해 함수 본문에 나머지 본문에서도 이 변수에 접근할 수 있게 된다.

     

    하지만 Address 인스턴스를 post()를 호출할 때 한 번만 써야 한다면,

    함수에 나머지 부분에서 이 인스턴스에 마음대로 접근할 수 있는 것은 바람직하지 않다.

    run() 함수를 사용해 지역 변수의 가시성(Visibility)을 좀 더 세밀하게 제어할 수 있다.

    결과 타입이 Unit일 수 있으므로 주의해야 한다.


    with() 함수는 run()과 상당히 비슷하다.

    유일한 차이점은 with()가 확장 함수 타입이 아니므로 문맥 식을 with의 첫 번째 인자로 전달해야 한다는 것 이다.

    이 함수를 일반적으로 사용하는 경우는 문맥 식의 멤버 함수와 프로퍼티에 대한 호출을 묶어

    동일한 영역 내에서 실행하는 경우이다.

    class Address(val city: String, val street: String, val house: String) {...}
    
    fun main() {
        val message = with(Address("서울", "여의도", "아파트")){
            "address : $city, $street, $house"
        }
        println(message)
    }

    위 코드에서 with을 사용하지 않으면 아래와 같은 코드로 작성해야 한다.

    class Address(val city: String, val street: String, val house: String)
    
    fun main() {
        val addr = Address("서울", "여의도 사거리", "자이 아파트")
        val message = "Address: ${addr.house}, ${addr.city}, ${addr.street}"
        println(message)
    }

    - 문맥이 없는 run

    코틀린 표준 라이브러리는 RUN()을 오버로딩한 함수도 제공한다.

    이 함수는 문맥 식이 없고 람다의 값을 반환하기만 한다. 람다 자체에는 수신 객체와 파라미터가 없다.

     

    이 함수를 사용하는 경우는 주로 어떤 식이 필요한 부분에서 블록을 사용할 때 이다.

    class Address(val city: String, val street: String, val house: String){
        fun asText() = "$city, $street, $house"
    }
    
    fun main() {
        val addr = Address("서울", "여의도 사거리", "자이 아파트")
        println(addr.asText())
    }

    표준 입력에서 city, street, house를 가져온 뒤 사용하고 싶은 경우, 읽어온 여러 값에 대해 여러개의 변수를 정의하는 방법이 먼저 떠오른다.

    fun main() {
        val city = readLine() ?: return
        val street = readLine() ?: return
        val house = readLine() ?: return
        val addr = Address(city, street, house)
        println(addr.asText())
    }

    하지만 모든 변수가 main()의 다른 지역 변수(예 : city) 코드가 바람직하지 않다.

    Address 인스턴스만을 처리하기 위한 영역을 따로 만들고 싶다면 이 방법이 나을 것 이다.

     

    위와 같은 문제는 run()을 사용하면 해결된다.

    fun main() {
        val addr = run{
            val city = readLine() ?: return
            val street = readLine() ?: return
            val house = readLine() ?: return
            Address(city, street, house)
        }
        println(addr.asText())
    }

    - let 함수

    let 함수는 run과 비슷하지만,

    확장 함수 타입의 람다를 받지 않고 인자가 하나뿐인 함수 타입의 람다를 받는다는 차이점이 있다

    외부 영역에 새로운 변수를 도입하는 일을 피하고 싶을 때 let()을 사용한다.

    class Address(val city: String, val street: String, val house: String){
        fun post(message: String) {}
    }
    
    fun main() {
        Address("서울", "여의도 사거리", "자이 아파트").let { 
            // 이 안에서는 it 파라미터를 통해 Address 인스턴스에 접근 가능
            println("To city: ${it.city}")
            it.post("Hello!")
        }
    }

    다른 람다와 마찬가지로 모호함을 없애고 가독성을 높이기 위해 파라미터에 원하는 이름을 작성할 수 있다.

    fun main() {
        Address("서울", "여의도 사거리", "자이 아파트").let { addr ->
            // 이 안에서는 it 파라미터를 통해 Address 인스턴스에 접근 가능
            println("To city: ${addr.city}")
            addr.post("Hello!")
        }
    }

    let의 일반적인 사용법 중 nullable 값을 안전성 검사를 거쳐 not-null 함수에 전달하는 방법이 있다.

    fun readInt() = try {
        readLine()?.toInt()
    }catch (e: NumberFormatException){
        null
    }
    
    fun main(args: Array<String>){
        val index = readInt()
        val arg = if(index != null) args.getOrNull(index) else null
        if(arg != null){
            println(arg)
        }
    }

    getOrNull() 함수는 주어진 인덱스가 정상이라면 배열 원소를 반환하지만, 그렇지 않으면 null을 반환한다.

    이 함수의 파라미터가 nullable 타입이기 때문에 readInt()의 결과를 getOrNull에 직접 전달하는 것은 불가능하다.

    그래서 위에서 if(index != null) 조건문을 이용해 스마트 캐스트 한 뒤 not-null인 값을 getOrNull()에 전달했다.

    let을 사용하면 위 코드를 더 단순화할 수 있다.

    fun main(args: Array<String>){
        val index = readInt()
        val arg = index?.let { args.getOrNull(it) }
    }

    let 호출은 index가 널이 아닌 경우에만 호출되기 때문에 컴파일러는 람다 안에서 it 파라미터가 not-null 값임을 알 수 있다.


    - apply/also 함수

    apply 함수는 확장 람다를 받는 확장 함수이며 자신의 수신 객체를 반환한다.

    이 함수는 run()과 달리 반환값을 만들어내지 않고 객체의 상태를 설정하는 경우에 일반적으로 사용한다.

    class Address(var city: String = "", var street: String = "", var house: String = ""){
        fun post(message: String) {}
    }
    
    fun main(){
        val message = readLine() ?: return
        Address().apply{
            city = "서울"
            street = "여의도"
            house = "자이아파트"
        }.post(message)
    }

    비슷한 함수로 also()가 있다. apply()와 달리 it을 이용한다.

    fun main(){
        val message = readLine() ?: return
        Address().also{
            it.city = "서울"
            it.street = "여의도"
            it.house = "자이아파트"
        }.post(message)
    }

    2) 클래스 멤버인 확장

    클래스 안에서 확장 함수나 프로퍼티를 선언하면 일반적인 멤버나 최상위 확장과 달리 

    it 함수나 프로퍼티에는 수신 객체가 두 개 있다. 

    이 때 확장 정의된 수신 객체 타입의 인스턴스를 확장 수신 객체(extension receiver)라 부르고,

    확장이 포함된 클래스 타입의 인스턴스를 디스패치 수신 객체(dispatch receiver)라고 부른다.

     

    두 수신 객체를 가킬 때는 this 앞에 dispatch receiver의 경우 클래스 이름을 붙이고

    extension receiver의 경우 확장 이름을 붙여 한정시킨다.

     

    따라서 지역적인 클래스나 수신 객체 지정 람다, 내포된 확장 함수 선언이 없다면

    일반적으로 this는 this가 속한 확장 함수의 extension receiver가 된다.

    class Address(val city: String, val street: String, val house: String)
    
    class Person(var firstName: String, var lastName: String){
        fun Address.post(message: String){
            val city = city  // 암시적 this: extension receiver(Address)
            val street = this.street // 한정시키지 않은 this: extension receiver(Address)
            val house = this@post.house // 한정시킨 this: extension receiver(Address)
            val firstName = firstName // 암시적 this : dispatch receiver(Person)
            val lastName = this@Person.lastName // 한정시킨 this : dispatch receiver(Person)
    
            println("From $firstName, $lastName at $city, $street, $house")
            println(message)
        }
        fun test(address: Address){
            // dispatch receiver : 암시적
            // extension receiver : 명시적
    
            address.post("Hello!")
        }
    }

    test() 안에서 post() 함수를 호출하면, test()가 Person의 멤버이므로 디스패치 수신 객체가 자동으로 제공된다.

    하지만 extension receiver는 address 식을 통해 명시적으로 전달된다.


    마치며

    코드를 작성해보고 실행해가면서 연습해봤는데 개인적으로는 반도 이해가 안된 것 같다.

    용어도 어렵고, 개념적인 부분이 굉장히 많아서 복습을 몇번 해야 반은 이해를 할 수 있을 것 같다.

     

    다음장은 6장 특별한 클래스 사용하기로

    이넘 클래스, 데이터 클래스, 인라인 클래스에 대한 내용이다.

    솔직히 이번주 분량이 많아서 다 못할줄 알았는데 다행이고, 다음주는 좀 널널해서 이번주보다는 덜 힘들 것 같다.

     

    반응형

    댓글

Designed by Tistory.