Zac Sweers
05/13/2020, 7:33 PMsealed typealias Pet = Dog | Cat | String
• Keywords already exist. RHS expression syntax would need a little work but it could work
• At runtime they’re all just objects getting casted, but there’s plenty of precedent for this (generics)
• Self-contained in the alias expression
• Doesn’t allow anonymous expressions like typescript or similar languages do, which I think is a net positive
• Can be compiler-checked just like sealed classes but without requiring inheritance. Instance check just results in a smart cast like sealed classes or other instance checks do today
when (somePet) {
is Dog ->
is Cat ->
is String ->
}
Ruckus
05/13/2020, 7:37 PMCan be complier-checked
Not really, especially when taking any sort of interior into account
stojan
05/13/2020, 8:00 PMZac Sweers
05/13/2020, 8:19 PMRuckus
05/13/2020, 8:24 PMZac Sweers
05/13/2020, 8:25 PMZac Sweers
05/13/2020, 8:28 PMgildor
05/14/2020, 6:45 AMsealed typealias Pet = Dog | Cat
sealed typealias Animal = Dog | Cat | Lion
fun Animal.feed()
val pet: Pet = Cat()
pet.feed()
??gildor
05/14/2020, 6:47 AMlouiscad
05/14/2020, 6:51 PMUberto Barbini
05/14/2020, 7:11 PMlouiscad
05/14/2020, 7:23 PMwhen
anyway with smart casts.
Java interop would be not ideal, but interop is still there, you can cast in java.Uberto Barbini
05/14/2020, 7:38 PMUberto Barbini
05/14/2020, 7:38 PMelizarov
05/14/2020, 7:42 PMlouiscad
05/14/2020, 7:42 PMtypealias
and everything Kotlin, and this feature proposal would also work in consumer Kotlin projects thanks to that metadata that would support this as well.
I once unintentionnaly ditched Kotlin metdata from a library publication, and it was recognized as a java library using only info from the class files, which meant no extensions, no named arguments, not type aliases, etc.Uberto Barbini
05/14/2020, 7:43 PMelizarov
05/14/2020, 7:43 PMRuckus
05/14/2020, 7:46 PMelizarov
05/14/2020, 7:46 PMlistOf(1, "A")
then? Now it is List<Any>
. But with unions should it become List<Int | String>
then?elizarov
05/14/2020, 7:49 PMlouiscad
05/14/2020, 7:50 PMList<Any>
as an explicit type for a list of mixed types where inference would now generate an implicit type union.
That said, Zac's original proposal would never be implicit as it requires an explicit typealias declaration that becomes the union type.Uberto Barbini
05/14/2020, 7:52 PMRuckus
05/14/2020, 7:53 PMelizarov
05/14/2020, 7:53 PMlouiscad
05/14/2020, 7:58 PMlouiscad
05/14/2020, 8:00 PMelizarov
05/14/2020, 8:01 PMtypealias DogOr<T> = Dog | T
. What you do with if (dogOrT is T)
condition when you have DorOr<Dog>
. Even more trickier are things like Either<A, B> = A | B
that FP people would love to have. How would they even work? How'd you even deconstruct it into its left and right parts using when
or if
?elizarov
05/14/2020, 8:03 PMelizarov
05/14/2020, 8:06 PMUberto Barbini
05/14/2020, 8:12 PMlouiscad
05/14/2020, 10:51 PMInt | Int
at runtime.raulraja
05/15/2020, 7:46 PMraulraja
05/15/2020, 7:48 PMraulraja
05/15/2020, 7:49 PMraulraja
05/15/2020, 7:50 PMraulraja
05/15/2020, 7:51 PMraulraja
05/15/2020, 7:53 PMraulraja
05/15/2020, 7:54 PMraulraja
05/15/2020, 8:06 PMUberto Barbini
05/15/2020, 9:56 PMUberto Barbini
05/15/2020, 9:57 PMUberto Barbini
05/15/2020, 10:02 PMZac Sweers
05/18/2020, 3:44 PMkushalp
05/20/2020, 8:37 AMUnion
container?raulraja
05/22/2020, 3:33 PMraulraja
05/22/2020, 3:39 PMraulraja
05/22/2020, 3:41 PMraulraja
05/22/2020, 3:41 PMelizarov
05/22/2020, 3:42 PMraulraja
05/22/2020, 3:43 PMelizarov
05/22/2020, 3:44 PMAny
or to generics and how that behaves w.r.t. subtypes checking at runtime using is
operator.raulraja
05/22/2020, 3:45 PMraulraja
05/22/2020, 3:45 PMraulraja
05/22/2020, 3:46 PMraulraja
05/22/2020, 3:47 PMraulraja
05/22/2020, 3:47 PMraulraja
05/22/2020, 3:48 PMelizarov
05/22/2020, 3:48 PMraulraja
05/22/2020, 3:49 PMraulraja
05/22/2020, 3:49 PMraulraja
05/22/2020, 3:49 PMraulraja
05/22/2020, 3:50 PMraulraja
05/22/2020, 3:51 PMraulraja
05/22/2020, 3:53 PMraulraja
05/22/2020, 3:55 PMraulraja
05/22/2020, 3:55 PMraulraja
05/22/2020, 3:55 PMraulraja
05/22/2020, 3:56 PMelizarov
05/22/2020, 3:58 PMDog | Cat
then, in Kotlin, one should be able to abstract over both of them via some fun <A, B> foo(): A | B
. Now, it begs a question of what syntax one shall use when they have val x: A | B = ...
to figure out whether x is A
or x is B
and what happens when the check for x is SomeInterface
is performed when both A
and B
at run time implement this interface. Should this even be allowed and what should be the effect of this check?
TL;DR: The question is not just how it integrates into compile-time type system, but also into a run-time type system (as they are different).raulraja
05/22/2020, 4:01 PMraulraja
05/22/2020, 4:02 PMfun <A: Persistence & Datasource> A.foo(): Unit
raulraja
05/22/2020, 4:02 PMraulraja
05/22/2020, 4:03 PMfun <A: Persistence | Datasource> A.foo(): Unit
raulraja
05/22/2020, 4:04 PMraulraja
05/22/2020, 4:04 PMraulraja
05/22/2020, 4:06 PMraulraja
05/22/2020, 4:06 PMraulraja
05/22/2020, 4:06 PMelizarov
05/22/2020, 4:10 PMreadData(): Data | Failure
That's great and concise, everyone wants it. But I also want fun retry(block: () -> Data | Failure)
function so that I can write retry { readData() }
So far so good. But now I want to abstract retry
over the type of Data
to fun <D> retry(block: () -> D | Failure)
. and then maybe over the type of Failure
too, to fun <D, F> retry(block: () -> D | F)
. How that's going to work? How's the retry
function going to be syntactically written?elizarov
05/22/2020, 4:15 PMelizarov
05/22/2020, 4:18 PMfun <D, F> retry(block: () -> D | F): D
should ideally look like assuming that it simply retries endlessly on failure?raulraja
05/22/2020, 5:31 PMraulraja
05/22/2020, 5:31 PMsuspend fun <D, F> retry(block: suspend () -> D | F): D =
when (val df = block()) {
is D -> df
is F -> retry(block)
}
raulraja
05/22/2020, 5:31 PMelizarov
05/22/2020, 5:43 PMval df: Any
type. Would is D
and is F
check still work and should it be allowed? Now how df is SomeInterface
is going to conceptually work when, at runtime, both D
and F
implement this interface?elizarov
05/22/2020, 5:46 PMis T
performs a runtime type check. What I see in this code that union types in your example will (should?) allow for is
to perform a compile-time-aided (for a lack of a better word) type check. Thus interaction between compile-time and run-time type systems becomes important.elizarov
05/22/2020, 5:49 PMis
operator that does slightly different thing in different contexts? Should we have two different operators or can we design it so that there are no error-prone ambiguities?raulraja
05/22/2020, 5:54 PMis D
or is F
can be allowed if df : Any
if you add an else clause to become exhaustive and that would work in the same way as it works today if types were reified in this case. But in the case of the compiler knowing the match is exhaustive it can be turned into an int indexed table-switchraulraja
05/22/2020, 5:58 PMraulraja
05/22/2020, 5:58 PMraulraja
05/22/2020, 6:00 PMraulraja
05/22/2020, 6:00 PMis (A | B)
raulraja
05/22/2020, 6:01 PMA?
is used for nullability to represent Nullable Araulraja
05/22/2020, 6:03 PMA & B
raulraja
05/22/2020, 6:05 PMUberto Barbini
05/22/2020, 6:26 PMraulraja
05/22/2020, 6:44 PMraulraja
05/22/2020, 6:45 PMraulraja
05/22/2020, 6:45 PMraulraja
05/22/2020, 6:46 PMtypealias Pet = Cat | Dog
raulraja
05/22/2020, 6:46 PMtypealias MyPet = Dog | Cat
raulraja
05/22/2020, 6:46 PMraulraja
05/22/2020, 6:47 PMPet? == Cat | Dog | null
raulraja
05/22/2020, 6:48 PMMyPet? == Dog | null | Cat
raulraja
05/22/2020, 6:48 PMraulraja
05/22/2020, 6:48 PMDog | Dog
raulraja
05/22/2020, 6:50 PMDog
raulraja
05/22/2020, 6:50 PMraulraja
05/22/2020, 6:51 PMval animal: Animal = myPet // Cat | Dog
raulraja
05/22/2020, 6:52 PMraulraja
05/22/2020, 6:53 PMAnimal?
if it was C`at | Dog | null`raulraja
05/22/2020, 6:57 PMraulraja
05/22/2020, 6:57 PMraulraja
05/22/2020, 6:59 PM|
to separate what in kotlin we do as sealed class hierarchies?raulraja
05/22/2020, 7:02 PMdata Bool = False | True
typealias Bool = False | True
raulraja
05/22/2020, 7:03 PMUberto Barbini
05/22/2020, 7:29 PMUberto Barbini
05/22/2020, 7:30 PMUberto Barbini
05/22/2020, 7:42 PMreadData(): Data | Failure
As a dev that use Outcome<ERR, T> everywhere in a big codebase 8 hours a day for years... I don't see a big advantage here using typealias.
Not sure why the last example fun <D, F> retry(block: () -> D | F)
would be preferrable over a real type Outcome, and I can think to several reasons why having a specific type would be better.
My motivation for union types would be more having stuff like:
parseJsonValue(): Int | String | Date | Node
Where having a sealed classes just to throw them away seems unnecessarily and a typealias would do ok.raulraja
05/22/2020, 7:55 PMlouiscad
05/22/2020, 8:47 PMraulraja
05/22/2020, 9:02 PMlouiscad
05/22/2020, 9:32 PMraulraja
05/22/2020, 9:35 PMZac Sweers
05/22/2020, 9:35 PMtrue
or null
wasn’t in this proposal and sort of confuses the discussion here. I’d suggest dropping that for nowraulraja
05/22/2020, 9:37 PMAny?
Zac Sweers
05/22/2020, 9:40 PMfun <T?> foo(): T where T : Serializable
Zac Sweers
05/22/2020, 9:42 PMfun foo(): (Dog | Cat)?
raulraja
05/22/2020, 9:42 PMtypealias Foo = String?
sealed typealias Foo = String? | Int?
Zac Sweers
05/22/2020, 9:44 PMsealed typealias
could have different rules, same way fun interface
or inline class
have different rules from regular interfaces or classesraulraja
05/22/2020, 9:44 PMZac Sweers
05/22/2020, 9:45 PMsealed typealias Foo = String | Int
fun returnsNullableFoo(): Foo?
raulraja
05/22/2020, 9:50 PM(A | B | C)?
then the typealias restriction you propose would work.
fun a(): A | B | C
fun b(): (A | B)?
fun c(): C
fun z() = if (predicate) a() else b() ?: c()
Uberto Barbini
05/22/2020, 10:41 PMA | B | null
seems much better than
( A | B )?
to meraulraja
05/22/2020, 10:48 PMZac Sweers
06/21/2020, 3:41 AMcatch (ExceptionA | ExceptionB | ExceptionC e) {
}
Supporting this could facilitate supporting multi-catch blocks too: https://youtrack.jetbrains.com/issue/KT-7128Zac Sweers
09/01/2020, 9:37 PM|
, then you don’t need to denote typealias or anything. It also becomes easy to write
typealias Pet = Dog | Cat
fun pet(input: Dog | Cat): {
}
fun getPet(): Dog | Cat {
}
val Shelter.getPet: Dog | Cat get() = ...
fun (Cat | Dog).pet()
It gets harder to read with higher order functions, but that’s an existing cost anyway. A complex example
fun (Cat | Dog).pet(chooser: (List<(Cat | Dog)>) -> (Cat | Dog)) {
// do stuff
}
raulraja
09/02/2020, 9:25 AM(List<Cat | Dog>) -> Cat | Dog
raulraja
09/02/2020, 9:26 AM(List<Union2<Cat, Dog>>) -> Union2<Cat, Dog>
elizarov
09/02/2020, 9:27 AM<...>
but whether you need to put braces to the right hand-side of ->
could be an interesting (albeit minor) design issue. The major design problem here is how union types would interact with Kotlin type system and generics.Zac Sweers
09/03/2020, 2:44 AMAny
until type checked?louiscad
09/03/2020, 7:26 AMAny?
raulraja
09/04/2020, 9:40 AMAny? == Any | null
and Any | Any == Any
etc, unions and intersections can’t be implemented any other way in the current Kotlin type system without breaking generic type constrains in generic functions which are based on the dual of the union, the intersection.
I think a good baseline for what Unions can look in Kotlin is the Scala dotty impl and spec since Dotty and Scala are also subtype based and Kotlin does not need to account for path dependent types or other esoteric features.
https://dotty.epfl.ch/docs/reference/new-types/union-types.html
https://dotty.epfl.ch/docs/reference/new-types/intersection-types.htmlraulraja
09/04/2020, 10:15 AMwouldn’t the type in this case be treated asThe super type of a union type is where their hierarchy intersect. Not sure if I understood your question but in the case of something likeuntil type checked?Any
Dog | Cat
the compiler knows the bound is Animal
in the same way as sealed classes. Union types untag sealed hierarchies but the inference rules are the same since they are still constrained by sub typing.
@elizarov Regarding your initial question about generic constrains in functions they should naturally work in bounds if the rules regarding subtyping and making them part of the hierarchy are respected. For example in this case:
fun <A: Animal, B: Plant> foo(creature : A | B)
Both A and B are constrained, used alongside a Union and don’t necessarily need a common parent. If they had Creature
as parent the Api of Creature
is available, if they don’t their upperbound is still Any
in all cases. Was that your concern about mixing subtyping, generic constrains and unions or did I missunderstood? Thanks.Zac Sweers
09/04/2020, 3:08 PMlouiscad
09/05/2020, 10:56 PMNSInteger
that is Int
on watchOS and iosArm32, and Long
otherwise?