Restricting interfaces to only allow some of the subtypes of a `sealed class`


#1

I’m trying to make a type to represent, let’s say, different image types in Android. Depending on how the image got to my function, it may be a URI to some resource on the device, a bitmap in memory, or an android drawable. (Note: this example is purely for explanatory purposes, I’m really thinking about a generic problem where something can be one of a discrete number of types. Don’t point out Android or bitmap-specific solutions!)

The way I’d normally write this is with something like:

sealed class UriOrBitmapOrDrawable {
    data class Uri(val uri: Uri) : UriOrBitmapOrDrawable ()
    data class Bitmap(val bitmap: Bitmap) : UriOrBitmapOrDrawable ()
    data class Drawable(val drawable: Drawable) : UriOrBitmapOrDrawable ()
}

And then my function can be

fun displayImage(image: UriOrBitmapOrDrawable, view: ImageView) {
    when(image) {
        is UriOrBitmapOrDrawable.Uri -> displayUri(image.uri, view)
        is UriOrBitmapOrDrawable.Bitmap -> displayBitmap(image.bitmap, view)
        is UriOrBitmapOrDrawable.Drawable -> displayDrawable(image.drawable, view)
    }
}

… or something like that.

However, in some places my code already knows for sure that it can’t be a Uri, so any functions like the above really don’t need a branch for UriOrBitmapOrDrawable.Uri. Moreover, in these places maybe a Uri can’t be processed. Keeping the branch for Uri here would just be dangerous code, that I hope never gets hit. The easiest solution is to just do throw RuntimeException("unreachable") in the Uri branch and hope this is made rigorous by Unit Tests, but I’d really prefer that the compiler could pick up on this instead of relying on runtime exceptions.

One way to solve this is by using several sealed classes, with all the different combinations: UriOrBitmap, UriOrDrawable, BitmapOrDrawable, as well as the UriOrBitmapOrDrawable class above. This allows me to be completely prescriptive as to what is possible in all my interfaces - and avoids having to deal with impossible cases.

sealed class UriOrBitmap {
    data class Uri(val uri: Uri) : UriOrBitmap ()
    data class Bitmap(val bitmap: Bitmap) : UriOrBitmap ()
}

sealed class UriOrDrawable {
    data class Uri(val uri: Uri) : UriOrDrawable ()
    data class Drawable(val drawable: Drawable) : UriOrDrawable ()
}

sealed class BitmapOrDrawable {
    data class Bitmap(val bitmap: Bitmap) : BitmapOrDrawable ()
    data class Drawable(val drawable: Drawable) : BitmapOrDrawable ()
}

sealed class UriOrBitmapOrDrawable {
    data class Uri(val uri: Uri) : UriOrBitmapOrDrawable ()
    data class Bitmap(val bitmap: Bitmap) : UriOrBitmapOrDrawable ()
    data class Drawable(val drawable: Drawable) : UriOrBitmapOrDrawable ()
}

fun getContactAvatar(contact: Contact, defaultAvatar: Drawable): UriOrDrawable {
    // try to get the cached drawable first, else look up in the Android contacts, else use default
    return getCachedAvatarDrawable(contact)?.let { UriOrDrawable.Drawable(it) }
        ?: lookupContactAvatarUri(contact)?.let { UriOrDrawable.Uri(it) } 
        ?: UriOrDrawable.Drawable(defaultAvatar)
}

My question is: is there a nicer way to do this? A few things that would be nice, that we don’t get with the solution above:

  • UriOrDrawable can be safely cast to UriOrBitmapOrDrawable

    • This is clearly desirable, as if something is a Uri or a Drawable, it’s certainly a Uri or a Bitmap or a Drawable.
    • In the above, UriOrDrawable and UriOrBitmapOrDrawable are both separate sealed classes. It seems difficult to see how I could make UriOrDrawable a subclass of UriOrBitmapOrDrawable, while also making UriOrBitmap a subclass of it. How would I represent a Uri?
  • Less bloat

    • This clearly doesn’t scale very well for classes with four possibilities, or five.

#2

(Posting as a reply as this is just one approach to the above problem - I’m still open to exploring others.)

One approach I explored, but got stuck on, was to use generics with the Nothing type. For example, lots of places seem to use this implementation of the Either type:

sealed class Either<out A, out B> {
    data class First<out A>(val first: A) : Either<A, Nothing>
    data class Second<out B>(val second: B) : Either<Nothing, B>
}

you could in theory use Either<Nothing, Bitmap> to encode something that has to be a Bitmap (as it’s always an Either.Second<Bitmap>)… but most people would just use the Bitmap type on its own. But with three it becomes more interesting:

sealed class Choice<out A, out B, out C> {
    data class First<out A>(val first: A) : Choice<A, Nothing, Nothing>()
    data class Second<out B>(val second: B) : Choice<Nothing, B, Nothing>()
    data class Third<out C>(val third: C) : Choice<Nothing, Nothing, C>()
}

Now, I can use Choice<Uri, Bitmap, Drawable> instead of UriOrBitmapOrDrawable, but I can also use Choice<Uri, Bitmap, Nothing> instead of UriOrBitmap. That’s great, right? I’ve solved it!

Alas, no. The following code does not compile:

fun displayImage(image: Choice<Uri, Bitmap, Nothing>, view: ImageView): String {
    return when (image) {
        is Choice.First -> displayUri(image.first, view)
        is Choice.Second -> displayBitmap(image.second, view)
    }
}

… despite the fact that the missing branch (Choice.Third) is unreachable (since it casts image to a data class containing a Nothing, which is uninstantiable). Allowing this would be a neat language improvement.

The best solution I’ve found is the following:

// This function can never actually be called during runtime.
fun Choice<Nothing, Nothing, Nothing>.isUnreachable(): Nothing {
    throw RuntimeException("unreachable")
}

fun displayImage(image: Choice<Uri, Bitmap, Nothing>, view: ImageView): String {
    return when (image) {
        is Choice.First -> displayUri(image.first, view)
        is Choice.Second -> displayBitmap(image.second, view)
        is Choice.Third -> image.isUnreachable() // This only compiles when this branch is unreachable.
    }
}

This is good because developers can always slap on an isUnreachable(), and if it compiles then it genuinely is unreachable and they don’t have to worry about that branch.


#3

I’ve not studied much category theory or type theory, but I think I’m essentially trying to implement the following in Kotlin:

  • A
  • A+B
  • A+C
  • B+C
  • A+B+C

(where + is a disjoint union) with an easy way to represent A+B as (isomorphic to) a subtype of A+B+C.

My answer in the reply above attempts to represent A+B as A+B+⊥ (where ⊥ is the bottom/empty type,
represented by Nothing in kotlin).