Hi, I'm learning Kotlin. I liked what I saw until ...
# getting-started
s
Hi, I'm learning Kotlin. I liked what I saw until I ran into pattern matching. I was disappointed to see that, AFAIK, there's no record pattern matching (destructuring) or enumeration (sealed class) pattern matching (with
when
..
is
). AFAICS, pattern match is superior in Java. Am I missing something? Are improvements on the roadmap?
e
compared to Java 21, yes record patterns and
switch
pattern matching have become more expressive than the equivalents in Kotlin
s
but there is simple positional destructuring. e.g. da data class can be destructured like this: val (str, nr) = Pair("adf", 34) more here: https://www.baeldung.com/kotlin/destructuring-declarations
👍 1
also you can definitely have a when-expression with is-Statements which also tests for completness of all possible values for sealed classes
Copy code
when(val outer: Outer = ... ) {
        is Outer.Inner1 -> {println(outer.str)}
        is Outer.Inner2 -> {println(<http://outer.nr|outer.nr>)}
    }
the complete example is here
e
Kotlin currently only has positional destructuring and you need to do so separately from the type check, so the equivalent to Java's
Copy code
switch (any) {
    case Pair(var first, var second) -> // ...
}
takes the form of
Copy code
when (any) {
    is Pair -> {
        val (first, second) = any
        // ...
👍 1
as a workaround for the lack of name-based destructuring, you can use scope functions like
Copy code
with(pair) {
    first + second // can use directly
but it's not really the same as destructuring, it's just bringing those properties into scope
❤️ 1
I'd also be happy if we got case guards in Kotlin (
when
in Java)… you can see a request in https://youtrack.jetbrains.com/issue/KT-13626/Guard-conditions-in-when-with-subject which is a child of one of the previous links
👀 1
c
Java Records are in some ways superior to Kotlin Data Classes. I wonder if Kotlin has room for them or if Record Class and Data Class would be too similar.
s
Thanks all. I had missed simple destructuring declarations with
componentN
functions! @Stephan Schröder, I saw the
when ... is
, but that is what is disappointing compared with Java. @ephemient, the scoped functions look interesting as a workaround to
Copy code
when (any) {
    is Pair -> {
        val (first, second) = any
which makes me sad
This, of course, gets worse when you have nested patterns.
The conversations on the issue tracker are well worth the read, thanks. Those are old issues. Does anyone know if language features like these will accelerate once the switch to the Kotlin 2 compiler happens?
e
Kotlin 2 isn't a language change, it's a compiler implementation change, and these are language design issues, so I wouldn't consider them to be connected
s
@ephemient I understand. However, I read that the v2 compiler will allow some enhancements to come through that haven't been possible with the v1 compiler.
s
Hi @Steven Shaw yes, language features will accelerate, once we have the K2 Compiler because, there is only one backend to change, pattern matching is not on the map but name based destructoring is, which sounds close. These (see screenshot) are the most requested features (from

KotlinConf23-Keynote

). Patternn-matching is brand new and won't even be available in Java for another 3 months (at least without non-LTS-Java versions without feature flag, actually I'm not sure how many of the feature is avalable on Java yet, since we only use LTS-versions at work 🤔, so I haven't encountered Java's new pattern matching yet).
👍 1
s
Thanks @Stephan Schröder. I'll check out the talk! As for Java, as I'm only exploring what's happened since 2014-5, I'm able to use the latest JDK with the previews enabled (without any corporate shackles). However, all this stuff (and virtual threads) goes GA as soon as 21 LTS is released! That's pretty exciting. Protecting oneself from NPE in Java is impossible/tedious, so Kotlin is still ahead there. I haven't found the right solution for null-tracking in Java. There used to be FindBugs, but now seem to be a few options, including https://jspecify.dev/ and https://checkerframework.org/. Scala 3 is experimentally looking at "Explicit Nulls".
Targeting the JVM is annoying for enlightened programming languages. Compromises are made at the behest of compatibility. Given that Java is adopting something akin to algebraic data types, I am hopeful that in given another 10 years, Java may integrate "type classes" and have options for "multiversal equality", etc. It would be good to see more deprecations in Object.
s
the Java folks seem to take a look at nullability as well as part of Valhalla: Briefest summary of Design document on nullability and value types • I don't think this will make it into JDK25-LTS (maybe behind feature flags??) • they seem to propose a best effort nullabilty that is less sound than Kotlin's !?! (even though Kotlin's nullability isn't 100% sound either, I do have examples if you want to see them, I've never encontered a NPE while working with Kotlin though, so Kotlin's unsoundness doesn't seem to make itself known very much) It's definitely a development to take note of.
👀 1
s
Thanks for the pointers, @Stephan Schröder. It's good to hear that they're talking about nullability. I've subscribed to the mailing list. They are quite strong-armed by how object construction and initialisation work (it seems that the root cause is there somewhere). Is that where the problems turn up in Kotlin's null safety too? I would like to see those examples!
s
It would be better to eradicate null, but I can't imagine this is possible to do in an incremental way (for the Java designers).
I appreciate Kotlins nullability tracking, but it—out of necessity—embraces nulls rather than eradicating them.
s
@Steven Shaw I disagree regarding "It would be better to eradicate null". A nullable type in Kotlin is basically the same as an Option in Rust, it's no longer malicious, because it never comes as a surprise. examples for unsoundness in Kotlin: a)
Copy code
fun main() {
    println(A.s.length)
}

class A {
    companion object {
        val s: String = B.s
    }
}

class B {
    companion object {
        val s: String = A.s
    }
}
playground: https://pl.kotl.in/RwDuuVq41 b)
Copy code
fun main() {
    B()
}

abstract class A {
    constructor() {
        print()
    }

    abstract fun print()
}

class B:A() {
    val s: String = "abc"
    override fun print() {
        println(s.length)
    }
}
you can check it here: https://pl.kotl.in/ujk8CcpOw c) I forgot c 😅 So you can produce NPEs. I'm pretty sure b) is the most likely candidate. But It never happened to anyone I know in the 5 years I've been using Kotlin professionally 🤷‍♂️ I think the kind of NPEs you can trigger in Kotlin are very likely to be triggered every single time, not randomly depending on incoming data (though I'm sure you can construct those case).
❤️ 1
🤯 1
j
agree with Stephan,
null
is a natural concept, we just have to be deliberate about handling it
what's bad about
null
is not knowing if something is
null
or not
s
@Stephan Schröder, thanks for the examples. The companion object example is a surprising one!
On eliminating nulls. I understand the perspective that it's more-or-less like Option. However, it does have practical consequences when used in an API like
find
.
Since nulls are currently an intrinsic part of the Java Platform, it's preferable that Kotlin has the nullable and non-nullable types. I still think that what would be even better is that they were eradicated from the Java Platform and that Kotlin has no need for nullable types.
I can see how a particular style of coding in Java or Scala can lead to fewer NPEs. I've experienced this myself. The Java designers are calling it "data-oriented programming". With Kotlin, this should be even more greatly reduced because of the null tracking but also the nice static analysis (there was a warning for your second example, for instance). However, even in Kotlin, I will endeavour to use non-nullable types for everything. Then it's slightly annoying that nulls creep into my program through standard functions like
find
.
s
Again, I disagree, nulls can be useful (and not in the way that Go uses them, where it's trying to provide default behaviour as if each data type hade a useful default value). They denote the absence of things. Take for example a PersonDto with a secondary nullable AddressDto. It's perfectly fine for it to be null. Of course you could make PersonDto a sealed class with a variant with one address and a variant with two addresses, but than you'd had to match every time between them and add up with code a bit more complex than just with code that is fine with a nullable AddressDto as long as you don't need a non-nullable one. The problem with typesystems without nullability isn't that null exists, but that you don't know which values can't contain null and therefore everything is implicitly nullable. To your example
Copy code
println(xs.find { it == null }) // Is the null found or not?  -> yes it's null because it returns the object what matches the predicate, and the object is clearly null
find
is the wrong function, it returns the object, not the boolean if it's in there. nulls are not meant to be used as a form of boolean values. What you're looking for is
any
Copy code
val xs = listOf(3, null)
println(xs.any { it == 3 }) -> true
println(xs.any { it == 4 }) -> false
println(xs.any { it == null }) -> true
and this will of course return
true
A nullable type is excatly ok, if data can be absent in the domain model your code is representing. Typesystems with nullable types allow you to get rid of the bad source of nulls (invalid data) but retain the good source of nulls (possible absence of something within the domain).
s
@Stephan Schröder, I understand this point of view. I agree with
The problem with typesystems without nullability isn't that null exists, but that you don't know which values can't contain null and therefore everything is implicitly nullable.
However, imagine if you don't have
null
then you don't need a special type system that tracks it with special operators that handle it. This doesn't mean that I don't appreciate Kotlin. Since Java has nulls, then it's good to track them in Kotlin. I quibble with the following:
nulls ... denote the absence of things.
People do use
null
to indicate absence, but they need not. Ideally, we can use Option for that and not be penalised at runtime. To me, it seems strange to model two choices with a nullable Address (i.e. present, not present) but a different mechanism when there are more than 2 choices. Preferring "sum types" for each of these cases keeps the language smaller. Another idea, that Scala 3 and TypeScript use, is to handle nullability using union types which also has an economy of expression. I'm unsure if union types are necessary.
find
is the wrong function
Switching to
any
works only in the small, not in the large. It comes back to the fact that it isn't possible to distinguish the return value of
find
from a
null
that was found from a
null
meaning "not found". It's a case where there are three choices but the API limits to just 2. This could be fixed by using Option. To see that switching to
any
isn't sufficient, imagine that we want to do something with the item if it was found, not just knowing that it is present. Then I wouldn't know if I had found the
null
or not.
e
eliminating
null
references requires a different initialization model, which you can see if you read the previous links or compare to Swift
s
@ephemient yes agreed. I read it. Thanks!
j
to be clear, Kotlin's null system is based on union types as well (and Scala is JVM just as Kotlin is)
s
| People do use
null
to indicate absence, but they need not. Ideally, we can use Option @Steven Shaw I don't see much difference between
Option
and a nullable type. If you have a language with Option your syntax for
null
is basically
Option::None
🤷‍♂️ If you force me to pick a prefered method, I'd pick a typesystem with nullable types anyway, because I can simply assign a non-nullable value to a nullabple one, but I can't assign a value to an option, I have to explicitly wrap it. I do use Rust and therefor Option. so I have experience and do like both approaches (because both give me the same thing, they force me to handle the possible of absence of data). I guess people with a functional background would prefer Option, since Option is a Monad ("look at all the things we can achieve with this one concept") and nullable types are probably way more heavy-weight, alas, I don't have a functional background and I don't have to implement a nullable typesystem either, I can simply use one 🤷‍♂️
of course, your example is a toy example, but all you prove is that
find
is simply the wrong method to use here, not that there's something wrong with nullable types. Not sure what you mean with "'any' doesn't scale", both methods have the same runtime (O(n)), which means both are equally inefficient. So let me fix your method for you
Copy code
fun <T> Iterable<T>.findAndPrint(x: T) {
    if (this.any{it==x) {
        println(x)
    }
}
additionally most of the time I filter out nullable values as soon as possible, so at least 95% (made up number but feels correct) of my collections contain a non-nullable type, so you could actually even use
find
on those.
Copy code
fun <T> Iterable<T & Any>.findAndPrint(x: T) {  // it doesn't make that much sense to look for a nullable value in a non-nullable collection. I just wanted to stay close to your code
    val item = this.find { it == x }
    if (item != null) {
        // We found it! or did we? Yes, yes, we did!
        println(item)
    }
}
of course the next step would be to shorten this method Kotlin-style 😎
Copy code
fun <T> Iterable<T & Any>.findAndPrint(x: T) {
    this.find { it == x }?.let{println(it)} // yes, i know, sometimes I do use null as a boolean, but only in a non-nullable context (<T & Any>)
}
Since you're new to Kotlin let me point you into the direction of filterNotNull. I love how it not only removes all the nulls from an Iterable but also retains this information in the fact that its return type is now an Iterable of an non-nullable type. And of course you could do the same with Option, my point isn't that nullable types are better, but that both approaches are functionally equivalent. I'm just confused that you seem to think that Option is better somehow.
👍 1
j

https://youtu.be/YR5WdGrpoug

great talk by Rich Hickey of Clojure fame about the value of union types (like Kotlin) over Option
s
@JasonB Kotlin's null tracking isn't built from a more general system of union types though, is it? I'm not sure what point you are making.
j
I agree that Kotlin does not have union types other than for null, I'm just saying that its null handing is built off of union types, as much as any other language
s
No worries, @JasonB. I agree that Kotlin's null handling is a bit like union types for that one specific use case. It's unfortunate that a special facility has to be built to handle null (because it's there in the Java Platform). What's more, because it is there, people use it to model optionality (because they can and it's convenient). Also, they are encouraged to. It does seem to be idiomatic Kotlin, though, so something I will get used to.
I've seen most of Rich Hickey's talks. I like a lot of them (Simple Made Easy is my favourite). However, he has a particular distaste for some FP concepts. That doesn't mean that he's right about it.
@Stephan Schröder thanks for taking the time to fix my example and the tip about
filterNotNull
(
catMaybes
to me). I do like the conciseness of idiomatic Kotlin (with the scope function). I haven't seen the
& Any
syntax and so don't know that that is. This will make a good study for me. In a way, I was kind of surprised that my code compiled (when using
T
instead of
T?
). I imagine that a type parameter can take on any type (including a nullable one), but it is a bit confusing. My function
findEm
operates on a generic container — I think subtyping must allow the use of integers and null there 🤷‍♂️. Note that I didn't say that "any" doesn't scale, but that "switching to any" doesn't work in the large. What I mean is that you won't always be able to find workarounds when using nullable types instead of Option (at least not without duplicating code). I'll see if I can come up with another small example which shows this more clearly. It probably boils down to being able to have
Option<Option<String>>
but
String??
— which isn't illegal as I suspected — is just synonymous with
String?
. If Kotlin allowed nullable of nullable of String, it couldn't be represented as a nullable reference in the runtime (because there's another case to consider).
@Stephan Schröder see what you think of this small tweak to the toy example of finding nulls. What if we have
findAndPrint
use a predicate instead of a value to compare? I don't think we can use
any
then as it doesn't return the value found. Here's the example. I haven't looked up about the
& Any
thing yet!
Copy code
fun main() {
    fun <T> Iterable<T>.findAndPrint(predicate: (T) -> Boolean) {
        val item = this.find(predicate)
        if (item != null) {
            // Are we sure that we found it?
            println("item = $item")
        }
    }

    fun <T> findEm(xs: Iterable<T>) {
        xs.findAndPrint { it == 3 }
        xs.findAndPrint { it == 4 }
        xs.findAndPrint { it == null }
    }

    val xs: Iterable<Int?> = listOf(1, 2, 3, null, 5, 6, 7)
    println("xs")
    findEm(xs)
    val ys: Iterable<Int> = listOf(1, 2, 3, 4, 5, 6, 7)
    println("ys")
    findEm(ys)
}
https://pl.kotl.in/5H8puOH86
Because I'm not using
any
, there is the problem that the null cannot be found.
There is also a strange warning in IntelliJ
But the code produces the output that I expect.
s
| Because I'm not using
any
, there is the problem that the null cannot be found. well, technically the null is found, it's just not discernable if a null is due to a null being found or the null value not being present. ok, so if we'd be using Option here, we'd be getting an Option.of(Option.None) and it would still be discernable. got it. So when working on a collection of an optional/nullable type (something like)
find
is a valid choice in a language with Option, and a flawed choice for a language with a nullable typesystem. Luckily, there or other choices/methods that are not even more cumbersome to use to work around this "problem" 🤷‍♂️
I thought a bit more about it, and you can even use
find
in Kotlin, as long as the predicate you provide doesn't match on
null
. While I do understand that in a mathematical sense I can construct a predicate that also matches on
null
for every predicate that doesn't, in a practical sense I don't remember . Of course academic purity is its own reward.
null
is basically an
Option::None
with an automatic flattening, where any amount of nested
Option::Some(Option::Some(...(Option::None)))
collapses to
Option::None
. So on the on hand you loose structural information, on the other hand you also loose a lot of verbosity. "eradicate null from the language" sounds so powerful, it's a bit disappointing once you realize the difference is not that big or in other words: In a language with a nullable typesystem
null
is extremely close to
Option::None
. Our options are not that far from each others either 😅 you even agreed that give Kotlin's interaction with Java a nullable typesystem is preferable. I think we might have slipped a bit into an uncanny valley of sorts 😅
s
null
is basically an
Option::None
with an automatic flattening, where any amount of nested
Option::Some(Option::Some(...(Option::None)))
collapses to
Option::None
.
Yes, that's the crux of it. However, Kotlin does have Option, so the
find
function could be changed. However, the fact that it is defined as-it-is leads me to the conclusion that idiomatic Kotlin leads to structural loss.
I'm not sure what an uncanny valley is, but I suspect we agree on many points for practical programming with Kotlin. We perhaps agree less on the big picture of PL design. Thanks for working through this with me, @Stephan Schröder!