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:
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 aString
,StockDataPeriodicity
and returning anEither<Throwable, Any>
. Notice that the compiler doesn't know second type parameter because there is nothing in common betweenStockPriceInterdayValues
andStockPriceIntradayValues
storer
is a function that takesStock
and{ StockPriceIntradayValues & StockPriceInterdayValues }
, and returnsEither<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 theinsertNonExisting
takes aStockPriceIntradayValues
, and the other takes aStockPriceInterdayValues
, which is an unrelated type. The overallstorer
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.