코틀린 입문 스터디 (8) Functional Programming

mook2_y2
16 min readFeb 26, 2019

--

스터디파이 코틀린 입문 스터디 (https://studypie.co/ko/course/kotlin_beginner) 관련 자료입니다.

코틀린 입문반은 Kotlin을 직접 개발한 개발자가 진행하는 Coursera 강좌인 “Kotlin for Java Developers” (https://www.coursera.org/learn/kotlin-for-java-developers) 를 기반으로 진행되며 아래는 본 강좌 요약 및 관련 추가 자료 정리입니다.

목차

(1) Introduction

(2) From Java to Kotlin

(3) Basics

(4) Control Structures

(5) Extensions

(6) 실습 : Mastermind game

(7) Nullability

(8) Functional Programming

(9) 실습 : Mastermind in a functional style, Nice String, Taxi Park

(10) Properties

(11) Object-oriented Programming

(12) Conventions

(13) 실습 : Rationals, Board

(14) Inline functions

(15) Sequences

(16) Lambda with Receiver

(17) Types

(18) 실습 : Game 2048 & Game of Fifteen

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 아이디어 기반의 특징들을 잘 활용하면 실무적 관점에서 함수형 스타일로 코딩할 수 있으며 이는 충분한 이점을 제공합니다.

--

--