Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Recap: Kotlin Language Features in 2.0 and Beyo...

Recap: Kotlin Language Features in 2.0 and Beyond (Michail Zarečenskij)

코틀린 2.0 이상의 변경점을 다룹니다.

Leonardo YongUk Kim

June 28, 2024
Tweet

More Decks by Leonardo YongUk Kim

Other Decks in Technology

Transcript

  1. Copyright © 2024 Coupang, Inc. All right reserved. All Coupang

    trademarks and Coupang logos and service marks used are registered in the United States and other countries, Coupang, inc. And/or the property of its affiliates (collectively referred to as “Coupang”). Other companies mentioned herein are mentioned for identification purposes only, and Coupang acknowledges that the name of the company used may be a registered trademark of the company and that company alone has exclusive ownership of the trademark. The information contained herein is based on the author's personal experience as an executive and employee and does not represent Coupang's views or opinions. Coupang has not verified the adequacy, fairness, accuracy, or stability of the information contained herein, nor does it make any representations about it
  2. Intro and Format - 코틀린 여정: 과거 기능들 - 코틀린의

    현재 상태: 2.0의 기능들 - 나아갈 길: 향후 기능들
  3. 역사: 1.0 이후의 기능들 • 멀티플랫폼 프로젝트 • 코루틴 •

    인라인 밸류 클래스 • 트레일링 콤마 • 함수형 인터페이스 • 위임 프로퍼티 • 바운드 호출가능한 레퍼런스 • 람다에서 비구조화 • 어노테이션의 배열 리터럴 • 지역 지연 초기화 변수 • 옵트인 어노테이션 • 확실히 널이 아닌 타입 • 서브젝트가 있는 when • 어노테이션 클래스의 인스턴스화 • … • 타입 별칭 • 실드 클래스와 인터페이스 • 계약 • when 안에 break/continue • 철저한 when • 빌더 추론 • ..< 연산자 • 데이터 객체 • 효율적인Enum.entries
  4. What is Kotlin? • 코틀린은 언어이고 하고, • Kotlin/JVM, Kotlin/Native,

    Kotlin/JS, Kotlin/WASM • 코틀린 스크립트, Gradle Kotlin DSL • 안드로이드와 서버사이드 코틀린 • 도구: IntelliJ IDEA, Android Studio 확장, Fleet • 코틀린 라이브러리: 코루틴, 직렬화, Kover… • 컴파일러 도구: Compose, KSP, Power Assert, Arrow 플러그인…
  5. 새 코틀린 컴파일러는 왜 필요한가요? • 언어의 몇몇 기능은 코틀린에서

    예상하지 못하게 추가됨 ◦ 컴파일러를 유지보수하고 발전하기가 어려움 • 컴파일러 플러그인과 IDE의 상호작용 ◦ 많은 임기 응변의 해결법, 엄격하지 않은 계약 관계, 안정된 API의 부재 • 컴파일 시간 성능
  6. 코틀린 2.0 • 코틀린 2.0 출시 • 여러 서브 시스템에서

    80개 이상의 기능 • 언어 측면에서 25개 기능과 작은 향상들 • 여전히, 성능과 정확성이 최우선
  7. 일반적인 컴파일러 변화는? • 프론트엔드 중간 표현, Frontend Intermediate Representation

    (FIR) ◦ 언어 구성은 더 단순한 구성으로 나뉩니다. • 분석에 있어 단계별 접근 ◦ Import, 어노테이션, 타입등의 언어 구성에 대한 개별적인 단계 ◦ IDE와 컴파일러 플러그인을 위한 확장 • 새로운 제어 흐름 엔진, 향상된 타입 추론과 레졸루션 ◦ 코드를 통해 데이터 흐름 정보를 전달
  8. fun f(mList: MutableList<Long>) { mList[0] = mList[0] + 1 }

    Long 정수 리터럴 타입 • Long과 정수 리터럴 타입의 조합 프론트엔드 중간 표현
  9. 프론트엔드 중간 표현 • 연산자와 산술 변환의 조합 Error: 1L가

    필요 fun f(mList: MutableList<Long>) { mList[0] += 1 // Kotlin 1.x에서 에러 }
  10. 프론트엔드 중간 표현 • 연산자와 산술 변환의 조합 디슈거링 결과:

    mList.set(0, mList.get(0).plus(1)) fun f(mList: MutableList<Long>) { mList[0] += 1 // OK in 2.0 }
  11. 프론트엔드 중간 표현 • 널 가능 연산자 호출의 조합 class

    Box(val mList: MutableList<Long>) fun g(box: Box?) { box?.mList[0] += 1 // Error in 1.x box?.mList[0] += 1L // Error in 1.x }
  12. 프론트엔드 중간 표현 Desugared into: box .run { mList.set(0, mList.get(0).plus(1))

    } • 널 가능 연산자 호출의 조합 class Box(val mList: MutableList<Long>) fun g(box: Box?) { box?.mList[0] += 1 // OK in 2.0 }
  13. 새 제어 흐름 엔진 - 새 제어 흐름 엔진… 또는

    스마트 캐스트의 추가! - KT-7186 Smart cast for captured variables inside changing closures of inline functions - KT-4113 Smart casts for properties to not-null functional types at invoke calls - KT-25747 DFA variables: propagate smart cast results from local variables - KT-1982 Smart cast to a common supertype of subject types after || (OR operator) - …
  14. 변수로부터 스마트 캐스트 class Cat { fun purr() { println("Purr

    purr") } } fun petAnimal(animal: Any) { if (animal is Cat) { animal.purr() } }
  15. 변수로부터 스마트 캐스트 class Cat { fun purr() { println("Purr

    purr") } } fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() // Error in Kotlin 1.x } } 1.x: 변수는 어떤 자료 흐름 정보를 가지지 않는다
  16. 변수로부터 스마트 캐스트 class Cat { fun purr() { println("Purr

    purr") } } fun petAnimal(animal: Any) { val val isCat = animal is Cat if (isCat) { animal.purr() // OK in 2.0 } } 2.0: 합성 데이터 흐름 변수가 스마트 캐스트에 관한 정보를 전파한다.
  17. 제약을 포함하여 모든 스마트 캐스트에 대한 일반 규칙이 작동한다. class

    Card(val holder: String?) fun foo(card: Any): String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } 변수로부터 스마트 캐스트
  18. class Card(val holder: String?) fun foo(card: Any): String { val

    cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } Any -> Card String? -> String String으로 스마트캐스트 제약을 포함하여 모든 스마트 캐스트에 대한 일반 규칙이 작동한다. 변수로부터 스마트 캐스트
  19. 인라인 람다의 클로져 안에서 스마트 캐스트 fun indexOfMax(a: IntArray): Int?

    { var maxI: Int? = null a.forEachIndexed { i, value > // 1.x: ‘Int’로 스마크 캐스트가 불가능, // 'maxI'은 클로즈 변경에 캡쳐되는 지역 변수임 if (maxI == null | a[maxI!!] <= value) { maxI = i } } return maxI }
  20. 인라인 람다의 클로져 안에서 스마트 캐스트 fun indexOfMax(a: IntArray): Int?

    { var maxI: Int? = null a.forEachIndexed { i, value > // 1.x: ‘Int’로 스마크 캐스트가 불가능, // 'maxI'은 클로즈 변경에 캡쳐되는 지역 변수임 if (maxI == null | a[maxI!!] <= value) { maxI = i } } return maxI }
  21. 인라인 람다의 클로져 안에서 스마트 캐스트 2.0은 인라인 함수를 암묵적으로

    callInPlace 계약이 있는 것 처럼 취급합니다. fun indexOfMax(a: IntArray): Int? { var maxI: Int? = null a.forEachIndexed { i, value > // 2.0: OK if (maxI == null | a[maxI!!] <= value) { maxI = i } } return maxI }
  22. ‘||’뒤의 스마트 캐스트 interface Status { fun signal() } interface

    Ok : Status interface Postponed : Status interface Declined : Status
  23. ‘||’뒤의 스마트 캐스트 fun foo(signalStatus: Any) { if (signalStatus is

    Postponed || signalStatus is Declined) { // signalStatus는 Any로 추론 됨 signalStatus.signal() // Error } }
  24. ‘||’뒤의 스마트 캐스트: 공통 상위 타입으로 병합 fun foo(signalStatus: Any)

    { if (signalStatus is Postponed || signalStatus is Declined) { // signalStatus는 Status로 추론 됨 signalStatus.signal() // 2.0부터 OK } }
  25. 더 많은 “bugfixes” • KT-24901 No smart cast for `when`

    with early return • KT-13650 Right-hand side of a safe assignment is not always evaluated, which can fool smart-casts • KT-18130 Smart cast can be broken by expression in string template • KT-22454 Unsound smartcast in nested loops with labeled break from while-true • KT-17694 Smart cast impossible on var declared in init block with a secondary constructor • KT-56867 Green in K1 -> red in K2 for unsound code. `catch_end` to `good_finally` data flow • KT-26148 No smartcasts when not-null assertion or not-null assignment in lambda (contract functions with EXACTLY_ONCE or AT_LEAST_ONCE effects) • KT-23249 Inconsistent union type between platform type and non-platform type in K1. Fixed in K2 • KT-27261 Contracts for infix functions don't work (for receivers and parameters) • KT-30507 Unsound smartcast if null assignment inside index place and plusAssign/minusAssign is used • KT-52424 ClassCastException: Wrong smartcast to Nothing? with if-else in nullable lambda parameter • KT-37838 Support smart cast for inner/nested contracts • KT-30756 No smartcast if elvis operator as a smartcast source in while or do-while is used as the last statement • KT-53802 No smartcast after a while (true) infinite loop with break • …
  26. 다음은 무엇인가요? - 멀티플랫폼: 스위프트 익스포트, 안정된 klib 포맷, Kotlin/Native

    향상 - 도구: 앰퍼 그래들 프로젝트 격리, 노트북, 빌드 리포트 - 라이브러리: kotlinx-datetime, kotlinx-io, kotlinx-kover and others - 컴파일러: Kotlin/Wasm, 향상된 자바 상호호환, 타겟 전체의 인라인 문맥 통합 - 언어 자체…
  27. 왜 자료 흐름 프레임워크에 중점을 두나요? - 제어 흐름을 기술하는

    것은 개발자의 주요 업무 - 스마트 캐스트는 인지 부하를 줄여준다 - 추가적인 언어 구성은 없음 - 점진적인 확장 가능
  28. 데이터 인식과 비구조화 - 가드: 바인딩 없는 패턴 매칭 -

    문맥 인식 레졸루션 - 데이터 클래스 향상 - 이름 기반 비구조화 - 일관된 데이터 클래스 복사 가시성 - 일반화된 ADT - 이펙트 시스템 기능
  29. @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence<Data>, id: String ) { LazyColumn

    { val lastData = searchData.last { it.id == id } val searchPanel = selectedSearchPanel() when { searchPanel is SearchPanel.NewsPanel && !searchPanel.isBlocked -> item { NewsResult(lastData) } searchPanel == SearchPanel.SpeakersPanel -> item { /* … */ } searchPanel == SearchPanel.TalksPanel -> item { /* … */ } } } } 코드 좀 봅시다
  30. ‘searchPanel’의 반복이 보임 @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence<Data>, id: String

    ) { LazyColumn { val lastData = searchData.last { it.id == id } val searchPanel = selectedSearchPanel() when { searchPanel is SearchPanel.NewsPanel && !searchPanel.isBlocked -> item { NewsResult(lastData) } searchPanel == SearchPanel.SpeakersPanel -> item { /* … */ } searchPanel == SearchPanel.TalksPanel -> item { /* … */ } } } }
  31. { 서브젝트로 캡쳐 @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence<Data>, id: String

    ) { LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { is SearchPanel.NewsPanel && !searchPanel.isBlocked -> item { NewsResult(lastData) } SearchPanel.SpeakersPanel -> item { /* … */ } SearchPanel.TalksPanel -> item { /* … */ } } } } Error: expecting ‘->’
  32. { 가드된 조건: KEEP-371; KT-13626 @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence<Data>,

    id: String ) { LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { is SearchPanel.NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SearchPanel.SpeakersPanel -> item { /* … */ } SearchPanel.TalksPanel -> item { /* … */ } } } } 향상된 when: 가드 2.1부터 베타
  33. 더 나아질 수 있나요? @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence<Data>, id:

    String ) { LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { is SearchPanel.NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SearchPanel.SpeakersPanel -> item { /* … */ } SearchPanel.TalksPanel -> item { /* … */ } } } }
  34. } } } 문맥 인식 레졸루션 추가 한정어 없음: KT-16768

    @Composable fun DisplayLastSearchResultByPanelType( searchData: Sequence<Data>, id: String ) { LazyColumn { val lastData = searchData.last { it.id == id } when (val searchPanel = selectedSearchPanel()) { Is NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SpeakersPanel -> item { /* … */ } TalksPanel -> item { /* … */ } } } } 2.2에 실험 기능으로 도입
  35. enum class Status { Ok, Fail } fun process( status:

    Status = Status. Ok ) { /* … */ } 문맥 인식 레졸루션
  36. 문맥 인식 레졸루션 enum class Status { Ok, Fail }

    fun process( status: Status = Ok ) { /* … */ } 2.2에 실험 기능으로 도입
  37. } GADT (일반화된 ADT) 스타일 스마트 캐스트 sealed class Container<T>(val

    value: T) class IntContainer : Container<Int>(42) class StringContainer : Container<String>("Kotlin") fun <A> unbox(container: Container<A>): A = container.value // OK! fun <A> unboxAndProcess(container: Container<A>): A = when (container) { is IntContainer -> container.value // 컴파일 안됨 is StringContainer -> container.value // 컴파일 안됨 }
  38. } GADT 스타일 스마트 캐스트 Actual type: String Expected: A

    sealed class Container<T>(val value: T) class IntContainer : Container<Int>(42) class StringContainer : Container<String>("Kotlin") fun <A> unbox(container: Container<A>): A = container.value // OK! fun <A> unboxAndProcess(container: Container<A>): A = when (container) { is IntContainer -> container.value // 컴파일 안됨 is StringContainer -> container.value // 컴파일 안됨 }
  39. GADT 스타일 스마트 캐스트 } Actual type: String Expected: A

    sealed class Container<T>(val value: T) class IntContainer : Container<Int>(42) class StringContainer : Container<String>("Kotlin") fun <A> unbox(container: Container<A>): A = container.value // OK! fun <A> unboxAndProcess(container: Container<A>): A = when (container) { is IntContainer -> 42 is StringContainer -> “Kotlin” }
  40. } GADT 스타일 스마트 캐스트 연구중 sealed class Container<T>(val value:

    T) class IntContainer : Container<Int>(42) class StringContainer : Container<String>("Kotlin") fun <A> unbox(container: Container<A>): A = container.value // OK! fun <A> unboxAndProcess(container: Container<A>): A = when (container) { is IntContainer -> container.value // A = Int; GADT 스타일 스마트 캐스트 is StringContainer -> container.value // A = String; GADT 스타일 스마트 캐스트 }
  41. 이름 기반 비구조화 data class User(val name: String, val lastName:

    String) fun process(user: User) { val (surname, firstName) = user // … } Error in 2.x: - “surname” doesn’t match the property “name” - “firstName” doesn’t match the property “lastName”
  42. 이름 기반 비구조화 - 궁극적으로: 더 이상 이름 기반 비구조화에

    component 함수를 사용하지 않음 - 새로운 이름으로 대입할 때를 위한 특별한 문법 도입 data class User(val name: String, val lastName: String) fun process(user: User) { val (name, lastName) = usern // OK // … }
  43. 데이터 인식과 비구조화 - 가드: 바인딩 없는 패턴 매칭 (베타

    2.1) - 문맥 인식 레졸루셔션 (베타 2.2) - 일반화된 ADT - 데이터 클래스 향상 - 이름 기반 비구조화 (2.2에서 사전 작업) - 일관된 데이터 클래스 복사 가시성 (2.1에서 사전 작업) - 이펙트 시스템 기능 (여러 릴리스에서 향상)
  44. 라이브러리는 어떻게 하나요? LazyColumn { val lastData = searchData.last {

    it.id == id } when (val searchPanel = selectedSearchPanel()) { is NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SpeakersPanel -> item { /* … */ } TalksPanel -> item { /* … */ } } }
  45. @Composable fun LazyColumn( modifier: Modifier = Modifier, state: LazyListState =

    rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, content: LazyListScope.()-> Unit )
  46. 선택적 매개변수의 API 진화 • 기본 값을 가지는 새로운 매개변수는

    이전 버전과의 호환을 막음 ◦ 새로운 오버로드가 추가됨
  47. 선택적 매개변수의 API 진화 • 기본 값을 가지는 새로운 매개변수는

    이전 버전과의 호환을 막음 • 매번의 오버로드는 코드 중복과 문서 중복을 야기함.
  48. @Composable fun LazyColumn( modifier: Modifier = Modifier, state: LazyListState =

    rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, horizontalAlignment: Alignment.Horizontal = Alignment.Start, userScrollEnabled: Boolean = true, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), content: LazyListScope.() > Unit )
  49. 확장 가능한 데이터 인자 dataarg class ColumnSettings( val contentPadding: PaddingValues

    = PaddingValues(0.dp), val reverseLayout: Boolean = false, val verticalArrangement: Arrangement.Vertical = If (!reverseLayout) Arrangement.Top else Arrangement.Bottom, val horizontalAlignment: Alignment.Horizontal = Alignment.Start, val userScrollEnabled: Boolean = true ) KT-8214 2.2의 예비 변경 사항
  50. 확장 가능한 데이터 인자 dataarg class ColumnSettings( val contentPadding: PaddingValues

    = PaddingValues(0.dp), val reverseLayout: Boolean = false, val verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, val horizontalAlignment: Alignment.Horizontal = Alignment.Start, val userScrollEnabled: Boolean = true ) @Composable fun Lazy Column( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), dataarg args: ColumnSettings, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), content: @Composable RowScope.()-> Unit) { // … } LazyColumn(reverseLayout = true) { // … }
  51. 라이브러리 코드 진화 - 대규모로 진화하는 API: 확장 가능한 데이터

    인자 (2.2에서 실험 기능) - 시그니쳐 관리: 강제된 이름 인자 - 반환 값 확인: 반환 값 강제 확인 - KDoc 향상
  52. ‘Last’ 함수는 어떤가요? LazyColumn { val lastData = searchData.last {

    it.id == id } when (val searchPanel = selectedSearchPanel()) { is NewsPanel if !searchPanel.isBlocked -> item { NewsResult(lastData) } SpeakersPanel -> item { /* … */ } TalksPanel -> item { /* … */ } } }
  53. Right? /** * Returns the last element matching the given

    [predicate]. */ fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T { var result: T? = null for (element in this) if (predicate(element)) result = element return result ?: throw NoSuchElementException("Not found") }
  54. predicate가 { it == null } 이면? fun <T> Sequence<T>.last(predicate:

    (T) -> Boolean): T { var last: T? = null var found = false for (element in this) { if (predicate(element)) { last = element found = true } } if (!found) throw NoSuchElementException("Not found") @Suppress("UNCHECKED_CAST") return last as T }
  55. 충분할까요? private object NotFound fun <T> Sequence<T>.last(predicate: (T) -> Boolean):

    T { var result: Any? = null for (element in this) if (predicate(element)) result = element if (result == NotFound) throw NoSuchElementException("Not found") return result as T }
  56. Any?’ 타입의 사용 확인없는 캐스트 private object NotFound fun <T>

    Sequence<T>.last(predicate: (T) -> Boolean): T { var result: Any? = null for (element in this) if (predicate(element)) result = element if (result == NotFound) throw NoSuchElementException("Not found") return result as T }
  57. 에러를 위한 유니온 타입 오토 캐스트 에러를 위한 유니온 타입

    연구중 private error object NotFound fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T { var result: T | NotFound = NotFound for (element in this) if (predicate(element)) result = element if (result is NotFound) throw NoSuchElementException("Not found") return result }
  58. - 일반화된 유니온 타입은 없음, 에러만 가능. - 다른 타입

    위치에도 확장 가능 - 특별 연산자도 에러를 지원: ?. !. 연구중 에러를 위한 유니온 타입 private error object NotFound fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T { var result: T | NotFound = NotFound for (element in this) if (predicate(element)) result = element if (result is NotFound) throw NoSuchElementException("Not found") return result }
  59. 추상화 향상 - Context 파라미터(2.2 베타) - 명시적인 뒷받침 필드

    (2.0 실험 기능) - 에러를 위한 유니온 타입 (활발한 연구중) - 스트링 리터럴과 스트링 템플릿(2.1 예비 변경) - 깊은 불변성 (활발한 연구중)
  60. 명시적 뒷받침 필드 class MyViewModel : ViewModel() { private val

    _city = MutableLiveData<String>() val city: LiveData<String> get() = _city }
  61. 명시적 뒷받침 필드 • 깃헙의 100만개 이상의 파일에서 발견되는 패턴

    • List -> MutableList • SharedFlow -> MutableSharedFlow • LiveData -> MutableLiveData • … class MyViewModel : ViewModel() { private val _city = MutableLiveData<String>() val city: LiveData<String> get() = _city }
  62. 명시적 뒷받침 필드 class MyViewModel : ViewModel() { private val

    _city = MutableLiveData<String>() val city: LiveData<String> get() = _city } class MyViewModel : ViewModel() { val city: LiveData<String> field = MutableLiveData<String>() }
  63. 선택적 이름있는 뒷받침 필드 class MyViewModel : ViewModel() { val

    city: LiveData<String> field mutableCity = MutableLiveData<String>() } val background: Color field = mutableStateOf(getBackgroundColor) get() = field.value KEEP-278, KT-14663 2.2에 업데이트