본문 바로가기

Study/Kotlin

Kotlin / 함수 추가사항 알아보기

반응형

1. 함수형 프로그래밍이란

순수함수와 일급 객체 함수

함수는 참조 투명성(지역변수만 사용)을 갖춰야 항상 동일한 입력에 동일한 결과를 반환하는 순수 함수를 만들 수 있다.

 

순수 함수(pure function)의 조건

  • 동일한 인자로 실행하면 항상 동일한 값 반환
  • 함수 내부에서 반환값 이외의 결과로 부수효과가 발생하지 않는다

부수효과(side effect)

  • 함수가 실행되는 과정에서 함수 외부의 데이터를 사용 및 수정하거나 외부의 다른 기능을 사용하는 것을 말한다
  • 함수가 전역변수를 사용하거나 수정하는 것
  • 함수가 표준 입출력을 사용해서 키보드 입력과 화면 등에 출력
  • 함수가 파일을 읽고 쓰는 작업을 수행
  • 함수를 사용해서 데이터베이스에 연결
fun purefunc(a:Int, b:Int):Int{
    return a+b // 입력되는 인자에 의해 결정
}


fun nonpure1(a:String){
    println("비순수함수 $a") // 외부에 출력
}
var state = 100
fun nonpure2(x:Int):Int{
    state +=x
    return state
}

println(purefunc(10,20)) // 함수를 계속 호출해도 결과 같음
nonpure1("외부 출력") // 함수를 호출하면 인자와 관계없이 외부와 연계 처리
println("state : $state nonpure2 : ${nonpure2(108)}")
println("state : $state nonpure2 : ${nonpure2(108)}")

// 30
// 비순수함수 외부 출력
// state : 100 nonpure2 : 208
// state : 208 nonpure2 : 316

 

일급 객체 함수 (first class function)

  • 함수를 변수에 할당할 수 있다
  • 함수를 매개변수의 인자로 전달할 수 있다
  • 함수를 반환값으로 사용할 수 있다
  • 함수를 컬렉션 자료구조에 할당할 수 있다

지연 평가 함수 실행

특정 시점에 평가되는 지연평가를 처리하는 방법은 lazy 함수, 제너레이터 시퀀스 함수, 부분함수 등이 있다

지연 평가는 함수를 정의하고 필요할 경우 호출 처리를 하는 것이다.

val func by lazy { { x: Int -> x } } // 속성 위임에 초깃값을 함수로 전달

val seq = generateSequence(0) { it + 100 } // 무한 시퀀스 정의

println(func(100))
println(seq.take(5).toList())
 
// 100
// [0, 100, 200, 300, 400]

최상위 속성을 정의할 때 by로 속성 위임에 lazy 함수를 사용할  수 있다. 

이 속성을 참조할 때 람다표현식이 실행되어 초깃값이 만들어진다.

무한시퀀스도 함수인데, 이 시퀀스를 만들면 실제 실행이 되지 않고 액션 toList메서드가 실행되어야 무한 시퀀스가 실행된다.

 

커링함수(currying function) 알아보기

하나의 함수에서 매개변수를 분리해 외부함수와 내부함수로 지정해서 처리할 수 있다.

이렇게 함수를 부분으로 나눠 처리하는 것을 커링함수라고 한다.

 

fun add(a: Int, b: Int) = a + b
fun outer(a: Int): (Int) -> Int { // 함수로 부분 함수 정의
    fun inner(b: Int): Int = a + b // 내부 함수 정의
    return ::inner // 내부 함수 반환
}

val add2 = outer(1)
println(add2(2)) // 3

 

연속 호출하는 체이닝 처리

함수나 메서드를 연속으로 호출해서 처리하는 것을 메서드 체이닝이라고 한다.

객체를 반환하면 그 내부 메서드를 연속해서 실행할 수 있지만, 너무 많이 사용하면 실제 코드를 이해하는 데 어려울 수 있다.

class Car2(var ownerName: String) {
    fun changeOwner(newName: String): Car2 {
        this.ownerName = newName
        return this
    }

    fun info(): Unit = println("Car 소유자 : $ownerName")
}

val c = Car2("이은정")
c.info()
c.changeOwner("은정").info() // 메서드 체인 처리

// Car 소유자 : 이은정
// Car 소유자 : 은정

 

2. 고차함수, 합성함수, 재귀함수 알아보기

고차함수 정의

고차함수는 함수를 객체로 생각해서 인자로 전달되거나 반환값으로 처리되는 함수 패턴을 말한다.

  • 함수의 매개변수에 함수 자료형을 정의하고 함수를 호출할 때 인자로 다른 함수를 받는다
  • 함수 내부에서 반환값으로 다른 함수를 반환한다
  • 인자와 반환값으로 함수, 익명함수, 람다 표현식으로 처리할 수 있다.
typealias f = (Int, Int) -> Int // 함수 자료형을 타입 별칭으로 지정

fun highfunc(vararg x: Int, op: f): Int { // 함수를 매개변수로 받는 고차함수
    return x.toList().reduce(op)
}

println(highfunc(1, 2, 3, 4, op = { x: Int, y: Int -> x + y })) // 합산하는 람다표현식 전달

// 10

 

합성함수 정의

두 함수를 하나의 함수로 연결한 것을 합성함수라고 한다.

함수를 구성하려면 함수의 매개변수와 자료형이 일치해야 하므로 함수를 합칠 때는 주의해야 한다.

내부 처리는 전달되는 함수를 실행하고 그 결과를 받은 함수를 실행하는 순서로 처리한다.

fun composeF(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int {
    return { p1: Int -> f(g(p1)) }
}

val f = { x: Int -> x + 2 } // 첫 번째 함수
val g = { y: Int -> y + 3 } // 두 번째 함수 : 함수 내부에 결합되는 함수
val composeFunc = composeF(f, g) // 두 개의 함수를 인자로 전달

println(f(g(3))) // 두 함수를 결합해서 실행
println(composeFunc(3)) // 합성함수로 반환된 함수 실행

// 8
// 8

 

두 함수를 하나로 합성한 함수를 반환한다.

 

재귀함수 정의

함수를 정의해 자기 함수를 호출해서 계속 처리하는 방식이 재귀함수이다.

  • 무한 순환이 발생하지 않도록 함수의 종료 시점을 반드시 작성한다.
  • 함수 실행의 마지막 부분에 자기 자신을 호출하고 호출된 인자의 값을 조정한다.
  • 꼬리 재귀는 재귀 호출이 끝나면 아무 일도 하지 않고 결과만 바로 반환되도록 하는 방법이다. 
  • 꼬리 재귀는 함수가 다른 변수와 함수 호출 결과와의 연산도 함수의 인자로 전달되도록 작성한다.
  • 꼬리 재귀로 처리하면 코틀린은 내부적으로 함수스택을 추가로 만들지 않는다.
fun factorial(n: Int): Long {
    if (n == 1) { // 마지막 처리하는 코드
        return n.toLong()
    } else {
        return n * factorial(n - 1) // 재귀함수 작성. 인자는 항상 이전보다 작아야 함
    }
}

// 꼬리 재귀함수
tailrec fun factorial1(n: Int, total: Int = 1): Long {
    if (n == 1) {
        return total.toLong()
    } else {
        return factorial1(n - 1, n * total) // 꼬리 재귀를 위해 함수에 변경 값을 전달
    }
}

val number = 4
var result: Long = 0
result = factorial(number)
println("팩토리얼 계산 : $number = $result")

result = factorial1(number)
println("꼬리 재귀 팩토리얼 계산 : $number = $result")

// 팩토리얼 계산 : 4 = 24
// 꼬리 재귀 팩토리얼 계산 : 4 = 24

 

꼬리 재귀 tailrec 예약어를 붙어서 재귀함수를 만든다.

일반적인 재귀함수는 함수를 메모리 스택에 올려서 처리 -> 성능상의 문제

꼬리 재귀를 처리하면 내부적으로 코드를 변환해서 메모리 스택을 최소화

 

3. 함수의 추가 기능 알아보기

람다표현식에 수신 객체 반영

수신 객체를 함수 자료형에 붙인 다음 람다표현식을 받으면 수신 객체를 람다표현식 내에서 사용할 수 있다.

람다표현식을 메서드처럼 사용할 수 있는 방안을 추가하는 것.

val op: Int.(Int) -> Int = {x->x+this} // 함수 자료형에 특정 자료형을 수신 객체로 지정
println((100).op(100)) // 람다표현식에 수신 객체를 붙이면 메서드처럼 객체를 사용할 수 있다.
// 200

스코프 함수

코틀린에서는 다양한 기능을 처리하는 확장함수인 스코프 함수를 제공한다.

 

- 컨텍스트 : this 또는 it을 참조

  • 스코프 함수 중 run, with, apply 는 this 참조
  • 스코프 함수 중 let, also는 it을 참조

- 반환값 처리

  • apply는 this, also는 it을 반환
  • let, run, with는 람다표현식 결과를 반환

1. let

제네릭 확장함수로 구성되었고, 하나의 함수를 전달받아 함수의 실행결과를 반환하는 스코프 함수이다.

class Student1(val id: Int, var name: String, var age: Int)

val s = Student1(1, "ej", 20)
println(s.name)

val ss = s.let {
    it.name = "lee" // 내부 갱신
    it// 객체 전달
}
println("처리 결과 : ${ss.name} ${ss.age}")

// ej
// 처리 결과 : lee 20

위의 객체에서 let을 사용해 람다표현식을 전달받아 메서드처럼 사용할 수 있다.

클래스를 정의하고 객체를 만들었다. 이 객체의 속성 이름을 수정하고 it (전달받은 객체)를 반환했다.

객체의 name 속성만 변경된 것을 확인 할 수 있다.

val s1: Student1? = null // 널러블 처리도 가능
if (s1 == null) s1
else s1.let { it.name = "nullname" } // 널이 아니면 실행
println(s1?.let{it.name="ej ej"}) // 널이 아니면 함수 실행

// null

클래스를 널러블로 처리하면 null을 할당할 수 있다.

널러블을 처리하는 안전연산(?.)과 let 확장함수를 같이 사용하면 널러블을 안전하게 처리할 수 있다.

2. with

매개변수에 리시버 객체와 수신객체 함수자료형을 받아서 처리한다.

val lr: Int.(Int) -> Int = { x -> this + x }
println(lr(100, 200)) // 내부적으로 두 개의 인자로 처리
println((100).lr(200)) // 수신 객체 정의하고 람다표현식을 처리
println(with(100) { this + 200 }) // 수신 객체를 인자로 전달하고 this로 람다표현식 내부에서 처리

// 300
// 300
// 300

스코프 함수 with은 인자로 전달된 객체가 실제 리시버 객체이다.

뒤에 람다표현식의 this는 먼저 전달된 객체라서 이를 가지고 람다표현식에서 처리하고 결과를 반환한다.

3. run

확장함수 run은 하나의 함수를 인자로 받는다.

이 함수도 리시버가 붙은 함수 자료형이고, 스코프 함수의 반환값도 인자로 전달된 함수의 결과이다.

class Person3(var name: String, var age: Int)// 클래스를 정의
val person = Person3("Ej", 20) // 객체를 생성
val ageNextYear = person.run { // 객체로 run 함수 사용. 람다 표현식은 이 객체 내부의 속성 갱신
    ++age
    this
}
println("반환은 수신객체로 처리 = ${ageNextYear.age}")
// 반환은 수신객체로 처리 = 21

클래스를 정의하고 객체를 만들어 run 함수에 람다표현식을 전달하여 처리하면 기본 this가 전달된다. 

age는 속성이라 this없이 조회가 가능하다. 마지막 결과를 this로 반환했다.


4. also

inline 예약어를 사용하는 확장함수이면서 스코프함수이다.

결과는 리시버를 그대로 반환한다.

확장함수 also도 하나의 함수를 인자로 받는다. 인자로 전달되는 함수는 반환값이 없다.

전달받는 함수는 하나의 매개변수 즉 현재 리시버를 객체로 전달받는다. 그래서 it으로 객체를 갱신하거나 조회 등을 할 수 있다.

class Person4 {
    var name = "코틀린"
    private val id = "9999"
    var age = 0
}

val person4 = Person4()
val also1 = person4.also { it -> println("이름은 ${it.name}") }
println("also1 ${also1::class.simpleName}")

// 이름은 코틀린
// also1 Person4

객체를 하나 생성하고 also에서 객체의 속성을 출력한다. 단순하게 조회 처리가 가능하다.

5. apply

also처럼 인라인 함수이고 확장함수이다.

also와 차이점은 함수 자료형에 확장함수로 처리한다.

전달된 함수는 반환값이 없어서 리시버 객체를 조회하거나 갱신하는 데 사용된다.

val apply1 = person4.apply{println("이름은 $name")}
println("apply1 ${apply1::class.simpleName}")

// 이름은 코틀린
// apply1 Person4

 

--> 사용하는 경우

함수 이름과 연관지어 생각하면 좋다.

with : 이미 완성된 객체가 있고, 그 객체에 있는 value를 이용해서 작업들을 할때

           ex)viewBidning 같은 어떤 객체가 있어서 작업을 할때

apply : 인스턴스를 만들면서 만듦과 동시에 추가작업을 할 때 사용

 

run : T extention , 그냥 run 이 있다. 그냥 run 함수를 많이 씀. 보통.. 코드블럭을 만들고, 코드블럭 안에서 어떤 액션을 취해야 할 때. 

         ex) 어떤 매개변수 인자가 Nullable, 어떤 매개변수, elvis, return을 한다. return을 하면서 어떤 액션을 하고싶을때. 

내부적으로 this를 받아서 사용한다. this 스코프가 많아지면 좋지 않다. (run 안에 run이 있고.. 이러면 내가 접근하는 함수가 어떤 스코프에 있는 함수인지 알기가 어렵다. -> this를 안쓰는게 좋긴 하다. )

 

let : null check용, 체이닝같은거 할때. (요즘은 if로 null 체크하는 경우가 많다는 의견)

val nickname = name?.let {} ?: "empty"
val nickname = if ( name != null ) ?? else "emtpy"

위의 코드는 name이 null일때 empty를 반환하는 것이다.

name이 null일 수 있고, let 안에 있는 블럭이 null일 수 있으므로 널체크 용으로 let을 잘 쓰지 않는다.

 

also : 액션을 취하는데, 중간에 체크하고 확인해야할때. 잘 사용 엑스,,  (호출인자를 그대로 전달하는것. 호출인자를 변경하지 않을때 사용)

 

SAM(Single Abstract Method) 인터페이스

하나의 추상 메서드만 있는 인터페이스를 보다 단순하게 처리하기 위해 함수를 정의하는 fun 예약어를 인터페이스 앞에 붙여 SAM 인터페이스를 만든다. 

이 SAM 인터페이스는 재정의를 상속 없이 직접 람다식으로 받아서 처리할 수 있다.

fun interface StringSAMable { // sam은 인터페이스 앞에 fun을 붙인다
    fun accept(s: String): Unit // 추상 메서드 한 개만 가짐
}

val consume1 = StringSAMable{s -> println(s)} // sam에 재정의 함수를 람다표현식으로 전달
consume1.accept("바로 람다표현식을 전달해서 재정의")

// 바로 람다표현식을 전달해서 재정의

-> 가독성 차이 (원래는 object 로 인터페이스 구현하는 형식인데, 간략하게 사용가능하다)

 

4. 인라인 함수와 인라인 속성 알아보기

인라인 함수와 인라인 속성

인라인을 사용하면 이를 호출하는 곳에 컴파일러가 인라인으로 지정된 함수나 속성을 삽입해서 처리한다.

프로그램이 커지면 메모리를 효율적으로 사용해야하므로 너무 많은 함수를 호출하지 않고 이를 코드로 삽입하는 경우가 더 좋을 수 있다.

이때 인라인 함수를 사용한다.

 

inline fun compose_(
    a: Int,
    action: (Int) -> Int, // 고차함수를 인라인 함수로 처리
    block: (Int) -> Int // 두 개의 함수를 매개변수
): Int {
    return action(a) + block(a)
}

fun callingHOF() { // 인라인 함수를 내부에서 호출
    println(compose_(10, { x -> x * 10 }, { y -> y + 10 })) // 두 개의 람다표현식 전달
}

callingHOF() // 120

 

-> 인라인 함수 쓰는 케이스

: 콜 스택을 줄이기위해서, 긴 함수에 인라인 사용하려 그러면 intelliJ에서 사용하지 말라고 멘트가 나옴

보통 reified 키워드 사용해서 제네릭의 타입을 얻어올때

 

노인라인 처리하기

함수에 인자로 전달된 람다표현식 등을 인라인으로 호출한 곳에 코드를 삽입하지 않으려면 예약어 noinline을 지정해야 한다.

inline fun highNoinline(
    block: () -> Unit,
    noinline noinline: () -> Unit,
    block2: () -> Unit
) {
    block() // 인라인 처리
    noinline() // 노인라인 처리
    block2() // 인라인 처리
}

fun callingFunction() {
    highNoinline({ println("람다표현식 1") },
        { println("노인라인 람다표현식 2") },
        { println("람다표현식 3") })
}

callingFunction()
// 람다표현식 1
// 노인라인 람다표현식 2
// 람다표현식 3

noinline을 지정한 매개변수는 코드 삽입이 안된다.

크로스인라인 처리하기

인라인 처리된 함수는 호출되는 곳에 코드를 삽입한다. 따라서 람다표현식으로 전달할 때도 return 문을 사용할 수 있다. <- 비지역 반환

이런 비지역 반환이 실제 코드에 지정될 때 문제가 발생할 수 있다. 

  • 코틀린에서는 익명 함수를 종료하기 위해 return을 사용할 수 있으며 이때 특정 반환값 없이 return만 사용해야 함.
  • 람다식을 인자로 넘겨줄 때 람다식 내에서 return으로 반환하면, 람다식만 종료되는 것이 아니라 람다식을 사용한 함수까지 종료되는데, 이를 비지역반환이라고 한다.

이때 매개변수에 crossinline 예약어를 붙여 코드 삽입은 가능하지만 비지역 반환을 못하게 만들 수 있다.

 

inline fun higherOrderFunc(crossinline aLambda: () -> Unit) { // 실제 지역반환 처리를 금지시킨다
    normalFunc { // 다른 함수에서 람다표현식 실행
        aLambda()
    }
}

fun normalFunc(block: () -> Unit) {
    println("정상함수 호출 111")
    block()
}

fun callingFunc() {
    higherOrderFunc { // 고차함수 호출
        println("람다함수 호출 222")
        // return // 비지역 반환 금지
    }
}

callingFunc() 
// 정상함수 호출 111
// 람다함수 호출 222

 

변수의 스코프를 얼마나 가져갈거냐에 대한 것을 생각하며 코드를 짜야한다.

클래스 내부에서 사용하는 전역변수 : 클래스의 프로퍼티 -> 다른 함수에서 value가 달라지면 버그가 많이 생길 수 있다.

변수의 라이프사이클을 제한해야 한다. (함수 내부에서만 살아있는 건지, 클래스 인스턴스가 살아있을때 살아있는건지, 어플리케이션 단에서 살아 있는건지 생각해야 한다.)

 

--> 함수에 전달하는 인자에 따라서 반환값을 바뀌게 해라.

 

메모리 사용 측정 

- 안드로이드 프로파일러 : app inspect -> cpu, 네트워크, 메모리 얼마나 쓰는지 확인 가능

- 성능적으로 오래걸리는 것을 찾아볼 수 있다.

 

일급객체가 더 높은 개념, 고차함수가 일급객체처럼 사용할 수 있다. (고차함수가 그 개념을 따라가고 있다.)