스터디파이 코틀린 입문 스터디 (https://studypie.co/ko/course/kotlin_beginner) 관련 자료입니다.
코틀린 입문반은 Kotlin을 직접 개발한 개발자가 진행하는 Coursera 강좌인 “Kotlin for Java Developers” (https://www.coursera.org/learn/kotlin-for-java-developers) 를 기반으로 진행되며 아래는 본 강좌 요약 및 관련 추가 자료 정리입니다.
목차
(9) 실습 : Mastermind in a functional style, Nice String, Taxi Park
1. Lambdas
- Lambda는 인자(argument)를 반환하는 익명함수 (an anonymous function that can be used as an expression)입니다. (관련 링크 : 람다식 — 나무위키)
- 익명함수란 함수명이 없는 함수로
fun(x:Int):Int = x+1
형태로 직접 작성할 수 도 있고, Lambda 형태로 작성할 수 도 있습니다. (관련 링크 : 익명함수와 선언적함수의 차이, 코틀린 익명함수) Lambda를 통해 보다 간결한 문법을 사용할 수 있습니다. - Lambda를 통해 Collection을 함수형 스타일로 코딩할 수 있습니다. (관련 링크 : 함수형 프로그래밍 소개)
- Lambda를 사용할 때 아래와 같이 코드를 간소활 수 있습니다.
//1. 기본 형태 (Lambda expression을 mapValues 확장함수의 인자로 받음)pairList.filter({ i:Employee? -> i?.city == "PRAGUE" })//2. Lambda를 괄호 뒤에 씀
employees.filter() { i:Employee? -> i?.city == "PRAGUE" }//3. 괄호가 비는 경우 괄호 생략 (Groovy 에서 고안하여 검증되었고 차용한 아이디어)
employees.filter { i:Employee? -> i?.city == "PRAGUE" }//4. Lambda의 인자 타입 생략
employees.filter { i -> i?.city == "PRAGUE" }//5. Lambda의 인자가 1개인 경우 인자 생략하고 it으로 처리
employees.filter { it?.city == "PRAGUE" }
- Lambda가 인자로 Map.Entry나 Pair를 받는 경우 아래와 같이 코드를 간소화할 수 있습니다.
//1. 기본 형태
map.mapValues { entry -> "${entry.value}!"}//2. Destructuring syntax (인자를 Pair 형태로 명시하여 바로 value 접근 가능)
map.mapValues { (key, value) -> "$value!"}//3. 사용되지 않는 인자를 "_" 처리하여 사용되지 않음을 명시 & 네이밍 고민안해도 됨
map.mapValues { (_, value) -> "$value!" }
2. Common Operations on a collections
- Kotlin 라이브러리는 Java Collections에 대한 다양한 operation 확장함수를 제공합니다.
filter / partition / count
- filter : Lambda로 정의된 predicate를 만족하는 원소들로 구성된 Collection을 반환하며 만족하는 원소가 없다면 null을 반환.
- partition : Lambda로 정의된 predicate를 만족하는 원소들과, 만족하지 않는 원소들로 나누어 Pair of Collectoins를 반환. 상황에 따라 만족하지 않는 원소들도 필요한 경우 filter 대신 partition 사용.
- count: Lambda로 정의된 predicate를 만족하는 원소 개수를 반환.
- predicate : boolean test(T) 메소드가 정의된 인터페이스로 T 타입 데이터에 대한 특정 조건 부합 여부를 확인하여 bool을 반환 (관련 링크 : 람다표현식과 Predicate Functional Interface를 사용하여 코드량 확줄이기)
map / flatMap
- map : Collection의 모든 원소 각각에 대해 Lambda로 정의된 transform을 통해 변경된 원소들로 구성된 Collection을 반환. 반환되는 Collection의 원소개수는 대상 Collection의 원소개수와 동일.
- flatMap : map의 transform이 List를 반환하는 경우, map operation 후에 flatten (list of list를 flat하게 바꿈) 처리를 하여 반환
any / all / none
- any : Lambda로 정의된 predicate를 만족하는 원소가 1개 이상 존재하는지 여부(Boolean) 반환.
- all : Lambda로 정의된 predicate를 모든 원소가 만족하는지 여부(Boolean) 반환.
- none : Lambda로 정의된 predicate를 모든 원소가 만족하지않는지 여부(Boolean) 반환.
find / firstOrNull / first / lastOrNul / last
- find / firstOrNull : Collection의 원소들 중에서 Lambda로 정의된 predicate를 만족하는 가장 첫번째 원소를 반환. 만족하는게 없는 경우 null 반환
- first : find와 동일한 기능을 하나 만족하는게 없는 경우 NoSuchElementException 발생. 대신 first의 반환값은 non-nullable임이 보장되어 safe access, non-null assertion 등 불필요. Lambda(predicate)가 없는 경우 Collection의 첫번째 원소 반환.
- lastOrNull : Collection의 원소들 중에서 Lambda로 정의된 predicate를 만족하는 가장 마지막 원소를 반환. 만족하는게 없는 경우 null 반환
- last : lastOrNull과 동일한 기능을 하나 만족하는게 없는 경우 NoSuchElementException 발생. 대신 last의 반환값은 non-nullable임이 보장되면 Lambda(predicate)가 없는 경우 Collectoin의 마지막 원소 반환
groupBy / associateBy
- groupBy : Lambda로 정의된 argument를 기준으로 Collection의 원소들을 그룹화 하여 나눈 기준과 기준에 따라 분류된 Collection으로 구성된 Map 반환.
- associateBy : groupBy와 유사하나 나눈 기준 마다 1개의 원소만을 반환. 그룹별 원소가 unique하다고 판단될 경우 Collection 없이 반환되므로 편리하나, unique 하지 않을 경우 가장 마지막 원소를 제외하고 지워짐.
Zip
- 2개의 Collection에 대해 같은 index의 원소들끼리 Pair로 만든 Collection 반환.
collection1.zip(collection2)
형태로 사용. - 2개의 Collection 중 원소 개수가 적은 쪽을 기준으로 하여 많은 쪽 Collection의 뒷 원소들은 버려짐.
maxBy / minBy
- maxBy : Lambda로 정의된 argument를 기준으로 비교하여 가장 큰 값을 가지는 원소를 반환.
- minBy : Lambda로 정의된 argument를 기준으로 비교하여 가장 작은 값을 가지는 원소를 반환
- 공통적으로 해당 기준의 max/min이 2개 이상일 경우 앞쪽 index가 반환되며, Collection이 비어있는 경우 null 반환.
distinct
- Collection에서 중복되는 원소를 제거하고 unique한 원소들만으로 구성된 Collection을 반환.
3. Operations Quiz — I / Operations Quiz — II
- Map에서 key에 해당되는 value를 조회할 때
map["key"]
는 해당 key가 없는 경우 null을 반환하며,map.getValue("key")
는 해당 key가 없는 경우 NoSuchElementException을 반환하고,map.getOrElse("key") { }
는 해당 key가 없는 경우 lambda로 정의된 argument를 디폴트값으로 하여 반환합니다. (lambda 호출과 연산은 key가 없는 경우에만 진행되므로 불필요한 연산을 줄일 수 있습니다.) - Lambda 속에 Lambda가 중첩되어 사용되거나, 여러 Collection operation 확장함수를 연달아 사용하여 Lambda가 여러번 사용될 때, it은 의미를 혼동하게할 가능성이 있으므로 인자를 명시적으로 네미밍하는 것을 장려합니다.
- Collection operation은 매우 다양하며 동일한 연산을 위해 가장 간단하고 명확한 operation을 선택해야 합니다. 아래 2개 코드는 사실상 동일한 연산이지만 후자가 보다 명료합니다.
/**
* 1번.
* Collection의 원소들을 2개씩 Pair로 묶은 모든 가능한 조합에 대해
* 두 원소의 age의 차이가 가장 큰 Pair를 찾음
*/
val allPossiblePairs = heroes
.flatMap { firstHero ->
heroes.map{
secondHero -> firstHero to secondHero
}
}
val (oldest, youngest) = allPossiblePairs
.maxBy { it.first.age - it.second.age }!!
println(oldest.name)/**
* 2번.
* 1번은 사실상 가장 age가 큰 원소와 가장 age가 작은 원소를 찾는 것과 동일
*/
val oldestHero = heroes.maxBy { it.age }
val youngestHero = heroes.minBy { it.age }
println(oldestHero?.name)
4. Function Types
- Lambda, 명시적인 익명함수 정의, 그리고 다음 강의(5. Member Reference)에서 배울 함수의 reference를 통해 함수를 Variable에 저장할 수 있으며 이 때 Function Type이 정의됩니다. 변수 타입 추론과 마찬가지로 타입 추론도 가능합니다.
val isEven : (Int) -> Boolean = { i : Int -> i % 2 == 0 }
val isEven = { i : Int -> i % 2 == 0 } //타입 추론
isEven(5) // 함수를 변수에 저장하는 경우 변수를 함수처럼 사용할 수 있음
- Function Type 역시 non-nullable/nullable 이 존재합니다. Nullable인 경우 null이 할당될 수 있으며, null 여부를 명시적으로 확인하거나 safe access(
?
)를 통해 사용해야합니다.
val f : (() -> Int)? = null //nullable function type으로 null 할당 가능
if (f != null) { f() } //null 여부를 명시적으로 확인하여 smart cast한뒤 사용
f?.invoke()?.compareTo(5) //safe access 방식으로 사용
- lambda를 정의하며 바로 호출할 수 있는데 이 경우 사용됨을 명시하기 위해
run
을 사용할 수 있습니다. (run
함수는 추후 Lambda with receiver 파트에서 보다 자세히 다룹니다.)
{ println("hey") }()
run { println("hey") } //위 코드와 동일하게 동작하며 보다 명시적
5. Member References
- 앞서 Collection operation에 대해 lambda를 사용한 이유는 operation이 함수 (ex: predicate, 연산을 거친 argument)를 인자로 사용하기 때문입니다. 한편, Kotlin에서 선언적인 function을 variable에 저장하고 인자로 사용할 수 없지만 lambda 외에 function reference는 variable에 저장하고 인자로 사용할 수 있습니다.
fun isEven(i:Int):Boolean = i % 2 == 0
val predicate = isEven() // fun을 통해 선언한 함수는 변수에 저장 못하며 오류발생
val predicate2 = ::isEven // function reference는 변수에 저장 가능
val predicate3 = {i:Int -> isEven(i)} // lambda 형태로 변수에 저장 가능
- 이미 선언된 함수를 인자로 사용하고자 하는 경우 lambda보다 function reference 방식이 보다 코드가 간소화됩니다.
//Android 코드 예시 (정의된 클릭 리스너 함수를 버튼 어댑터에 인자로 넣어야 하는 경우)// 1. lambda 방식
val buttonAdapter = ButtonAdapter(buttons, {button: Button -> buttonClicked(button) })// 2. function reference 방식
val buttonAdapter = ButtonAdapter(buttons, ::buttonClicked)
- Class에 memeber function이 있고 이에 대한 member function reference를 변수에 저장하고자 할 때, Class로 생성한 특정 객체에 대한 member reference를 저장하면 bound reference라 하고, 특정 객체가 아닌 Class에 대한 memeber reference 혹은 top-level function의 reference는 non-bound reference라고 합니다.
class Person(val name:String, val age:Int){
fun isOlder(ageLimit: Int) = age > ageLimit
val jeff = Person("Jeff", 35)// 1. bound reference
val agePredicate = jeff::isOlder
println(agePredicate(21))// 2. non-bound reference (변수에 저장된 함수 호출시 객체를 인자로 넣어야 함)
val agePredicate2 = Person::isOlder
println(agePredicate2(jeff, 21))
6. Return from Lambda
- Kotlin에서 return은 기본적으로 fun 키워드로 명시된 곳에 대응됩니다. 한편, lambda는 fun 키워드로 선언되지 않으므로 lambda내부에 return을 사용할 때 주의해야 합니다. labeling 또는 return을 사용하지 않는 방법이 있고, local function을 정의하거나, 명시적인 익명함수를 사용하는 방법이 있습니다.
/**
* 1. 아래 코드에서 원소가 0인 경우 해당 원소에 대한 transform 결과물이 []가 되는
* 것이 아니라 duplicateNonZero의 반환값이 []가 되어 버림
*/fun duplicateNonZero(list:List<Int>):List<Int>{
return list.flatMap {
if(it==0) return listOf()
listOf(it, it)
}
}// 2. lambda에 l@, @l 라벨링을 통해 return 이 대응되어야 할 지점 설정
fun duplicateNonZeroLabel(list:List<Int>):List<Int>{
return list.flatMap l@{
if(it==0) return@l listOf()
listOf(it, it)
}
}// 3. lambda에 return을 사용하지 않음
fun duplicateNonZeroNoReturn(list:List<Int>):List<Int>{
return list.flatMap {
if(it==0) listOf()
else listOf(it, it)
}
}// 4. local function (fun 키워드 선언)과 function reference를 사용
fun duplicateNonZeroLocalFunction(list:List<Int>):List<Int>{
fun duplicateNonZeroElement(e: Int): List<Int>{
if(e==0) return listOf()
return listOf(e, e)
}
return list.flatMap(::duplicateNonZeroElement)
}// 5. anonymous function을 정의 (fun 키워드 선언)
fun duplicateNonZeroAnonymousFunction(list:List<Int>):List<Int>{
return list.flatMap(fun (e): List<Int>{
if(e==0) return listOf()
return listOf(e,e)
})
}
- lambda와 명시적인 익명함수는 bytecode level에서는 동일하나 return에 대한 대응 지점만 달라집니다. (Kotlin이 return을 fun 키워드 기준으로 대응시키는 규칙 때문)
7. Is Kotlin a functional language
- Kotlin은 Haskell처럼 순수한 functional language는 아니며, 최신 언어들은 OOP, Structural, Functional Programming 등 다양한 패러다임의 아이디어를 조합하여 만들어졌습니다. Kotlin의 class와 interface 부분은 OOP 아이디어 기반이며, lambda, immutability, high-order function , immutability부분은 FP 아이디어 기반입니다.
- Kotlin의 FP 아이디어 기반의 특징들을 잘 활용하면 실무적 관점에서 함수형 스타일로 코딩할 수 있으며 이는 충분한 이점을 제공합니다.