Home > OS >  Kotlin - Handling Conditional Types on similar high-level operations
Kotlin - Handling Conditional Types on similar high-level operations

Time:08-26

I have to perform high-level operations that are always the same: fetch something, and store it locally. The issue I am facing is that the "fetching" and the "storing" deal with different types depending on certain conditions. Given the proximity of the operations, it seemed like code waste to repeat the code for every type, so I tried making it generic... without much success.

Here is a code example that shows the issue I am facing:

    suspend fun startFetching(dataPeriodicity: StockDataPeriodicity) {
        val stock = stockRepository.insertStock(stockDetails)
            .ifDuplicateGetExistingWith { stockRepository.getStockBy(stockDetails.symbol).trust() }
            .trust()

        val (fetcher, storer) =
            if (dataPeriodicity.isIntraday)
                stockDataClient::getIntradayPriceDataForStock to intradayTimeSeriesRepository::insertNonExisting
            else
                stockDataClient::getInterdayPriceDataForStock to interdayTimeSeriesRepository::insertNonExisting

        while (true) {
            fetchAndStore(stock, dataPeriodicity, fetcher, storer)
            delay(fetchingInterval.inWholeMilliseconds)
        }
    }

    private suspend inline fun <reified T, reified R : Comparable<R>> fetchAndStore(
        stock: Stock,
        periodicity: StockDataPeriodicity,
        crossinline fetcher: suspend (symbol: String, periodicity: StockDataPeriodicity) -> T,
        storer: (stock: Stock, values: T) -> Either<Throwable, List<Entity<R>>>
    ) = storer(stock, fetcher(stockDetails.symbol, periodicity))

Here is a screenshot with intellij type inferences and errors for context:

enter image description here

Here are the signatures for getInterdayPriceDataForStock and getIntradayPriceDataForStock

    suspend fun getInterdayPriceDataForStock(
        stockSymbol: String,
        periodicity: StockDataPeriodicity
    ): Either<Throwable, StockPriceInterdayValues>

    suspend fun getIntradayPriceDataForStock(
        stockSymbol: String,
        periodicity: StockDataPeriodicity
    ): Either<Throwable, StockPriceIntradayValues>

Here are the signatures for both insertNonExisting functions for the two different repositories

fun insertNonExisting(relatedStock: Stock, values: StockPriceIntradayValues): Either<Throwable, List<IntradayTimeSeries>>

fun insertNonExisting(relatedStock: Stock, values: StockPriceInterdayValues): Either<Throwable, List<InterdayTimeSeries>>

I was expecting that the compiler was able to, with the reified types, understand the types involved in runtime when the "fetchAndStore" call is reached.

It didn't, and don't really know how to untangle it from here.

Already tried to have a shared interface by the return of getIntradayPriceDataForStock and getInterdayPriceDataForStock, but it didn't work. Also, the signature on the storer that looks like {type1 & type2} sounds fishy for me.

Bottom-line: I tried my best with my knowledge of generics but I guess I am missing something. Any help is appreciated.

CodePudding user response:

You should put the whole loop into the inline method, and call it earlier, when the types of the functions are still known.

private suspend inline fun <reified T, reified R : Comparable<R>> fetchAndStore(
    stock: Stock,
    periodicity: StockDataPeriodicity,
    // note that you made a mistake here - the function type should return 
    // Either<Throwable, T>, not T
    crossinline fetcher: suspend (symbol: String, periodicity: StockDataPeriodicity) -> Either<Throwable, T>,
    storer: (stock: Stock, values: T) -> Either<Throwable, List<Entity<R>>>
) {
    while (true) {
        storer(stock, fetcher(stockDetails.symbol, periodicity).second)
        delay(fetchingInterval.inWholeMilliseconds)
    }
}

// in startFetching:
if (dataPeriodicity.isIntraday)
    fetchAndStore(stock, dataPeriodicity, stockDataClient::getIntradayPriceDataForStock, intradayTimeSeriesRepository::insertNonExisting)
else
    fetchAndStore(stock, dataPeriodicity, stockDataClient::getInterdayPriceDataForStock, interdayTimeSeriesRepository::insertNonExisting)

If you don't like passing stock and dataPeriodicity twice, you can make fetchAndStore a local function in startFetching and remove the parameters, but that means it can't be inline anymore.


Explanation

Reified types do not allow the compiler to "understand the types involved in runtime when the 'fetchAndStore' call is reached". Reified types allow generic types arguments to be known at runtime, by inlining them into the callsite. This all depends on the compiler knowing what type it is at compile-time, and at the line,

fetchAndStore(stock, dataPeriodicity, fetcher, storer)

the compiler doesn't know which branch of the if statement will be executed, so all it can work out at compile time are the following types:

  • fetcher is a suspending function taking a String, StockDataPeriodicity and returning an Either<Throwable, Any>. Notice that the compiler doesn't know second type parameter because there is nothing in common between StockPriceInterdayValues and StockPriceIntradayValues
  • storer is a function that takes Stock and { StockPriceIntradayValues & StockPriceInterdayValues }, and returns Either<Throwable, List<LongEntity>>. { StockPriceIntradayValues & StockPriceInterdayValues } means a type that is a subtype of both of those two types, which I don't think is even possible. This is because one of the insertNonExisting takes a StockPriceIntradayValues, and the other takes a StockPriceInterdayValues, which is an unrelated type. The overall storer can take neither :(

As you can see, these are totally not the types that you want fetcher and storer to have, but these are the types that the compiler would use to call your generic method. And this is why it fails.

  • Related