코틀린 함수형 프로그래밍

de_sj_awa·2023년 4월 15일
0
post-custom-banner

1. 배열과 컬렉션

1) 배열

val array: Array<Int> = arrayOf(100, 200)

for (i in array.indices) {
	println("${i} ${array[i]}")
}

for ((idx, value) in array.withIndex()) {
	println("$idx $value")
}
    
array.plus(300)
  • Kotlin에서는 array.indices를 통해 0에서 마지막 index까지의 Range를 가져올 수 있다.
  • Kotlin에서는 withIndex()를 사용하면 인덱스와 값을 한번에 가져올 수 있다.
  • Kotlin에서는 plus()를 통해 Java에 비해 쉽게 배열에 값을 추가할 수 있다.

2) 코틀린에서의 Collection - List, Set, Map

  • Kotlin에서 컬렉션을 만들어 줄 때 불변인지, 가변인지를 설정해야 한다. (List, MutableList, Set, MutableSet, Map, MutableSet)
  • 가변(Mutable) 컬렉션 : 컬렉션에 element를 추가, 삭제할 수 있다.
  • 불변 컬렉션 : 컬렉션에 element를 추가, 삭제할 수 없다.
  • 불변 컬렉션을 만들기 위해서는 Collection을 만들자마자 Collections.unmodifiableList() 등을 붙여준다.
  • 불변 컬렉션이라 하더라도 Reference Type인 Element의 필드는 바꿀 수 있다.
  • Kotlin의 List의 기본 구현체는 Java의 ArrayList이고, Kotlin의 Set의 기본 구현체는 Java의 LinkedHashSet이고, Kotlin의 Map의 기본 구현체는 Java의 LinkedHashMap이다.
val numbers = listOf(100, 200)			// 불변 리스트
val emptyList = emptyList<Int>()		// 타입 추론이 되지 않는 경우 명시

numbers.get(0)
numbers[0]

for (number in numbers) {
	println(number)
}

for ((idx, value) in numbers.withIndex()) {
	println("${idx} ${value}")
}

val numbers2 = mutableListOf(100, 200)
numbers.add(300)

val oldMap = mutableMapOf<Int, String>()
oldMap[1] = "MONDAY"
oldMap[2] = "TUESDAY"

mapOf(1 to "MONDAY", 2 to "TUESDAY")

for (key in oldMap.keys) {
	println(key)
    println(oldMap[key])
}

for ((key, value) in oldMap.entries) {
	println(key)
    println(value)
}

3) 컬렉션의 null 가능성

  • List<Int?> : 리스트에 null이 들어갈 수 있지만, 리스트는 절대 null이 아님
  • List<Int>? : 리스트에는 null이 들어갈 수 없지만, 리스트는 null일 수 있음
  • List<Int?>? : 리스트에 null이 들어갈 수도 있고, 리스트가 null일 수도 있음

4) Java와 함께 사용할 때 주의점

  • Java에서는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않는다.
  • Java는 nullable 타입과 non-nullable 타입을 구분하지 않는다.
  • 결국 Kotlin 쪽의 컬렉션이 Java에서 호출되면 컬렉션 내용이 변할 수 있음을 감안해야 한다. 또는 코틀린 쪽에서 Collections.unmodifiableXXX()를 활용하면 변경 자체를 막을 수 있다.
  • Kotlin에서 Java 컬렉션을 가져다 사용할 때 플랫폼 타입을 신경써야 한다. 그래서 이런 경우에 Java 코드를 보면서 맥락을 확인하고 Java 코드를 가져오는 지점을 wrapping 해야 한다.

2. 다양한 함수

1) 확장함수

  • Kotlin은 Java와 100% 호환을 목표로 하고 있다. 그래서 기존 Java 코드 위에 자연스럽게 Kotlin 코드를 추가해서 Java로 만들어진 라이브러리를 유지 보수 및 확장하고자 했다. 그래서 어떤 클래스 안에 있는 메소드처럼 호출할 수 있지만 함수는 밖에 만들 수 있게 하고자 확장함수를 도입했다.
  • 확장함수 사용법은 클래스. 를 통해 생성하고, this를 통해 호출한 인스턴스에 접근 가능하다.
  • 확장함수를 사용하면 원래 클래스에 있는 멤버함수처럼 사용할 수 있다.
  • 확장함수는 클래스에 있는 private 또는 protected 멤버를 가져올 수 없다.
  • 멤버함수와 확장함수의 시그니처가 같다면 멤버함수가 우선적으로 호출된다.
  • 확장함수가 오버라이드 된다면 해당 변수의 현재 타입, 즉 정적인 타입에 의해 어떤 확장함수가 호출될지 결정된다.
fun main() {
	val str = "ABC"
    println(str.lastChar())
}

fun String.lastChar(): Char {
	return this[this.length - 1]
}
fun 확장하려는클래스(수신객체타입).함수이름(파라미터): 리턴타입 {
	// this(수신객체)를 이용해 실제 클래스 안의 값에 접근
}
  • 확장함수라는 개념은 확장프로퍼티와도 연결된다. 확장 프로퍼티의 원리는 확장함수 + custom getter와 동일하다.
fun String.lastChar(): Char {
	return this[this.length - ]
}

val String.lastChar: Char {
	get() = this[this.length - 1]

2) infix 함수(중위 함수)

  • 중위 함수는 함수를 호출하는 새로운 방법이다.
  • downTo, step도 함수이다. 변수와 argument가 각각 하나씩만 있는 경우에 변수.함수이름(argument) 대신 변수 함수이름 argument로 호출할 수 있다.
fun Int.add(other: Int): Int {
	return this + other
}

infix fun Int.add2(other: Int): Int {
	return this + other
}

3.add(4)
3.add2(4)
3 add2 4

3) inline 함수

  • inline 함수는 함수가 호출되는 대신, 함수를 호출한 지점에 함수 본문을 그대로 복붙하고 싶은 경우 사용된다.
  • inline 함수를 사용하면 함수를 파라미터로 전달할 때 오버헤드를 줄일 수 있다. 그러나 코틀린 라이브러리에서는 성능 최적화를 위해 적절하게 inline 함수가 이미 붙어있다.
fun main() {
	3.add(4)
}

inline fun Int.add(other: Int): Int {
	return this + other
}

4) 지역함수

  • 지역함수는 함수 안에 함수를 선언한 것이다.
  • 지역함수는 함수를 추출하면 좋을 것 같은데, 이 함수를 지금 함수 내에서만 사용하고 싶을 때 사용한다.
fun createPerson(firstName: String, lastName: String): Person {
	fun validate(name: String, fieldName: String) {
    	if (name.isEmpty()) {
        	throw IllegalArgumentException("${fieldName}은 비어 있을 수 없습니다! 현재 값 : $name")
        }
    }
    
    validateName(firstName, "firstName")
    validateName(lastName, "lastName")
    
    return Person(firstName, lastName, 1)
}

3. Lambda

1) Java에서 Lambda(익명 함수)를 다루기 위한 노력

private List<Fruit> filterFruit(List<Fruit> fruits, Predicate<Fruit> fruitFilter) {
	return fruits.stream()
    	.filter(fruitFilter)
        .collect(Collectors.toList());
}

filterFruits(fruits, fruit -> fruit.getName().equals("사과"));
filterFruits(fruits, Fruit::isApple);
  • Java에서는 함수를 넘겨주는 것처럼 보이지만 실제로는 Predicate라는 인터페이스로 받고 있다. 즉 Java에서 함수는 2급 시민으로 변수에 할당되거나 파라미터로 전달할 수 없다.

2) Kotlin에서의 Lambda

  • Kotlin에서는 함수가 그 자체로 값이 될 수 있다. 변수에 할당할 수도 있고, 파라미터로 넘길 수도 있다.
  • Kotlin에서는 함수가 1급 시민이다.
  • Lambda를 여러 줄 작성할 수 있고, 마지막 줄의 결과가 람다의 반환값이다.
fun main() {
	val fruits = listOf(
    	Fruit("사과", 1_000),
        Fruit("사과", 1_200),
        Fruit("사과", 1_200),
        Fruit("사과", 1_500),
        Fruit("바나나", 3_000),
        Fruit("바나나", 3_200),
        Fruit("바나나", 2_500),
        Fruit("수박", 10_0000)
     )
     
     val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
     	return fruit.name == "사과"
     }
     
     val isApple2: (Fruit) -> Boolean = { fruit: Fruit -> fruit.name == "사과 }
     
     isApple(fruits[0])
     isApple.invoke(fruits[0])
     
     filterFruits(fruits, isApple)
     filterFruits(fruits, fun(fruit: Fruit): Boolean {
     	return fruit.name == "사과"
     })
     filterFruits(fruits) { fruit: Fruit -> fruit.name == "사과 }	// 마지막 파라미터가 함수인 경우, 소괄호 밖에 람다 사용 가능
     filterFruits(fruits) { it.name == "사과" } // 람다를 작성할 때, 람다의 파라미터를 it으로 직접 참조할 수 있다.
 }

private fun filterFruits (
	fruits: List<Fruit>, filter: (Fruit) -> Boolean
): List<Fruit> {
	val results = mutableListOf<Fruit>()
    for (fruit in fruits) {
    	if (filter.invoke(fruit)) {
        	results.add(fruit)
        }
    }
    return results
}

3) Closure

String targetFruitName = "바나나";
targetFruitName = "수박";
filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getName()));
  • Variable used in lambda expression should be final or effectively final : Java에서는 Lambda를 쓸 때 사용할 수 있는 Lambda 밖 변수에 제약이 있다. 그러나 Kotlin에서는 non-final 변수를 사용해도 아무런 문제 없이 동작한다.
  • 이 이유는 Kotlin에서는 Lambda가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다. 이렇게 해야만 Lambda를 진정한 일급 시민으로 간주할 수 있다. 이 데이터 구조를 Closure라고 부른다.

4) try with resources

fun readFile(path: String) {
	BufferedReader(FileReader(path)).use { reader ->
    	println(reader.readLine())
    }
}
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
  • .use는 Closeable 구현체에 대한 확장함수이다.

4. 컬렉션을 함수형으로 다루는 방법

1) 필터와 맵

  • filter
  • filterIndexed
  • map
  • mapIndexed
  • mapNotNull
val apples = fruits.filter { fruit -> fruit.name == "사과" }
val apples = fruits.filterIndexed { idx, fruit -> 
	println(idx)
    fruit.name == "사과"
}
val applePrices = fruits.filter { fruit -> fruit.name == "사과" }
	.map { fruit -> fruit.currentPrice }
val applePrices = fruits.filter { fruit -> fruit.name == "사과 }
	.mapIndexed { idx, fruit -> 
    	println(idx)
        fruit.currentPrice
    }
val values = fruits.filter { fruit -> fruit.name == "사과" }
	.mapNotNull { fruit -> fruit.nullOrValue() }
    
private fun filterFruits(
	fruits: List<Fruit>, filter: (Fruit) -> Boolean
): List<Fruit> {
	return fruits.filter(filter)
}

2) 다양한 컬렉션 처리 기능

  • all : 조건을 모두 만족하면 true, 그렇지 않으면 false
val isAllApple = fruits.all { fruit -> fruit.name == "사과" }
  • none : 조건을 모두 불만족하면 true, 그렇지 않으면 false
val isNoApple = fruits.none { fruit -> fruit.name == "사과" }
  • any : 조건을 하나라도 만족하면 true, 그렇지 않으면 false
val isAnyApple = fruits.none { fruit -> fruit.factoryPrice >= 10_000 }
  • count : 개수를 센다
val fruitCount = fruits.count()
  • sortedBy : (오름차순) 정렬을 한다
val fruits = fruits.sortedBy { fruit -> fruit.currentPrice }
  • sortedByDescending : (내림차순) 정렬을 한다
val fruits = fruits.sortedByDescending { fruit -> fruit.currentPrice }
  • distinctBy : 변형된 값을 기준으로 중복을 제거한다
val distinctFruitNames = fruits.distinctBy { fruit -> fruit.name }
	.map { fruit -> fruit.name }
  • first : 첫번째 값을 가져온다 (무조건 null이 아니어야 함)
  • firstOrNull : 첫번째 값 또는 null을 가져온다
fruits.first()
fruits.firstOrNull()
  • last : 마지막 값을 가져온다 (무조건 null이 아니어야 함)
  • lastOrNull : 첫번째 값 또는 null을 가져온다
fruits.last()
fruits.lastOrNull()

3) List를 Map으로

val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
val map: Map<Long, Fruit> = fruits.associateBy { fruit -> fruit.id }
val map: Map<String, List<Long>> = fruits
	.groupBy({ fruit -> fruit.name }, { fruit -> fruit.factoryPrice })
val map: Map<Long, Long> = fruits
	.associateBy({ fruit -> fruit.id }, { fruit -> fruit.factoryPrice })
val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
	.filter { (key, value) -> key == "사과" }

4) 중첩된 컬렉션 처리

  • flatMap을 사용하게 되면 List<List>가 단일 List로 바뀌게 된다.
val fruitsInList: List<List<Fruit>> = listOf(
	listOf(
    	Fruit(1L, "사과", 1_000, 1_500),
        Fruit(2L, "사과", 1_200, 1_500),
        Fruit(3L, "사과", 1_200, 1_500),
        Fruit(4L, "사과", 1_500, 1_500),
    ),
    listOf(
    	Fruit(5L, "바나나", 3_000, 3_200),
        Fruit(6L, "바나나", 3_200, 3_200),
        Fruit(7L, "바나나", 2_500, 3_200),
    ),
    listOf(
    	Fruit(8L, "수박", 10_000, 10_000),
    )
)

val samePriceFruits = fruitsInList.flatMap { list ->
	list.filter { fruit -> fruit.factoryPrice == fruit.currentPrice }
}
data class Fruit(
	val id: Long,
    val name: String,
    val factoryPrice: Long,
    val currentPrice: Long,
) {
	val isSamePrice: Boolean 
    	get() = factoryPrice == currentPrice
}

val List<Fruit>.samePriceFilter = List<Fruit>
	get() = this.filter(Fruit::isSamePrice)

val samePriceFruits = fruitsInList.flatMap { list -> list.samePriceFilter }
  • List<List>를 그냥 List로 바꿔야 하는 상황에서는 flatten()이라는 것을 사용한다.
fruitsInList.flatten()

참고

profile
이것저것 관심많은 개발자.
post-custom-banner

0개의 댓글