https://kotlinlang.org logo
#language-proposals
Title
# language-proposals
a

andylamax

12/01/2023, 9:30 AM
I don't know if this has been brought up yet, but, won't bringing user defined intersection types invalidate the usecase of contex receiver's?? something like
Copy code
typealias LoggableDatabase = Loggable & Database

fun LoggableDatabase.listAll() : List<Thing> {
   log("listing") // from (this as Loggable)
   return query("select * from things") // from (this as Database)
}
Would endup being almost close to
Copy code
context(Loggable,Database)
fun listAll() {
   log("listing") // from (this as Loggable)
   return query("select * from things") // from (this as Database)
}
This question can be phrased in another way. e.g. Shouldn't the kotlin team work on intersection types and that will give us both, intersection types and context receivers functionality as well???? Thoughts????
s

Sam

12/01/2023, 9:34 AM
One big difference is that the intersection type requires you to provide one object that implements both types, whereas the context receiver version allows you to use two separate objects.
a

andylamax

12/01/2023, 11:45 AM
aren't you mixing up between inherited types and intersected types?
Copy code
interface A {}

interface B {}

typealias TAB = A & B // intersected type

class CAB : A,B {} // inherited type
What you just explained fits the description of inherited types but not intersected types. (But again, I think all inherited types do also intersect their respective interface abstractions)
// given
Copy code
val tab: TAB = getTAB()
val cab: CAB = getCAB()

fun useType(ab: TAB) {

}

fun useClass(ab: CAB) {

}
// then
Copy code
useClass(cab) // should work
useClass(tab) // should fail ?? not sure

useType(tab) // should work
useType(cab) // should work
e

edrd

12/01/2023, 12:27 PM
This is possible even today:
Copy code
fun <T> T.listAll(): List<Thing> where T : Loggable, T : Database { /* ... */ }
The difference is in the call site, as Sam said. You’d need to create a type that implemented both
Loggable
and
Database
a

andylamax

12/01/2023, 12:36 PM
So, what is possible today, is not fully a user defined intersection type (That's why we need to implemented both
Loggable
and
Database
). With intersected types,
Loggable
and
Database
can even be final classes (instead of interface), which would not need a user to implement both of them because they are not only limited to interfaces
e

edrd

12/01/2023, 1:42 PM
With intersected types,
Loggable
and
Database
can even be final classes (instead of interface)
You mean with context receivers, right? Because with intersection types, the type must implement all specified types, either by inheriting an open/abstract/sealed class or by implementing an interface
a

andylamax

12/01/2023, 6:49 PM
I actually meant intersected types when i typed that If user defined intersected types will be introduced, one should be able to define intersected types just the same way one will be able to to define union types. something like
Copy code
typealias IntOrList = Int | List<Int>
for unions and something like
Copy code
typealias IntAndList = Int & List
these are clearly just examples, but should be doable
f

Federico d'Alonzo

12/01/2023, 7:49 PM
I'm guessing there are some misunderstandings here: typealiases in kotlin are nothing more than a utility for developers, they don't "define" anything. The typealias
IntOrList
you provided will be inlines and expanded directly to
Int | List<Int>
at all use sites. Compiled code will contain no reference to
IntOrList
. A context receiver is nothing more than an extra parameter of the function that can be used (almost) the same way as a direct receiver, so for our purposes the functions
Copy code
fun String.direct(): Int = length

context(String)
fun ctx(): Int = length
are the same (albeit called differently where they are used) both of them will be compiled to extra arguments anyways. So when you have a function that let's say looks like this
fun foo(obj: Int | List<Int>)
you're defining a function that can take as input an Int or a List<Int>. When you have
fun foo(obj: Number & Collection<Char>)
you are defining a function that takes as input a singular object instance that has as a superclass the abstract class
Number
and implements the
Collection<Char>
interface. Of course,
&
types must still satisfy the constrains imposed by type inheritance. As such the type
String & Int
will have no possible instances, same as
Nothing
because both String and Int are sealed types which you cannot extend.
a

andylamax

12/01/2023, 8:00 PM
It should be noted that this is a proposed tentative sytax. Unions and Intersections are not yet supported in the kotlin language. So what I have been typing in this thread. Is not valid sytax and won't even currently compile
f

Federico d'Alonzo

12/01/2023, 8:01 PM
yes, that is true, but what I wrote about is how the feature will most likely work out to be if it will be implemented in the future
e

edrd

12/01/2023, 8:01 PM
I actually meant intersected types when i typed that
So it’s not possible…
Int
class is final, for example, so no type will ever be able to implement it.
a

andylamax

12/01/2023, 8:04 PM
@edrd I think it is possible if you are capable of looking at intersection types beyond OOP. Something like
Copy code
with(3) {
  with(listOf("a","b")) {
    val x = this // x here can be refered to as intersection of Int and List<String>
  }
}
But ofcourse, this is very similar to how one would use context receivers which brings me to my whole point above
A good example can be derived from how typescript handles intersection types
f

Federico d'Alonzo

12/01/2023, 8:22 PM
Here's the difference in your example
Copy code
context(Int, List<String>)
fun ctx() {
    val a = this // error: 'this' is not defined in this context
    val b = this@Int // explicitly reference the first Int type object in the context list
    val c = this@List // explicitly reference the first List<*> type object in the context list

    size // can call List<String> methods
    toLong() // can call Int methods
}

fun (Int | List<String>).type() {
    // you don't know if this is either an Int or a List yet, so you can't really call anything with this receiver
    println(this::class.simpleName) // there is only one "this", of type Int | List<String>
}

fun main() {
    with(3) int@{ // this: Int
        with(listOf("a", "b")) list@{ // this: List<String>
            val a = this // reference to the closest receiver: List<String>
            val b = this@int // explicit reference to the Int receiver
            val c = this@list // explicit reference to the List<String> receiver

            type() // type called here will *always* output some list class, never an int,
                   // because it takes the *closest* receiver that fully satisfyes the type
        }
    }
}
Obviously
fun (Int | List<String>).type()
is not real syntax
also, if you define the context as
context(Int, Number)
you get this error, because you would have to always explicitly reference one or the other, which would defeat the purpose of context receivers in the first place.
d

Daniel Pitts

12/01/2023, 9:27 PM
It seems to me that both Context and Type Intersections, while they both feel similar, really are quite different and solve different types of problems.
Type Intersection is when you have one object that is of multiple types, and Context is when you have multiple objects that are available implicitly, each with their own type.
👌 2
a

andylamax

12/02/2023, 12:03 AM
You do have a point @Daniel Pitts
and the question I am trying to ask here, is. If the language had intersection types implemented, would there be a need to have context receivers?? (implementation difference aside)
@Federico d'Alonzo I like how you elaborated the potential difference between the two. You made it easy to understand the difference in implementation completely
d

Daniel Pitts

12/02/2023, 12:47 AM
Yes, there would still be a need for context receivers, as they solve a different problem.
a

andylamax

12/02/2023, 1:38 AM
Can you come up with an example problem that context receiver would be able to solve and intersected types would fail to solve it?
d

Daniel Pitts

12/02/2023, 5:34 AM
Creating an extension method intended to be called from within a nested context of a DSL.
Copy code
context(Outer, TheContext, Nested)
fun Parameter.doThing() = ...

class Outer { fun nested(block: Nested.(Parameter)->Unit) {...} }
...
outer {
   nested { parameter ->
      with (theContext) {
         parameter.doThing()
      }
   }
}
There are 4 objects, not one. Interesected types is for a single object that has multiple inheritance.
a

andylamax

12/02/2023, 7:46 AM
I do see your point. And yes, a problem like this is better approached with context receivers than intersected types. Thanks for taking your time and sharing this