코틀린 아카데미-함수형 프로그래밍. 쉽게 읽기!!!

1장. 코틀린을 사용한 함수형 프로그래밍

1-1. 코틀린은 함수형 언어인가?

함수형에서는 세상을 함수의 집합으로 간주한다.

코틀린은 함수형 언어인가? 코틀린은 함수형 프로그래밍 언어의 전형적 특징을 잘 지원한다, 코틀린은 순수한 함수형 언어는 아니다.

함수형 프로그래밍의 특징 코틀린은 지원하는가?
함수를 객체처럼 다룬다. 함수 타입, 람다 표현식, 함수 참조
고차 함수 완벽히 지원
데이터 불변성 val 지원, 기본 컬렉션은 읽기 전용이다, 데이터 클래스에서 copy 함수를 제공한다.
표현식 if-else, try-catch, when 문이 모두 표현식이다.
지연 연산 lazy 연산자
패턴 매칭 스마트 캐스팅을 지원하는 when 문
재귀 함수 호출 tailrec 제어자

 

이 책은 함수형 프로그래밍의 본질인 '함수를 객체로 사용하는 법'에 초점을 맞춘다.

  • 함수 타입
  • 익명 함수
  • 람다 표현식
  • 함수 참조
  • 컬렉션을 함수형 스타일로 다루는 법
  • 안전한 DSL qlfej
  • 스코프 함수 사용법

1-2. 왜 함수를 객체로 다뤄야 할까?

 

sum과 product의 두 함수의 본문은 거의 비슷하다. (sum은 다 더하고, product는 다 곱한다)

이럴 때 함수(즉, 중괄호 부분...)를 객체로 다루지 못한다면 공통 부분을 추출하기가 어려울 것이다. (각 연산을 클래스로 만들고, 이에 대한 인터페이스도 작성해야 한다)

 

이럴 때 함수형 프로그래밍의 특징인 람다 표현식을 사용하면 좋다.

operation 인자에 함수 본문{}을 그대로 전달한다.

사실 코틀린의 기본 라이브러리에 fold라는 함수가 구현돼있다.

fold 함수 설명

함수 참조(::)를 사용할 수도 있다.

컬렉션 처리를 사용할 수도 잇다.


2장. 함수 타입... 요거임! [   () ->   ]

함수를 객체로 표현하려면, 객체가 그러하듯, 함수를 표현할 타입이 필요하다.

타입은 객체가 담고 있는 메서드와 프로퍼티를 명시함으로써 객체가 무엇을 할 수 있는지를 명시한다.

  • 메서드 : 멤버 함수와 확장 함수 모두 메서드이다. (확장 함수에 대해선 뒤에 나온다)

함수 타입은 객체가 함수여야 함을 명시한다. 이렇게 정의된 함수는 invoke 메서드로 호출할 수 있다.

2-1. 함수 타입 정의

(파라미터타입1, 파라미터타입2, 파라미터타입3) -> 결과타입

함수 타입은 소괄호로 시작한다.

괄호 안엔 매개변수 타입이 있다.

소괄호 뒤에 화살표(->)와 결과 타입이 와야한다.

* 결과 타입은 필수이므로 만약 의미없는 값을 반환할 경우 Unit으로 지정한다. (void와 같다)

 

여러가지 함수 타입이 있다.

  • () -> Unit
    인수가 없고 무의미한 값을 반환한다.
  • (Int) -> Unit Int 타입의 인수를 하나 받고...
  • (String, String) -> Unit: String
    ... 무의미한 값을 반환한다.
  • () -> User
    인수가 없고 User 타입의 객체를 반환하는 함수 타입
  • (String, String) -> String: String
    ...String 타입의 객체를 반환하는 함수타입
  • (String) -> Name: String... Name 타입의 객체를 반환.
  • (T) -> Boolean
    predicate
  • (T) -> R
    값 하나를 다른 값으로 변환하는 함수를 transform이라고 하고
  • (T) -> Unit
    Unit을 반환하는 함수를 operation이라고 한다.

2-2. 함수 타입 활용

함수 타입은 invoke라는 메서드를 사용 할 수 있다. (사실 이 메서드 하나밖에 못 쓴다.)

함수 타입이 invoke 메서드를 사용함

함수타입에 onSuccess라는 이름을 붙여주고 invoke 메서드를 사용하고 있음!

이 invoke는 암묵적으로 호출할 수도 있다.

클래스 정의, 제네렉 타입 인수, 매개변수 정의 등에도 함수 타입을 사용할 수 있다. (왜?)

2-3. 명명된 매개변수

함수 타입에 적힌 매개변수에 이름을 지정할 수도 있다. positon: Int 이런식으로.

2-4. 타입 별명 typealias

2-5. 함수 타입은 인터페이스이다.


3장. 익명함수

이제 함수 타입을 구현한 객체를 만드는 법을 배울 차례다.

코틀린에 익숙한 개발자들은 람다 함수를 기대하겠지만 익명함수부터 시작한다.

자바 스크립트에서도 익명 함수가 람다 표현식의 전임자였다. 흔히 화살표 함수라고 부른다.

익명 함수는 단일 표현식이나 일반적인 구문을 사용할 수도 있다.

val add2 = fun(a: Int, b: Int) = a + b

4장. 람다 표현식

함수를 나타내는 객체가 결괏값으로 생성되는 표현식을 함수 리터럴(function literal)이라고 합니다. 따라서 람다 표현식과 익명 함수 모두 함수 리터럴이다.

4-1. 까다로운 중괄호

람다 표현식은 중괄호 안에 정의한다. 빈 중괄호로도 정의가 가능하다.

코틀린 구조에 해당하지 않는 중괄호는 모두 람다 표현식이기 때문에 주의해야한다. 아래 코드는 아무것도 출력하지 않는다.

4-2. 매개변수

람다 표현식 안에서 매개변수는 어떻게 표시할까. 중괄호 안에서 화살표(->)를 사용하여 분리시켜야 한다. 화살표 다음에 함수 본문을 작성한다.

대체로 람다 표현식은 어떤 함수의 인수로 정의된다.

일반함수는 매개변수 타입을 명시하여 람다 표현식의 매개변수 타입을 추론할 수 있도록 도와주는 게 좋다.

일반함수:사각형, 매개변수 타입: 밑줄, 람다표현식: 동그라미

매개변수를 무시하고 싶으면 이름 대신에 밑줄을 사용하면 된다.

람다 표현식의 매개변수를 정의할 때 구조 분해를 사용할 수도 있다.

4-3. 후행 람다(trailing lambda)

코틀린은 람다 표현식을 사용할 때 마지막 매개변수가 함수타입인 함수를 호출하면 람다 표현식을 괄호 밖에 정의 할 수 있게 했다. 왜 이딴짓을... 이를 후행 람다(trailing lambda)라고 한다.

함수 타입이 유일한 매개변수라면 매개변수용 괄호를 생략하고 람다 표현식을 바로 정의할 수도 있다.

setOnClickListener({ _, _ ->
	println("Clicked")
})

setOnclickListener { _, _ ->
	println("Clicked")
}

4-4. 결괏값

람다 표현식은 처음에는 짧은 함수를 구현하기 위해 설계됐다.

본문의 크기를 최소로 줄이고자!

명시적인 return 대신에 마지막 문장의 결과를 반환한다! { 42 } 는 42를 반환한다.

 

람다 표현식 중간에 return을 사용하려면(걍 쓰면 컴파일 에러가 난다) 레이블이라는 특정한 기능을 써야한다.

return@someLable

람다 표현식이 함수의 인수로 사용되면 함수의 이름이 기본 레이블이 된다...

4-5. 람다 표현식 예시

4-6. 단일 매개변수의 암묵적 이름

람다 표현식의 매개변수가 딱 하나라면 it(잇)을 사용해서 참조 가능. (it은 타입을 명시할 수 없음)

val printNumber: (Int) -> Unit = { println(it) }

val newsItemAdapters = news
	.filter { it.visible }
    .sortedByDescending { it.publishedAt }
    .map { it.toNewsItemAdapter() }

매개변수가 하나 일 경우 굳이 명시하지 않고 바로 람다식에 넣어버릴 수 있다. 어차피.. 하나니까...

news에 전달되는 매개변수가 하나라면...

4-7. 클로저

나왔다...

람다 표현식은 자신이 정의된 스코프 안의 변수를 사용하고 수정할 수 있다.

한편 자신의 스코프 바깥에서 정의된 객체를 참조하는 람다 표현식을 클로저라고 한다.

count는 클로저가 아니라서 1만 출력되지만

makeCounter는 클로저라서 { i++ } 값이 누적되는 모습...

4-8. 람다 표현식과 익명 함수 비교

람다 표현식은 주로 단일 표현식 함수로 설계 되었으므로 여러 문장으로 구성된 함수는 익명 함수를 사용하라고 권한다.

함수를 표현하는 객체를 만드는 또 다른 방법을 소개한다. 이 방법은 더 짧고, 함수형 스타일에 걸맞다. 함수 참조.


5장. 함수 참조

객체로 사용할 수 있는 함수가 필요하다면 람다 표현식으로 새로운 객체를 생성해봤습니다.

기존의 함수를 참조할 수도 있습니다.

함수 참조는 코틀린 리플렉션 API의 일부이며 인트로스펙션을 지원한다.

* 리플렉션(reflection)은 실행 시간에 객체의 메타 데이터에 접근하여 원래라면 접근할 수 없는 타입 정보, 프로퍼티, 멤버 함수등을 수정하는 행위를 말한다.

* 인트로스펙션은 리플렉션의 일부이며 실행 시간에 메타 데이터를 읽는 기능을 말한다.

5-1. 최상위 함수 참조

최상위 함수는 클래스 바깥, 즉 파일 수준에서 정의된 함수이다.

최상위 함수는 ::와 함수 이름을 사용해 참조한다.

프로젝트에 kotlin-refliect 의존성을 추가하면 함수 참조를 이용하여 참조된 함수에 open 제어자가 붙어있는지 어떤 애너테이션들이 달려있는지 등의 정보를 확인할 수 있다.

add를 함수참조하여 사용한 모습

함수 참조를 할 때는 함수 타입에 유의해야하는 듯?하다..?

5-2. 메서드 참조

메서드 (멤버 함수)를 참조하려면 리시버의 타입부터 명시한 다음 ::와 메서드 이름을 적어야한다.

1장에서 소개한 sum과 product 함수도 메서드 참조를 사용하여 표현할 수 있다.

 

5-3.확장 함수 참조

확장 함수도 멤버 함수와 같은 방법으로 참조가 가능하다.

5-4. 메서드 참조와 제네릭 타입

뭔소리야

5-5. 한정된 함수 참조

특정 객체의 메서드를 참조하는 방법도 있다.

data class Number(val num: Int) {
	fun toFloat(): Float = num.toFloat()
}

fun main() {
	val num = Number(10) // 특정 객체
    
    val getNumAsFloat: () -> Float = num::toFloat // 한정된 함수 참조 (특정객체인 num이 data Class의 toFloat을 참조하고 있다)
    println(getNumAsFloat()) // 10.0 // 객체에 한정하여 참조하기 때문에 함수 타입에 리시버 타입이 필요하지 않음
}

한정된 함수 참조의 리시버는 무조건 this이다. (생략이 가능하다)

this::show는 ::show가 될 수 있다.

5-6. 생성자 참조

코틀린에서는 생성자 역시 함수로 취급한다.

data class Complex(val real: Double, val imaginary: Double)

fun main() {
	// 생성자 참조
    val produce: (Double, Double) -> Complex = ::Complex
    
    println(produce(1.0, 2.0) // Complex(real=1.0, imaginary=2.0)
}

생성자를 사용하여 한 타입의 원소들을 다른 타입으로 매핑할 때 생성자 참조가 유용하다. 특히 래퍼 클래스(wrapper class)로 매핑할 때.

5-7. 한정된 객체 선언 참조

한정된 함수 참조가 도입된 계기는 객체 선언의 메서드를 간단하게 참조하기 위함이었다.

모든 객체 선언은 싱글턴이며 객체 이름이 싱글턴 객체를 참조하는 유일한 방법이다.

한정된 함수 참조 덕분에 두 개의 콜론(::), 메서드 이름을 사용해 객체 선언의 메서드들을 참조 할 수 있다.

5-8.함수 오버로딩과 참조

5-8. 프로퍼티 참조


6장. 코틀린에서 SAM 인터페이스 지원


7장. 인라인 함수

fun <T, R> Iterable<T>.fold(
	initial: R,
    operation: (acc: R, T) -> R
): R {
	var accumulator = initial
    for (element in this) {
    	accumulator = operation(accumulator, element)
    }
    return accumulator
}

// 람다 표현식으로 나타낼 경우 for문에서 각각의 학생마다 람다식을 호출하고 다시 반복문이 실행된다.
fun main() {
	val points = students.fold(0) { acc, s -> acc + s.points }
    println(points)
}

// 위 같은 오버헤드를 반대한 결과 java의 코드는...
fun main() {
	var points = 0
    for (student in students) {
    	points += student.points
    }
    println(points)
}

같은 알고리즘을 코드 이곳저곳에서 반복 작성해야 한다.

효율을 고려하면서도 함수를 인수로 넘겨주는 편리함을 위해서 인라인 함수를 사용하면 된다. 인라인 함수는 람다 표현식을 호출하는 비용을 없애준다.

7-1. 인라인 함수

inline 제어자가 붙은 함수는 다른 함수들과 다르게 '호출되지 않습니다.'

대신 컴파일 할 때 함수 본문이 호출자 쪽에 복사되어 들어간다.

가장 간단한 예는 코틀린 표준 라이브러리의 print 함수다. print는 인라인 함수고, 컴파일하면 print는 System.out.print로 대체된다.

fun main() { print("A") }
// 내부적으로는...
fun main() { System.out.print("A") }

이 때 매개변수를 사용하는 코드는 관련한 인수 표현식으로 대체된다. 이 방식에는 몇 가지 이점이 있는데...

  • 함수형 매개변수가 있는 함수는 인라인이 되면 더 효율적이다
  • 비지역(non-local) 반환이 허용된다
  • 타입 인수가 구체화(reified)된다

7-2. 함수형 매개변수가 있는 인라인 함수

인라인 함수가 함수 타입의 매개변수를 받는다면 매개변수도 기본적으로 인라인된다.

컴파일 후에는 아래 코드가 된다. 람다 표현식의 본문 역시 사용되는 곳으로 모두 인라인된다. 왜... action이 print로 바뀌는거지... Int를 받아서 void하는 함수라서...? 왜지..

inline fun <T, R> Iterable<T>.fold(
    initial: R,
    operation: (acc: R, T) -> R
): R {
    var accumulator = initial
    for (element in this) {
        accumulator = operation(accumulator, element)
    }
    return accumulator
}

fun main() {
    val points = students.fold(0) { acc, student -> acc + student.points }
    println(points)
}

// 내부적으로는 다음과 같이 컴파일
fun main() {
    var accumulator = 0
    for (element in students) {
        accumulator = accumulator + element.points
    }
    val points = accumulator
    println(points)
}

효율적일 뿐 아니라 생성된 객체의 수 또한 적다.

이런 장점 덕분에 함수형 매개변수가 있는 최상위 함수를 인라인으로 만드는 것은 흔한 관행이 되었다.

인라인 함수를 호출할 때 사용되는 람다 표현식은 객체를 생성하지 않으므로 인라인이 아닌 함수에는 없는 기능이 생겨나는데...

7-3. 비지역 반환

람다 표현식에는 한계가 있다. 람다 표현식에서는 함수의 본문이 다른 함수이기 때문에, for문 안에서 바로 return을 사용하여 곧장 반환할 수 없다.

하지만 람다 표현식이 인라인된다면 이런 문제가 사라진다.

 

inline 함수인 repeat의 람다 표현식 안에서 return을 사용한 예시를 보자.
컴파일 과정에서 repeat가 인라인 되기 때문에, 람다 표현식 또한 복사되어 들어가기 때문에 return을 작성해도 문제가 없다.

이것을 비지역 반환(non-local return)이라고 한다.

컴파일 된 모습은 아래와 같다.

for문에서 return을 하는 건 가능하니까

7-4. crossinline과 noinline

함수를 인라인으로 지정하고 싶지만 인수로 받은 함수 중 일부는 인라인할 수 없을 때도 있다.

이런 경우 인수로 받은 함수들에 다음 제어자를 추가해야 한다.

  • crossinline
    • 인수로 받은 함수가 비지역 반환이 허용되지 않는 스코프에서 실행될 때 사용한다.
    • 인라인은 되지만, 비지역 반환은 허용하지 않는다.
  • noinline
    • 인수가 전혀 인라인이 되지 않는다.
    • 인라인이 아닌 다른 함수의 인수로 전달할 때 주로 사용한다.

7-5. 구체화 된 타입 매개변수 - reified

타입 매개변수

타입 매개변수는 타입의 저장소이다. 때문에 주로 T1, T2, T, R과 같이 표기한다.

타입 인수

타입 인수는 제네릭 함수를 호출할 때 실제 사용되는 타입이다.

printTypeName<Int>() 함수 호출에서 Int 타입은 타입 인수로 사용된다.

 

함수를 인라인으로 만들고 타입 매개변수*에 reified 제어자를 붙이면 타입에 대한 작업이 가능해진다... (안 그럼 못한다고 하네요...)

구체화 된 타입 매개변수를 사용한 곳이 타입 인수로 교체된다...

컴파일 과정에서 printTypeName 함수를 호출한 곳은 모두 printTypeName의 본문으로 교체된다.

구체화 된 타입 매개변수 T는 타입 인수들(Int, Char, String)으로 교체된다.

reified를 사용하여 특정 타입의 원소만 뽑아낼 수도 있다.

구체화 된 매개변수를 활용하면 제네릭 함수에서 타입 매개변수를 넘기거나 반환하는 과정을 간단하게 만들 수 있다.

7-6. 인라인 프로퍼티

접근자를 사용해서 정의된 프로퍼티는 결국 함수로 컴파일된다.

val User.fullName: String
	get() = "$name $surname"
    
// 내부적으로 다음 코드와 동일하다.
fun getFullName(user: User) =
	"${user.name} ${user.surname}

함수이므로, 이런 프로퍼티라면 inline 제어자를 붙일 수 있다. 그러면 컨파일 과정에서 프로퍼티를 사용하는 곳이 모두 프로퍼티의 본문으로 교체된다.

class User(val name: String, val surname: String) {
	inline val fullName: String get() = "$name $surname"
}

fun main() {
	val user = User("A", "B")
    println(user.fullName) // A B
    
    // 컴파일 과정에서 아래 코드로 바뀐다
    println("${user.name} ${user.surname}")
}

7-7. inline 제어자의 비용

inline은 비용과 한계 때문에 아무곳에나 사용하면 안된다.

public으로 지정된 인라인 함수에서는 private이나 내부용으로 제한된 함수 혹은 프로퍼티를 사용할 수 없다. (inline은 가시성이 제한 된 원소들에 접근할 수 없다.)

7-8. 인라인 함수 사용하기

인라인 함수를 사용하는 주된 이유는 다음 두가지이다.

  • 함수형 매개변수를 받는 함수의 성능 개선
  • 구체화 된 타입 매개변수 지원

인라인 함수는 헬퍼 함수로 사용하기에 적합하다. 예를 들어, 다른 클래스 메서드를 간단하게 만들기 위해 이용되는 최상위 함수나 반복되는 메서드라면 인라인 함수로 만들만하다.


8장. 컬렉션 처리

8-1. forEach, onEach

8-2. filter

8-3.map

8-4. mapNotNull

8-5. flatMap

8-6. fold

8-7. reduce

8-8. sum

8-9. withIndex와 변형된 함수들

8-10. take, takeLast, drop, dropLast, subList

8-11. 특정 위치의 원소 얻기

8-12. 원소 찾기

8-13. 원소 갯수 세기

8-14. any, all, none

8-15. partition

8-16. groupBy

8-17. 맵으로 짝지우기

8-18. distinct와 distinctBy

8-19. sorted, sortedBy, sortedWith

8-20. 가변 컬렉션 정렬하기

8-21. 최댓값과 최솟값

8-22. suffled와 random

8-23. zip과 zipWithNext

8-24. 윈도잉

8-25. joinToString

8-26.Map, Set, String처리

8-27.컬렉션 처리 함수를 모두 함께 사용하기

'【 개발 이야기 】 > 도서' 카테고리의 다른 글

[서블릿 컨테이너의 이해]  (0) 2024.09.09