본문 바로가기

책/코틀린 리액티브 프로그래밍

2. 코틀린과 RxKotlin을 사용한 함수형 프로그래밍

함수형 프로그래밍 소개

  • 함수형 프로그래밍의 정의 : 불변의 데이터를 사용한 수학적인 함수의 평가를 통해 프로그램을 구조화 하는 동시에 상태의 변화를 방지한다. 
  • 언어의 인터페이스와 지원을 필요로 한다.
  • 함수형 프로그래밍을 지원하는 언어
    • 리스프
    • 클로저
    • 울프램
    • 얼랭
    • 오캐멀
    • 헤스켈
    • 스칼라
    • F#
    • 자바8 버전 이상 부터 지원
  • 코틀린은 객체지향, 함수형프로그래밍 모두를 지원한다.
  • 함수형 리액티브 프로그래밍(Functional Reactive Programming)은 두가지를 혼합한 개념
    • 함수형 프로그래밍 : 쉽게 모듈화 가능한 프로그램을 구현
    • 리액티브 프로그래밍 : 모듈화된 프로그래밍(함수형 프로그래밍)은 반응형 또는 리액티브 선언문의 네가지 원칙을 구현하는데 필요하다.

함수형 프로그래밍의 기초

람다 표현식

일반적으로 이름이 없는 익명함수를 의미

fun main(args: Array<String>){
    var sum = { x: Int, y: Int -> x + y }       // 1
    println("Sum ${ sum(12, 14) }")             // 2
    val anonymousMult = { x: Int -> (Random().nextInt(15) + 1) * x }  // 3
    print("random output ${anonymousMult(2)}")  // 4
}

순수 함수

함수의 반환값이 인수/매개 변수에 전적으로 의존하는 함수

  1. 이름이 있는 순수함수
  2. 람다 순수함수

3을 어떤함수에 매번 전달하면 매번 동일한 값이 반환된다. 즉 순수 함수에는 부작용(Side Effect)가 없다.

fun square(n: Int): Int {           // 1
    return n*n
}

fun main(args: Array<String>){
    println("named pure func square = ${ square(3) }")
    var qube = { n: Int -> n*n*n }  // 2
    println("lambda pure func qube = ${ qube(3) }"
}

고차 함수

함수를 인자로 받아들이거나 반환하는 함수

fun highOrderFunc(a: Int, validityCheckFunc:(a:Int)->Boolean) {           // 1
    if ( validityCheckFunc(a) ) {            // 2
        println("a $a is Valid")
    } else {
        println("a $a is Invalid")
    }
}

fun main(args: Array<String>){
    highOrderFunc(12, { a:Int -> a.isEven() })  // 3
    highOrderFunc(19, { a:Int -> a.isEven() })
}

인라인 함수

 

 함수는 이식 가능한 코드를 작성하는 좋은 방법이지만 함수의 스택 유지 관리 및 오버헤드로 인해 프로그램 실행 시간이 늘어나고 메모리 최적화를 저하시킬 수 있다 인라인 함수의 사용은 함수형 프로그래밍에서 이런 난관을 피할 수 있는 좋은 방법이다.

 인라인 함수는 프로그램의 성능 및 메모리 최적화를 향상시키는 개선된 기능이다. 함수 정의를 호출할 때마다 그것을 인라인으로 대체할 수 있도록 컴파일러가 지시할 수 있다. 따라서 함수 호출, 스택 유지 보수를 위해 많은 메모리를 필요로 하지 않으며 동시에 함수의 장점도 얻을 수 있다. 

 

아래 프로그램은 두 개의 숫자를 더하고 그 결과를 반환하는 함수를 선언하고 루프에서 함수를 호출한다. 이를 위해 함수를 선언하지 않고 함수가 호출되는 위치에 덧셈을 수행하는 코드를 작성할 수 있지만, 함수로 선언하면 기존 코드에 영향을 주지 않고 덧셈로직을 수정할 수 있다.

예를 들면 덧셈이 아니라 곱셈이나 다른 연산으로 교체가 가능하다.

fun doSomeStuff(a: Int = 0) = a+(a*a)
    
fun main(args: Array<String>){
    for( i in 1..10 ) {
        println("$i Output ${doSomeStuff(i)}")
    }
}

 

/**
함수를 인라인으로 선언하면 함수호출이 함수 내부 코드로 교체되는데, 함수 선언으로 얻는 자유는 지키며
동시에 성능도 향상시킬수 있다.
*/
inline fun doSomeStuff(a: Int = 0) = a+(a*a)
    
fun main(args: Array<String>){
    for( i in 1..10 ) {
        println("$i Output ${doSomeStuff(i)}")
    }
}

 

코틀린이 인라인 함수와 관련해 제공하는 또 다른 기능이 있는데 inline 키워드로 코차함수를 선언하면 inline 키워드는 함수 자체와 전달된 람다에 모두 영향을 미친다. 고차 함수를 사용하는 코드를 인라인으로 수정해보자.

inline fun highOrderFuncInline(a: Int, validityCheckFunc:(a:Int)->Boolean) {           // 1
    if ( validityCheckFunc(a) ) {            // 2
        println("a $a is Valid")
    } else {
        println("a $a is Invalid")
    }
}

fun main(args: Array<String>){
    highOrderFuncInline(12, { a:Int -> a.isEven() })  // 3
    highOrderFuncInline(19, { a:Int -> a.isEven() })
}

컴파일러는 highOrderFuncInline 함수의 정의에 따라 validityCheckFunc의 모든 호출을 람다로 대체한다. 보시다 시피 코드의 수정은 별로 없으며, 성능 향상을 위해 함수 선언문 앞에 inline키워드를 추가했다.

ReactiveCalculator 클래스에 함수형 프로그래밍 적용

1. 리액티브 프로그래밍 소개의 ReactiveCalculator 클래스

 

1. 리액티브 프로그래밍의 소개

리액티브 프로그래밍이란 무엇인가 리액티브 프로그래밍은 데이터 스트림에 영향을 미치는 모든 변경사항을 관련된 모든 당사자(최종사용자, 컴포넌트, 하위 구성요소, 연결되어있는 프로그램

abstractask.tistory.com

ReactiveCalculator 클래스 최적화하기

하나를 제외한 모든 subscriber를 제거했다.

class ReactiveCalculator(a: Int, b: Int) {
    val subjectCalc: io.reactivex.subjects.Subject<ReactiveCalculator> 
             = io.reactivex.subjects.PublishSubject.create()
    
    var nums: Pair<Int, Int> = Pair(0, 0)
    
    init {
        nums = Pair(a, b)
        
        subjectCalc.subscribe({
            with( it ) {
                calculateAddition()
                calculateSubstraction()
                calculateMultiplication()
                calculateDivision()
            }
        })
        
        subjectCalc.onNext(this)
    }
    inline fun calculateAddition: Int {
        val result = nums.first + nums.second
        println("Add = $result")
        return result
    }
    inline fun calculateSubstraction: Int {
        val result = nums.first - nums.second
        println("Substract = $result")
        return result    
    }
    inline fun calculateMultiplication: Int {
        val result = nums.first * nums.second
        println("Multiply = $result")
        return result        
    }
    inline fun calculateDivision: Double {
        val result = (nums.first*1.0) / (nums.second*1.0)
        println("Divide = $result")
        return result            
    }
    
    inline fun modifyNumbers( a: Int = nums.first, b: Int = nums.second ) {
        nums = Pair(a, b)
        subjectCalc.onNext(this)
    }
    fun handleInput( inputLine: String? ) {
        if( !inputLine.equals("exit") ) {
            val pattern: java.util.regex.Pattern = 
                java.util.regex.Pattern.compile("([a|b])(?:\\s)?=(?:\\s)?(\\d*)")
                        var a: Int? = null
            var b: Int? = null
            
            val matcher: Matcher = pattern.matcher(inputLine)
            
            if( matcher.matches() && matcher.group(1) != null && matcher.group(2) != null ) {
                if( matcher.group(1).toLowerCase().equals("a") ) {
                    a = matcher.group(2).toInt()
                } else if (  matcher.group(1).toLowerCase().equals("b") ) {
                    a = matcher.group(2).toInt()
                }
            }
            
            when {
                a != null && b != null -> modifyNumbers(a, b)
                a != null -> modifyNumbers(a = a)
                b != null -> modifyNumbers(b = b)
                else -> println("Invalid Input")
            }
        }
    }    
}

코루틴

 코루틴은 코틀린 1.1에 추가됬으며 여전히 실험적인 기능이기 때문에 상용 환경에서 사용하기 전에 다시 생각해봐야 한다.

 RxKotlin은 아직 코루틴을 사용하지 않는다. 그 이유는 코루틴과 RxKotlin의 스케줄러 내부 구조가 동일하기 때문인데, 코루틴은 최근에야 도입되었지만 스케줄러는 RxJava, RxJs, RxSwift같은 라이브러리와 오랜기간 사용됬다.

 

코루틴이란 애플리케이션을 개발하는 동안 긴 시간에 걸쳐 작업을 수행해야 하는 상황이 자주 발생하는데 이 문제를 해결할 수 있는 간단하면서도 강력한 API 이다. 

 

gradle에 추가

  • aplly plugin
    • JVM인 경우에는 'kotlin'이 옴
    • 안드로이드인 경우에는 'kotlinandroid' 가 옴
apply plugin : 'kotlin'
kotlin {
    experimantal { 
        coroutines 'enable'
    }
}

//의존성 추가
repositories {
    ...
    jcenter()
}

dependencies { 
    ...
    compile "org.jetvrains.kotlinx:kotlinx-coroutines-core:0.16"
}

메이븐이라면 pom.xml 에 다음 코드 추가

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    ...
    <configuration>
        <args>
            <arg>-Xcoroutines=enable</arg>
        <args>
    </configuration>
</plugin>
<repositories>
    ...
    <repository>
        <id>central</id>
        <url>http://centrer.bintray.com</url>
    </repository>
<repositories>
<dependencies>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-coroutines-core</artifactId>        
        <version>0.16</version>
    </dependency>
</dependencies>

코루틴 시작하기

  1.  함수로 일시중지로 표시하는 suspend 키워드 사용. 함수를 사용하는 동안 프로그램은 결과를 기다려야 한다.
  2. measureTimeMills의 연산은 전달된 블록을 실행하고 실행시간을 측정후 반환한다.
  3. delay 함수를 사용해 의도적으로 프로그램 실행을 2초 지연시킨다.
  4. runBlockint 블록은 longRunningTsk 함수가 완료될 때까지 프로그램을 대기 상태로 만든다.
suspend fun longRunningTsk(): Long {    // 1
    val time = measureTimeMills {       // 2
        print("Please wait")
        delay(2, TimeUnit.SECONDS)      // 3
        println("Delay Over")
    }
    return time
}

fun main(args: Array<String>) {
    runBlocking {                       // 4
        val exeTime = longRunningTsk()  // 5
        println("Execution Time is $exeTime")
    }
}

 

  1. async 코드 블록은 전달된 코루틴 콘텍스트에서 비동기적으로 블록 내부의 코드를 실행한다.
  2. time 변수가 사용 가능할 때까지 main 함수를 기다리도록 하는 블록킹 코드를 실행한다.  await 함수의 도움을 받아 이 작업을 수행하는데 runBlocking 코드 블록에게 async 코드 블록의 실행이 완료돼 time 변수가 사용 가능할 때까지 기다리게 한다.
fun main(args: Array<String>) {
    /*
    코루틴 컨텍스트의 세가지 유형
      - Unconfined : 기본 스레드에서 실행
      - CommonPool : 
        - 공통 스레드 풀에서 실행
        - 새로운 코루틴 컨텍스트를 만들어 실행
    */
    val time = async(CommonPool) {  longRunningTsk() }         // 1
    println("Print after async")
    runBlocking { println("Printing time ${ time.await() }") } // 2
}

시퀀스 생성하기

// 인쇄할 수를 사용자 입력으로 받을 때 문제 발생
fun main(args: Array<String>) {
    var a = 0
    var b = 1
    print("$a, ")
    print("$b, ")
    
    for( i in 2..9 ) {
        val c = a + b
        print("$c, ")
        a = b
        b = c
    }
}

/*
  주석 1 에서 fibonacciseries가 buildSequence 블록으로 채워지도록 선언한다.
  시퀀스 값을 출력할 값을 계산할 때마다(주석 2, 3) 그 값을 산출(yield)한다.
  주석 4에서 fibonacciseries를 호출해 10번째 변수까지 계산하고 시퀀스의 요소를 쉼표(,)로 결합시킨다.
*/
fun main(args: Array<String>) {
    var fibonacciseries = buildSequence {  // 1
        var a = 0
        var b = 1
        yield(a)           // 2
        yield(b)
        
        while( true ) {
             val c = a + b
             yield(c)      // 3
             a = b
             b = c
        }
    }
    println(fibonacciseries.take(10) join "," )  // 4
}

//결과
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

코루틴을 사용한 ReactiveCalculator 클래스

이전에 만들었던 ReactiveCalculator 클래스

  1. handleInput 함수를 suspend로 선언한다. 이는 JVM에게 함수의 실행이 오래 걸릴 것이라고 알려주는데, 함수를 호출하는 컨텍스트가 실행되면 완료될 때까지 기다려야한다.
  2. 함수의 일시 중단은 메인 컨텍스트에서 호출될 수 없다. 그래서 주석 2에서는 함수를 호출하는 async 블록을 만들었다.
class ReactiveCalculator(a: Int, b: Int) {
    val subjectCalc: io.reactivex.subjects.Subject<ReactiveCalculator> 
             = io.reactivex.subjects.PublishSubject.create()
    
    var nums: Pair<Int, Int> = Pair(0, 0)
    
    ......
    
    suspend fun handleInput( inputLine: String? ) { // 1
        if( !inputLine.equals("exit") ) {
            val pattern: java.util.regex.Pattern = 
                java.util.regex.Pattern.compile("([a|b])(?:\\s)?=(?:\\s)?(\\d*)")
                        var a: Int? = null
            var b: Int? = null
            
            val matcher: Matcher = pattern.matcher(inputLine)
            
            if( matcher.matches() && matcher.group(1) != null && matcher.group(2) != null ) {
                if( matcher.group(1).toLowerCase().equals("a") ) {
                    a = matcher.group(2).toInt()
                } else if (  matcher.group(1).toLowerCase().equals("b") ) {
                    a = matcher.group(2).toInt()
                }
            }
            
            when {
                a != null && b != null -> modifyNumbers(a, b)
                a != null -> modifyNumbers(a = a)
                b != null -> modifyNumbers(b = b)
                else -> println("Invalid Input")
            }
        }
    }    
}

fun main(args: Array<String>) {
    println("Initail Out put with a=15, b=10")
    var calculator: ReactiveCalculator = ReactiveCalculator(15, 10)
    println("Enter a = <number> or b = <number> in sparate lines\nexit to exit the program")
    
    var line: String?
    do {
        line = readLine()
        async( CommonPool ) {  // 2
            calculator.handleInput(line)
        }
    } while( line != null && !line.toLowerCase().contains("exit"))
}

함수형 프로그래밍: 모나드

모나드의 정의 : 값을 캡슐화하고 추가 기능을 더해 새로운 타입을 생성하는 구조체

  • MayBe
    • 모나드로서 Int 값을 캡슐화 하고 추가 기능을 제공
    • 모나드인 MayBe는 값을 포함할 수도 있고 포함하지 않을 수도 있으며, 값 또는 오류 여부에 관계없이 완료된다.
    • 오류가 발생했을때는 onError가 호출된다.
    • 오류가 발생하지 않고 값이 존재하면 onSuccess가 값과 함께 호출된다.
    • 값이 없고 오류도 없는 경우 onComplete가 호출된다.
    • onError, onSuccess, onComplete는 터미널 메서드라고 하는데, 모나드에서 하나가 호출되면 나머지는 호출되지 않는다.
fun main( args: Array<String> ) {
    // 오류가 발생한다면 둘다 maybeValue, maybeEmpty 모두 onError를 호출한다.
    
    // 성공하면 값이 14이므로 onSuccess가 호출된다.
    val maybeValue: MayBe<Int> = MayBe.just(14) // 1
    maybeValue.subscribeBy(                     // 2
        onComplete = { println("Completed Empty") },
        onError = { println("Error $it") },
        onSuccess = { println("Completed with value $it") }
    )
    
    
    // 성공하면 값이 비어 있으므로 onComplete가 호출된다.
    val maybeEmpty: MayBe<Int> = MayBe.empty() // 3
    maybeEmpty.subscribeBy(
        onComplete = { println("Completed Empty") },
        onError = { println("Error $it") },
        onSuccess = { println("Completed with value $it") }
    )    
}

단일 모나드 

Maybe는 단순히 모나드의 한 유형이고 MayBe 외에도 다수 있다.