educative - kotlin - 10

Sung Jun Jin·2021년 4월 4일
0

Functional Programming with Lambdas

Imperative vs Declarative

만약 name이라는 이름이 담긴 리스트에서 Nemo라는 이름을 찾고 싶다면?

  • imperative style(명령형) : name 리스트를 돌면서 하나씩 비교
for (name in names) {
    if (name.equals("Nemo")) {
        return true
    }
}

return false
  • declarative style(선언형) : contains 메소드 호출
val names = listOf("Nemo", "Tom", "James")
val result = names.contains("Nemo")
println(result) // true

Imperative Programming 은 How 의 개념으로 어떻게 결과를 얻을 것인지 하나하나 생각하면서 프로그래밍하는 것이라고 생각할 수 있다. 즉, 어떤 연산을 수행할지 미리 정해주는 방식이다.

반면 Declarative Programming 은 What 의 개념으로 어떤 결과를 얻고 싶은지를 생각하면서 프로그래밍하는 것으로 생각할 수 있다. 즉, 값들을 명시하는 방식으로 프로그래밍을 하게 된다.

// Imperative
var doubleOfEven = mutableListOf<Int>()

for (i in 1..10) {
    if (i % 2 == 0) {
        doubleOfEven.add(i * 2)
    }
}

println(doubleOfEven) //[4, 8, 12, 16, 20]

// Declarative
val doubleOfEven = (1..10)
    .filter { e -> e % 2 == 0}
    .map { e -> e * 2 }

println(doubleOfEven) //[4, 8, 12, 16, 20]

Lambda Expressions

코틀린에서 람다 표현식 syntax는 다음과 같다. Return 타입과 함수 이름이 없다.

{ parameter list -> body }
  • 중괄호로 감싼다 {}
  • 인자와 본문은 ->로 구분한다
  • 인자는 ()로 감싸지 않는다
  • 인자는 타입추론이 가능하므로 타입 명시를 생략할 수 있다
  • 변수에 람다식을 담는 경우에는 인자의 타입을 생략할 수 없다

passing lambdas

fun isPrime(n: Int) = n > 1 && (2 until n).none({ i: Int -> n % i == 0 })
fun isPrime(n: Int) = n > 1 && (2 until n).none { i -> n % i == 0 } //괄호 생략 가능

fun isPrime(n: Int) = n > 1 && (2 until n).none { n % it == 0 } //인자가 1개면 it(implicit parameter)로 대체 가능

여기서 none()은 매칭되는 element 가 없으면 true를 리턴한다

Receiving lambdas

fun walk1To(action: (Int) -> Unit, n: Int) = 
  (1..n).forEach { action(it) }
  
walk1To({ i -> print(i) }, 5) //12345

첫번째 인자를 iteration을 돌 범위를 지정해주고, 두번째 인자는 첫번째 인자로 들어온 값으로 람다 식을 호출한다.

Tip!
코드의 가독성을 위해 람다 표현식으로 넘어가는 인자는 맨 마지막에 두는것이 좋다.

fun walk1To(n: Int, action: (Int) -> Unit) = (1..n).forEach { action(it) }

walk1To(5, { i -> print(i) })
walk1To(5) { i -> print(i) }
walk1To(5) { print(it) }

Using functional references

인자로 별다른 연산을 수행하지 않는 람다식 같은 경우에는 다음과 같은 식으로 간략화 할 수 있다.

({x -> someMethod(x)})

(::someMethod) // 간단하게 사용 가능

앞서 만들었던 walk1To() 함수의 호출을 더 간단하게 해보자

fun walk1To(n: Int, action: (Int) -> Unit) = (1..n).forEach { action(it) }

walk1To(5, { i -> print(i) }) // 이거를 
walk1To(5, ::print) // 이렇게 간단하게 사용 가능

Functions returning functions

만약 각자의 이름을 가진 컬렉션에서 길이가 4 혹은 5인 이름을 찾아야 한다고 가정해보자. find() 함수는 람다를 만족하는 한개의 요소를 찾을 수 있다.

val names = listOf("Pam", "Pat", "Paul", "Paula")

println(names.find {name -> name.length == 5 }) //Paula

println(names.find { name -> name.length == 4 }) //Paul

위 코드는 찾고자 하는 길이에 따라 똑같은 코드를 반복해서 써야하는 번거로움이 있다. 람다를 return 하는 함수로 다시 리팩토링 해보자

fun predicateOfLength(length: Int): (String) -> Boolean {
  return { input: String -> input.length == length }
}

println(names.find(predicateOfLength(5))) //Paula

println(names.find(predicateOfLength(4))) //Paul

Using lambads

val names = listOf("Pam", "Pat", "Paul", "Paula")

val checkLength5 = { name: String -> name.length == 5 } 
println(names.find(checkLength5)) //Paula

checkLength5는 String 매개변수를 가진 람다를 가진다.

val checkLength5: (String) -> Boolean = { name -> name.length == 5 }

위와 같은 식으로 변수의 타입을 지정하고 람다 표현식의 매개변수의 타입을 추론하도록 할 수 있다.

Using anonymous functions

변수명을 사용하지 않는 방법으로 anonymous function(익명함수)를 람다 대신 사용할 수 있다.

val checkLength5 = fun(name: String): Boolean { return name.length == 5 }

names.find(fun(name: String): Boolean { return name.length == 5 })

하지만 return 키워드를 사용해야하는 등 람다 표현식을 사용하는 것보다 코드가 길어지므로 권장하지는 않는 방법이다.

Closures and Lexical Scoping

Closure : 람다 표현식에서 외부 범위에서 선언된 변수에 접근을 할 수 있는 개념이다.
Lexical Scoping : 함수를 어디서 선언하였는지에 따라 상위 스코프가 결정되는 개념.

val factor = 2

val doubleIt = { e: Int -> e * factor }

위 람다 표현식 바디안에서 factor 변수는 표현식 밖의 상위 scope인 closure 바로 위에 위치한 val factor = 2로 간주한다.

fun predicateOfLength(length: Int): (String) -> Boolean { 
  return { input: String -> input.length == length }
}

위와 같이 람다 바디 내부에는 length라는 변수가 없기 때문에 가장 가까운곳에 위치한 인자로 내려오는 length를 참조한다.

Mutual variables

클로저가 참조하는 변수는 가급적이면 val을 사용하자. 다음과 같은 이상한 상황이 일어날 수 있다.

var factor = 2

val doubled = listOf(1, 2).map { it * factor }
val doubledAlso = sequenceOf(1, 2).map { it * factor }

factor = 0

doubled.forEach { println(it) }
doubledAlso.forEach { println(it) }

실행결과

2
4
0
0

Non-Local and Labeled return

코틀린 람다에서 return 키워드를 사용하지 않는 이유에 대한 예제를 보자

fun invokeWith(n: Int, action: (Int) -> Unit) {
  println("enter invokeWith $n")
  action(n)
  println("exit invokeWith $n")
}

fun caller() { 
  (1..3).forEach { i -> 
    invokeWith(i) {
      println("enter for $it")
      
      if (it == 2) { return } //ERROR, return is not allowed here
      
      println("exit for $it")
    }
  }

  println("end of caller")
} 

caller()
println("after return from caller")

여기서 if (it == 2) {return} 라인이 문제되는 이유는 다음 상황을 컴파일러가 판단하기가 어렵기 때문이다.
1. 람다를 종료하고 다음 라인으로 continue를 해야 하는지
2. 바로 위의 for loop를 종료하는건지
3. caller() 함수를 종료해야하는 건지

따라서 1번처럼 람다식을 종료하고자 하는 의도를 명시해야 할 경우 labled return을 사용해주면 된다.

fun caller() {
    (1..3).forEach { i ->
        invokeWith(i) here@ { 
            println("enter for $it")
            
            if (it == 2) {
                return@here 
            }
            
            println("exit for $it")
        }
    }

    println("end of caller")
} 

caller()
println("after return from caller")

""" 실행결과
enter invokeWith 1
enter for 1
exit for 1
exit invokeWith 1
enter invokeWith 2
enter for 2
exit invokeWith 2
enter invokeWith 3
enter for 3
exit for 3
exit invokeWith 3
end of caller
after return from caller
"""

여기서 return@herecontinue와 같은 역할을 한다. 즉 람다를 종료하는 역할을 한다.

fun caller() {
  (1..3).forEach { i ->
    invokeWith(i) { 
      println("enter for $it")
      
      if (it == 2) {
        return@invokeWith 
      }
      
      println("exit for $it")
    }
  }

  println("end of caller")
}

이런식으로 함수이름을 명시해줘도 된다

No inline optimization by default

Inline optimization

람다를 받는 함수 앞에 inline이라는 키워드를 사용해 성능을 개선시킬 수 있다. 이렇게 되면 함수 호출부에 해당 함수가 바이트코드로 대체된다. 함수를 호출하는 부분에서 오버헤드를 없앨 수 있지만, 길이가 큰 함수를 inline으로 대체하는것은 시스템 부하를 일으킬 수 있다.

inline fun invokeTwo( 
  n: Int,
  action1: (Int) -> Unit, 
  action2: (Int) -> Unit 
  ): (Int) -> Unit {

내부 call stack

enter invokeTwo 1

called with 1, Stack depth: 28
Partial listing of the stack: 
Inlineoptimization.report(inlineoptimization.kts:31) 
Inlineoptimization.callInvokeTwo(inlineoptimization.kts:20) 
Inlineoptimization.<init>(inlineoptimization.kts:23)

called with 1, Stack depth: 28
Partial listing of the stack: 
Inlineoptimization.report(inlineoptimization.kts:31) 
Inlineoptimization.callInvokeTwo(inlineoptimization.kts:20) 
Inlineoptimization.<init>(inlineoptimization.kts:23)
exit invokeTwo 1

만약 인라인을 사용한 최적화를 의도한것이 아니라면 noineline 키워드를 사용해준다.

inline fun invokeTwo( 
  n: Int,
  action1: (Int) -> Unit, 
  noinline action2: (Int) -> Unit 
  ): (Int) -> Unit {

crossinline parameter

noline으로 명시해주지 않는 이상 람다 표현식으로 이루어진 매개변수는 inline 처리된다. 해당 람다가 호출되는 부분을 보두 람다 표현식으로 처리하기 위해서는 crossinline 키워드를 이용하면 된다.

inline fun invokeTwo( 
    n: Int,
    action1: (Int) -> Unit, 
    action2: (Int) -> Unit //ERROR 
    ): (Int) -> Unit {

    println("enter invokeTwo $n") 
    
    action1(n)
    
    println("exit invokeTwo $n")
    return { input: Int -> action2(input) } 
}

Good practices for inline and returns

  • Unlabeld return은 람다가 아닌 함수에서의 반환형이다
  • none-inlined 람다에서는 Unlabeld return이 허용되지 않는다.
  • labed return을 사용하면 항상 함수에 custom name을 명시해주자.
  • inline 최적화를 하기 전에는 성능 검사를 한다
  • 성능 검사를 할 수 있는 부분에서 inline을 활용하자
profile
주니어 개발쟈🤦‍♂️

0개의 댓글