devlog of ShinJe Kim

[TIL] 2019-09-09

|

Today I Learned

  • 맵리듀스(MapReduce)란? 맵(map)과 리듀스(reduce)란 두 개의 메소드로 구성되어 있다. 맵 메서드는 키 값을 읽어 필터링하거나 다른 값으로 변환하는 작업을 수행한다. 리듀스는 맵 함수를 통해 출력된 결과 값을 새로운 키를 기준으로 그룹화(grouping)한 후 집계연산(aggregation)을 수행한 결과를 출력한다.
  • 상수와 리터럴의 차이
    • 상수(constant): 변하지 않는 변수를 의미한다. 상수에는 숫자뿐만 아니라 클래스, 구조체 등의 데이터도 들어갈 수 있다. 참조변수를 상수로 지정한다는 것의 의미는 참조변수에 넣은 인스턴스 안의 데이터가 상수라는 것이 아니라, 참조변수 메모리의 주소값이 변하지 않는다는 의미이다. 즉,메모리 위치를 의미한다.
    • 리터럴(literal): 변수에 넣는 데이터 그 자체를 의미한다. 적, 메모리 위치 안의 값을 의미한다.
  • type-safe란 어떠한 연산(operation)도 정의되지 않은 결과를 내놓지 않는 것, 즉 예측 불가능한 결과를 내지 않는 것을 의미한다.
  • 콜백 함수(Callback function): 콜백 함수란 다른 함수에 인자로 전달되는 함수를 의미하며, 외부 함수 내에서 어떠한 동작이나 루틴을 완성하기 위해(즉, 어떠한 이벤트에 의해) 호출된다.

[Kotlin] 함수(Functions)

|

함수 선언(Functions declarations)

코틀린에서의 함수 선언은 fun 키워드를 사용합니다.

fun double(x: Int): Int {
    retunr 2 * x
}

함수의 사용(Functions usage)

함수 호출은 기존의 방식과 동일합니다:

val result = double(2)

멤버 함수를 호출할때에는 dot(.) 표기를 사용합니다.

Stream().read)() // Stream 클래스의 인스턴스를 생성하고 read() 함수를 호출함

매개변수(Parameters)

함수의 매개변수는 파스칼 표기법(name: type)으로 표기합니다. 여러개의 매개변수는 comma(,)로 구분하며, 각각의 타입을 명시적으로 표기해주어야합니다.

fun powerOf(number: Int, exponent: Int) { /*...*/ }

디폴트 인자(Default arguments)

함수의 매개변수(parameters)는 기본값(default values)을 가질 수 있으며 함수의 인자(arguments)가 생략되었을 때 사용됩니다. 이로인해 코틀린에서는 다른 언어보다 적은 코드로 오버로딩을 표현할 수 있습니다.

fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) { /*...*/ }

기본값은 위와 같이 매개변수의 타입 뒤에 = 연산자를 사용하여 지정할 수 있습니다.

매개변수(parameter)와 인자(argument)는 비슷해보이지만 다른 개념입니다. 매개변수는 함수의 정의 부분에서 나열되는 변수를 의미합니다. 인자는 함수를 호출할 때 전달되는 실제 값을 의미합니다.

오버라이딩 되는 메소드는 기존의 메소드와 동일한 기본 매개변수 값(default parameter values)을 갖습니다. 디폴트 파라미터 값을 가지는 메소드를 오버라이딩 할 때에는 아래와 같이 디폴트 파라미터 값의 자리에 아무것도 지정할 수 없습니다.

open class A {
    open fun foo(i: Int = 10) { /*...*/ }
}

class B : A() {
    override fun foo(i: Int) { /*...*/ } // 여기서 디폴트 값을 지정할 수 없습니다.
}

디폴트 값이 있는 매개변수가 디폴트 값이 없는 매개 변수보다 앞에 오는 경우에는, 아래와 같이 지명 인자(named arguments)로 함수를 호출해야만 디폴트 값을 사용할 수 있습니다.

fun foo(bar: Int = 0, baz: Int) { /*...*/ }

foo(baz = 1) // 기본값인 bar = 0 이 사용됨

만약 디폴트 값이 있는 매개변수 뒤에 오는 마지막 인자가 람다라면 지명 인자를 사용하여 전달될 수도 있고 소괄호 밖에서 전달될 수도 있습니다.

fun foo(bar: Int = 0, baz: Int = 1, qux: () -> Unit) { /*...*/ }

// 기본값 baz = 1 이 사용됨
foo(1) { println("hello") } 

// 기본값 bar = 0 과 baz = 1 이 모두 사용됨
foo(qux = { println("hello") }) 

// 기본값 bar = 0 과 baz = 1 이 모두 사용됨
foo { println("hello") }

지명 인자(Named arguments)

함수를 호출할 때 매개변수의 이름을 지정할 수도 있습니다. 이는 함수에 매개변수가 많거나 기본값이 있는 매개변수가 있을 때 사용하기 편리합니다.

아래와 같은 함수가 있다고 가정해봅시다.

fun reformat(str: String,
            normalizeCase: Boolean = true,
            upperCaseFirstLetter: Boolean = true,
            divideByCamelHumps: Boolean = false,
            wordSeparator: Char = ' ') {
    / *...*/
}

이 때 아래와 같이 디폴트 인자(default arguments = default parameter values)를 사용하여 함수를 호출할 수 있습니다.

reformat(str)

하지만 디폴트 인자가 없는 매개변수를 호출할때에는 아래와 같이 호출해야합니다.

reformat(str, true, true, false, '_')

지명 인자(named arguments)를 사용하면 훨씬 가독성있게 코드를 작성할 수 있습니다:

reformat(str,
        normalizeCase = true,
        upperCaseFirstLetter = true,
        divideByCamelHumps = false,
        wordSeparator = '_'
)

위와 같이 지명 인자를 사용하면 아래의 코드처럼 모든 인자를 지정할 필요 없이 함수를 호출할 수 있습니다.

reforamt(str, wordSeparator = '_')

순서대로 전달되는 인자(positional arguments)와 지명 인자(named arguments)를 둘 다 사용하여 함수를 호출할 때, 모든 positional arguments는 가장 첫번째 지명 인자보다 앞에 위치해야만 합니다. 예를 들어 f(1, y =2)는 가능하지만, f(x = 1, 2)는 사용할 수 없습니다.

가변 길이 인자를 사용하는 함수-호출 시 인자 개수가 달리질 수 있는 함수-(variable number of arguments: vararg)는 아래와 같이 spread(*) 연산자를 사용하여 전달합니다.

fun foo(vararg strings: String) { /*...*/ }

foo(strings = *arrayOf("a", "b", "c"))

On the JVM: 지명 인자 문법은 자바 함수를 호출할 때에는 사용할 수 없습니다. 자바의 바이트코드가 항상 매개변수의 이름을 보관하는 것은 아니기 때문입니다.

Unit을 반환하는 함수(Unit-returning functions)

리턴값이 없는 함수의 리턴 타입은 Unit입니다. Unit은 오직 하나의 값을 가지는 함수이며, 그 값 또한 Unit입니다. 이 값은 명시적으로 작성할 필요는 없습니다.

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello ${name}")
    else
        println("Hi there!")
    // `return Unit` 혹은 `return`은 생략 가능합니다.
}

Unit 리턴 타입 역시 생략 가능합니다. 리턴 타입인 Unit을 생략하여 위의 예제 코드를 아래와 같이 표현할 수도 있습니다.

fun printHello(name: String?) { ... }

단일 표현 함수(Single-expression functions)

함수가 반환하는 식이 한 줄이라면, 중괄호를 생략하고 = 기호를 사용하여 표현할 수 있습니다.

fun double(x: Int): Int = x * 2

만약 컴파일러가 함수의 리턴 타입을 유추할 수 있다면 아래와 같이 리턴 타입을 생략해도 됩니다.

fun double(x: Int) = x * 2

명시적 리턴 타입(Explicit return types)

블록 본문이 있는 함수는 Unit을 리턴(이 경우에는 생략 가능)하도록 의도되지 않은 한 리턴 타입을 명시적으로 지정해야합니다. 코틀린은 블록 본문이 있는 함수를 유추하지 않습니다. 블록 본문이 있는 함수는 복잡한 제어 흐름이 있을 수 있으며 이러한 함수의 리턴 타입은 때로는 컴파일러도 알기 어렵기 때문입니다.

가변 길이 인자(Variable number of arguments: varargs)

함수의 매개변수(일반적으로 가장 마지막 매개변수)는 vararg 변경자를 사용하여 표기할 수 있습니다:

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts는 Array
        result.add(t)
    return result
}

위와 같이 vararg 변경자를 사용하면 가변 길이의 인자를 아래와 같이 전달할 수 있습니다.

val list = asList(1, 2, 3)

함수 내에서 T 타입의 vararg-parameter는 T의 배열로 나타납니다. 즉, 위의 예제코드의 ts 변수는 Array<out T>라는 타입을 가집니다.

오직 하나의 매개변수에만 vararg 변경자를 지정할 수 있습니다. vararg 매개변수가 마지막 매개변수가 아닌 경우에는 이름이 있는 인자 구문(named argument syntax)을 사용하여 전달할 수도 있고, 만약 함수 타입의 매개변수가 있다면 소괄호 밖에서 람다를 이용하여 전달할수도 있습니다.

vararg-function을 호출하면 인자를 하나씩 전달할 수 있습니다. 예를 들면 asList(1, 2, 3)과같은 배열이 있고, 이 내용을 함수에 전달하려면 spread(*) 연산자를 배열 앞에 붙입니다.

val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

중위 함수 호출 구문(infix notation)

infix 키워드가 붙은 함수는 중위 함수 호출(infix notation)을 이용하여 호출할 수 있습니다. 중위 함수 호출이란 dot(.)과 소괄호를 생략하여 함수를 호출하는 것을 의미합니다. 중위 함수 호출을 하기 위해서는 아래의 사항을 지켜야합니다.

  • 해당 함수가 멤버 함수이거나 확장 함수이어야만 합니다
  • 매개변수가 하나(single)이어야만 합니다.
  • 매개변수가 가변 인자를 받지 않으며 기본 값이 없어야 합니다.
infix fun Int.shl(x: Int): Int { ... }

// 중위 함수 호출(infix notation)을 이용.
1 shl 2

// 위의 코드는 아래의 코드와 같은 의미입니다.
1.shl(2)

중위 함수 호출은 산술 연산자(arithmetic operator), 타입 캐스트(type cast) 그리고 rangeTo 연산자(rangeTo operator)보다 우선순위가 낮습니다. 아래의 표현식들은 각각 같은 의미입니다.

  • 1 shl 2 + 31 shl (2 + 3)
  • 0 until n * 20 until (n * 2)
  • xs union ys as Set<*>xs union (ys as Set<*>)

반면에 중위 함수 호출은 부울 연산자(boolean operators), && 연산자, || 연산자, is- 연산와 in-checks 등 일부 다른 연산자보다 높은 우선순위를 가지고 있습니다. 아래의 표현식들은 각각 같은 의미를 가집니다.

  • a && b xor ca && (b xor c)
  • a xor b in c(a xor b) in c

이외의 전체 연산자 우선 순위 계층 구조는 Grammar reference에서 볼 수 있습니다.

중위 함수 호출을 사용하기 위해서는 항상 수신자(receiver)와 매개 변수를 지정해야만 합니다. 중위 함수 호출 표기법을 사용하여 현재의 수신자에서 메소드를 호출할 때에는 this를 명시적으로 사용해야만 합니다. 명확한 구문 분석을 위해서입니다. 이 때 this는 일반적으로 메소드를 호출할때처럼 생략할 수 없습니다.

class MyStringCollection {
    infix fun add(s: String) { /*...*/ }

    fun build() {
        this add "abc" // 맞는 문법입니다.
        add("abc") // 맞는 문법입니다.
        // add "abc" // 틀린 문법입니다. receiver가 명확하게 지정되어야만 합니다.
    }
}

함수의 범위(Function scope)

코틀린에서 함수는 파일에서 최상위에 선언될 수 있습니다. 즉, Java/C#/Scala와 같이 함수를 가지기 위해 클래스를 작성할 필요가 없습니다. 최상위 함수 이외에도, 지역(local) 선언으로 멤버 함수 및 확장 함수를 선언할 수 있습니다.

지역 함수(Local functions)

코틀린은 지역 함수(함수 안에 다른 함수를 선언하는 것)를 지원합니다.

fun dfs(graph: Graph) {
    fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v, visited)
    }

    dfs(graph.vertices[0], HashSet())
}

지역 함수는 외부 함수(outer functions)의 지역 변수(local variables), 즉 클로저(closure)에 접근할 수 있으므로 위의 코드에서는 visited가 지역 변수가 될 수 있습니다:

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }

    dfs(graph.verticesp[0])
}

멤버 함수(Member functions)

멤버 함수는 클래스나 객체 안에서 정의된 함수를 의미합니다:

class Sample() {
    fun foo() { print("Foo") }
}

멤버 함수는 구분자 dot(.) 를 사용하여 호출할 수 있습니다.

Sample().foo() // Sample 클래스의 인스턴스를 생성하고 foo 함수를 호출함.

ClassesInheritance 부분에 클래스와 오버라이딩 멤버에 대한 내용이 나와있습니다.

제네릭 함수(Generic functions)

함수 이름 앞에 꺾쇠 괄호 <> 를 사용하여 제네릭 매개변수를 지정할 수 있습니다:

fun <T> sungletonList(item: T): List<T> { /*...*/ }

제네릭 함수에 대한 설명은 제네릭(Generics) 부분에서 볼 수 있습니다.

인라인 함수(Inline functions)

인라인 함수에 대한 설명은 이곳에서 볼 수 있습니다.

확장 함수(Extension functions)

확장 함수 부분에서 내용을 확인할 수 있습니다.

고차원 함수와 람다(Higher-order functions and lambdas)

고차원 함수와 람다 포스팅에 설명되어 있습니다.

꼬리 재귀 함수(Tail recursive functions)

코틀린은 꼬리 재귀(tail recursion)로 알려진 함수형 프로그래밍을 방식을 지원합니다. 이를 통해 스택 오버플로(stack overflow)의 위험 없이 재귀를 사용할 수 있습니다. 함수에 tailrec 변경자가 지정되고 필요한 조건을 충족하였다면, 컴파일러가 재귀를 최적화하여 빠르고 효율적인 루프 기반 버전으로 남겨줍니다.

val eps = 1E-10 // "good enough", could be 10^-15 

tailrec fun findFixPoint(x: Double = 1.0): Double
    = if (Math.abs(x = Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))

이 코드는 수학적인 상수인 코사인(cosine)의 고정점(fixpoint)을 계산합니다. 결과가 더 이상 변하지 않을때까지 1.0부터 반복적으로 Math.cos 함수를 호출하여 지정된 eps 정밀도와 비교하여 0.7390851332151611라는 결과를 도출합니다.

val eps = 1E-10 // "good enough", could be 10^-15

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (Math.abs(x - y) < eps) return x
        x = Math.cos(x)
    }
}

tailrec 변경자를 사용하기 위해서는 함수가 마지막으로 수행될 때 자기 자신을 호출해야만 합니다. 만약 재귀 호출 후에 실행될 코드가 있다면 꼬리재귀를 사용할 수 없습니다. 또한 꼬리재귀는 try/catch/finally 블록 내에서 사용할 수 없습니다. 현재는 JVM 기반의 코틀린과 Kotlin/Native에서 꼬리 재귀를 지원합니다.

참고 문헌

[TIL] 2019-09-05

|

Today I Learned

  • 암달의 법칙(Amdal’s Law): 프로그램은 병렬처리가 가능한 부분과 불가능한 순차적인 부분으로 구성되므로 프로세서를 아무리 병렬화 시켜도 더 이상 성능이 향상되지 않는 한계가 존재한다는 법칙. 암달의 저주라고도 불린다.
  • 구스타프슨의 법칙(Gustafson’s Law): 컴퓨터 과학에서 대용량 데이터 처리를 효과적으로 병렬화할 수 있다는 법칙이다. 즉, 계산 과학에서 커다란 문제는 병렬화를 효과적으로 하여 처리할 수 있다는 개념이다.
  • 동기/비동기 vs 블록/논블록
    • 동기/비동기는 OS의 관점
    • 블록/논블록은 추상적인 event의 관점
    • read 연산을 수행한다고 가정해보자.
      • 동기: 기존의 실행 흐름에서 read 연산으로 흐름이 바뀜. read 연산이 끝날때까지 제어는 read 연산에 있음. read 연산이 끝나고서야 기존의 제어가 실행됨.
      • 비동기: 기존의 실행 흐름을 그대로 유지하면서 또 하나의 실행 흐름(read)이 병행되는 것.
      • 블록: 버퍼에 read 연산이 들어오기전까지는 CPU를 대기 상태로 유지하는 것
      • 논블록: 버퍼에서 실행해야 하는 것이 없어도 대기 상태로 유지하지 않는 것
  • 만약 직렬화를 하고 싶으면? 리플렉션을 이용하여 필드와 필드의 값이 뭔지를 알아내야 한다.
  • 리플렉션(Reflection)이란?
    • 컴퓨터과학에서의 리플렉션이란 프로그램이 실행중에 자신의 구조와 동작을 검사하고, 조사하고, 수정하는 것.
    • 리플렉션은 프로그래머가 데이터를 보여주고, 다른 포맷의 데이터를 처리하고, 통신을 위해 직렬화를 수행하고, bundling을 하기 위해 일반 소프트웨어 라이브러리를 만들도록 도와줌.
    • 자바 리플렉션을 사용하면 구체적인 클래스 타입을 알지 못해도 해당 클래스의 메소드, 타입, 변수 등에 접근할 수 있음.
    • 하지만 리플렉션을 사용하는 것에는 문제점이 있다. 아래는 이펙티브 자바의 “리플렉션보다는 인터페이스를 사용하라” 챕터에서 발췌하였다.
      • 컴파일타임 타입 검사가 주는 이점을 하나도 누릴 수 없다: 프로그램이 리플렉션 기능을 써서 존재하지 않는 혹은 접근할 수 없는 메서드를 호출하려 시도하면(주의해서 대비 코드를 작성해두지 않았다면) 런타임 오류가 발생한다.
      • 리플렉션을 이용하면 코드가 지저분하고 장황해진다
      • 성능이 떨어진다: 리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다.
      • 따라서 리플렉션은 아주 제한된 형태로만 사용해야 그 단점을 피하고 이점만 취할 수 있다.
      • 컴파일 타임에 이용할 수 없는 클래스를 사용해야만 하는 프로그램은 비록 컴파일 타임이라도 적절한 인터페이스나 상위 클래스를 이용할 수는 있을 것이다. 이런 경우라면 리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.
  • 프로그램 프로파일링: 프로그램의 성능 분석을 의미한다. 프로그램의 시간 복잡도 및 메모리 등이 얼마나 효율적으로 관리/사용되는지 분석하는 것.
  • 자바의 익명 클래스: 클래스의 선언과 객체의 생성을 동시에 한다. 한 번만 사용될 수 있으며 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.
  • 지연 초기화(lazy initialization): 모든 객체를 한 번에 생성하는 것이 아니라, 실제로 처음 쓰이는 시점에 생성하겠다는 것. 자바에서는 이를 직접 구현해야만 함. 이 떄 스레드 안정성에 문제가 생김.
  • 스레드 안전(Thread safe)이란? 멀티 스레드 프로그래밍에서 어떠한 함수나 변수 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없는 것을 의미함. 보다 엄밀하게는 하나의 함수가 한 스레드로부터 호출되어 실행중일 때 다른 스레드가 그 함수를 호출하여 동시에 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바로 나오는 것을 의미함. 코틀린의 오브젝트 선언이 이를 안전하게 실행할 수 있도록 해줌.
  • 스레드 안전을 지키기 위한 방법
    • Re-entrancy: 어떤 함수가 한 스레드에 의해 호출되어 실행중일 때, 다른스레드가 그 함수를 호출하더라도 그 결과가 각각에게 올바로 주어져야 함.
    • Thread-local storage: 공유 자원의 사용을 최대한 줄여 각각의 스레드에서만 접근 가능한 저장소들을 사용함으로써 동시 접근을 막음. 이 방식은 동기화 방법과 관련되어 있으며 공유상태를 피할 수 없을 때 사용함.
    • Mutual exclusion: 공유 자원을 꼭 사용해야 할 경우 해당 자원의 접근을 세마포어 등의 락으로 통제함.
    • Atomic operations: 공유 자원에 접근할 떄 원자 연산을 이용하거나 ‘원자적’으로 정의된 접근 방법을 사용함으로써 상호 배제를 구현할 수 있음.

참고 자료

[Kotlin] 고차원 함수와 람다(Higher-Order Function and Lambdas)

|

코틀린 함수는 1급 객체입니다. 이것은 변수나 데이터 구조에 저장되어 인자로 전달될 수 있고, 다른 고차원 함수의 리턴값으로 사용될 수 있다는 의미입니다.

정적 타입을 사용하는 코틀린은 function types를 사용하여 함수를 나타내고 람다 식과 같은 특별한 언어 구조를 지원합니다.

고차원 함수(Higher-Order Functions)

고차원 함수는 함수를 인자로 받거나 리턴값으로 사용할 수 있는 함수를 의미합니다. 컬렉션 함수의 functional programming idiom fold가 좋은 예입니다. accumulator의 초기값과 결합 함수를 사용하여 현재 accumulator 값을 각 컬렉션 요소와 연속적으로 결합하여 accumulator를 대체한 리턴 값을 만듭니다. (which takes an initial accumulator value and a combining function and builds its return value by consecutively combining current accumulator value with each collection element, replacing the accumulator)

fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

위의 예제코드에서 combine 파라미터의 타입은 (R, T) -> R이라는 함수입니다. 따라서 combine은 RT 두 가지를 인자로 받으며 R 타입을 리턴합니다. combine은 for 루프에서 호출되어 accumulator에 리턴 값이 할당됩니다.

fold를 호출하기 위해서는 함수 타입의 인스턴스를 인자로 전달 해야만하는데, 람다식(아래에서 더 자세히 설명)은 이를 위해 고차원 함수를 호출하는 곳에서 주로 사용됩니다.

val items = listOf(1, 2, 3, 4, 5)

// 람다는 중괄호 안의 코드블록을 의미합니다.
items.fold(0, {
    acc: Int, i: Int ->
        print("acc = $acc, i = $i, ")
    val result = acc + i
    println("result = $result")
    result
})

// 람다의 파라미터 타입은 (유추할 수 있다면) 생략가능합니다.
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })

// 고차원 함수 호출에서도 함수 레퍼런스를 사용할 수 있습니다.
val product = items.fold(1, Int::times)

함수 타입(Function types)

코틀린은 (Int) -> String 과 같이 함수를 선언하는 방식을 사용합니다: val onClick: () -> Unit = ...

이러한 합수 타입에는 함수의 서명, 즉 파라미터와 리턴 값을 표현하는 특수한 표기법이 있습니다.

  • 모든 함수 유형에는 괄호로 묶인 파라미터 타입 리스트와 리턴 타입 리스트가 있습니다. (A, B) -> CA 타입과 B 타입의 파라미터를 받아 C 타입의 값을 리턴하는 함수를 의미합니다. 파라미터 타입 리스트는 () -> A와 같이 생략할 수도 있지만, Unit return type은 생략할 수 없습니다.
  • 함수 타입은 선택적으로 추가적인 수신(receiver) 객체 타입을 가질 수 있으며 다음과 같이 dot(.) 앞에 표기합니다: A.(B) -> C 이 함수는 파라미터 B를 전달받는 리턴 값 CA 수신 객체에서 호출합니다. function literals with receiver(수신 객체 지정 함수 리터럴)는 주로 이러한 유형의 함수에 사용됩니다.
  • Suspending functions(지연 함수)suspend () -> Unit 혹은 suspend A.(B) -> C와 같이 표기법에 지연 수정자(suspend modifier)가 있는 특수한 함수 유형에 속합니다.

함수 유형 표기법은 다음과 같이 선택적으로 파라미터의 이름을 포함할 수 있습니다: (x: Int, y: Int) -> Point. 이러한 이름은 파라미터의 의미를 문서화하는데 사용될 수 있습니다.

// 함수가 null이 될 수 있는 타입이면 아래와 같이 표기합니다.
((Int, Int) -> Int)?

// 함수 타입은 중괄호를 사용하여 결합할 수 있습니다. 
(Int) -> ((Int) -> Unit)

(Int) -> (Int) -> Unit //이 코드는 위의 예제와 동일한 의미입니다.
((Int) -> (Int)) -> Unit // 이 코드는 위의 예제와 다른 의미입니다. 

아래와 같이 typealias를 이용하여 함수의 타입을 지정할 수도 있습니다.

typealias ClickHandler = (Button, ClickEvent) -> Unit

함수 타입을 인스턴스화 하기

함수 타입의 인스턴스를 얻는 방법에는 여러가지가 있습니다.

  • 함수 리터럴 내부의 코드 블록을 사용하여 아래 중 하나의 방식으로 할 수 있습니다.

    수신자(receiver)가 있는 함수 리터럴(function literals with receiver)은 수신자가 있는 함수 타입의 값으로 사용될 수 있습니다.

  • 기존 선언에 대한 호출가능한 참조를 사용하는 방법은 아래와 같습니다.
    • 최상위(top-level), 지역(local), 멤버(member), 혹은 확장 함수(extension function): ::isOdd, String::toInt,
    • 최상위(top-level), 멤버(member), 혹은 확장 프로퍼티(extension property): List<Int>::size,
    • 생성자(constructor): ::Regex

    여기에는 특정 인스턴스의 멤버를 가리키는 바인딩 된 호출 가능 참조(bound callable references)가 포함됩니다: foo::toString.

  • 함수 타입을 인터페이스로 구현하는 사용자 정의 클래스 사용하면 아래와 같습니다.
class IntTransforner: (Int) -> Int {
    override operator fun invoke(x: Int) : Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

아래와 같이 타입에 관한 정보가 있다면 컴파일러는 함수의 타입을 유추할 수 있습니다.

val a = { i: Int -> i + 1 } // 유추된 타입은 (Int) -> Int 입니다.

수신자가 있거나 없는 함수 타입의 non-literal 값은 서로 교환이 가능하므로, 수신자가 첫 번째 인자를 사용할 수 있으며 그 반대로도 가능합니다. 예를 들면, (A, B) -> C 타입의 값은 A.(B) -> C가 예상되는 곳이나 다른 방법으로 전달/할당 될 수 있습니다.

val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OK

fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}
val result = runTransformation(repeatFun) // OK

확장 함수를 참조하여 변수를 초기화하더라도, 수신자가 없는 함수 타입이 디폴트로 유추됩니다. 이를 변경하기 위해서는 변수 타입을 명시적으로 지정해야만 합니다.

함수형 인스턴스 호출(Invoking a function type instance)

함수 유형의 값은 invoke(…) 연산자를 사용하여 호출할 수 있습니다: f.invoke(x) 혹은 f(x)

값에 수신자 타입이 있으면 수신 객체가 첫 번째 인자로 전달되어야 합니다. 수신자와 함수 타입의 값을 함께 호출하는 다른 방법은, 해당 값이 확장 함수인것처럼 다음과 같이 수신 객체 앞에 추가하는 것입니다: 1.foo(2)

val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus

println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "workd!"))

println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // 확장함수처럼 호출

인라인 함수(Inline functions)

때로는 고차원 함수가 유연한 제어 흐름을 가질 수 있도록 하는 인라인 함수를 쓰는 것이 좋습니다.

람다식과 익명 함수(Lambda Expressions and Anonymous Functions)

람다식과 익명 함수는 ‘함수 리터럴(function literals)’입니다. 함수는 선언되지 않고 표현식으로 즉시 전달됩니다. 아래의 예시를 보겠습니다.

max(strings, { a, b -> a.length < b.length })

max 함수는 고차원함수로서 함수 값을 두번째 인자로 가집니다. 두번째 인자는 함수 리터럴인데, 그 자체가 함수인 표현식입니다. 두번째 인자는 아래의 함수와 그 의미가 같다고 볼 수 있습니다.

fun compare(a: String, b: String): Boolean = a.length < b.length

람다 표현식(Lambda expression syntax)

람다식의 전체 구문 형태는 아래와 같습니다.

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

람다식은 항상 중괄호{}로 묶습니다. 전체 구문 형태(full syntactic form)로 선언된 파라미터는 중괄호 안에 들어가며, and have optional type annotations, 본문은 -> 기호 뒤에 옵니다. 람다의 리턴 타입이 Unit타입이 아닌 경우, 람다 본문 내부의 가장 마지막 표현식 혹은 단일 표현식이 람다의 리턴값으로 처리됩니다.

위의 예제 코드에서 생략 가능한 코드를 모두 생략한다면 아래와 같은 형식이 됩니다.

val sum = { x, y -> x + y }

람다를 마지막 매개변수로 전달하기(Passing a lambda to the last parameter)

코틀린에서는 함수의 마지막 매개변수가 함수이면 해당 인수로 전달된 람다식을 괄호 밖에 넣을 수 있습니다.

val product = items.fold(1) { acc, e -> acc * e }

이러한 문법을 trailing lambda라고 부릅니다.

만약 함수를 호출할 때 람다식이 유일한 인자이면, 소괄호 전체를 생략해도됩니다.

run { println("...") }

it: implicit name of a single parameter

람다식의 매개변수는 주로 한 개입니다. 컴파일러가 시그니처 자체를 알아낼 수 있는 경우에는 유일한 매개변수 이름을 선언하지 않고 ->를 생략할 수 있습니다. 이 때 매개변수는 it이라는 이름으로 암시적으로(implicitly) 선언될 수 있습니다.

ints.filter { it > 0 } // 이 리터럴은 '(it: Int) -> Boolean' 타입입니다.

람다식에서 값을 반환하기(Returning a value from a lambda expression)

qualified return 구문을 사용하여 람다에서 값을 명시적으로 리턴할 수 있습니다. 그렇지 않으면 마지막 표현식의 값이 암시적으로(implicitly) 리턴됩니다. 따라서 아래의 두 코드는 동일합니다.

ints.filter {
    val shouldFilter = it > 0
    shoulFilter
}

ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter
}

이러한 규칙은 소괄호 밖에서 람다식을 전달하는 것과 더불어 LINQ 스타일의 코드를 사용할 수 있게 해줍니다.

strings.filter { it.length == 5 }.sortedBy { it }.map { it.toUpperCase() }

사용하지 않는 변수는 언더스코어로 표현하기(Underscore for unused variables/코틀린 1.1부터)

만약 람다식에 사용되지 않는 매개변수가 있다면, 변수명 대신 언더스코어(_)를 사용할 수 있습니다.

map.forEach { _, value -> println("$value!")}

Destructuring in lambdas(코틀린 1.1부터)

destructuring declarations에서 확인할 수 있습니다.

익명 함수(Anonymous functions)

코틀린에서는 함수의 리턴 타입을 지정할 수 있습니다. 대부분의 경우에는 리턴 타입을 자동으로 유추 할 수 있으므로 굳이 필요하지 않습니다. 그러나 명시 적으로 리턴 타입을 지정해야하는 경우에는 익명 함수(Anonymous functions)를 사용할 수 있습니다.

fun(x: Int, y: Int): Int = x + y

익명 함수는 함수의 이름이 생략된다는 점을 제외하면 일반적인 함수 선언과 매우 비슷합니다. 본문에는 식 또는 블록(구문) 모두 사용할 수 있습니다.

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

매개변수의 타입을 유추할 수 있다면 매개변수 타입을 생략할 수 있다는 점을 제외하면, 일반 함수 선언과 동일하게 매개변수와 리턴 타입을 선언할 수 있습니다.

ints.filter(fun(item) = item > 0)

익명 함수의 리턴 타입을 유추하는 과정은 일반 함수와 동일합니다: 본문이 식(expression) 익명 함수의 리턴 타입은 자동적으로 유추될 수 있습니다. 본문이 블록/문(block body/statement)인 익명 함수의 리턴 타입은 명시적으로 지정되어야 하며 그렇지 않으면 Unit으로 가정됩니다.

익명 함수의 매개변수는 항상 소괄호 안에서 전달되며, 함수를 소괄호 밖에 둘 수 있는 경우는 람다식에만 적용된다는 사실을 주의해야합니다.

람다식과 익명 함수의 또 다른 차이점은 둘 다 비지역 반환을 한다는 것입니다. label이 없는 return문은 항상 fun 키워드로 선언된 함수에서 반환됩니다. 즉, 람다식 내부의 return은 식을 둘러싸는 함수 안에서 반환되고, 익명함수의 return은 익명 함수 자기 자신에서 반환됩니다.

클로저(Closures)

람다식 또는 익명 함수(지역 함수-local function 혹은 객체식-object expression)는 해당 클로저(외부에 선언된 변수) 에 접근할 수 있습니다. 클로저로 접근한 변수(the variables captured in the closure)는 람다에서 수정할 수 있습니다.

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

리시버가 있는 함수 리터럴(Function literals with receiver)

A.(B) -> C와 같이 리시버가 있는 함수 타입(function types)은 ‘리시버가 있는 함수 리터럴(function liteals with receiver)’이라는 특수한 형식의 함수 리터럴로 인스턴스화 할 수 있습니다.

위에서 언급했듯이 코틀린은 리시버에 수신객체(receiver object) 를 제공하여 함수 타입의 인스턴스를 호출할 수 있도록 합니다.

함수 리터럴의 본문 안에서, 호출되어 전달된 수신 객체는 암시적인 this가 됩니다. 따라서 추가적인 qualifier없이 수신 객체의 멤버에 접근할 수 있으며 this 표현식으로 수신 객체에 접근할 수 있습니다.

이러한 동작 방식은 확장 함수와 유사하며, 함수 본문 내에서 수신 객체의 멤버에 접근할 수도 있습니다.

다음은 타입이 존재하며 리시버가 있는 함수 리터럴의 예시입니다. 이곳에서 수신 객체로 plus가 호출됩니다.

val sum: Int.(Int) -> Int = { other -> plus(other) }

익명 함수를 사용하면 함수 리터럴의 리시버 타입을 직접 지정할 수 있습니다. 리시버로 함수 타입의 변수를 선언한 뒤 나중에 사용해야 할 때 유용하게 쓸 수 있습니다.

val sum = fun Int.(other: Int): Int = this + other

리시버 타입이 유추될 수 있는 경우에는 람다식을 리시버가 있는 함수 리터럴(function literals with receiver)로 사용할 수 있습니다. 이를 활용하는 방법 중 가장 중요한 예제는 type-safe builder입니다.

class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // 수신 객체를 생성함.
    html.init()        // 수신 객체를 람다에 전달함.
    return html
}

html {       // 이곳부터 수신자가 있는 람다가 시작됨.
    body()   // 수신 객체에서 메소드를 호출함.
}

[TIL] 2019-09-04

|

Today I Learned

  • 오버헤드란? 프로그램의 실행 흐름 도중에 동떨어진 위치의 코드를 실행해야 할 때, 추가적으로 시간과 메모리 자원이 사용되는 현상. 이러한 현상은 프로그래밍 시 외부 함수를 사용할 떄 나타남. 외부 함수를 사용하며 실행 흐름이 도중에 끊겨버리고 함수를 사용하기 위해 스택 메모리를 할당함. 이외에도 함수를 호출하기 위해 많은 과정을 진행하게 되며, 이 때 예상치 못한 자원들이 소모되는 현상을 오버헤드라고 함. 이를 해소하기 위해 인라인 함수를 사용한다. 코틀린 컴파일러(JVM) getter와 setter는 인라인 함수를 사용하기 때문에 함수 오버헤드가 발생하지 않는다. 인라인 함수를 사용하는지, 외부 호출을 하는지는 컴파일러마다 다르다.
  • 함수를 사용하는 이유는 ‘재사용성’의 목적이 크다.