Higher-Order Functions 리뷰



 고차 함수에 대해 좀 더 직관적으로 알 수 있도록 예제를 몇 개 더 살펴보겠습니다. 파라미터가 없는 경우는 다음과 같이 쉽게 구현할 수 있습니다.


fun main(args: Array<String>) {

    getName({setCommon()})

}


fun setCommon() : String {

    return "My name is "

}


fun getName(body: () -> String) {

    println(body())

}

 My name is  


 아직 이름을 가져오지 못했습니다. 이제 common 문장에 고유한 이름을 추가할 수 있도록 파라미터를 하나 추가해보겠습니다. 


fun main(args: Array<String>) {

    getName({name -> setCommon() + "$name"})

}


fun setCommon() : String {

    return "My name is "

}


fun getName(body: (String) -> String) {

    println(body("Park"))

 My name is Park 


 파라미터를 추가한 후 getName()을 사용하는 방법을 보면 다소 생소합니다. 바로 람다 표현식(이하 람다식)을 사용해서 익숙한 모습이 아닙니다. 지금은 간략하게 람다식에 대해 다음의 설명 정도만 익혀두면 도움이 될 것입니다.


 - 람다식은 중괄호 { }로 묶임

 - 파라미터(타입 생략 가능)가 있는 경우 -> 전에 선언

 - body는 -> 후에 선언


 위 규칙을 보면 getName() 사용시에 선언한 부분이 이해가 됩니다. 파라미터가 있으므로 -> 전에 name이라는 이름으로 선언되어 있습니다. 파라미터가 String 타입으로 명백하니 타입은 생략 가능합니다. 그리고 -> 다음으로 body, 즉 함수가 선언되었습니다.


 이제 파라미터가 두 개라도 끄덕없습니다. -> 전에 파라미터 두 개를 선언해주면 그만입니다.


fun main(args: Array<String>) {

    getName({lastName, firstName -> setCommon() + "$firstName" + " $lastName"})

}


fun setCommon() : String {

    return "My name is "

}


fun getName(body: (String, String) -> String) {

    println(body("Park", "yeol"))

 My name is yeol Park 


 이때 getName({lastName, firstName -> setCommon() + "$firstName" + " $lastName"})에서 ( )는 생략이 됩니다. 이것 역시 람다식에 의해서 가능한 것입니다.


마치며


 퇴근 후 지속적으로 달려왔더니 조금 지쳐 내용이 짧습니다. 다음에는 여러번 언급된 람다식이 뭔지 살펴보도록 하겠습니다.



업데이트


 파라미터로 넘어오는 함수(setCommon)가 파라미터를 가질 경우 그 함수(setCommon)에 넘겨줄 파라미터를 고차 함수(getName)의 파라미터로 받아왔습니다.

fun main(args: Array<String>) {

    getName("yeol", "Park", {a, b -> setCommon(a, b)})

}


fun setCommon(firstName: String, lastName: String) : String {

    return "My name is $firstName $lastName"

}


fun getName(firstName: String , lastName: String, body: (String , String) -> String) {

    println(body(firstName, lastName))

}

 My name is yeol Park 




Higher-Order Functions


 코틀린은 Higher-Order Functions(이하 고차 함수)를 제공합니다. 고차 함수는 함수의 파라미터로 함수를 받거나 함수를 리턴하는 함수입니다.


 먼저 레퍼런스에 소개된 코드를 한 번 보겠습니다.


fun <T> lock(lock: Lock, body: () -> T): T {

    lock.lock()

    try {

        return body()

    }

    finally {

        lock.unlock()

    }

}


 이 코드를 봐서는 고차 함수를 이해하기에 직관적이지 않아 보입니다. 왜냐하면 어떠한 결과가 나오는지 정보가 부족하기 때문입니다. 좀 더 구체적인 예제가 필요할 것 같습니다.


 다만 위 예제에서 알아야 할 부분이 있습니다. 형태를 보면 파라미터로 함수를 받고 있고 리턴값으로 함수를 사용하고 있습니다.


 파라미터로 사용된 함수 body: () -> T 의미

body: 파라미터명

() -> T 파라미터가 없는 함수를 의미하며 리턴 타입은 T(generic)


 이 정도만 알고 예제를 만들어 보았습니다. 간단한 예제인데 이 코드를 작성하기까지 많은 시간이 소요되었습니다. 그 이유는 바로 공부하는 순서가 절차지향적이지 않아서 발생한 문제입니다. 다음 예제를 작성하면서 Function Reference 부분을 찾아보게 되었네요.


 일단 제가 먼 길 둘러간, 소위 삽질한 결과를 다음 예제로 설명드리겠습니다.


fun main(args: Array<String>) {

    println(apply("Kim", {increase(3)}))

}


fun increase(num: Int) : Int {

    println("num $num")

    return num + 10

}


fun apply(name: String, body: (Int) -> Int) : Int {

    println("name $name")

    return body(0)

}

name Kim

num 3

13 


 고차 함수는 파라미터로 함수를 가질 수 있다고 하였습니다. 여기서 고차 함수는 apply()이고 두번째 파라미터에서 함수를 받고 있습니다. 이 때 body 함수는 (Int) -> Int이기 때문에 파라미터로 Int 타입을 갖고 리턴 타입도 Int 입니다.


 그러면 body에 그에 상응하는 함수를 넘겨 줄 수 있겠죠? 그래서 increase 함수를 하나 생성하였습니다. 물론 increase 함수는 파라미터로 Int 타입을 리턴 타입으로 Int 타입을 가지고 있습니다.


 이제 apply 함수를 사용해보고 싶습니다. 제가 난관을 겪었던 곳은 지금부터 입니다. 도대체 increase 함수를 어떻게 넘겨주는걸까요?


 결과적으로 위 예제처럼 println(apply("Kim", {increase(3)})) 이렇게 중괄호를 사용하였습니다. 그런데 이것은 람다 표현식(lambda expression)입니다. 중괄호를 사용하려고 하면 type mismatch로 사용이 안됩니다. 왜냐하면 앞서 말했던 Function Reference를 보면 함수를 다른 함수에 값으로 넘겨주려면 ::을 사용하여야 합니다.


fun main(args: Array<String>) {

    println(apply("Kim", ::increase))

}


fun increase(num: Int) : Int {

    println("num $num")

    return num + 10

}


fun apply(name: String, body: (Int) -> Int) : Int {

    println("name $name")

    return body(3)

name Kim

num 3

13 


 이런식으로 넘겨주니 increase에 인자값을 줄 수가 없었습니다. 물론 apply에서 추가적으로 increase에서 사용할 인자를 받아서 사용하는 방법도 있겠죠. 하지만 인자값을 넘겨 줄 수 있는 방법이 궁금하네요. (좀 더 찾아보고 공부해야겠습니다.)

Control Flow (if, when, for, while) #2


1. for 


 코틀린에서의 for문은 foreach문을 떠올리면 됩니다. iterator를 제공하는 것들에 대해서 반복을 하기 때문입니다. 이때에 collection에 포함하는지 확인하는 in 키워드를 사용합니다.


코틀린

fun main(args: Array<String>) {

    val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    for (i in alphabet) print("$i, ")

}

A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,  


 위 코드를 Java로 표현하려면 어떻게 해야할까요? String은 array나 iterable의 객체가 아니기 때문에 foreach문을 사용하지 못합니다. 대신 다음과 같이 구현할 수는 있겠죠.


String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

  for (int i = 0; i < alphabet.length(); i++) {    

   System.out.print(alphabet.charAt(i) + ", "); 

}


 Java에서 foreach문으로 적용하려면 List로 사용하면 됩니다. 쉽게 생각해서 collection은 iterator() 메서드를 재정의해서 사용한다는 정도로 넘어가면 될 것 같습니다.


List<String> alphabetList = new ArrayList<>(); 

  alphabetList.add("A"); 

  alphabetList.add("B"); 

  alphabetList.add("C"); 

    for (String s : alphabetList) {    

     System.out.print(s + ", "); 

    } 


 Java의 String과 다르게 코틀린에서는 List는 물론이고 String도 for문을 통해서 반복될 수 있습니다. 앞서 말했듯이 for문은 iterator를 제공하는 것들에 대해서 반복을 합니다.

 즉, 다음의 세 가지 규칙이 있습니다.


1. 멤버 함수 또는 확장 함수가 리턴 타입이 있는 iterator()를 가진다.

 2. 멤버 함수 또는 확장 함수가 next()를 가진다.

 3. 멤버 함수 또는 확장 함수가 Boolean을 리턴하는 hasNext()를 가진다.


 배열에 대한 for 루프는 iterator 객체를 만들지 않는 인덱스 기반 루프로 컴파일됩니다. 인덱스를 가지고 배열이나 리스트를 반복하려면 indices 키워드를 사용하면 됩니다. (indices = index의 복수)


 먼저 indices가 없는 경우를 살펴보겠습니다.


fun main(args: Array<String>) {

    val x: IntArray = intArrayOf(10, 20, 30)

    for (a in x) {

        println("value : $a")

    }

value : 10

value : 20

value : 30 


 위와 같이 배열의 값을 곧바로 가져옵니다. foreach와 비슷와 같죠? 그런데 배열의 인덱스를 사용해서 값을 가지고 오고 싶을 때는 다소 곤란한 형태입니다. 이럴때 indices를 사용하면 됩니다.


fun main(args: Array<String>) {

    val x: IntArray = intArrayOf(10, 20, 30)

    for (a in x.indices) {

        println("index : $a")

        println("value : ${x[a]}")

    }

index : 0

value : 10

index : 1

value : 20

index : 2

value : 30


 indices는 해당 collection의 유효한 인덱스들의 범위를 리턴해줍니다. 따라서 위 예제에서 index를 사용할 수 있는 것입니다. 이러한 '범위를 통한 반복'은 추가 객체(ex. iterable 객체)를 만들지 않는 최적의 구현으로 컴파일됩니다.


 인덱스와 값을 모두 사용하는 방법으로는 withIndex()라는 라이브러리 함수를 사용하는 것입니다.


fun main(args: Array<String>) {

    val x: IntArray = intArrayOf(10, 20, 30)

    for ((index, value) in x.withIndex()) {

        println("the element at $index is $value")

    }

the element at 0 is 10

the element at 1 is 20

the element at 2 is 30 


 그럼 Java에서 for문을 통해 아주 흔하게(?) 사용하는 증감 연산은 어떻게 할까요? 다음과 같이 range를 정하고 step 키워드를 이용합니다.


Java

public static void main(String[] args) {

for (int i = 0; i <= 10; i+=2) {  

System.out.println("i = " + i); 

}


코틀린

 fun main(args: Array<String>) {

    for (i in 0..10 step 2) {

        println("i = $i")

    }

}

i = 0

 i = 2

 i = 4

 i = 6

 i = 8

 i = 10



2. while & do while


 while문과 do while 문은 흔히 알고 있는 것과 다를게 없습니다.


  fun main(args: Array<String>) {

    var x = 3

    while(x > 0) {

        println("x = $x")

        x --

    }


    var y = 3

    do {

        println("y = $y")

        y --

    } while (y > 0)

}

x = 3

x = 2

x = 1

y = 3

y = 2

y = 1 


+ Recent posts