https://velog.io/@lsb156/covariance-contravariance

참고자료) 

https://edykim.com/ko/post/what-is-coercion-and-anticommunism/

에 따르면 기본적으로 return type은 공변성을 가지고 argument type은 반공변성을 가진다.

변성

변성이란 제너릭 프로그래밍을 하면서 상속에 관련되어 생기는 이슈가 생길때 접하곤합니다.
변성에 대해서 간단한 예시를 통해서 설명을 하자면 아래와 같이 쉽게 설명이 가능합니다.

IntegerNumber를 상속받아 만들어진 객체입니다.
그래서 IntergerNumber의 하위 타입이라고 할 수 있어 아래와 같은 코딩이 가능합니다.

public void test() {
    List<Number> list;
    list.add(Integer.valueOf(1));
}

하지만 List<Integer>List<Number>의 하위타입이 될 수 없습니다.
이러한 상황에서 Java나 Kotlin에서는 type parameter타입 경계를 명시하여 Sub-Type, Super-Type을 가능하게 해줍니다.
그걸 변성이라고 합니다.

image

(리턴타입은 공변성을 가지면 함수안에서 나가는 것이기때문에 out을 사용한것으로 추측)

(변수타입은 반공변성을 가지면 함수안으로 들어가는 것이기때문에 in을 사용한것으로 추측)

변성에 대한 의미는 아래와 같이 간결하게 설명이 가능합니다.

image

위에서 설명한것과 같이 타입경계를 지정하여 적용이된 Kotlin과 Java에 대한 설명은 아래와 같이 해석이 가능합니다.

image

공변성 (Covariant)

공변성은 타입생성자에게 리스코프 치환 법칙을 허용하여 유연한 설계를 가능하게 해줍니다.

interface Cage<T> {
    fun get(): T
}

open class Animal

open class Hamster(var name: String) : Animal()

class GoldenHamster(name: String) : Hamster(name)

fun tamingHamster(cage: Cage<out Hamster>) {
    println("길들이기 : ${cage.get().name}")
}

fun main() {

    val animal = object : Cage<Animal> {
        override fun get(): Animal {
            return Animal()
        }
    }
    val hamster = object : Cage<Hamster> {
        override fun get(): Hamster {
            return Hamster("Hamster")
        }
    }
    val goldenHamster = object : Cage<GoldenHamster> {
        override fun get(): GoldenHamster {
            return GoldenHamster("Leo")
        }
    }

    tamingHamster(animal) // compile Error
    tamingHamster(hamster)
    tamingHamster(goldenHamster)
}

tamingHamster 함수는 Hamster의 서브타입만을 받기때문에 animal 변수는 들어갈 수 없습니다.

반공변성 (Contravariant)

공변성의 반대 개념으로 자기 자신과 부모 객체만을 허용합니다.

interface Cage<T> {
    fun get(): T
}

open class Animal

open class Hamster(var name: String) : Animal()

class GoldenHamster(name: String) : Hamster(name)

fun ancestorOfHamster(cage: Cage<in Hamster>) {
    println("ancestor = ${cage.get()::javaClass.name}")
}

fun main() {

    val animal = object : Cage<Animal> {
        override fun get(): Animal {
            return Animal()
        }
    }
    val hamster = object : Cage<Hamster> {
        override fun get(): Hamster {
            return Hamster("Hamster")
        }
    }
    val goldenHamster = object : Cage<GoldenHamster> {
        override fun get(): GoldenHamster {
            return GoldenHamster("Leo")
        }
    }

    ancestorOfHamster(animal) 
    ancestorOfHamster(hamster)
    ancestorOfHamster(goldenHamster) // compile Error
}

ancestorOfHamster에서 햄스터의 조상을 찾는 함수를 구현하여 햄스터를 포함한 그 조상들만 허용하도록 제한하였습니다.
하위타입인 Cage<GoldenHamster>는 제한에 걸려있어 compile error가 나는것을 확인 할 수 있습니다.

무공변성 (invariant)

Java, Kotlin의 Generic은 기본적으로 무공변성으로 아무런 설정이 없는 기본 Generic을 말합니다.

interface Cage<T> {
    fun get(): T
}

open class Animal

open class Hamster(var name: String) : Animal()

class GoldenHamster(name: String) : Hamster(name)

fun matingGoldenHamster(cage: Cage<GoldenHamster>) {
    val hamster = GoldenHamster("stew")
    println("교배 : ${hamster.name} & ${cage.get().name}")
}

fun main() {

    val animal = object : Cage<Animal> {
        override fun get(): Animal {
            return Animal()
        }
    }
    val hamster = object : Cage<Hamster> {
        override fun get(): Hamster {
            return Hamster("Hamster")
        }
    }
    val goldenHamster = object : Cage<GoldenHamster> {
        override fun get(): GoldenHamster {
            return GoldenHamster("Leo")
        }
    }

    matingGoldenHamster(animal) // compile Error
    matingGoldenHamster(hamster) // compile Error
    matingGoldenHamster(goldenHamster)
}

위 코드의 Cage<Animal>, Cage<Hamster>, Cage<GoldenHamster>는 서로 각각 연관이 없는 객체로서 무공변성의 적절한 예입니다.

지점에 따른 변성

변성에 따른 타입을 나누는 것에 대해서 지점에 따른 변성으로 선언 지점 변성과 사용 지점 변성으로 나뉠 수 있습니다.

선언 지점 변성

클래스를 선언하면서 클래스 자체에 변성을 지정하는 방식(클래스에 in/out을 지정하는 방식)을 선언 지점 변성(declaration-site variance)이라고 합니다.
선언 하면서 지정하면, 클래스의 공변성을 전체적으로 지정하는게 되기 때문에 클래스를 사용하는 장소에서는 따로 타입을 지정해줄 필요가 없어 편리하게 됩니다.

사용 지점 변성

사용 지점 변성(use-site variance)은 메소드 파라미터에서, 또는 제네릭 클래스를 생성할 때 등 구체적인 사용 위치에서 변성을 지정하는 방식입니다.
Java에서 사용하는 한정 와일드카드(bounded wildcard)가 바로 이 방식입니다.
이는 타입 파라미터가 있는 타입을 사용할 때마다 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할 수 있는지를 명시해야 합니다.

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow

StateFlow and SharedFlow are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers.

StateFlow

StateFlow    is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property. To update state and send it to the flow, assign a new value to the    value    property of the MutableStateFlow class.

class LatestNewsViewModel(
   private val newsRepository: NewsRepository
) : ViewModel() {

   // Backing property to avoid state updates from other classes
   private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
   // The UI collects from this StateFlow to get its state updates
   val uiState: StateFlow<LatestNewsUiState> = _uiState

   init {
       viewModelScope.launch {
           newsRepository.favoriteLatestNews
               // Update View with the latest favorite news
               // Writes to the value property of MutableStateFlow,
               // adding a new element to the flow and updating all
               // of its collectors
               .collect { favoriteNews ->
                   _uiState.value = LatestNewsUiState.Success(favoriteNews)
               }
       }
   }
}

// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
   data class Success(news: List<ArticleHeadline>): LatestNewsUiState()
   data class Error(exception: Throwable): LatestNewsUiState()
}

The class responsible for updating a MutableStateFlow is the producer, and all classes collecting from the StateFlow are the consumers. Unlike a cold flow built using the flow builder, a StateFlow is hot: collecting from the flow doesn’t trigger any producer code. A StateFlow is always active and in memory, and it becomes eligible for garbage collection only when there are no other references to it from a garbage collection root.

When a new consumer starts collecting from the flow, it receives the last state in the stream and any subsequent states. You can find this behavior in other observable classes like LiveData.

Flow는 cold이지만 StateFlow는 hot이다.

class LatestNewsActivity : AppCompatActivity() {
   private val latestNewsViewModel = // getViewModel()

   override fun onCreate(savedInstanceState: Bundle?) {
       ...
       // Start a coroutine in the lifecycle scope
       lifecycleScope.launch {
           // repeatOnLifecycle launches the block in a new coroutine every time the
           // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
           repeatOnLifecycle(Lifecycle.State.STARTED) {
               // Trigger the flow and start listening for values.
               // Note that this happens when lifecycle is STARTED and stops
               // collecting when the lifecycle is STOPPED
               latestNewsViewModel.uiState.collect { uiState ->
                   // New value received
                   when (uiState) {
                       is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                       is LatestNewsUiState.Error -> showError(uiState.exception)
                   }
               }
           }
       }
   }
}

activity에서 코루틴 사용할때는 lifecycleScope 를 사용한다. viewModel과 다르게 repeatOnLifecycle(Lifecycle.State.STARTED) 를 통해 lifecycle이 STARTED이상의 상태에서 작동하게 한다. StateFlow, Flow는 lifecycle에 자동으로 맞추어져서 소멸되지 않고 background에서도 계속 작동하게된다. 이를 방지 하기 위해서는 repeatOnLifecycle를 앞에서 언급한것과 같이 사용해서 특정 상태의 경우에만 작동하게 한다. viewModel의 경우는 Activity, fragment와는 다르게 lifecycle이 없으므로 그안에서 사용하는 경우는 좀 다르다. 

StateFlow or any other flow does not stop collecting automatically. To achieve the same behavior,you need to collect the flow from a Lifecycle.repeatOnLifecycle block.

Warning: Never collect a flow from the UI directly from launch or the launchIn extension function if the UI needs to be updated. These functions process events even when the view is not visible. This behavior can lead to app crashes. To avoid that, use the repeatOnLifecycle API as shown above.

To convert any flow to a StateFlow, use the    stateIn    intermediate operator.

StateFlow, Flow, and LiveData

StateFlow and LiveData have similarities. Both are observable data holder classes, and both follow a similar pattern when used in your app architecture.

Note, however, that StateFlow and LiveData do behave differently:

  • StateFlow     requires an initial state to be passed in to the constructor, while LiveData does not.
  • LiveData.observe()    automatically unregisters the consumer when the view goes to the STOPPED state, whereas collecting from a StateFlow or any other flow does not stop collecting automatically. To achieve the same behavior,you need to collect the flow from a Lifecycle.repeatOnLifecycle block.

Making cold flows hot using shareIn

StateFlow is a hot flow—it remains in memory as long as the flow is collected or while any other references to it exist from a garbage collection root. You can turn cold flows hot by using the shareIn operator.

callbackFlow에 관해서는 https://developer.android.com/kotlin/flow를 확인해보면 정보를 얻을수 있다.

Using the callbackFlow created in Kotlin flows as an example, instead of having each collector create a new flow, you can share the data retrieved from Firestore between collectors by using shareIn. You need to pass in the following:

  • A CoroutineScope that is used to share the flow. This scope should live longer than any consumer to keep the shared flow alive as long as needed.
  • The number of items to replay to each new collector.
  • The start behavior policy.

SharedFlow

The    shareIn    function returns a SharedFlow, a hot flow that emits values to all consumers that collect from it. 

You can create a SharedFlow without using shareIn. As an example, you could use a SharedFlow to send ticks to the rest of the app so that all the content refreshes periodically at the same time.

// Class that centralizes when the content of the app needs to be refreshed
class TickHandler(
   private val externalScope: CoroutineScope,
   private val tickIntervalMs: Long = 5000
) {
   // Backing property to avoid flow emissions from other classes
   private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
   val tickFlow: SharedFlow<Event<String>> = _tickFlow

   init {
       externalScope.launch {
           while(true) {
               _tickFlow.emit(Unit)
               delay(tickIntervalMs)
           }
       }
   }
}

class NewsRepository(
   ...,
   private val tickHandler: TickHandler,
   private val externalScope: CoroutineScope
) {
   init {
       externalScope.launch {
           // Listen for tick updates
           tickHandler.tickFlow.collect {
               refreshLatestNews()
           }
       }
   }

   suspend fun refreshLatestNews() { ... }
   ...
}

https://developer.android.com/kotlin/flow

In coroutines, a flow is a type that can emit multiple values sequentially, as opposed to suspend functions that return only a single value.      

Flows are built on top of coroutines and can provide multiple values. A flow is conceptually a stream of data that can be computed asynchronously. The emitted values must be of the same type. For example, a Flow<Int> is a flow that emits integer values.

There are three entities involved in streams of data:

  • A    producer    produces data that is added to the stream. Thanks to coroutines, flows can also produce data asynchronously.
  • (Optional)    Intermediaries    can modify each value emitted into the stream or the stream itself.
  • A    consumer    consumes the values from the stream.

Creating a flow

To create flows, use the    flow builder    APIs. The flow builder function creates a new flow where you can manually emit new values into the stream of data using the    emit    function.

The flow builder is executed within a coroutine. Thus, it benefits from the same asynchronous APIs, but some restrictions apply:

  • Flows are sequential. As the producer is in a coroutine, when calling a suspend function, the producer suspends until the suspend function returns. (producer 코루틴 블럭 또는 함수를 다 처리 하고 emit될때까지 suspend된다는 이야기)
  • With the flow builder, the producer cannot emit values from a different CoroutineContext. Therefore, don’t call emit in a different CoroutineContext by creating new coroutines or by using withContext blocks of code. You can use other flow builders such as callbackFlow in these cases. (producer안에서 새로 코루틴을 만들거나 withContext를 이용할수 없다)

Modifying the stream

Learn more about intermediate operators in the Flow reference documentation.

Intermediate operators can be applied one after the other, forming a chain of operations that are executed lazily when an item is emitted into the flow. Note that simply applying an intermediate operator to a stream does not start the flow collection.

Collecting from a flow

 You can learn more about terminal operators in the official flow documentation.

As    collect    is a suspend function, it needs to be executed within a coroutine. It takes a lambda as a parameter that is called on every new value. Since it’s a suspend function, the coroutine that calls    collect    may suspend until the flow is closed.

Collecting the flow triggers the producer that refreshes the latest data and emits the result. the stream of data will be closed when the ViewModel is cleared and viewModelScope is cancelled. (collector, consumer 쪽에서의 scope가 종료, 취소되는 경우 flow는 종료, 취소된다.)

Flow collection can stop for the following reasons:

  • The coroutine that collects is cancelled, as shown in the previous example. This also stops the underlying producer.
  • The producer finishes emitting items. In this case, the stream of data is closed and the coroutine that called collect resumes execution.

 To optimize and share a flow when multiple consumers collect at the same time, use the   shareIn    operator.

Catching unexpected exceptions

The implementation of the producer can come from a third party library. This means that it can throw unexpected exceptions. To handle these exceptions, use the    catch    intermediate operator.

collect operator가 catch operator 이후에 있는 경우 when an exception occurs, the collect lambda isn’t called, as a new item hasn’t been received.

catch can also emit items to the flow. The example repository layer could emit the cached values instead (catch operator안에 emit 메소드를 통해 값을 리턴할수 있다)

Executing in a different CoroutineContext

By default, the producer of a flow builder executes in the CoroutineContext of the coroutine that collects from it(collector의 코루틴 컨텍스트를 기본적으로 producer도 이용하는데 바꾸어야 할때가 생긴다) 기본적으로 viewmodel의 경우 Dispatchers.Main that is used by viewModelScope.

flowOn위에있는 부분만 flowOn CoroutineContext를 이용하게 되고 나머지는 collector coroutine context를 사용하게 된다.

To change the CoroutineContext of a flow, use the intermediate operator    flowOn.    flowOn changes the CoroutineContext of the upstream flow, meaning the producer and any intermediate operators applied before (or above) flowOn. The downstream flow (the intermediate operators after flowOn along with the consumer) is not affected and executes on the CoroutineContext used to collect from the flow. If there are multiple flowOn operators, each one changes the upstream from its current location.

Convert callback-based APIs to flows

callbackFlow    is a flow builder that lets you convert callback-based APIs into flows. As an example, the Firebase Firestore Android APIs use callbacks. 

Unlike the flow builder, callbackFlow allows values to be emitted from a different CoroutineContext with the    send    function or outside a coroutine with the    offer    function.

(자세한 이해를 위해 따로 공부할 필요가 있다.)

참고자료:

https://youtu.be/emk9_tVVLcc

https://youtu.be/VcOGu1Y0Qes

.

.

https://youtu.be/CIvjwIfOG5A

image
image
image
image
image
image
image
image
image

.

.

.

https://youtu.be/xch4aw7hNcY

asynchrony를 수행하는데 발생하는 일반적인 문제점들

image

코루틴과 채널을 사용한 경우의 예시

image

채널을 통과한 데이터가 각각 데이터를 받을 준비된 코루틴에 나누어서 배급되는점을 볼것

image
image
image
image
image

terminal operator collect가 수행되기 전까지는 cold상태이다.

image
image
image
image
image
image
image
image
image
image
image

위와 같이 기존 안드로이드 livedata와 flow를 섞어 사용가능하다 (view 단계까지 flow를 사용할수도 있다)

image
image
image
image
image

위까지는 livedata와 flow를 같이 사용한 경우의 예시이고 아래는 flow만을 이용한 경우의 예시이다.

image
image

.

.

.

필립

https://youtu.be/B_3iTVJT8Zs

image

위그림에서 producer (emit이 있는) 부분과 consumer (collect가 있는) 부분은 하나의 코루틴이다. 그래서 producer부분에서 1초 지연 consumer부분에서 2초지연 그래서 매 엘리먼트 당 3초식 지연된다. 

image

위그림은 buffer()를 이용 producer, consumer를 다른 코루틴으로 분리한것이다. 이경우 producer에서의 지연과 consumer에서의 지연이 동시에 다른 곳에서 생기게 되고 결과적으로는 전체 프로세스 시간은 가장 늦은consumer지연과 같게 된다.  

image

.

.

.

https://youtu.be/Qk2mIpE_riY

stateflow와 livedata의 차이점

– stateflow는 initial 값을 항상 지정해 주어야 한다.

– back stage로 process가 넘어간 뒤에도 state flow를 돌아간다.

– 다양한 terminal operator를 제공한다. map, zip, filter ……

image

launchWhenStarted 는 start 상태에서만 돌아가는 coroutine이 만들어진다. 일반 launch도 있는데 이를 사용하면 background 상태에서도 계속 돌아간다. 

.

.

.

https://youtu.be/RoGAb0iWljg

shared flow

– initial 값이 없어도 된다.

– state flow는 기본적으로 conflation을(처리를 못하는 consumer의 경우 밀린 element를 무시하고 최근 값만 받아들인다) 속성으로 가지나 shared flow는 아니며 모든 element값을 받는다.

– replay가 가능하다. (새로 연결된 최근 consumer의 경우 지난 값을 받아올수 있다.) 

– shareIn(), stateIn() extension function을 이용해서 일반 flow를 shared flow로 바꿀수 있다. 

data structure playlist

https://www.youtube.com/playlist?list=PL2_aWCzGMAwI3W_JlcBbtYTwiQSsOTa6P

https://youtu.be/gXgEDyodOJU

image
image
image

tree의 경우 node가 N개 있는 경우 edge는 N-1개가 있다. 

graph는 이런 rule 이 없음

image

tree는 graph의 한 형태 이다.

image

ordered pair를 표현할때는 ( )

unordered pair를 표현할때는 { } 를 사용한다.

image
image
image
image
image
image
image

.

.

.

.

.

https://youtu.be/AfYqN3fGapc

image
image

self loop의 한 예로 한웹페이지에 있는 자기자신으로의 link를 예로 들수 있다. 링크를 클릭하면 자기자신 페이지가 리프레쉬 된다.

image
image
image
image

만약 self-loop, multiedge가 없는 경우 simple graph라고 한다.

image
image

simple graph라고 가정할때 n 개의 node는 n(n-1)의 directed edges를 가질수 있다.

image

simple graph의 경우 n개의 node는 n(n-1)/2의 edges를 가진다.

image

edges를 많이 가진 graph의 경우 dense하다고 하며

edges를 적게 가진 graph의 경우 sparse하다고 한다.

image

simple graph : self-loop , multiedges를 가지지 않는 graph

simple path : path가 반복되는 vertices(node)를 가지지 않는 path (물론 당연히 반복되는 edges 도 없게된다)

image

또 반복되는 vertices(node)를 가지지 않는 walk (물론 당연히 반복되는 edges 도 없게된다)를 흔히 path라고 하기도 한다.

image

trail의 경우 vertices(nodes)는 반복되도 되지만 edge는 반복되지 말아야 한다.

image

어느 node에서건 다른 모든 node로 갈수 있는 경우(다른 node를 거쳐서 가는것 허용됨) strongly connected graph 라고 한다.

image
image
image
image
image