Davide Giuseppe Farella
05/27/2023, 8:49 AMfun getTvShow(id: TvShowId): TvShow
fun getTvShowWithDetails(id: TvShowId): TvShowWithDetails
fun getTvShowWithExtras(id: TvShowId): TvShowWithExtras
and models
data class TvShow(...)
data class TvShowWithDetails(
val tvShow: TvShow,
val genres: Genres,
...
)
data class TvShowWithExtras(
...
val media: Media,
val credits: Credits,
...
)
This means that sometimes I’m fetching data I don’t need, for example, media, when I only want credits. Not a big deal, but I will add more stuff, like seasons, progress, etc.
What I would like to have, is something like
val tvShow = getTvShow(id, Genres, Credits)
tvShow.credits // ok, NOT nullable
tvShow.media // compile error
Suggest some ideas please 🙂 I’m thinking about codegen or Lint, but I would like to try something using language features only, maybe interfaces, extension functions, delegation, dunno 🤔
I also use arrow-kt, altho I’m basically using only Either
, Option
and optics, but maybe there’s a feature that I’m missing. I know there was some plan about union types, but it seems like the project has been discontinueddarkmoon_uk
05/27/2023, 8:55 AMDavide Giuseppe Farella
05/27/2023, 8:57 AMdarkmoon_uk
05/27/2023, 9:00 AMAdam S
05/27/2023, 9:01 AMval tvShow = getTvShow(...)
if (tvShow.credits != null) {
tvShow.credits.forEach { ... } // credits is inferred to be non-null
}
Davide Giuseppe Farella
05/27/2023, 9:05 AMtvShow.firstAirDate
is a real nullable, because a tv show might not be released yet, but I wouldn’t use nullable in these cases. I mean, I have a separate Ui Model, so I could run assertions there, but:
1. It is my pet project and I wanna experiment with borderline stuff 🙂
2. sometime I need a plain domain model, so I don’t have any gateway in between (for example, when generating suggestions, I pick a tv show rated from the user, so I do need the personal rating, and I want to be sure that it’s there 😄 )Davide Giuseppe Farella
05/27/2023, 9:07 AMAdam S
05/27/2023, 9:14 AMtvShow.firstAirDate
you need to differentiate between ‘no data’ and ‘not released’, then that sounds like a candidate for a sealed interface with specific subtypes
sealed interface FirstAirDate
object NoData : FirstAirDate
class Unreleased(val plannedDate: LocalDate?) : FirstAirDate
class Released(val date: LocalDate) : FirstAirDate
Davide Giuseppe Farella
05/27/2023, 9:45 AMDavide Giuseppe Farella
05/27/2023, 9:47 AMdarkmoon_uk
05/27/2023, 9:53 AMDavide Giuseppe Farella
05/27/2023, 9:56 AMScreenplayWithExtra {
data class WithCredits...
data class WithGenres...
data class WithCreditsAndGenres...
... x9999
}
Youssef Shoaib [MOD]
05/27/2023, 10:35 AMfun main() {
val tvShow = getTvShow(TvShowId(42), WithGenres, WithCredits)
tvShow.credits // Fine
tvShow.genres // Fine
tvShow.media // Error
val otherTvShow = getTvShow(TvShowId(42), WithMedia, WithGenres)
otherTvShow.media // Fine
otherTvShow.credits // Error
}
class Credits
class Genres
class Media
sealed interface Screenplay
sealed interface Extra<S: Screenplay>
sealed interface WithCredits: Screenplay {
val credits: Credits
companion object: Extra<WithCredits>
}
sealed interface WithGenres: Screenplay {
val genres: Genres
companion object: Extra<WithGenres>
}
sealed interface WithMedia: Screenplay {
val media: Media
companion object: Extra<WithMedia>
}
class TvShow: WithCredits, WithGenres, WithMedia {
override lateinit var credits: Credits
override lateinit var genres: Genres
override lateinit var media: Media
}
// Repeat for 3 extras, 4 extras, etc
@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER")
fun <S1: Screenplay, S2: Screenplay, SR: Screenplay> getTvShow(id: TvShowId, e1: Extra<S1>, e2: Extra<S2>): SR where SR: S1, SR: S2 {
// some code to only fetch the needed fields
TODO()
}
data class TvShowId(val id: Int)
Youssef Shoaib [MOD]
05/27/2023, 10:35 AMInlineOnly
functions that the stdlib defines, this pattern is just fine, and it is used for some collection functions.Davide Giuseppe Farella
05/27/2023, 10:41 AMdarkmoon_uk
05/27/2023, 10:43 AMsealed interface Optional<T> {
object NoData : Optional<Nothing>
object NotRetrieved : Optional<Nothing>
@JvmInline
value class(val value: T): Optional<T>
}
...
data class TvShow(
val credits: Optional<Credits>,
val media: Optional<Media>,
...
)
...you get it. TBH it's just an enhanced nullable but to differentiate between a value being unavailable because it doesn't exist, or just because you haven't retrieved it yet.Youssef Shoaib [MOD]
05/27/2023, 10:45 AMdarkmoon_uk
05/27/2023, 10:47 AMdarkmoon_uk
05/27/2023, 10:47 AMDavide Giuseppe Farella
05/27/2023, 10:48 AMfirstAirDate
is also part of the “main” tv show json), I just wanted to show a case for which I would use nullable, insteadDavide Giuseppe Farella
05/27/2023, 10:49 AMYoussef Shoaib [MOD]
05/27/2023, 10:54 AMgetTvShow
to spell out to the compiler that you need those ones only, and they all can be backed with a common implementation (that takes List<Extra>
) that knows nothing about types, and simply just returns the TvShow
and only fills the fields that it knows it needsDavide Giuseppe Farella
05/27/2023, 11:03 AMfun toUiModel(show: SomeType)
)
Do you think there is a way to work this around? Maybe with context receivers? (Although I can’t use them, since I’m on KMP, it would be nice to know I could simplify this sooner or later)
I’m trying to adapt it to my domain and let’s see how it plays out.Youssef Shoaib [MOD]
05/27/2023, 11:11 AMfun main() {
val tvShow = getTvShow(TvShowId(42), WithGenres, WithCredits)
tvShow.credits // Fine
tvShow.genres // Fine
useTvShowWithCreditsAndGenres(tvShow) // Fine
tvShow.media // Error
val otherTvShow = getTvShow(TvShowId(42), WithMedia, WithGenres)
otherTvShow.media // Fine
otherTvShow.credits // Error
useTvShowWithCreditsAndGenres(otherTvShow) // Error
}
fun <T> useTvShowWithCreditsAndGenres(tvShow: T) where T: WithCredits, T: WithGenres {
tvShow.credits // Fine
tvShow.genres // Fine
tvShow.media // Error
}
So basically, the way to represent a type that has credits and genres is to have a generic type T that has upper bounds WithGenres, WithCredits
.Davide Giuseppe Farella
05/27/2023, 11:13 AMI have to use generics throughoutBut actually it doesn’t look that bad. I imagined it worse in my head 😛
Youssef Shoaib [MOD]
05/27/2023, 11:15 AMuseTvShow(tvShow: WithCredits & WithGenres)
, and the suppression hack will be replaced with just getTvShow(...): S1 & S2
Davide Giuseppe Farella
05/27/2023, 11:16 AMDavide Giuseppe Farella
05/27/2023, 11:16 AMYoussef Shoaib [MOD]
05/27/2023, 11:17 AMDavide Giuseppe Farella
05/27/2023, 11:20 AMdarkmoon_uk
05/27/2023, 12:24 PMdarkmoon_uk
05/27/2023, 12:25 PMYoussef Shoaib [MOD]
05/27/2023, 12:27 PMdarkmoon_uk
05/27/2023, 12:28 PMdarkmoon_uk
05/27/2023, 12:30 PMYoussef Shoaib [MOD]
05/27/2023, 12:32 PMsealed value interface
that can possibly come to Kotlin but it's still in the worksdarkmoon_uk
05/27/2023, 12:48 PMYoussef Shoaib [MOD]
05/27/2023, 12:49 PMdarkmoon_uk
05/27/2023, 12:50 PMdarkmoon_uk
05/27/2023, 12:51 PMDavide Giuseppe Farella
05/27/2023, 2:26 PMTODO
?
I drafted something but looks quite dirty 😄 Not really an issue since it’s encapsulated in a single class which will be covered with test, but perhaps I could improve it
val extra1 = resolve(id, e1)
return ShowWithExtra(tvShow).applyExtras(extra1) as SR
private fun ShowWithExtra.applyExtras(vararg extras: Any) {
for (extra in extras) {
when (extra) {
is Credits -> credits = extra
is Genres -> genres = extra
is Media -> media = extra
else -> throw IllegalArgumentException("Unknown extra: $extra")
}
}
}
@Suppress("UNCHECKED_CAST")
private fun <S : WithExtra> resolve(
id: ShowId,
extra: Extra<S>
): S = when (extra) { // nice that here we have sealed hierarchy
WithCredits -> getCredits(id.tmdb)
WithGenres -> getGenres(id.tmdb)
WithMedia -> getMedia(id.tmdb)
} as Flow<Either<NetworkError, S>>
Youssef Shoaib [MOD]
05/27/2023, 2:29 PMvararg
or list or whatever of Extras that then sets the lateinit fields as it wishesDavide Giuseppe Farella
05/27/2023, 2:30 PMapplyExtras
is setting the lateinit
valuesYoussef Shoaib [MOD]
05/27/2023, 2:33 PMresolve
deal with that maybe, or you can have the Extra interface somehow know about that? There's many possibilities here, and it's hard to say which is the more "correct" one. It's an implementation detail anyways, so just do the one that's best for you.Davide Giuseppe Farella
05/27/2023, 2:36 PMresolve
is doing something else 🙂 I trimmed some parts for brevity, indeed there’s a as Flow<Either<NetworkError, S>>
leftover 😛
I may need to make a “sealed parent” for the actual extras, then (Credits, Media, etc), so I can get rid of the Any
, which is the part that bothers me more 😄
Thank you again!Youssef Shoaib [MOD]
05/27/2023, 2:36 PMDavide Giuseppe Farella
05/27/2023, 3:22 PMWhen("called with all the extras") {
val scenario = TestScenario(
screenplay = ScreenplaySample.Inception,
credits = ScreenplayCreditsSample.Inception,
genres = ScreenplayGenresSample.Inception,
keywords = ScreenplayKeywordsSample.Inception,
media = ScreenplayMediaSample.Inception,
personalRating = ScreenplayWithPersonalRatingSample.Inception.personalRating.some(),
isInWatchlist = true
)
val result = scenario.sut(
screenplayIds = screenplayId,
refresh = true,
refreshExtras = true,
WithCredits,
WithGenres,
WithKeywords,
WithMedia,
WithPersonalRating,
WithWatchlist
)
Then("the screenplay with extras is returned") {
result.test {
val item = awaitItem().getOrNull().shouldNotBeNull()
with(item) {
screenplay shouldBe ScreenplaySample.Inception
credits shouldBe ScreenplayCreditsSample.Inception
genres shouldBe ScreenplayGenresSample.Inception
keywords shouldBe ScreenplayKeywordsSample.Inception
media shouldBe ScreenplayMediaSample.Inception
personalRating.orNull() shouldBe ScreenplayWithPersonalRatingSample.Inception.personalRating
isInWatchlist shouldBe true
}
awaitComplete()
}
}
}
Davide Giuseppe Farella
05/27/2023, 3:43 PMDavide Giuseppe Farella
05/28/2023, 10:03 AMStateFlow
, is this know? Is it expected?
Here’s how I fixed it for my use case
https://github.com/fardavide/CineScout/commit/f2e3993816452afaf6c3f1e7c7a384eb9a66c06eDavide Giuseppe Farella
05/28/2023, 10:05 AMYoussef Shoaib [MOD]
05/28/2023, 10:29 AMDavide Giuseppe Farella
05/28/2023, 10:31 AMYoussef Shoaib [MOD]
05/28/2023, 10:32 AMDavide Giuseppe Farella
05/28/2023, 10:33 AM