Hello people :wave: I’m tryin’ to find a way to s...
# random
d
Hello people 👋 I’m tryin’ to find a way to simulate a “dynamic type”; let me explain: As of now I have something like
Copy code
fun getTvShow(id: TvShowId): TvShow
fun getTvShowWithDetails(id: TvShowId): TvShowWithDetails
fun getTvShowWithExtras(id: TvShowId): TvShowWithExtras
and models
Copy code
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
Copy code
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 discontinued
d
Sounds like you are looking for sealed classes
d
Well, not really, I don’t want to have 10000 subclasses for every combination 🙂 The optional data are: • credits • genres • images • videos • seasons ◦ with episodes ◦ without episodes • watchlist (whether it is or not in the watchlist) • personal rating • progress • other stuff that I don’t remember or I didn’t plan to implement, just yet
d
IDK man sounds like nullable fields are actually the right domain modelling choice here. If you're only wanting to fetch some arbitrary subset then GraphQL is a good fit for the transport.
plus1 1
a
I would just use nullable properties. So long as the properties have a backing field you’ll be able to use type inference
Copy code
val tvShow = getTvShow(...)

if (tvShow.credits != null) {
  tvShow.credits.forEach { ... } // credits is inferred to be non-null
}
d
I don’t like nullable in these cases, for example
tvShow.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 😄 )
I’m not trying to balance value/effort, I just want the most value, no matter the effort 🙂 I rather spend a whole month for it 😄
a
if for
tvShow.firstAirDate
you need to differentiate between ‘no data’ and ‘not released’, then that sounds like a candidate for a sealed interface with specific subtypes
Copy code
sealed interface FirstAirDate

object NoData : FirstAirDate
class Unreleased(val plannedDate: LocalDate?) : FirstAirDate
class Released(val date: LocalDate) : FirstAirDate
👍 1
d
I kinda did something already 😄
That’s what I did so far
d
Yes, a generic sealed interface value for each field, that either gives the data or a reason why it isn't present. You can have value classes as members of a sealed interface hierarchy, which reduces the runtime overhead, approaching something like a union type in practice.
plus1 1
d
Could you describe your suggestion, perhaps with a brief example? 🙏 Because from my understanding I’d need N subclasses, like
Copy code
ScreenplayWithExtra {

  data class WithCredits...
  data class WithGenres...
  data class WithCreditsAndGenres...
  ... x9999
}
y
There's a hacky way to do it with sealed interfaces and intersection types. Basically, you need to have a sealed interface for every field that you want to optionally control. Then (playground):
Copy code
fun 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)
The only hacky part is that suppression because the compiler forbids these weird bounds, but only because they're not java-compatible. In fact, for
InlineOnly
functions that the stdlib defines, this pattern is just fine, and it is used for some collection functions.
d
Uh, this sounds interesting! I'll check it better as soon as I'm on PC, thank you!
d
I had a simpler suggestion in mind, though not sure if it addresses your use case(?)
Copy code
sealed interface Optional<T> {
    object NoData : Optional<Nothing>
    object NotRetrieved : Optional<Nothing>
    @JvmInline
    value class(val value: T): Optional<T>
}
...
Copy code
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.
y
Issue with that is there's no compile-time guarantee as to what values exist. My solution, as hacky as it is, makes the compiler aware of exactly what fields should exist.
👍 1
d
🤔 But I didn't read that there's any design time guarantees as to what fields should exist either.
@Davide Giuseppe Farella please clarify do you want to define specific subsets of fields that should exist at any given time?
d
Yes, exactly, perhaps I created some confusion with the `firstAirDate`: with that I didn’t mean that using nullable would create an issue with it (as
firstAirDate
is also part of the “main” tv show json), I just wanted to show a case for which I would use nullable, instead
As @Youssef Shoaib [MOD] mentioned, I seeking a compile-time guarantee to a have a given field, if I requested it
y
Your other option, btw, would be to create n^n methods for all the possible permutations of n Extras, and n! (that's factorial) subinterfaces that represent each combination of Extras that a user might want. For the hacky solution, you only need n classes that represent the Extras, and n variations of
getTvShow
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 needs
d
I’m looking into your suggestion, and it seems promising! The only doubt I have is that I have to use generics throughout (e.g. there is no type to represent it, so I can’t just use
fun 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.
y
You can generate those types using multiple type-bounds, which don't even need a suppression (because those are supported by Java) (playground):
Copy code
fun 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
.
d
Yes, that’s what I meant with
I have to use generics throughout
But actually it doesn’t look that bad. I imagined it worse in my head 😛
y
And look, soon enough (don't take my word for this) we are going to have intersection and union types in the language, and then you'll be able to just say
useTvShow(tvShow: WithCredits & WithGenres)
, and the suppression hack will be replaced with just
getTvShow(...): S1 & S2
🤩 2
d
It seems like the IDE is also very smart about it 🙂
I like it! Thank you!
y
Yeah, that's because intersection types are actually in the language, but only internally. Their semantics are well-defined in the language specifications, and they're used especially for smart casts. They're just not exposed directly to the user, except through generics.
😵 1
d
I thought it was science fiction till yesterday (to stay in context 🙂). Thanks for the info!
d
Clever solution @Youssef Shoaib [MOD] 👏
Since you've clearly thought about this... Would you say in practice a sealed interface with all value classes is like a union type already?
y
Sort of. Only issue is that, when you pass the value classes as the sealed interface type, they get boxed. There's some tricks you can employ with inline functions so that the compiler can always know that you're using a value class, but it's very difficult to do it right without sacrificing semantics. Perhaps with project Valhalla though that will be possible.
👍 1
d
Yes, I read about the boxing, but the documentation is very vague about when boxing applies 🤔
I see, at least when passed virtually.
y
I think that's because there's hopes of optimising it. For instance, there's
sealed value interface
that can possibly come to Kotlin but it's still in the works
👍 1
d
I was a little disappointed that inner classes didn't get treated as sealed since they can only be defined in a limited scope (the class) making them, presumably, enumerable
y
Sealed has very specific semantics though, including that no one outside of the module can implement it. That's totally unrelated to being an inner class (which just has a receiver of the outside class, similar to context classes)
d
Unless I'm mistaken you can't extend the inner class of another class
Which would mean it does have that first property
👀 1
d
Hey @Youssef Shoaib [MOD] may I ask you some ideas about how to implement the part you marked as
TODO
? 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
Copy code
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>>
y
Not sure what the point of applyExtras is since it does nothing internally, but yes that's basically what I had in mind: a method that takes a
vararg
or list or whatever of Extras that then sets the lateinit fields as it wishes
🙏 1
d
applyExtras
is setting the
lateinit
values
y
Ohhhhh, I'd say have
resolve
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.
d
resolve
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!
y
No problem! Don't hesitate to ask if you run into any issues
🍻 1
d
We did it K
Copy code
When("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()
        }
    }
}
K 2
It also brought visible performance improvements, by fetching only strictly needed data 🤩
I noticed intersection types don’t play well with
StateFlow
, is this know? Is it expected? Here’s how I fixed it for my use case https://github.com/fardavide/CineScout/commit/f2e3993816452afaf6c3f1e7c7a384eb9a66c06e
P.S. I just quickly patched it 😄 I’ll add some tests later and add more details if needed (and perhaps move the discussion elsewhere)
y
I don't think it's an intersection type issue in this case, I think it's just how compose works
d
True, I was assuming the StateaFlow was not emitting, but indeed it could be Compose instead!
y
Easy way to check might be to cast the stream as a regular non-intersection type. In general, they really shouldn't be affecting how any code is generated
d
I will add some tests in the coming days, thank you