본문 바로가기

Programming/Kotlin

[Kotlin]코틀린을 이용한 안드로이드 프로그래밍 실습 03

<퀴즈 >

열거형 CIRCLE, TRIANGLE, RECT, POLYGON 을 만들고

탑레벨의 열거형 데이터 5개를 가지는 immutable 리스트(initDataList란 이름의 리스트) 1개를 생성하고, 

draw 와 printInfo 라는 인터페이스 메소드를 가진 IShape 인터페이스를 만들고, IShape 을 상속한 Circle, Triangle, Rect, Polygon를 x, y, w, h 를 가지는 데이터 클래스로 만들고 인터페이스 메소드를 오버로딩한다. 

해당 클래스는 생성자를 private 으로 하고 동반객체를 통해 팩토리 메소드 제공한다.

오버로딩하는 메소드에서는 어느클래스에서 어떤 함수가 호출되었는지를 출력하게 한다.

initDataList 에 들어있는 열거형 타입에 맞는 클래스를 생성해서 도형 리스트(shapeList)에 넣는다. (when  을 사용한다)


shapeList를 반복하면서 draw 와 printInfo 를 호출한다. (이때 서로 다른 객체이지만, 하나의 코드로 동작해야한다)


# Kotlin - 함수형 프로그래밍

- 부작용(공용 변수에 따라 in/output이 다를 수 있는 것)을 없애기 위한 (위해 노력하는) 프로그래밍

 * 순수함수를 지향 : 부작용이 없고 입력이 같으면 항상 출력이 보장되는 함수

 * 순수함수의 조합으로 프로그래밍 지향

 * mutable 데이터를 지양하고 immutable 지향

 * 1급객체인 함수를 이용, 고차함수를 통한 재사용성 지향

- 공유 상태(Shared state)와 부작용(Side effects) 대신 순수 함수(Pure function) 사용

- 변경 가능한 데이터보다는 불변성(Immutability)

- 명령형(Imperative) 흐름 제어보다는 합성 함수(Function composition) 사용

- 많은 데이터 유형에 대해 작업할 수 있도록 고차 함수(Higher order functions)를 사용

- 명령적(Imperative)인 코드보다는 선언적으로(Declarative, 어떻게 하는지보다는 무엇을 해야하는지)

- 구문(statement)보다는 표현식(expression)을 사용

- ad-hoc polymorphism(가장 단순한 형태의 다형성)보다는 컨테이너와 고차 함수를 사용


# Kotlin - 1급 객체

- Kotlin의 함수는 1급 객체 함수가 데이터처럼 사용

 * 1급 객체 조건 : 변수나 데이터에 할당 가능, 인자로 사용 가능, 리턴값으로 사용 가능

 * 함수 타입 필요

(paramType1, paramType2, … , paramTypeN) -> returnType

 - 함수를 1급객체로 다룰려면 함수타입이 있어야함

 - 함수 타입은 변수, 파라메터, 리턴 모두 사용할 수 있음.

 - typealias 으로 정의할 수 있음

typealias mytype = (Int, Int)->Int


// Int 하나를 인자로 받고 Boolean 리턴하는 함수타입
var l5: ((Int) -> Boolean)? = null
l5 = { it > 0 }
println("람다5 : ${l5(2)}, ${l5.invoke(2)}")

// 인자 없고 String 리턴하는 함수타입
var l6: (() -> String)? = null
l6 = { "인자 없고 String 리턴" }
println("람다6 : ${l6()}, ${l6.invoke()}")

var l7: (Int, Int) -> Int = { x, y -> x * y }
println("람다7 : ${l7(1, 2)}, ${l7.invoke(3, 4)}")

var l8: (Int, Int) -> Unit = { x, y -> println("l8 : $x, $y") }
println("람다8 : ${l7(1, 2)}, ${l8.invoke(3, 4)}")

var l9: ((Int, Int) -> Int, Int, Int, Int) -> Int = { x, y, z, k -> x(x(y, z), k) }
println("람다9 : ${l9({ x, y -> println("$this : $x, $y"); if (x > y) x else y }, 1, 3, 2)}")

// () -> (...) 인자가 없는 함수
// x()(x,y) : x, y 둘중에 큰 값 리턴
var l10: (() -> (Int, Int) -> Int, Int, Int, Int) -> Int = { x, y, z, k -> x()(x()(y, z), k) }
println("람다10 : ${l10({ { x, y -> println("$this : $x, $y"); if (x > y) x else y } }, 1, 3, 2)}")

- 출력

람다5 : true, true

람다6 : 인자 없고 String 리턴, 인자 없고 String 리턴

람다7 : 2, 12

l8 : 3, 4

람다8 : 2, kotlin.Unit

com.inu.kotlinlecture1.Lecture3@368102c8 : 1, 3

com.inu.kotlinlecture1.Lecture3@368102c8 : 3, 2

람다9 : 3

com.inu.kotlinlecture1.Lecture3@368102c8 : 1, 3

com.inu.kotlinlecture1.Lecture3@368102c8 : 3, 2

람다10 : 3



# Kotlin - 고차함수(Higher order function)

- 함수를 인자로 받거나 함수를 리턴하는 함수

 * 이전엔 함수 포인터, 익명 객체로 처리

 * 함수가 1급 객체이기 때문에 고차 함수 작성 가능

- 다른 함수를 이용해서 새로운 함수를 조립하는 방법으로 프로그램 가능 ex) 합성함수



# Kotlin - Lambda 함수

- 이름없는 함수(익명함수)를 표현식으로 기술한 것

- 함수를 선언하지 않고 곧바로 식으로 전달돼서 표현

  * 함수가 1급객체이기 때문에 람다함수도 변수에 할당하거나 파라메터, 리턴으로 사용 가능

- 중괄호 { } 로 시작하고 끝난다.

{ p1:type, p2:type -> statement1; statement2 }

  * 람다 body 가 여러 표현식이면 ; 으로 구분

  * 리턴값은 return 을 하지 않고, 마지막 표현식의 값이 리턴. 값이 없으면 void 가 아닌 Unit 타입

  * 표현식에서 생략 가능한것은 생략 가능

    + 타입을 유추할 수 있고, 파라메터가 1개면 생략 가능  (생략된 파라미터는 it 으로 참조 가능)

   + 파라메터가 (생략되어서라도) 없으면 -> 연산자 생략가능

  * 함수의 마지막 인자가 람다라면 () 에서 빼서 밖에서 표현가능

   + 인자가 하나면 () 생략 가능

- 클로저 생성(외부 변수 사용 가능)

- 실제로는 내부적으로 별개의 익명 클래스로 생성되어 처리된다.

  * 실제 함수 호출은 익명 클래스의 Invoke 함수 (코틀린에서는() 연산자) 에 의해 호출된다.  

- 장점으로 코드를 간결하게 만들 수 있는 여지가 있지만 디버깅이 어려울 수 있다.

fun f1(a: Int, b: Int): Int {
return a + b
}
println("일반 함수 : ${f1(1, 2)}")

fun f2(a: Int, b: Int) = a + b
println("표현식 함수 : ${f2(1, 2)}")

val f3: (Int, Int) -> Int = fun(x, y) = x + y
println("익명 함수 : ${f3(1, 2)}, $f3")

val f4 = fun(x: Int, y: Int) = x + y
println("익명 함수2 : ${f4(1, 2)}, $f4")

var l1 = { a: Int, b: Int -> println("l1");a + b }
var l1_1 = { a: Int, b: Int -> a + b; println("l1_1") }
println("람다1: ${l1(1, 2)}")
println("람다1 마지막 식의 값 : ${l1_1(1, 2)}")

var l2: (Int) -> Boolean = { it > 0 }
println("람다2 : ${l2(2)}, ${l2.invoke(2)}")

// 파라미터 없고, 리턴은 Unit
var l3 = { println("l3") }
println("람다3 : $l3 : ${l3()}")

val tmp = 10
var l4 = { println("$tmp") }
println("람다4 : $l4 : ${l4()}")

// 마지막 파라미터가 람다식이면 밖으로 뺄 수 있음
fun f5(a: Int, b: (Int, Int) -> Int): Int = b(a, a)
var f5r = f5(3) { x, y -> x * y } // var f5r = f5(3, { x, y -> x * y })
println("람다11: : $f5r")

fun f6(b: (Int, Int) -> Int): Int = b(5, 5)
var f6r = f6 { x, y -> x * y }
println("람다12: $f6r")

- 출력

일반 함수 : 3

표현식 함수 : 3

익명 함수 : 3, Function2<java.lang.Integer, java.lang.Integer, java.lang.Integer>

익명 함수2 : 3, Function2<java.lang.Integer, java.lang.Integer, java.lang.Integer>

l1

람다1: 3

l1_1

람다1 마지막 식의 값 : kotlin.Unit

람다2 : true, true

l3

람다3 : Function0<kotlin.Unit> : kotlin.Unit

10

람다4 : Function0<kotlin.Unit> : kotlin.Unit

람다11: : 9

람다12: 25



# Kotlin - Closure(클로저)

- 람다식이나 익명함수의 경우 함수외부 범위에서 선언된 변수에 접근할 수 있다.

- Java는 final로 선언된 변수만 접근할 수 있지만 코틀린은 모두 접근 가능하다.

- 캡쳐된 변수는 수정이 가능하다.

var tmp = 10
var l4 = {println("$tmp")}
println("람다4 : $l4 : ${l4()}")

var l4_2 = {tmp += 1; tmp}
println("람다4_1 : ${l4_2()}")
println("람다4 다시 : ${l4()}")

- 출력

10

람다4 : Function0<kotlin.Unit> : kotlin.Unit

람다4_1 : 11

11

람다4 다시 : kotlin.Unit



# Kotlin - it, _

- it : 단일 매개변수의 암시적 이름

  * 람다식에서 인자가 하나일 경우 생략 가능

  * 그래서 생략되었을 경우에 it 키워드로 참조

- _(언더바) : 사용되지 않는 변수. 파라미터를 비우고 함수를 호출하고 싶을 때

  ex) map.forEach { _, value -> println("$value") } // key를 _로 아무것도 안넘김


# Kotlin - inline

- 고차 함수를 이용할때 익명함수 -> 익명클래스 생성과 같은 런타임 오버헤드가 발생할 때, 이를 줄이는 방법

- 전달된 함수 파라메터가 inline 된다.

- 고차 함수 선언 앞에 inline 키워드 사용

- 특정 함수 파라메터를 inline 시키지 않으려면 파라메터 앞에 noinline 키워드 사용

- Inline 이 불가능한 경우는 무시

- 전달된 함수 파라메터를 가공(저장) 하려면 오류

- Inline이 되면 람다에서 return 사용 가능 : 람다함수 리턴이 아니고 outer 함수(고차함수. 그 람다함수를 호출한 외부 함수) 리턴이 됨. (non-local return). local return을 위해 라벨(return@label), 익명함수 가능

- 그냥 간단하게 컴파일시에 inline 키워드가 붙은 함수들은 모두 복붙느낌. 람다식을 그 함수 안에 코드를 넣어버림

- 단점은 컴파일 시간이 오래걸린다는 것(여러 함수에서 동일한 함수를 inline 했을 경우에 중복되는 부분이 많아지므로)

- 하지만 성능이 더 좋아짐(빨라지겠지?)

data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice",29), Person("Bob", 31))

fun lookForAlice(people: List<Person>){
people.forEach{
if(it.name == "Alice"){
println("Found!")
return
}
println("if문 바깥")
}
println("Alice is not found!")
}

fun lookForAlice2(people: List<Person>){
people.forEach{
if(it.name == "Alice"){
println("Found!")
return@forEach
}
println("if문 바깥")
}
println("Alice is not found!")
}

fun lookForAlice3(people: List<Person>){
people.forEach(fun (person){
if(person.name == "Alice"){
println("Found!")
return
}
println("if문 바깥")
})
println("Alice is not found!")
}

lookForAlice(people)
lookForAlice2(people)
lookForAlice3(people)

- 출력

Found!

Found!

if문 바깥

Alice is not found!

Found!

if문 바깥

Alice is not found!

- 좀더 자세히 공부 필요. 인라인 개념을 잘 모르겠군



# Kotlin - SAM(Single Abstract Method)

- 추상 메소드가 하나만 있는 인터페이스를 파라메터로 받을때 람다로 표현할 수 있게 변환

- 선언부(interface, setOnClickListener)가 Java에 있고 코틀린에서 setOnClickListener을 호출하였을 때만 SAM변환이 동작

- 코틀린에서 선언부가 있을때는 객체 표현 표현식으로 처리해야함. 

- 코틀린에서 자바의 functional interface를 호출시 람다식으로 바로 표현할 수 있다.
- 내부적으로 람다식은 익명클래스로 치환된다.
- lambda capturing이 발생하지 않는다면, 익명클래스는 한번만 생성되어 재사용된다.
- lambda capturing이 발생하면, 익명클래스는 매번 생성되어 사용된다.
class JavaSAMSample {

static SAMInterface ss = null;

public static void setSAM(SAMInterface sam) {

ss = sam;
}

public static void doFire(int pos) {
if (ss != null)
ss.onClick(pos);
else
System.out.println("ss is null");
}

interface SAMInterface {
void onClick(int position);
}
}
fun T03() {
JavaSAMSample.setSAM { println("sam-onclick : $it") }
JavaSAMSample.doFire(100)
}

- setOnClickListener(new ...) 가 위와 같이 단순화됨



# Kotlin - Stream

- Collection 의 멤버함수 대부분은 고차함수. 대표적으로 filter, map, reduce, all, any, count, find, flatMap 등이 있음

 * filter : 조건에 맞는 컬렉션을 뽑아 리스트를 만들어 주는 함수. Ex) 짝수만 걸러내기 : 짝수를 판단하는 람다를 필터에 인자로 넣어주면 리턴값으로 2,4,6,8,… 리스트가 리턴됨

 *  map : filter는 골라주기만 하지만 map은 변환까지 리턴시켜줌. 각 요소를 변환시켜 새로운 타입의 리스트를 만드는 함수. 변환하는 방법은 람다로 받음

 * reduce : 조건에 맞게 합쳐주는 함수. 각 요소를 더하게끔 람다를 만들어 주면 리턴값은 요소, 각 리스트를 더하는 것을 람다로 넘기면 된다.

-  필터로 걸러진 값들은 같지만 실제로 객체는 다르다(주소가 다르겠지?). 이때 객체 생성/소멸이 계속 이뤄지기 때문에 오버헤드가 발생

- 그래서 중간 과정을 새로운 객체 생성 없이 진행할 수 없을까 라는 생각에서 stream이 만들어졌고, 그래서 중간에 객체를 생성하는 오버헤드를 줄일 수 있게 되었다.

- 연속해서 사용할 때 분리되어 실행. 

- Stream 을 만들어 연속 실행하면 연속되어 실행 

 * 중간값을 생성하는 부하 감소

 * 인라인되지 않음

   + 인라인 vs 중간값부하 (???? 설명 못들었음)

- 자바의 stream 와 다르게 병렬 (parallel) 지원 안함(아직은…)

 * - PararellStream 은 스레드로 병행 처리 해주지만(Java 8) 코틀린은 아직 안됨. 일반 스트림만 됨

- 종단함수를 호출해야 스트림이 실행되며, 종단함수 호출 이후에는 스트림이 종료됨(Stream close)

 * 종단함수 : forEach, reduce

var list = listOf(1,2,3,4,5,6,7,8,9,0)
var list2 = list.filter { println("filter"); (it % 2 ) == 0 }.map { println("map"); it * 100 }
list2.forEach { println(it) }

var list3 = list.stream().filter { println("filter"); (it % 2 ) == 0 }.map { println("map"); it * 100 }
list3.forEach { println(it) }
// list3.forEach { println(it) } // 런타임 에러. Stream operation 실행 불가. 스트림이 닫혔습니다.
var avg = list.asSequence().filter { println("filter"); (it % 2 ) == 0 }.map { println("map"); it * 100 }.reduce { acc, i -> acc + i }
println(avg)

- 출력값 귀찮



# Kotlin - Standard function : run

- 인자가 없고 리턴은 있는 확장 멤버함수 람다를 인자로 받음

- 람다가 리턴하는 값을 리턴함

- 람다를 실행하여 결과를 리턴함

  * 람다는 마지막 식이 결과임

  * 멤버함수이기 때문에 this 가 호출한 객체임

- 제너릭으로 모든타입에 대해 확장함수로 구현

public inline fun <T, R> T.run(f: T.() -> R): R = f()
// <T, R> : 타입 파라미터(제너릭)
// T 타입의 run 이라는 함수(확장함수)! 인자가 없는 리턴 R인 함수! 그 함수의 리턴을 F()가 리턴하는 값으로 한다. (T에 속하는 파라미터를 받는다.) f()를 T의 멤버함수처럼 쓸 수 있다 (T타입의 this 쓸 수 있다)
val user = User("importre").run {
email = "importre@example.com"
profile = "http://path/to"
this // 얘가 리턴
}
println("run : $user")

/*

User 타입의 멤버함수를 파라미터로 받을 것인데, 그 함수는 파라미터가 없는 User의 멤버함수를 파라미터로 받는 run 함수.

리턴은 user

User.email

User.profile 도 같음.

초기화할 때 run이 없었으면 user. user. 붙여야함!

*/



# Kotlin - Standard function : let

- 인자가 자기자신 타입 한 개를 넘기고 리턴이 있는 람다를 인자로 받음

- 람다가 리턴하는 타입을 리턴함

- 람다를 this 인자로 실행하여 결과를 리턴함

  * this 인자를 넘기기 때문에 this 를 it 으로 사용할 수 있음

  * 람다는 마지막 식이 결과임

- 제너릭으로 모든타입에 대해 확장함수로 구현

- Safe call 로 if 검사 대신 많이 쓰임 obj?.let {} // obj가 null이 아니면 {} 모두 실행

public inline fun <T, R> T.let(f: (T) -> R): R = f(this)

val user2 = User("importre").let {
it.email = "importre@example.com"
it.profile = "http://path/to"
this
}
println("let : $user2")



# Kotlin - Standard function : apply

- 인자가 없고 리턴도 없는 확장 멤버함수 람다를 인자로 받음

- 자신의 타입을 리턴함

- 람다를 실행하고 this 를 리턴함

  * inline 이기 때문에 람다지만 return 가능  

  * 멤버함수이기 때문에 this 가 호출한 객체임

- 제너릭으로 모든타입에 대해 확장함수로 구현

public inline fun <T> T.apply(f: T.() -> Unit): T { f(); return this }
val user3 = User("importre").apply {
email = "importre@example.com"
profile = "http://path/to"
}

println("apply : $user3")


Functions

블록내 argument

블록내 return 

Function Type

비고

 run()

 this

 블록내 마지막 객체

 normal

 

 with(T)

 this

 Unit

 normal

 

 T.apply()

 this

 T

 extension

 

 T.run()

 this

 블록내 마지막 객체

 extension

 

 T.let()

 it

 블록내 마지막 객체

 extension

it을 블록 내에서 rename 가능함

 T.also()

 it

 T 

 extension

 



# Kotlin - Etc.

- Currying – 커링

* 다중인자를 받는 함수를 하나의 인자를 받는 함수의 합성으로 처리하는 방법

 * 인자를 고정시켜 제외한 함수를 만들 수 있다.

- Memoization – 메모이제이션

 * 고차함수를 이용한 함수형 기법으로 캐쉬기능 제공하는 기법



# 출처 : 모베란 백지훈 대표이사님