ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin] 코틀린 함수와 함수형 프로그래밍 2
    Kotlin/Function 2021. 9. 7. 23:39

    [Kotlin/Function] - [Kotlin] 코틀린 함수와 함수형 프로그래밍 1

    이전 포스팅에서 함수형 프로그래밍, 일급 객체, 일급함수, 고차함수, 람다표현식의 개념에 대해 알아보았다.

    이번에는 코틀린 ref를 보고 문법 내용을 정리해보려고 한다.

     

    • High-Order function

     

    고차함수의 인자로 함수타입의 인스턴스를 (instance of function type) 전달해야한다.

    밑의 이미지를 보면 함수타입을 인스턴스화 하는 방법이 있다.

    그중 하나인 람다표현식부터 살펴보고 능력이 되는데까지 정리해보려고 한다.


    • 람다표현식은 고차함수에서 인자를 전달하는 목적으로 폭넓게 사용된다.

     

    1. 람다 표현식 {  } 은 항상 중괄호로 둘러져있어야 한다.

    { x: Int, y: Int -> x + y }

    2. 람다 표현식 {  }  내부에서 -> 다음에 바디(구현부)가 온다.

    { x: Int, y: Int -> 바디(구현) }

    3. return타입이 Unit이 아닌경우 바디의 마지막 표현식이 return value로 취급된다.

    fun main() {
        val sum: (Int, Int) -> Int = { x: Int, y: Int ->
            println("test")
            x + y //return value
        }
    }

    4. 함수 타입이 추론 가능한 경우 함수 타입을 생략할 수 있다.

    val sum = { x: Int, y: Int -> x + y }
    //val sum : (Int,Int)->Int = { x: Int, y: Int -> x + y }

    • 람다 표현식과 invoke함수(코틀린 operator)

     

    https://wooooooak.github.io/kotlin/2019/03/21/kotlin_invoke/

     

    -- invoke 함수란 https://kotlinlang.org/docs/operator-overloading.html#invoke-operator

     

    +,-등등 코틀린 연산자에 해당하는 함수가 존재하여 operator의 동작을 입맛에 맞게 변경할 수 있다.

    invoke함수 또한 연산자로 동작하는데 이름없이 함수를 호출할 수 있는 기능이다.

    invoke함수를 오버로딩한 후 연산자처럼 사용하기 위해서는 dot notation . 없이 함수의 파라메터에 해당하는 인자를 넣어주면 된다.

    class TestClass() {
        operator fun invoke() {
            println("파라메터가 없는 함수 호출")
        }
    
        operator fun invoke(str: String) {
            println("파라메터가 $str 인 함수 호출")
        }
    }
    
    fun main() {
        val t = TestClass()
    
        t.invoke() //파라메터가 없는 함수 호출
        t() //파라메터가 없는 함수 호출
    
        t.invoke("String") //파라메터가 String 인 함수 호출
        t("String") //파라메터가 String 인 함수 호출
    }

     

    파라메터가 없는 함수 호출
    파라메터가 없는 함수 호출
    파라메터가 String 인 함수 호출
    파라메터가 String 인 함수 호출

    디컴파일된 코드 

    public static final void main() {
          TestClass t = new TestClass();
          t.invoke();
          t.invoke();
          t.invoke("String");
          t.invoke("String");
     }

    invoke함수에 대해서 알아봤으니 람다표현식과 invoke함수의 관계에 대해서 알아보자

     

     

    -- 람다표현식과 invoke함수

     

    결론: 함수를 받는 변수는 실제로는 인터페이스 변수이다.

    fun main() {
        //인자가 하나인 람다 표현식
        val vFunc1 = { str: String -> str.length } 
        //동일 val vFunc1: (String) -> Int = { str: String -> str.length } 
        val vFunc2 = { i: Int -> i.toString() } //(Int)->String
        
        //인자가 두개인 람다 표현식
        val vFunc3 = { str: String, str2: String -> str.length + str2.length }
    }

    디컴파일된 코드

    public static final void main() {
          Function1 vFunc1 = (Function1)null.INSTANCE;
          Function1 vFunc2 = (Function1)null.INSTANCE;
          Function2 vFunc3 = (Function2)null.INSTANCE;
    }

    인터페이스인 Function1,Function2 타입 참조변수로 변경되었다. 

     

    Function인터페이스들을 살펴보면 구현내용은 invoke함수이다.

    package kotlin.jvm.functions
    
    /** A function that takes 0 arguments. */
    public interface Function0<out R> : Function<R> {
        /** Invokes the function. */
        public operator fun invoke(): R
    }
    /** A function that takes 1 argument. */
    public interface Function1<in P1, out R> : Function<R> {
        /** Invokes the function with the specified argument. */
        public operator fun invoke(p1: P1): R
    }
    ......
    .......
    ..........
    ............
    public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
        /** Invokes the function with the specified arguments. */
        public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
    }

     

     

    파라메터가 하나인 람다표현식은 Function1인터페이스 참조변수에 대입되고

    파라메터가 두개인 람다표현식은 Function2 인터페이스 참조변수에 대입되고있다.

    Function인터페이스 참조변수에 람다표현식이 대입되므로

    람다표현식은 invoke메서드를 구현한 어떤 객체라는 것을 알수 있다.

     

    fun main() {
        val vFunc1 = { str: String -> str.length }
        //val vFunc1: (String) -> Int = { str: String -> str.length }
    
        println(vFunc1("세글자")) //실제로 vFunc1.invoke("세글자") 가 호출됨
        //3
    }

    println(vFunc1("세글자")) 부분을 보면, 마치 vFunc이 참조변수가 아닌 자기가 함수인마냥 vFunc(인자) 사용되고 있다.

    연산자인 invoke함수가 실제로 호출되는 것이고

    -> 이는 Function인터페이스변수.invoke(인자)  vFunc.invoke(인자)인 것이다.

    또한 invoke함수의 특성으로 vFunc(인자)로 함수인 것 처럼 사용할 수 있는 것이다.

    fun main() {
        //인자가 하나인 람다 표현식
        val vFunc1 = { str: String -> str.length } //(String)->Int
        //val vFunc1: (String) -> Int = { str: String -> str.length }
        println(vFunc1.invoke("세글자"))
    
        val vFunc2 = { i: Int -> i.toString() } //(Int)->String
        println(vFunc2.invoke(3))
    
        //인자가 두개인 람다 표현식
        val vFunc3 = { str: String, str2: String -> str.length + str2.length }
        println(vFunc3.invoke("세글자","세글자"))
    }

     

    3
    3
    6

     

    또한 람다표현식은 Function인터페이스를 구현한 어떠한 객체가 되기 때문에 

    람다표현식 대신 Function인터페이스를 직접 구현하여 람다 표현식처럼 구현할 수 있다.

    fun main(args: Array<String>) {
    
        val sum: (Int, Int) -> Int = { p1: Int, p2: Int -> p1 + p2 }
        val sum2: Function2<Int, Int, Int> = object : Function2<Int, Int, Int> {
            override operator fun invoke(p1: Int, p2: Int): Int {
                return p1 + p2
            }
        }
        println(sum(5, 7))
        println(sum2(5, 7))
    }

    • 선언된 함수를 고차함수의 인자에 넣기

     

    https://kotlinlang.org/docs/lambdas.html#instantiating-a-function-type

    https://kotlinlang.org/docs/reflection.html#function-references

    람다표현식은 즉흥적으로 만든 일회성 함수 인스턴스이다. 

    이미 존재하는 선언된 함수를 변수에 대입하거나,  고차함수의 인자에 넣고 싶다면

    ::함수명 을 사용할 수 있다.

    fun highOrder(para: (String) -> Int) {
        println(para("세글자")) //받은 함수에 인자를 넣어 호출
    }
    
    fun sizeOfString(str: String): Int { 
        return str.length
    }
    
    fun main() {
        val funSize = ::sizeOfString
        highOrder(funSize) //3
        
        highOrder(::sizeOfString) //3
    }

    고차함수의 인자에는 함수자체가 넘어가고 , 고차함수의 내부 para(스트링)을 통해 String인자를 넘어간 함수를 호출하고 있다.

     

     

    • 익명 함수( Anonymous function)

     

    람다 표현식도 이름이 없는 함수이지만 코틀린에서 익명 함수가 따로 존재한다. 

    이름이 없다는 것을 빼고는 일반 함수 선언 문법과 비슷하다

    https://juyeop.tistory.com/8?category=823844 

    fun(x: Int, y: Int): Int = x + y
    fun(x: Int, y: Int): Int {
        return x + y
    }

     

     

    람다표현식의 내용이 복잡해지면 가독성도 떨어지기 때문에 가독성을 위해 익명 함수를 쓸 수 있다.

    fun highOrder(para: (String) -> Int) {
        println(para("세글자"))
    }
    
    fun main() {
    
        //익명 함수
        highOrder(
            fun(str: String): Int {
                return str.length
            }
        )
    
        //람다식
        highOrder(
            { str: String -> str.length }
        )
    
    }

     

    익명 함수의 파라메터 타입은 문맥에 맞춰 자동 추론이 가능하면 생략할 수 있다.

    fun highOrder(para: (String) -> Int) {
        println(para("세글자"))
    }
    
    fun main() {
    
        //자동 타입 추론없이 모두 명시한 경우
        highOrder(
            fun(str: String): Int {
                return str.length
            }
        )
    
        //자동 타입 추론
    
        //단일 표현식 함수 - 리턴타입 Int 추론
        highOrder(
            fun(str: String) = str.length
        )
    
        //고차함수의 인자 (String)->Int 로부터 함수 파라메터 str이 String임 추론
        highOrder(
            fun(str): Int {
                return str.length
            }
        )
        
        //모두 생략하면
        highOrder(
            fun(str)=str.length
        )
    
    }

    코틀린 관습으로 사용하는 람다 표현식 문법을 조금 더 알아보고 익명 함수와의 차이를 알아보자.


     

    • 코틀린 관습에 해당하는 trailing lambda

     

    코틀린의 관습으로 함수타입의 마지막 파라메터가 함수인 경우 함수에 맞는 람다식을 ()바깥으로 분리하여 { } 중괄호를 통해 람다 표현식을 사용할 수 있다. 이를 trailing lambda라 부른다.(끝에 달려있는 람다)

    함수타입의 파라메터가 하나일때가 아니라 꼬다리 파라메터가 함수일 때면 항상 사용가능

    인자가 두개이고 마지막 인자가 함수인 경우와 비교

     

    • 고차함수의 파라메터가 함수하나라면 trailing lambda를 쓰면서 ()는 생략이 가능하다.

    꼬다리의 파라메터가 함수인 경우 trailing lambda를 사용할 수 있다.

    인자가 하나만 있고 파라메터가 함수인 경우 trailing lambda를 사용하면서 ()를 생략할 수 있다.

     

    • 꼬다리의 인자가 함수이고 해당 함수의 매개변수가 하나인 경우  trailing-lambda에서 넣어질 함수의 인자 선언 대신 it으로 사용할 수 있다.

    highOrder{ str: String -> str.length } 와 같이 str:String을 선언한 경우에는 it키워드를 사용하지 못한다.

    단 하나의 인자인 str:String을 생략한 경우에 it키워드를 사용할 수 있다.

     

    trailing lambda -> 함수타입의 꼬다리인자가 함수인 경우 ()에 넣어야 하는 람다식을 ()바깥의 {}로 뽑아냄

    함수타입 인자가 함수하나일때 trailing lambda를 사용한 경우 ()에는 아무것도 없어서 생략가능

    trailing lambda를 사용할때 꼬다리 함수에 해당하는 인자가 하나일 때 람다식 바디에서 인자 선언 생략하면 it을 통해 사용할 수 있음. 기존으롤 인자를 선언하면 it은 사용못함

     

    • 람다 표현식에서의 return 키워드

     

    람다 표현식은 { }다. -> 뒤의 바디에 해당하는 코드문 중 마지막줄 표현식이 return 값이 된다. 

    람다식은 자기 자신의 블록 범위를 가지지 않는다.

    return키워드를 사용하면 람다식의 최근접한 함수의 return으로 취급된다.

    context는 main함수가 되고 return 키워드를 사용하면 main함수를 리턴한다는 것이다.

    main함수의 리턴타입이 Unit이라서 return값을 Unit으로 바꾸라고 하고 있다.

    명시적으로 return키워드를 사용하기 위해서는 적절한 코틀린 리턴 라벨을 사용하여 return 키워드를 사용해야 한다.

     

     (자세한 내용 밑의 블로그, kotlin reference 참고)

    https://wooooooak.github.io/kotlin/2019/02/16/kotlin_label/

    https://kotlinlang.org/docs/returns.html#return-to-labels

     

     

    • 익명 함수와 람다 표현식의 차이

    익명함수는 항상 ()안에 전달해야한다. 람다 표현식의 trailing lambda처럼 {}로 빼내어 { 익명함수 }와 같이 할 수없다.

     

    지역 함수의 return에 차이가 있다.

    라벨이 붙지 않은 return문은 항상 가장 가까운 fun키워드를 함수를 리턴하게되는데

    람다표현식 내부에서 return을 사용하면 람다표현식을 감싸고 있는 fun이 return된다.

    (위의 예제에서 main함수가 리턴되는 것처럼)

    반면에 익명함수도 fun키워드를 사용하기 때문에 내부에서 return을 사용하면 최근접 둘러싼 fun이 자기 자신이기 때문에 자기 자신을 리턴하게 된다. 

    (익명함수는 리턴라벨없이 return 키워드 사용가능)

     

    위에서 다룬 익명함수 코드에서 return 키워드를 그냥 사용해도 문제가 없던 것을 확인해보자.


    함수 타입 선언시 문법과 람다식 문법이 비슷해서 화살표랑 () 치는것이 헷갈릴 것이다. 

    다음 포스팅에 이어서 함수 타입과 수신 객체 지정 함수를 다뤄본다

    [Kotlin/Function] - [Kotlin] 코틀린 함수와 함수형 프로그래밍 3

    댓글

Designed by Tistory.