From f604143f54410af26f1ddb06df0ba5a06a56bc56 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Thu, 7 May 2020 23:42:55 +0200 Subject: [PATCH 01/46] Write first draft of keep --- proposals/patter-matching.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 proposals/patter-matching.md diff --git a/proposals/patter-matching.md b/proposals/patter-matching.md new file mode 100644 index 000000000..e69de29bb From 2e69e4c86b56fe11823b20f7abc7fcf307dd0108 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 00:01:23 +0200 Subject: [PATCH 02/46] Write first draft of keep --- proposals/patter-matching.md | 230 +++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/proposals/patter-matching.md b/proposals/patter-matching.md index e69de29bb..91baa87cf 100644 --- a/proposals/patter-matching.md +++ b/proposals/patter-matching.md @@ -0,0 +1,230 @@ +# Pattern Matching + +* **Type**: Design proposal + +## Synopsis + +Support pattern matching in `when` clauses, using existing `is` syntax and +destructuring semantics + +## Motivation + +Almost all languages have mechanisms to test whether an object has a certain +type or structure, and allow extracting information from it depending on the +results of those checks. At the time of writing, Kotlin has the practical +`when` clauses, which combined with [smart +casting](https://kotlinlang.org/docs/reference/typecasts.html#smart-casts) +and occasionally +[destructuring](https://kotlinlang.org/docs/reference/multi-declarations.html), +provide this kind of functionality. We propose further enhancing existing +functionality through full blown pattern matching, which allows to type +check, equality test, and extract information in a single, intuitive +construct already in use in many popular languages. + +A clear immediate advantage of this extension is avoiding nested `when`s, for +example. + +The syntax proposed below aims to not introduce new keywords and leverage the +existing `when` idiom that the community has already grown used to, but +discussion is encouraged and welcome on how it can be improved. + +### Simple textbook example + +``` +data class Prospect(val email: String, active: Boolean) +data class Customer(val name: String, val age: Int, val email: String) +... + +when(elem) { + is Customer(name, _, addr) -> + Mail.send(addr, "Thanks for choosing us, $name!") + + is Prospect(addr, true) -> + Mail.send(addr, "Please consider our product...") +} +``` + +The syntax proposed uses the already existing `is` construct to check the +type of the subject, but adds what semantically looks a lot like a +destructuring delcaration with an added equality checks. This approach is +intuitive in that the `componentN()` operator functions are used to +destructure a class. + +Then we pass an already defined expression (or it could be restricted to a +constant) to further specify the desired match (a `Prospect` wich is +`active`, in the example above). This check can be implemented with +`equals()`. + +Additionally, nested patterns could further look at the members of the class +(or whatever `componentN()` might return). + +The type name after `is` could be omitted entirely to simply destructure +something that has some `componentN()` functions like so: +``` +val list : List = // ... + +for (p in list) { + when (p) { + is (addr, true) -> ... + } +} +``` + + +Below some examples from existing, open source Kotlin projects are listed, +along with what they would look like if this KEEP was implemented. The aim of +using real-world examples is to show the immediate benefit of adding the +proposal (as it currently looks) to the language. + +### Comparisons + +#### From the [Arrow](https://github.com/arrow-kt/arrow-core/blob/be173c05b60471b02e04a07d246d327c2272b9a3/arrow-core/src/main/kotlin/arrow/core/extensions/option.kt) library: + +Without pattern matching: +``` +fun Kind.eqK(other: Kind, EQ: Eq) = + (this.fix() to other.fix()).let { (a, b) -> + when (a) { + is None -> { + when (b) { + is None -> true + is Some -> false + } + } + is Some -> { + when (b) { + is None -> false + is Some -> EQ.run { a.t.eqv(b.t) } + } + } + } + } +``` + +With pattern matching: +``` +fun Kind.eqK(other: Kind, EQ: Eq) = + when(this.fix() to other.fix()) { + is (Some(a), Some(b)) -> EQ.run { a.eqv(b) } + else -> false + } +``` + +#### From JetBrains' [Exposed](https://github.com/JetBrains/Exposed/blob/master/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt): +Without pattern matching: +``` +infix fun Expression.and(op: Expression): Op = when { + this is AndOp && op is AndOp -> AndOp(expressions + op.expressions) + this is AndOp -> AndOp(expressions + op) + op is AndOp -> AndOp(ArrayList>(op.expressions.size + 1).also { + it.add(this) + it.addAll(op.expressions) + }) + else -> AndOp(listOf(this, op)) +} + +``` + +With pattern matching: +``` +infix fun Expression.and(op: Expression): Op = when(this to op) { + is (AndOp, AndOp(opExpres)) -> AndOp(expressions + opExpres) + is (AndOp, _) -> AndOp(expressions + op) + is (_, AndOp(opExpres)) -> + AndOp(ArrayList>(opExpres + 1).also { + it.add(this) + it.addAll(opExpres) + }) + else -> AndOp(listOf(this, op)) +} + +``` + +### More textbook comparisons + +#### From [Jake Wharton at KotlinConf '19](https://youtu.be/te3OU9fxC8U?t=2528) + +``` +sealed class Download +data class App(val name: String, val developer: Developer) : Download() +data class Movie(val title: String, val director: Person) : Download() +val download: Download = // ... + +``` + +Without pattern matching: +``` +val result = when(download) { + is App -> { + val (name, dev) = download + when(dev) { + is Person -> + if(name == "Alice") "Alice's app $name" else "Someone else's + else -> "Not by Alice" + } + } + is Movie -> { + val (title, diretor) = download + if (director.name == "Alice") { + "Alice's movie $title" + } else { + "Not by Alice" + } + } +} +``` +With pattern matching: +``` +val result = when(download) { + is App(name, Person("Alice", _)) -> "Alice's app $name" + is Movie(title, Person("Alice", _)) -> "Alice's movie $title" + is App, Movie -> "Not by Alice" +} +``` + +#### From Baeldung on [Binary Trees](https://www.baeldung.com/kotlin-binary-tree): +Without pattern matching: +``` +private fun removeNoChildNode(node: Node, parent: Node?) { + if (parent == null) { + throw IllegalStateException("Can not remove the root node without child nodes") + } + if (node == parent.left) { + parent.left = null + } else if (node == parent.right) { + parent.right = null + } +} +``` +With pattern matching: +``` +private fun removeNoChildNode(node: Node, parent: Node?) { + when (node to parent) { + is (_, null) -> + throw IllegalStateException("Can not remove the root node without child nodes") + is (n, Parent(n, _)) -> parent.left = null + is (n, Parent(_, n)) -> parent.right = null + } +} +``` + + + + +## Comparison to other languages + +- Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375)) +- [C# supports this](https://docs.microsoft.com/en-us/dotnet/csharp/pattern-matching) with a more verbose syntax through `case` .. `when` .. +- In [Haskell](https://www.haskell.org/tutorial/patterns.html) pattern matching is a core language feature extensively used to traverse data structures and to define functions, mathcing on their arguments. +- [Rust](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html) supports pattern matching through `match` expressions +- [Scala](https://docs.scala-lang.org/tour/pattern-matching.html) supports pattern matching with the addition of guards that allow to further restrict the match with a boolean expression (much like `case` .. `when` in C#) +- Python does not support pattern matching, but like Kotlin it supports destructuring of tuples and collections, though not classes. +- [Swift](https://docs.swift.org/swift-book/ReferenceManual/Patterns.html) can match tuples and `Optional`, and allows slightly more flexibility on what is a match through the `~=` operator. + +I have experience with only some of these languages so please feel free to correct any mistakes. + +## References + +[Pattern Matching for Java, Gavin Bierman and Brian Goetz, September 2018](https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html) + +[JEP 375](https://openjdk.java.net/jeps/375) From 9c82548be81b006f74c7f9182cd74dc9ea9308c9 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 00:06:19 +0200 Subject: [PATCH 03/46] Fix typos and bad name in Baeldung Example --- proposals/patter-matching.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/proposals/patter-matching.md b/proposals/patter-matching.md index 91baa87cf..eca7bde8c 100644 --- a/proposals/patter-matching.md +++ b/proposals/patter-matching.md @@ -5,7 +5,7 @@ ## Synopsis Support pattern matching in `when` clauses, using existing `is` syntax and -destructuring semantics +destructuring semantics. ## Motivation @@ -76,7 +76,7 @@ along with what they would look like if this KEEP was implemented. The aim of using real-world examples is to show the immediate benefit of adding the proposal (as it currently looks) to the language. -### Comparisons +## Comparisons #### From the [Arrow](https://github.com/arrow-kt/arrow-core/blob/be173c05b60471b02e04a07d246d327c2272b9a3/arrow-core/src/main/kotlin/arrow/core/extensions/option.kt) library: @@ -202,8 +202,8 @@ private fun removeNoChildNode(node: Node, parent: Node?) { when (node to parent) { is (_, null) -> throw IllegalStateException("Can not remove the root node without child nodes") - is (n, Parent(n, _)) -> parent.left = null - is (n, Parent(_, n)) -> parent.right = null + is (n, Node(n, _)) -> parent.left = null + is (n, Node(_, n)) -> parent.right = null } } ``` @@ -215,11 +215,11 @@ private fun removeNoChildNode(node: Node, parent: Node?) { - Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375)) - [C# supports this](https://docs.microsoft.com/en-us/dotnet/csharp/pattern-matching) with a more verbose syntax through `case` .. `when` .. -- In [Haskell](https://www.haskell.org/tutorial/patterns.html) pattern matching is a core language feature extensively used to traverse data structures and to define functions, mathcing on their arguments. +- In [Haskell](https://www.haskell.org/tutorial/patterns.html) pattern matching is a core language feature extensively used to traverse data structures and to define functions, mathcing on their arguments - [Rust](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html) supports pattern matching through `match` expressions - [Scala](https://docs.scala-lang.org/tour/pattern-matching.html) supports pattern matching with the addition of guards that allow to further restrict the match with a boolean expression (much like `case` .. `when` in C#) -- Python does not support pattern matching, but like Kotlin it supports destructuring of tuples and collections, though not classes. -- [Swift](https://docs.swift.org/swift-book/ReferenceManual/Patterns.html) can match tuples and `Optional`, and allows slightly more flexibility on what is a match through the `~=` operator. +- Python does not support pattern matching, but like Kotlin it supports destructuring of tuples and collections, though not classes +- [Swift](https://docs.swift.org/swift-book/ReferenceManual/Patterns.html) can match tuples and `Optional`, and allows slightly more flexibility on what is a match through the `~=` operator I have experience with only some of these languages so please feel free to correct any mistakes. From c8df55bb1fe7fb1d8663a3da87f78e009741a74d Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 09:38:15 +0200 Subject: [PATCH 04/46] Add coroutines example and design decisions section --- proposals/patter-matching.md | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/proposals/patter-matching.md b/proposals/patter-matching.md index eca7bde8c..62961285a 100644 --- a/proposals/patter-matching.md +++ b/proposals/patter-matching.md @@ -109,6 +109,35 @@ fun Kind.eqK(other: Kind, EQ: Eq) = else -> false } ``` +#### From [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt): +Without pattern matching: +``` +public val value: E get() { + _state.loop { state -> + when (state) { + is Closed -> throw state.valueException + is State<*> -> { + if (state.value === UNDEFINED) throw IllegalStateException("No value") + return state.value as E + } + else -> error("Invalid state $state") + } + } +} +``` +With pattern matching: +``` +public val value: E get() { + _state.loop { state -> + when (state) { + is Closed(valueException) -> throw valueException + is State<*>(UNDEFINED) -> throw IllegalStateException("No value") + is State(value) -> return value as E + else -> error("Invalid state $state) + } + } +} +``` #### From JetBrains' [Exposed](https://github.com/JetBrains/Exposed/blob/master/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt): Without pattern matching: @@ -207,6 +236,70 @@ private fun removeNoChildNode(node: Node, parent: Node?) { } } ``` +## Semantics + +The semantics of this pattern matching can defined through some examples, where +`when` gets called on a particular `subject`. + +The proposed syntax is to start a new `when` line with `is PATTERN -> RHS`, where `PATTERN` can be: +- `Person` + - `instanceof` check, same semantics as vanilla Kotlin. +- `Person(_const)` where `_const` is an expression literal + - `is` check on the subject + - compile time check on whether `Person.component1()` is defined in scope + - call to `subject.component1()` + - `component1().equals(_const)` check + - As with vanilla kotlin, a smart cast of the subject to `Person` happens in `RHS` +- `Person(_const, age)` where `age` is an undefined identifier + - `is` check on the subject + - compile time check whether both `Person.component[1,2]()` are defined in scope + - equality check of `_const` as above + - `age` is defined in `RHS` with value `subject.component2()` +- `Person(name)` where `name` is a __defined__ identifier + - see [Design decisions](#design) +- `Person(_const, PATTERN2)` where `PATTERN2` is a nested pattern + - `_const` is checked as above, and `PATTERN2` is checked recursively, as if `when(subject.component2()) { is PATTERN2 }` was being called. +- `(PATTERN2, PATTERN3)` + - pattern like this without a type check should only be performed when `componentN()` of the subject are in scope (known at compile time). +- `Person(age, age)` where age is an undefined identifier + - the first `age` should be matched as above + - the second destructured argument should also call `equals()` on the first destructured argument to enforce an additional equality constraint where both fields of `Person` must be equal + - A match that should never succeed (maybe because `Person` is defind as `(String, Int)` and `Person(age, age)` was defined) can be reported at runtime as it is likely to be a programmer mistake. Note that this match could succeed anyway in a scenario where two different types do `equals() = true` on each other. + +## Design decisions + +Some of the semantics of this pattern matching are up to debate in the sense that there is room to decide on behaviour that may or may not be desirable + +### Matching existing variables +consider Jake Wharton's example: +``` +val expected : String = / ... + +val result = when(download) { + is App(name, Person(expected), _)) -> "$expected's app $name" + is Movie(title, Person(expected, _)) -> "$expected's movie $title" + is App, Movie -> "Not by $expected" +} +``` +In this example it is clear that we wanted to match `Person.component1()` with `expected`. But consider: +``` +val x = 2 +/* use x for something */ +... +val result = when(DB.querySomehting()) { + is Success(x) -> "DB replied $x" + is Error(errorMsg) -> error(errorMsg) + else -> error("unexpected query result") +} +``` +...where a programmer might want to define `x` as a new match for the content of `Success`, but ends up writing `Success.reply == 2` because they forgot that `x` was a variable in scope. The branch taken would then be the undesired `else`. + +Some instances of scenario can be avoided with IDE hints clever enough to report matches unlikely to ever succeed (like checking equality for different types), and enforcing exhaustive patterns. + +Even then, it is possible that the already defined identifier does have the same type as the new match, and that the `else` branch exists. + +Accidental additional checks are undesired behaviour therefore discussion is encouraged. + From fb0c501037b1fa1d824b9f548021661c8ffb2ff6 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 09:54:55 +0200 Subject: [PATCH 05/46] Fix typo in Semantics section --- proposals/patter-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/patter-matching.md b/proposals/patter-matching.md index 62961285a..9951e6a7a 100644 --- a/proposals/patter-matching.md +++ b/proposals/patter-matching.md @@ -238,7 +238,7 @@ private fun removeNoChildNode(node: Node, parent: Node?) { ``` ## Semantics -The semantics of this pattern matching can defined through some examples, where +The semantics of this pattern matching can be defined through some examples, where `when` gets called on a particular `subject`. The proposed syntax is to start a new `when` line with `is PATTERN -> RHS`, where `PATTERN` can be: From c3f4fdcf01b2219c64889d8a84d29b890f281c1b Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 10:30:20 +0200 Subject: [PATCH 06/46] Fix typo in Motivations section and missing type argument in kotlinx.coroutines example --- proposals/patter-matching.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/patter-matching.md b/proposals/patter-matching.md index 9951e6a7a..835876cf8 100644 --- a/proposals/patter-matching.md +++ b/proposals/patter-matching.md @@ -46,7 +46,7 @@ when(elem) { The syntax proposed uses the already existing `is` construct to check the type of the subject, but adds what semantically looks a lot like a -destructuring delcaration with an added equality checks. This approach is +destructuring delcaration with added equality checks. This approach is intuitive in that the `componentN()` operator functions are used to destructure a class. @@ -132,7 +132,7 @@ public val value: E get() { when (state) { is Closed(valueException) -> throw valueException is State<*>(UNDEFINED) -> throw IllegalStateException("No value") - is State(value) -> return value as E + is State<*>(value) -> return value as E else -> error("Invalid state $state) } } From ba81a95f80c39e3e89d8285833c760231a3210f5 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 14:00:28 +0200 Subject: [PATCH 07/46] Expand on the existing variables problem and add Limitations section to talk about matching on collections --- proposals/patter-matching.md | 37 +++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/proposals/patter-matching.md b/proposals/patter-matching.md index 835876cf8..612528dc6 100644 --- a/proposals/patter-matching.md +++ b/proposals/patter-matching.md @@ -268,7 +268,7 @@ The proposed syntax is to start a new `when` line with `is PATTERN -> RHS`, wher ## Design decisions -Some of the semantics of this pattern matching are up to debate in the sense that there is room to decide on behaviour that may or may not be desirable +Some of the semantics of this pattern matching are up to debate in the sense that there is room to decide on behaviour that may or may not be desirable. ### Matching existing variables consider Jake Wharton's example: @@ -294,15 +294,42 @@ val result = when(DB.querySomehting()) { ``` ...where a programmer might want to define `x` as a new match for the content of `Success`, but ends up writing `Success.reply == 2` because they forgot that `x` was a variable in scope. The branch taken would then be the undesired `else`. -Some instances of scenario can be avoided with IDE hints clever enough to report matches unlikely to ever succeed (like checking equality for different types), and enforcing exhaustive patterns. +Some instances of this scenario can be avoided with IDE hints clever enough to report matches unlikely to ever succeed (like checking equality for different types), and enforcing exhaustive patterns when matching. -Even then, it is possible that the already defined identifier does have the same type as the new match, and that the `else` branch exists. +Even then, it is possible that the already defined identifier does have the same type as the new match, and that the `else` branch exists. Despite this edge case, matching existing variables **is part of the proposal**, but accidental additional checks are undesired behaviour therefore discussion is encouraged. -Accidental additional checks are undesired behaviour therefore discussion is encouraged. + - +## Limitations +### Matching on collections + +An idiom in Haskell or Scala is to pattern match on collections. This relies on the matched pattern 'changing' depending on the state of the collection. Because this proposal aims to use `componentN()` for destructuring, such a thing would not be possible in Kotlin as `componentN()` returns the Nth element of the collection (instead of its tail for some `componentN()`). + +This limitation is due to the fact that in Haskell, a list is represented more similarly to how sealed class work in Kotlin (and we can match on those). + +Pattern mathcing on collections is **not** the aim of this proposal, but such a thing *could* be achieved through additional extension functions on some interfaces with the sole purpose of matching them: +``` +inline fun List destructFst() = + get(0) to if (size == 1) null else drop(1) + +val ls = listOf(1,2,3,4) + +fun mySum(l: List) = when(l.destructFst()) { + is (head, null) -> head + is (head, tail) -> head + mySum(tail) +} + +// or: + +fun List.mySum2() = when(this.destructFst()) { + is (head, tail) -> head + tail?.mySum2()?:0 +} +``` ## Comparison to other languages From 86032ab4544954b87cd6c6d6be72f5c6329ed7d1 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 14:08:48 +0200 Subject: [PATCH 08/46] Add Ruby to comparisons and add header with type, author and status --- proposals/patter-matching.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proposals/patter-matching.md b/proposals/patter-matching.md index 612528dc6..e5ea614ac 100644 --- a/proposals/patter-matching.md +++ b/proposals/patter-matching.md @@ -1,6 +1,9 @@ # Pattern Matching * **Type**: Design proposal +* **Author**: Nicolas D'Cotta +* **Status**: New + ## Synopsis @@ -340,6 +343,7 @@ fun List.mySum2() = when(this.destructFst()) { - [Scala](https://docs.scala-lang.org/tour/pattern-matching.html) supports pattern matching with the addition of guards that allow to further restrict the match with a boolean expression (much like `case` .. `when` in C#) - Python does not support pattern matching, but like Kotlin it supports destructuring of tuples and collections, though not classes - [Swift](https://docs.swift.org/swift-book/ReferenceManual/Patterns.html) can match tuples and `Optional`, and allows slightly more flexibility on what is a match through the `~=` operator +- Ruby recently released pattern matching since [2.7](https://www.ruby-lang.org/en/news/2019/12/25/ruby-2-7-0-released/) I have experience with only some of these languages so please feel free to correct any mistakes. From 102954db8b407c1b735fa5896c646b06f727b4a7 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 14:09:24 +0200 Subject: [PATCH 09/46] Fix typo in proposal filename --- proposals/{patter-matching.md => pattern-matching.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proposals/{patter-matching.md => pattern-matching.md} (100%) diff --git a/proposals/patter-matching.md b/proposals/pattern-matching.md similarity index 100% rename from proposals/patter-matching.md rename to proposals/pattern-matching.md From f47d00b0084a6be391c9331347feed7f6fc9e635 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 15:43:19 +0200 Subject: [PATCH 10/46] Fix 2 typos and remove reified parameter to ext. fun. of collections example --- proposals/pattern-matching.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index e5ea614ac..9aa819725 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -191,7 +191,7 @@ val result = when(download) { val (name, dev) = download when(dev) { is Person -> - if(name == "Alice") "Alice's app $name" else "Someone else's + if(name == "Alice") "Alice's app $name" else "Someone else's" else -> "Not by Alice" } } @@ -313,11 +313,11 @@ TODO An idiom in Haskell or Scala is to pattern match on collections. This relies on the matched pattern 'changing' depending on the state of the collection. Because this proposal aims to use `componentN()` for destructuring, such a thing would not be possible in Kotlin as `componentN()` returns the Nth element of the collection (instead of its tail for some `componentN()`). -This limitation is due to the fact that in Haskell, a list is represented more similarly to how sealed class work in Kotlin (and we can match on those). +This limitation is due to the fact that in Haskell, a list is represented more similarly to how sealed classes work in Kotlin (and we can match on those). Pattern mathcing on collections is **not** the aim of this proposal, but such a thing *could* be achieved through additional extension functions on some interfaces with the sole purpose of matching them: ``` -inline fun List destructFst() = +inline fun List destructFst() = get(0) to if (size == 1) null else drop(1) val ls = listOf(1,2,3,4) From d360c0bf187af3ef4d6be34d57100c7f339c99e5 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 18:51:55 +0200 Subject: [PATCH 11/46] Fix missing case in Arrow example --- proposals/pattern-matching.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 9aa819725..386572ba1 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -109,6 +109,7 @@ With pattern matching: fun Kind.eqK(other: Kind, EQ: Eq) = when(this.fix() to other.fix()) { is (Some(a), Some(b)) -> EQ.run { a.eqv(b) } + is (None, None) -> true else -> false } ``` From 214d19f7c0fb430ff89a9d4b1f3e4da09cfd670b Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 18:54:10 +0200 Subject: [PATCH 12/46] Fix bad duplicate branch in Jake Wharton's example --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 386572ba1..bdcdcdda8 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -192,7 +192,7 @@ val result = when(download) { val (name, dev) = download when(dev) { is Person -> - if(name == "Alice") "Alice's app $name" else "Someone else's" + if(name == "Alice") "Alice's app $name" else "Not by Alice" else -> "Not by Alice" } } From fc535ed8eeabaf9b1424f58e8a1ffa73ba9e6395 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 19:03:01 +0200 Subject: [PATCH 13/46] Add comment on how the Jake Wharton example is exhaustive --- proposals/pattern-matching.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index bdcdcdda8..30e73f4e1 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -214,6 +214,7 @@ val result = when(download) { is App, Movie -> "Not by Alice" } ``` +Note how the pattern match is exhaustive without an `else` branch, allowing us to benefit as usual from the added compile time checks of using `with` and sealed classes. Alice might write a Book in the future, and we would not be able to miss it. #### From Baeldung on [Binary Trees](https://www.baeldung.com/kotlin-binary-tree): Without pattern matching: From 085269a3636a054eb9c50d04c15a56e7a08daa18 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Fri, 8 May 2020 23:15:08 +0200 Subject: [PATCH 14/46] Add section on a possible alternative for the syntax of destructuring tuples and fix bug in Jake Wharton example --- proposals/pattern-matching.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 30e73f4e1..a70ff4c66 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -61,6 +61,8 @@ constant) to further specify the desired match (a `Prospect` wich is Additionally, nested patterns could further look at the members of the class (or whatever `componentN()` might return). +#### Destructuring without a type check + The type name after `is` could be omitted entirely to simply destructure something that has some `componentN()` functions like so: ``` @@ -72,15 +74,15 @@ for (p in list) { } } ``` +See [design decisions](tuples-syntax) for an alternative syntax for destructuring tuples without a type check. +## Comparisons Below some examples from existing, open source Kotlin projects are listed, along with what they would look like if this KEEP was implemented. The aim of using real-world examples is to show the immediate benefit of adding the proposal (as it currently looks) to the language. -## Comparisons - #### From the [Arrow](https://github.com/arrow-kt/arrow-core/blob/be173c05b60471b02e04a07d246d327c2272b9a3/arrow-core/src/main/kotlin/arrow/core/extensions/option.kt) library: Without pattern matching: @@ -192,7 +194,7 @@ val result = when(download) { val (name, dev) = download when(dev) { is Person -> - if(name == "Alice") "Alice's app $name" else "Not by Alice" + if(dev.name == "Alice") "Alice's app $name" else "Not by Alice" else -> "Not by Alice" } } @@ -303,7 +305,32 @@ Some instances of this scenario can be avoided with IDE hints clever enough to r Even then, it is possible that the already defined identifier does have the same type as the new match, and that the `else` branch exists. Despite this edge case, matching existing variables **is part of the proposal**, but accidental additional checks are undesired behaviour therefore discussion is encouraged. +### Destructuring tuples syntax + +As an alternative to: +``` +when(person) { + is ("Alice", age) -> ... +} +``` +...could be: +``` +when (person) { + ("Alice", age) -> .. +} +``` +Where `is` is omitted as no actual type check occurs in this scenario. This proposal argues for keeping `is` as a keyword necessary to pattern matching. Consider this example: +``` +import arrow.core.Some +import arrow.core.None +val pairOfOption = 5 to Some(3) +val something = when (pairOfOption) { + (number1, Some(number2)) -> ... + (number, None) -> ... +} +``` +Here, a full blown pattern match happens where we extract number2 from Arrow's `Option` and do an exhaustive type check on the sealed class for `Some` or `None`. In this scenario, `instanceof` typechecks happen, but no `is` keyword is present. Thus keeping `is` is favourable as it clearly indicates a type check albeit at the price of some verbosity. + ## Comparison to other languages - Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375)) From dbd4bf70f8d5503fb43bedf7c1810e9af3da6f4d Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sat, 9 May 2020 20:08:39 +0200 Subject: [PATCH 22/46] Finish draft of implementation section --- proposals/pattern-matching.md | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index a34f94853..45e7c9e53 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -540,7 +540,7 @@ when(elem) { ...where the additional guard allows us to avoid a nested `if` if we only wish to contact customers that are not underage. It would also cover most cases [membership matching](#in-match) covers, and makes for very readable matching. Additionally, guards would solve the problem of matching existing identifiers. Consider this example: -``` +``` kotlin val expected : String = / ... val result = when(download) { @@ -553,9 +553,35 @@ val result = when(download) { ## Implementation > Disclaimer: contributions are welcome as the author has no background on the specifics of the Kotlin compiler, and only some on JVM bytecode. -Ideally, simple matching on n constructors is O(1) and implemented with a lookup table. This might only be possible on some platforms, as the JVM for example only permits typechecks using `instanceof`, which would have to be called on each match. - - +Ideally, simple matching on _n_ constructors is _O(1)_ and implemented with a +lookup table. In practice this may only be possible on some platforms, as the +JVM for example only permits typechecks using `instanceof`, which would have +to be called on each match. + +As discussed in [Semantics](#semantics), there is a `componentN()` call and +either one variable definition or one `equals()` call for each destructured +argument. Therefore complexity for each match is _O(m)_ for _m_ destructured +arguments, assuming all these function calls are O(1). Note this is not a +safe assumption (the calls are user defined) but it should be by far the +common case. + +While destructuring and checking for equality (with or without [guards](#guards) or [identifier matching](#match-existing-id)) should be mostly trivial, checking for exhaustiveness in nested patterns is not. The proposal suggests a naive implementation where a table is used for each level of nesting for each destructured element. For example, in order to call `when` on a `Pair` of `Option`s: + +```kotlin +when (Some(1) to Some(2)) { + is (Some(4), Some(y)) -> ... // case 1 + is (Some(x), None) -> ... // case 2 + is (None, Some(3)) -> ... // case 3 + is (_, None) -> ... // case 4 +} +``` +... where `Option` is a sealed class which is either `Some(a)` or `None`. +- In case 1, the right `Some` case has been matched, whereas on the left no case has been matched +- In case 2, we finally match the right for `Some`. There are only 2 possible cases for `Option`, so we are waiting to match `None` for both left and right. +- In case 3, we can make progress on matching `None` for the left, but not for the right. +- In case 4, `None` is finally matched for both left and right, so we can infer that an `else` branch is not necessary. + +This example uses `Some(1) to Some(2)` for the sake of briefness, but ideally, the compiler can infer that the matches on `None` can't ever succeed, because we are matching a `Pair`. ## Comparison to other languages From 463f623f842f73cc1f8bfd392769f45b1cd1e4ee Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sat, 9 May 2020 20:16:01 +0200 Subject: [PATCH 23/46] Formatting to add kotlin highlighting to snippets --- proposals/pattern-matching.md | 83 +++++++++++++++++------------------ 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 45e7c9e53..3e59de2f1 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -33,10 +33,10 @@ discussion is encouraged and welcome on how it can be improved. While this possi ### Simple textbook example -``` +```kotlin data class Customer(val name: String, val age: Int, val email: String) data class Prospect(val email: String, active: Boolean) -... +// ... when(elem) { is Customer(name, _, addr) -> @@ -65,12 +65,12 @@ Additionally, nested patterns could further look at the members of the class The type name after `is` could be omitted entirely to simply destructure something that has some `componentN()` functions like so: -``` +```kotlin val list : List = // ... for (p in list) { when (p) { - is (addr, true) -> ... + is (addr, true) -> //... } } ``` @@ -87,7 +87,7 @@ proposal (as it currently looks) to the language. #### From the [Arrow](https://github.com/arrow-kt/arrow-core/blob/be173c05b60471b02e04a07d246d327c2272b9a3/arrow-core/src/main/kotlin/arrow/core/extensions/option.kt) library: Without pattern matching: -``` +```kotlin fun Kind.eqK(other: Kind, EQ: Eq) = (this.fix() to other.fix()).let { (a, b) -> when (a) { @@ -127,7 +127,7 @@ fun Ior.eqv(b: Ior): Boolean = when (this) { ``` With pattern matching: -``` +```kotlin fun Kind.eqK(other: Kind, EQ: Eq) = when(this.fix() to other.fix()) { is (Some(a), Some(b)) -> EQ.run { a.eqv(b) } @@ -145,7 +145,7 @@ fun Ior.eqv(other: Ior): Boolean = when (this to other) { ``` #### From [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/channels/ConflatedBroadcastChannel.kt): Without pattern matching: -``` +```kotlin public val value: E get() { _state.loop { state -> when (state) { @@ -160,14 +160,14 @@ public val value: E get() { } ``` With pattern matching: -``` +```kotlin public val value: E get() { _state.loop { state -> when (state) { is Closed(valueException) -> throw valueException is State<*>(== UNDEFINED) -> throw IllegalStateException("No value") is State<*>(value) -> return value as E - else -> error("Invalid state $state) + else -> error("Invalid state $state") } } } @@ -176,7 +176,7 @@ Note that here we are testing for equality for an already defined identifier `UN #### From JetBrains' [Exposed](https://github.com/JetBrains/Exposed/blob/master/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Op.kt): Without pattern matching: -``` +```kotlin infix fun Expression.and(op: Expression): Op = when { this is AndOp && op is AndOp -> AndOp(expressions + op.expressions) this is AndOp -> AndOp(expressions + op) @@ -190,7 +190,7 @@ infix fun Expression.and(op: Expression): Op = when { ``` With pattern matching: -``` +```kotlin infix fun Expression.and(op: Expression): Op = when(this to op) { is (AndOp, AndOp(opExpres)) -> AndOp(expressions + opExpres) is (AndOp, _) -> AndOp(expressions + op) @@ -207,7 +207,7 @@ infix fun Expression.and(op: Expression): Op = when(t #### From [Jake Wharton at KotlinConf '19](https://youtu.be/te3OU9fxC8U?t=2528) -``` +```kotlin sealed class Download data class App(val name: String, val developer: Developer) : Download() data class Movie(val title: String, val director: Person) : Download() @@ -216,7 +216,7 @@ val download: Download = // ... ``` Without pattern matching: -``` +```kotlin val result = when(download) { is App -> { val (name, dev) = download @@ -237,7 +237,7 @@ val result = when(download) { } ``` With pattern matching: -``` +```kotlin val result = when(download) { is App(name, Person("Alice", _)) -> "Alice's app $name" is Movie(title, Person("Alice", _)) -> "Alice's movie $title" @@ -248,7 +248,7 @@ Note how the pattern match is exhaustive without an `else` branch, allowing us t #### From Baeldung on [Binary Trees](https://www.baeldung.com/kotlin-binary-tree): Without pattern matching: -``` +```kotlin private fun removeNoChildNode(node: Node, parent: Node?) { if (parent == null) { throw IllegalStateException("Can not remove the root node without child nodes") @@ -261,7 +261,7 @@ private fun removeNoChildNode(node: Node, parent: Node?) { } ``` With pattern matching: -``` +```kotlin private fun removeNoChildNode(node: Node, parent: Node?) { when (node to parent) { is (_, null) -> @@ -317,8 +317,8 @@ that there is room to decide on behaviour that may or may not be desirable. ### Matching existing identifiers Consider a modified version of Jake Wharton's example: -``` -val expected : String = / ... +```kotlin +val expected : String = // ... val result = when(download) { is App(name, Person(expected, _)) -> "$expected's app $name" @@ -328,9 +328,9 @@ val result = when(download) { ``` It is clear that we wish to match `Person.component1()` with `expected`. But consider: -``` +```kotlin val x = 2 -/* use x for something */ +// use x for something ... val result = when(DB.querySomehting()) { is Success(x) -> "DB replied $x" @@ -354,12 +354,12 @@ Kotlin: #### Shadowing This would make: -``` +```kotlin val x = "a string!" val someMapEntry = "Bob" to 4 when(someMapEntry) { - is (name, x) -> ... //RHS + is (name, x) -> // ... RHS } ``` ...valid code, but where `x` is not matched against, but redefined in the @@ -369,12 +369,12 @@ takes. #### Allowing matching, implicitly This would make -``` +```kotlin val x = 3 val someMapEntry = "Bob" to 4 when(someMapEntry) { - is (name, x) -> ... //RHS + is (name, x) -> // ... RHS } ``` ...valid code, where we are checking whether the second argument of the pair @@ -392,12 +392,12 @@ place for suspending functions). This would require an additional syntactic construct to indicate whether we wish to match the existing variable named `x`, or to extract a new variable named `x`. Such a construct could look like: -``` +```kotlin val x = 3 val someMapEntry = "Bob" to 4 when(someMapEntry) { - is (name, ==x) -> ... //RHS + is (name, ==x) -> // ... RHS } ``` ...which makes it clear that we aim to test for equality between `x` and the extracted second parameter of the pair. Scala uses this approach through [stable identifiers](https://www.scala-lang.org/files/archive/spec/2.11/08-pattern-matching.html#stable-identifier-patterns). @@ -407,12 +407,12 @@ suggestions on different ones are welcome. #### Not allowing matching existing identifiers at all This would make -``` +```kotlin val x = 3 val someMapEntry = "Bob" to 4 when(someMapEntry) { - is (name, x) -> ... //RHS + is (name, x) -> // ... RHS } ``` ...throw a semantic error at compile time, where `x` is defined twice in the @@ -432,30 +432,29 @@ semantics. ### Destructuring tuples syntax An alternative to: -``` +```kotlin when(person) { - is ("Alice", age) -> ... + is ("Alice", age) -> // ... } ``` -was suggested. It would look like: -``` +...was suggested. It would look like: +```kotlin when (person) { - ("Alice", age) -> .. + ("Alice", age) -> // ... } ``` Where `is` is omitted as no actual type check occurs in this scenario. This proposal argues for keeping `is` as a keyword necessary to pattern matching. Consider this example that uses the alternative syntax: -``` -import arrow.core.Some -import arrow.core.None +```kotlin +// A sealed class Option = Some(a) or None is in scope here val pairOfOption = 5 to Some(3) val something = when (pairOfOption) { - (number1, Some(number2)) -> ... - (number, None) -> ... + (number1, Some(number2)) -> // ... + (number, None) -> // ... } ``` Here, a full blown pattern match happens where we extract number2 from -Arrow's `Option` and do an exhaustive type check on the sealed class for +`Option` and do an exhaustive type check on the sealed class for `Some` or `None`. In this scenario, `instanceof` typechecks happen, but no `is` keyword is present. Thus keeping `is` is favourable as it clearly indicates a type check albeit at the price of some verbosity. @@ -488,7 +487,7 @@ those). Pattern mathcing on collections is **not** the aim of this proposal, but such a thing *could* be achieved through additional extension functions on some interfaces with the sole purpose of matching them: -``` +```kotlin inline fun List destructFst() = get(0) to if (size == 1) null else drop(1) @@ -514,7 +513,7 @@ and are in the spirit of Kotlin's idioms. ### Membership matching Consider: -``` +```kotlin data class Point(val x: Double, val y: Double) val p: Point = /... val max = Double.MAX_VALUE @@ -541,7 +540,7 @@ when(elem) { Additionally, guards would solve the problem of matching existing identifiers. Consider this example: ``` kotlin -val expected : String = / ... +val expected : String = // ... val result = when(download) { is App(name, Person(author, _)) where author == expected -> "$expected's app $name" From 055f232a56f2a3472b6d01c243b0dd759c1a8272 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sat, 9 May 2020 20:24:17 +0200 Subject: [PATCH 24/46] Add Alternative section --- proposals/pattern-matching.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 3e59de2f1..877b5ebb4 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -582,6 +582,25 @@ when (Some(1) to Some(2)) { This example uses `Some(1) to Some(2)` for the sake of briefness, but ideally, the compiler can infer that the matches on `None` can't ever succeed, because we are matching a `Pair`. +## Alternative + +Kotlin could do without pattern matching, as it has so far, and keep solely +relying on accessing properties through smart casting. Hopefully some of the +examples and potential features discussed in this KEEP help show that the +current idiom is limited in that it forces us to write nested constructs +inside `when`s when we want to perform additional checks. + +Additionally, pattern matching does not replace smart casting, but rather, +benefits from it and makes it even more useful. Haskell and Scala often need +to access not only things they destruct, bu also things matched on. In order +to overcome this, they have +[as-patterns](http://zvon.org/other/haskell/Outputsyntax/As-patterns_reference.html) +and [pattern +binders](https://riptutorial.com/scala/example/12230/pattern-binder----) +respectively. Note that this KEEP does not introduce such a construct because +a pattern can be accessed fine thanks to the smart casting idiom already +widely popular. + ## Comparison to other languages - Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375)) From 4785b2a3cddcddf4ef861115b823566437a29494 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sat, 9 May 2020 20:26:00 +0200 Subject: [PATCH 25/46] Fix typo in Alternatives section --- proposals/pattern-matching.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 877b5ebb4..76dbbff41 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -582,7 +582,7 @@ when (Some(1) to Some(2)) { This example uses `Some(1) to Some(2)` for the sake of briefness, but ideally, the compiler can infer that the matches on `None` can't ever succeed, because we are matching a `Pair`. -## Alternative +## Alternatives Kotlin could do without pattern matching, as it has so far, and keep solely relying on accessing properties through smart casting. Hopefully some of the @@ -592,11 +592,10 @@ inside `when`s when we want to perform additional checks. Additionally, pattern matching does not replace smart casting, but rather, benefits from it and makes it even more useful. Haskell and Scala often need -to access not only things they destruct, bu also things matched on. In order +to access things matched on (aside from the ones they destruct). In order to overcome this, they have [as-patterns](http://zvon.org/other/haskell/Outputsyntax/As-patterns_reference.html) -and [pattern -binders](https://riptutorial.com/scala/example/12230/pattern-binder----) +and [pattern binders](https://riptutorial.com/scala/example/12230/pattern-binder----) respectively. Note that this KEEP does not introduce such a construct because a pattern can be accessed fine thanks to the smart casting idiom already widely popular. From 2edf016d7f9e3c26bf863e81698059e2c13848c0 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sat, 9 May 2020 20:30:46 +0200 Subject: [PATCH 26/46] Update comparison to other languages --- proposals/pattern-matching.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 76dbbff41..2e3cfb556 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -604,12 +604,12 @@ widely popular. - Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375)) - [C# supports this](https://docs.microsoft.com/en-us/dotnet/csharp/pattern-matching) with a more verbose syntax through `case` .. `when` .. -- In [Haskell](https://www.haskell.org/tutorial/patterns.html) pattern matching is a core language feature extensively used to traverse data structures and to define functions, mathcing on their arguments +- In [Haskell](https://www.haskell.org/tutorial/patterns.html) pattern matching (along with guards) is a core language feature extensively used to traverse data structures and to define functions, mathcing on their arguments - [Rust](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html) supports pattern matching through `match` expressions -- [Scala](https://docs.scala-lang.org/tour/pattern-matching.html) supports pattern matching with the addition of guards that allow to further restrict the match with a boolean expression (much like `case` .. `when` in C#) +- [Scala](https://docs.scala-lang.org/tour/pattern-matching.html) supports pattern matching (along with guards) - Python does not support pattern matching, but like Kotlin it supports destructuring of tuples and collections, though not classes -- [Swift](https://docs.swift.org/swift-book/ReferenceManual/Patterns.html) can match tuples and `Optional`, and allows slightly more flexibility on what is a match through the `~=` operator -- Ruby recently released pattern matching since [2.7](https://www.ruby-lang.org/en/news/2019/12/25/ruby-2-7-0-released/) +- [Swift](https://docs.swift.org/swift-book/ReferenceManual/Patterns.html) can match tuples and `Optional`, and allows slightly more flexibility on what is a match through the `~=` operator. It does not allow class destructuring. +- Ruby recently supports pattern matching, since [2.7](https://www.ruby-lang.org/en/news/2019/12/25/ruby-2-7-0-released/) The author has experience with only some of these languages so additional comments are welcome. From fdbea975cbd7a42ab6b5b184c969d640eddd7b22 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sat, 9 May 2020 20:31:13 +0200 Subject: [PATCH 27/46] Fix typo in header comment --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 2e3cfb556..a00c9ebd2 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -3,7 +3,7 @@ * **Type**: Design proposal * **Author**: Nicolas D'Cotta * **Status**: New - + ## Synopsis From 720ac1d4d4644bba6574562a53d6af0b8b1b84a3 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sat, 9 May 2020 23:34:32 +0200 Subject: [PATCH 28/46] Fix lack of syntax highlighting in snippet in Guards subsection --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index a00c9ebd2..df0a9ed59 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -530,7 +530,7 @@ val location = when(p) { ### Guards A guard is an additional boolean constraint on a match, widely used in Haskell or Scala pattern matching. Consider a variation of the initial customers example: -``` +```kotlin when(elem) { is Customer(name, age, addr) where age > 18 -> Mail.send(addr, "Thanks for choosing us, $name!") is Prospect(addr, true) -> Mail.send(addr, "Please consider our product...") From 258bfd3593e5a783035cf6d6e0fe6e754e44151d Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Sun, 10 May 2020 01:02:20 -0700 Subject: [PATCH 29/46] Component Guards added to Beyond the proposal > Guards --- proposals/pattern-matching.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index df0a9ed59..00859f977 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -1,7 +1,8 @@ # Pattern Matching * **Type**: Design proposal -* **Author**: Nicolas D'Cotta +* **Author**: Nicolas D'Cotta +* **Contributors**: Ryan Nett * **Status**: New @@ -549,6 +550,30 @@ val result = when(download) { } ``` +#### Component Guards + +Guards could be extended to allow guards on destructured components, instead of just the entire case. + +An example: +```kotlin +data class Person(val name: String, val age: Int, val contacts: List) +val p: Person = // ... +when(p){ + is Person(name where { "-" !in it.substringAfterLast(" ") }, age where {it >= 18}, _) -> // ... + is Person(_, _, _ where {it.size >= 5}) -> // ... +} +``` + +The biggest benefit of this is allowing custom match comparisons instead of just equality, including inequalities, regex, case-insensitive equality, etc. +It also allows for some matching on collections or other types that don't destructure well. +A user could define their guards in functions like `is Person(name where ::isLastNameNotHyphenated, _, _)` for more complex guards. + +A large drawback is the verbosity. +This could cleaned up some by using a better syntax, but extra sytax will always be added to compoment declarations. +However, assuming normal guards are implemented, the guard would be just as verbose, +it would just cover all checks at once rather than doing the check where the component is declared, which may reduce readability. +Doing checks at the component declaration also allows for checks on non-assigned (`_`) components, as in the last case in the example. + ## Implementation > Disclaimer: contributions are welcome as the author has no background on the specifics of the Kotlin compiler, and only some on JVM bytecode. From 621807093cad350ca17f4938d83e93e507d7b8fe Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Sun, 10 May 2020 01:25:28 -0700 Subject: [PATCH 30/46] Adds a design decision for specifying extraction with val --- proposals/pattern-matching.md | 47 ++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index df0a9ed59..8aeeb0032 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -1,7 +1,8 @@ # Pattern Matching * **Type**: Design proposal -* **Author**: Nicolas D'Cotta +* **Author**: Nicolas D'Cotta +* **Contributors**: Ryan Nett * **Status**: New @@ -429,6 +430,50 @@ can be dropped (in favour of [shadowing](#shadow-match) or [not allowing it at all](#no-match), preferably with [guards](#guards)) if consensus is not reached on its semantics. +#### Specifying extraction with val + +This would require a `val` when a new variable is extracted, and would follow existing local variable semantics with regards to shadowing, etc. +Example: +```kotlin +val x = 3 +val someMapEntry = "Bob" to 4 + +when(someMapEntry) { + is (val name, x) -> // ... RHS. name is in scope here +} +``` +The new variable `name` has its scope limited to the case's body. + +This matches current `when` statement capturing syntax. +`var` could also be allowed to declare a mutable local variable, although this could cause issues if used with [guards](#guards). + +Requiring `val` will make highly nested matches considerably more verbose. Consider: +```kotlin +data class Name(val first: String, val middles: List, val last: String) +data class Address(val streetAddress: String, val secondLine: String, val country: String, val state: String, val city: String, val zip: String) +data class Person(val name: Name, val address: Address, val age: Int) + +val p: Person = // ... + +when(p){ + is Person( + Name(val first, _, val last), + Address(val streetAddress, val secondLine, val country, val state, val city, val zip), + _ + ) -> +} +``` +vs +```kotlin +when(p){ + is Person( + Name(first, _, last), + Address(streetAddress, secondLine, country, state, city, zip), + _ + ) -> +} +``` + ### Destructuring tuples syntax An alternative to: From 4065f1e421dec3adc99908472f29d7af9d6d7cba Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Sun, 10 May 2020 01:29:26 -0700 Subject: [PATCH 31/46] add mention in explicit matching, add sentance about verbosity --- proposals/pattern-matching.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 00859f977..cf8eae7a3 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -406,6 +406,8 @@ when(someMapEntry) { The syntactic construct presented in the example is rather arbitrary and suggestions on different ones are welcome. +This version works well with [component guards](#component-guards), as any equality checking could be done in the guard instead of using syntax like `==x`. + #### Not allowing matching existing identifiers at all This would make ```kotlin @@ -550,7 +552,7 @@ val result = when(download) { } ``` -#### Component Guards +#### Component Guards Guards could be extended to allow guards on destructured components, instead of just the entire case. @@ -568,7 +570,7 @@ The biggest benefit of this is allowing custom match comparisons instead of just It also allows for some matching on collections or other types that don't destructure well. A user could define their guards in functions like `is Person(name where ::isLastNameNotHyphenated, _, _)` for more complex guards. -A large drawback is the verbosity. +A large drawback is the verbosity. Large classes with lots of guards quickly become very long. This could cleaned up some by using a better syntax, but extra sytax will always be added to compoment declarations. However, assuming normal guards are implemented, the guard would be just as verbose, it would just cover all checks at once rather than doing the check where the component is declared, which may reduce readability. From a09eb907b8a8b0cc12f2c5edf35738aa64268030 Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Sun, 10 May 2020 01:38:45 -0700 Subject: [PATCH 32/46] change language a little, add filler comments --- proposals/pattern-matching.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 8aeeb0032..4476460e1 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -447,7 +447,7 @@ The new variable `name` has its scope limited to the case's body. This matches current `when` statement capturing syntax. `var` could also be allowed to declare a mutable local variable, although this could cause issues if used with [guards](#guards). -Requiring `val` will make highly nested matches considerably more verbose. Consider: +Requiring `val` will make highly nested matches a bit more verbose. Consider: ```kotlin data class Name(val first: String, val middles: List, val last: String) data class Address(val streetAddress: String, val secondLine: String, val country: String, val state: String, val city: String, val zip: String) @@ -460,7 +460,7 @@ when(p){ Name(val first, _, val last), Address(val streetAddress, val secondLine, val country, val state, val city, val zip), _ - ) -> + ) -> // ... } ``` vs @@ -470,7 +470,7 @@ when(p){ Name(first, _, last), Address(streetAddress, secondLine, country, state, city, zip), _ - ) -> + ) -> // ... } ``` From e8cc0214b42c8514b7527de575398f875117271c Mon Sep 17 00:00:00 2001 From: Nico D'Cotta <45274424+Cottand@users.noreply.github.com> Date: Sun, 10 May 2020 10:51:17 +0200 Subject: [PATCH 33/46] Clarify some of the wording a bit and show a nicer named lambda instead of a function reference. --- proposals/pattern-matching.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index cf8eae7a3..5ea2adc47 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -560,7 +560,7 @@ An example: ```kotlin data class Person(val name: String, val age: Int, val contacts: List) val p: Person = // ... -when(p){ +when(p) { is Person(name where { "-" !in it.substringAfterLast(" ") }, age where {it >= 18}, _) -> // ... is Person(_, _, _ where {it.size >= 5}) -> // ... } @@ -568,11 +568,11 @@ when(p){ The biggest benefit of this is allowing custom match comparisons instead of just equality, including inequalities, regex, case-insensitive equality, etc. It also allows for some matching on collections or other types that don't destructure well. -A user could define their guards in functions like `is Person(name where ::isLastNameNotHyphenated, _, _)` for more complex guards. +A user could define their guards like `is Person(name where isLastNameNotHyphenated, _, _)` through named lambdas or function references, for more complex guards. A large drawback is the verbosity. Large classes with lots of guards quickly become very long. -This could cleaned up some by using a better syntax, but extra sytax will always be added to compoment declarations. -However, assuming normal guards are implemented, the guard would be just as verbose, +This could cleaned up some by using a different, shorter syntax, but extra sytax will still always be added to compoment declarations. +However, assuming normal guards are implemented, a guard would be just as verbose, it would just cover all checks at once rather than doing the check where the component is declared, which may reduce readability. Doing checks at the component declaration also allows for checks on non-assigned (`_`) components, as in the last case in the example. @@ -646,4 +646,4 @@ The author has experience with only some of these languages so additional commen [JEP 375](https://openjdk.java.net/jeps/375) -[Scala specification on pattern matching](https://www.scala-lang.org/files/archive/spec/2.11/08-pattern-matching.html) \ No newline at end of file +[Scala specification on pattern matching](https://www.scala-lang.org/files/archive/spec/2.11/08-pattern-matching.html) From 59ce476f66e5b00501e68dd69f0916088913c95e Mon Sep 17 00:00:00 2001 From: Ryan Nett Date: Sun, 10 May 2020 02:11:06 -0700 Subject: [PATCH 34/46] Add named components proposal --- proposals/pattern-matching.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index df0a9ed59..1bfa889d2 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -1,7 +1,8 @@ # Pattern Matching * **Type**: Design proposal -* **Author**: Nicolas D'Cotta +* **Author**: Nicolas D'Cotta +* **Contributors**: Ryan nett * **Status**: New @@ -549,6 +550,32 @@ val result = when(download) { } ``` +### Named components + +In the existing proposal, components must be identified through order and accessed using the `componentN` functions. +This is very limiting. Ideally, we would be able to identify components by name as well as order, like with function parameters. +Order-based components would be limited to coming before named components, again like function parameters. +Similairly, any unused components wouldn't have to be specified with `_`. + +Resolution would simply be based off of the `.` operator. +In the example below, anything that would resolve for `p.$componentName` would be a valid component name. + +The problem is that we have yet to come up with good syntax for this. For example, consider: +```kotlin +data class Person(val name: String, val age: Int) + +val p: Person = // ... + +when(p){ + is Person(name: n) -> //... +} +``` + +The way we envision it, this would mean there is a variable `n` with the value of `p.name` in scope for the case's body (similar to Rust's syntax). +However, it could be interpreted in the opposite manner and re-uses `:`, which is used for defining types. +An arrow like operator would make this clearer (e.g. `name -> n`), but is still requires adding a new operator or reusing an existing one. + + ## Implementation > Disclaimer: contributions are welcome as the author has no background on the specifics of the Kotlin compiler, and only some on JVM bytecode. From 1f2c2d3e4ede4ba585d3069278b68db6e95e88ef Mon Sep 17 00:00:00 2001 From: Nico D'Cotta <45274424+Cottand@users.noreply.github.com> Date: Sun, 10 May 2020 11:57:06 +0200 Subject: [PATCH 35/46] Move little 'Conclusion' parragraph to end of 'Matching existing identifiers' section --- proposals/pattern-matching.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index c40d9bf5c..1f9302b7c 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -423,15 +423,6 @@ same scope and cannot be redefined. This would be the most explicit way of avoiding confusing behaviour but, like shadowing, it would prevent us from matching on non literals. -
- -Matching existing identifiers **is part of the proposal** (preferably -[explicitly](#explicit-match), possibly [implicitly](#implicit-match)), but -accidental additional checks are undesired. Therefore this kind of matching -can be dropped (in favour of [shadowing](#shadow-match) or [not allowing it at -all](#no-match), preferably with [guards](#guards)) if consensus is not reached on its -semantics. - #### Specifying extraction with val This would require a `val` when a new variable is extracted, and would follow existing local variable semantics with regards to shadowing, etc. @@ -476,6 +467,16 @@ when(p){ } ``` +#### Conclusion + +Matching existing identifiers **is part of the proposal** (preferably +[explicitly](#explicit-match), possibly [implicitly](#implicit-match)), but +accidental additional checks are undesired. Therefore this kind of matching +can be dropped (in favour of [shadowing](#shadow-match) or [not allowing it at +all](#no-match), preferably with [guards](#guards)) if consensus is not reached on its +semantics. Resolving the abiguity by [explicitly indicating delcarations](#specify-val) +was not part of the original proposal, but is also an option. + ### Destructuring tuples syntax An alternative to: From d667c5bc2bcaf950d760f793b3c8d9749a32e60a Mon Sep 17 00:00:00 2001 From: Nico D'Cotta <45274424+Cottand@users.noreply.github.com> Date: Mon, 11 May 2020 00:45:41 +0200 Subject: [PATCH 36/46] Update proposal with new syntax suggestions and included guards (#4) * Update proposal with new syntax suggestions, and argue cases for both. Also include guards. Remove section on matching existing identifiers * Fix typo in first snippet example * Remove 'it' in lambda * Remove comment on destructuring tuples without type checks * Fix bad arrows in Arrow example * Fix formatting and typos --- proposals/pattern-matching.md | 346 +++++++++++++--------------------- 1 file changed, 130 insertions(+), 216 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 1f9302b7c..3460060e9 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -40,7 +40,7 @@ data class Prospect(val email: String, active: Boolean) // ... when(elem) { - is Customer(name, _, addr) -> + is Customer(name, age, addr) where { age >= 18 } -> Mail.send(addr, "Thanks for choosing us, $name!") is Prospect(addr, true) -> @@ -54,12 +54,11 @@ destructuring delcaration with added equality checks. This approach is intuitive in that the `componentN()` operator functions are used to destructure a class. -Then we pass an already defined expression (or it could be restricted to -literals) to further specify the desired match (a `Prospect` wich is -`active`, in the example above). This check can be implemented with -`equals()`. +Then we pass a literal to further specify the desired match (a `Prospect` +wich is `active`, in the example above), or a [guard](#guards). A guard +simply allows us to 'guard' against the match through a predicate. -Additionally, nested patterns could further look at the members of the class +Additionally, nested patterns could look further at the members of the class (or whatever `componentN()` might return). #### Destructuring without a type check @@ -75,17 +74,13 @@ for (p in list) { } } ``` -See [design decisions](tuples-syntax) for an alternative syntax for -destructuring tuples without a type check. - - ## Comparisons Below some examples from existing, open source Kotlin projects are listed, along with what they would look like if this KEEP was implemented. The aim of using real-world examples is to show the immediate benefit of adding the proposal (as it currently looks) to the language. -#### From the [Arrow](https://github.com/arrow-kt/arrow-core/blob/be173c05b60471b02e04a07d246d327c2272b9a3/arrow-core/src/main/kotlin/arrow/core/extensions/option.kt) library: +#### From the [Arrow](https://github.com/arrow-kt/arrow-core/blob/be173c05b60471b02e04a07d246d327c2272b9a3/arrow-core/src/main/kotlin/arrow/core/extensions/option.kt) library: Without pattern matching: ```kotlin @@ -138,9 +133,9 @@ fun Kind.eqK(other: Kind, EQ: Eq) = fun Ior.eqv(other: Ior): Boolean = when (this to other) { - is (Ior.Left(a), Ior.Left(b)) = EQL().run { a.eqv(b) } + is (Ior.Left(a), Ior.Left(b)) -> EQL().run { a.eqv(b) } is (Ior.Both(al, ar), Ior.Both(bl, br)) -> EQL().run { al.eqv(bl) } && EQR().run { ar.eqv(br) } - is (Ior.Right(a), Ior.Right(b)) = EQR().run { a.eqv(b) } + is (Ior.Right(a), Ior.Right(b)) -> EQR().run { a.eqv(b) } else -> false } ``` @@ -166,7 +161,7 @@ public val value: E get() { _state.loop { state -> when (state) { is Closed(valueException) -> throw valueException - is State<*>(== UNDEFINED) -> throw IllegalStateException("No value") + is State<*>(value) where { value == UNDEFINED } -> throw IllegalStateException("No value") is State<*>(value) -> return value as E else -> error("Invalid state $state") } @@ -293,7 +288,7 @@ The proposed syntax is to start a new `when` line with `is PATTERN -> RHS`, wher - equality check of `_const` as above - `age` is defined in `RHS` with value `subject.component2()` - `Person(name)` where `name` is a __defined__ identifier - - see [Design decisions](#match-existing-id) + - a semantic error, as `name` is already defined in scope. - `Person(_const, PATTERN2)` where `PATTERN2` is a nested pattern - `_const` is checked as above, and `PATTERN2` is checked recursively, as if `when(subject.component2()) { is PATTERN2 }` was being called. @@ -311,134 +306,79 @@ The proposed syntax is to start a new `when` line with `is PATTERN -> RHS`, wher match could succeed anyway in a scenario where two different types do `equals() = true` on each other. -## Design decisions +Additionally, a line may optionally have a guard, which may ultimately make the match fail. -Some of the semantics of this pattern matching are up to debate in the sense -that there is room to decide on behaviour that may or may not be desirable. +### Guards -### Matching existing identifiers -Consider a modified version of Jake Wharton's example: +A guard is an additional boolean constraint on a match, widely used in Haskell, Scala, and C# pattern matching. Consider the initial customers example: ```kotlin +when(elem) { + is Customer(name, age, addr) where { age > 18 } -> Mail.send(addr, "Thanks for choosing us, $name!") + is Prospect(addr, true) -> Mail.send(addr, "Please consider our product...") +} +``` +...where the additional guard allows us to avoid a nested `if` if we only wish to contact customers that are not underage. + +Additionally, a guard predicate is just a Boolean function. +``` kotlin val expected : String = // ... +val movieTitleIsValid = { m: Movie -> '%' !in m.title } val result = when(download) { - is App(name, Person(expected, _)) -> "$expected's app $name" - is Movie(title, Person(expected, _)) -> "$expected's movie $title" + is App(name, Person(author, _)) where { author == expected } -> "$expected's app $name" + is Movie(title, Person("Alice", _)) where movieTitleIsValid -> "Alice's movie $title" is App, Movie -> "Not by $expected" } ``` -It is clear that we wish to match `Person.component1()` with `expected`. But -consider: -```kotlin -val x = 2 -// use x for something -... -val result = when(DB.querySomehting()) { - is Success(x) -> "DB replied $x" - is Error(errorMsg) -> error(errorMsg) - else -> error("unexpected query result") -} -``` -...where a programmer might want to define `x` as a new match for the content -of `Success`, but ends up writing `Success.reply == 2` because they forgot -that `x` was a variable in scope. The branch taken would then be the -undesired `else`. - -Some instances of this scenario can be avoided with IDE hints clever enough -to report matches unlikely to ever succeed (like checking equality for -different types), and enforcing exhaustive patterns when matching. - -Even then, it is possible that the already defined identifier does have the -same type as the new match, and that the `else` branch exists. Different -languages handle this scenario differently, and there are a few solutions for -Kotlin: - -#### Shadowing -This would make: -```kotlin -val x = "a string!" -val someMapEntry = "Bob" to 4 -when(someMapEntry) { - is (name, x) -> // ... RHS -} -``` -...valid code, but where `x` is not matched against, but redefined in the -RHS. Much like shadowing an existing name in an existing scope, this is the -approach [Rust](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html) -takes. +A guard predicate is defined as a function `(T) -> Boolean`. Ideally, when +using a lambda literal (`{autor == expected}` in the example) destructured +components are also in scope. This is of course not encoded in the type of +the predicate. -#### Allowing matching, implicitly -This would make -```kotlin -val x = 3 -val someMapEntry = "Bob" to 4 +Guards make for very powerful matching, and more possibilities are discussed in the [Component guards](#component-guards) subsection. -when(someMapEntry) { - is (name, x) -> // ... RHS -} -``` -...valid code, where we are checking whether the second argument of the pair -equals `x` (already defined as being 3). +## Design decisions -The compiler would look for an existing `x` in the scope to decide whether we -are declaring a new `x` or just matching against an existing one. +Some of the semantics of this pattern matching are up to debate in the sense +that there is room to decide on behaviour or syntax that may or may not be desirable. -This can lead to the issue described at the beginning of this section, but -IDE hinting could be used to indicate the matching attempt. Indicators could -be extra colours or a symbol on the left bar (like the one currently in -place for suspending functions). +### Restricting match expressions to literals -#### Allowing matching, explicitly -This would require an additional syntactic construct to indicate whether we -wish to match the existing variable named `x`, or to extract a new variable -named `x`. Such a construct could look like: +This proposal argues **in favour** of only allowing literals as match expressions. This would make the following invalid code: ```kotlin -val x = 3 -val someMapEntry = "Bob" to 4 - -when(someMapEntry) { - is (name, ==x) -> // ... RHS +// invalid code +when ("Bob" to 4) { + is Pair(DB.getBobsName(), num) -> // ... } ``` -...which makes it clear that we aim to test for equality between `x` and the extracted second parameter of the pair. Scala uses this approach through [stable identifiers](https://www.scala-lang.org/files/archive/spec/2.11/08-pattern-matching.html#stable-identifier-patterns). - -The syntactic construct presented in the example is rather arbitrary and -suggestions on different ones are welcome. - -This version works well with [component guards](#component-guards), as any equality checking could be done in the guard instead of using syntax like `==x`. - -#### Not allowing matching existing identifiers at all -This would make +... in favour of: ```kotlin -val x = 3 -val someMapEntry = "Bob" to 4 - -when(someMapEntry) { - is (name, x) -> // ... RHS +// valid code +val expected = DB.getBobsName() +when ("Bob" to 4) { + is Pair(name, num) where name == expected -> // ... } ``` -...throw a semantic error at compile time, where `x` is defined twice in the -same scope and cannot be redefined. This would be the most explicit way of -avoiding confusing behaviour but, like shadowing, it would prevent us from -matching on non literals. +This is for the following reasons: + - This discourages inlining for equality checks in favour of using guards, which makes for more readable matches. + - It resolves the ambiguouity where a deconstructed type match looks like a class constructor. + +### Specifying extraction with `val` + +This suggestion would require `val` when a new variable is extracted, and would lift the [literals only](#literals-only) restriction (along with [`is` for nesting](#use-nested-is)). -#### Specifying extraction with val -This would require a `val` when a new variable is extracted, and would follow existing local variable semantics with regards to shadowing, etc. -Example: +For example: ```kotlin val x = 3 val someMapEntry = "Bob" to 4 -when(someMapEntry) { +when (someMapEntry) { is (val name, x) -> // ... RHS. name is in scope here } ``` -The new variable `name` has its scope limited to the case's body. - -This matches current `when` statement capturing syntax. -`var` could also be allowed to declare a mutable local variable, although this could cause issues if used with [guards](#guards). +As with the alternative syntax, the new variable `name` has its scope limited to the case's body in the RHS. Requiring `val` will make highly nested matches a bit more verbose. Consider: ```kotlin @@ -456,7 +396,7 @@ when(p){ ) -> // ... } ``` -vs +...vs: ```kotlin when(p){ is Person( @@ -467,45 +407,41 @@ when(p){ } ``` -#### Conclusion - -Matching existing identifiers **is part of the proposal** (preferably -[explicitly](#explicit-match), possibly [implicitly](#implicit-match)), but -accidental additional checks are undesired. Therefore this kind of matching -can be dropped (in favour of [shadowing](#shadow-match) or [not allowing it at -all](#no-match), preferably with [guards](#guards)) if consensus is not reached on its -semantics. Resolving the abiguity by [explicitly indicating delcarations](#specify-val) -was not part of the original proposal, but is also an option. +### Nested patterns using `is` -### Destructuring tuples syntax +This suggestion involves re-using the keyword `is` every time a nested pattern matched is performed. Potentially, it could allow simply recursing on `when` conditions in general: -An alternative to: +Consider the original proposal syntax: ```kotlin -when(person) { - is ("Alice", age) -> // ... +val result = when(download) { + is App(name, Person("Alice", _)) -> "Alice's app $name" + is Movie(title, Person("Alice", _)) -> "Alice's movie $title" + is App, Movie -> "Not by Alice" } ``` -...was suggested. It would look like: +... as opposed to the alternative: ```kotlin -when (person) { - ("Alice", age) -> // ... +val result = when(download) { + is App(name, is Person("Alice", in 0..18)) -> "Alice's app $name" + is Movie(val title, Person("Alice", _)) -> "Alice's movie $title" + is App, Movie -> "Not by Alice" } ``` -Where `is` is omitted as no actual type check occurs in this scenario. This proposal argues for keeping `is` as a keyword necessary to pattern matching. Consider this example that uses the alternative syntax: -```kotlin -// A sealed class Option = Some(a) or None is in scope here +The argument in favour of this syntax is that the added keywords clarify any ambiguities. This proposal argues **against** this more verbose syntax for the following reasons: + - It lifts the [literals only](#literals-only) restriction, which would allow the user to inline things inside a pattern match (making for harder to read equality checks) + - It is extremely cumbersome to write for nested patterns of more than one level, or where there is a lot of inspection of destructured elements. Consider how using `is` and `val` every time would look for the [Arrow example](#arrow-example) -val pairOfOption = 5 to Some(3) -val something = when (pairOfOption) { - (number1, Some(number2)) -> // ... - (number, None) -> // ... -} -``` -Here, a full blown pattern match happens where we extract number2 from -`Option` and do an exhaustive type check on the sealed class for -`Some` or `None`. In this scenario, `instanceof` typechecks happen, but no -`is` keyword is present. Thus keeping `is` is favourable as it clearly -indicates a type check albeit at the price of some verbosity. +#### Conclusion + +There are two competing alternative syntax possibilites for pattern matching in Kotlin. While this proposal argues for one that +- is slightly more restrictive (no nested `when` conditions) +- is less verbose +- encourages guards + +...using `val` and `is` would make for a pattern mathcing that + - is more verbose (but offers some clarity) + - allows inlining for equality checking + - allows recursive `when` conditions (like `in 0..18`) ### Restricting matching to data classes only @@ -513,11 +449,6 @@ A possibilty suggested during the conception of this proposal was to restrict pa This proposal argues **against** this restriction. Matching anything that implements `componentN()` has the important benefit of being able to match on 3rd party classes or interfaces that are not data classes, and to extend them for the sole purpose of matching. A notable example is `Map.Entry`, which is a Java interface. - - ## Limitations ### Matching on collections @@ -532,72 +463,29 @@ This limitation is due to the fact that in Haskell, a list is represented more similarly to how sealed classes work in Kotlin (and we can match on those). -Pattern mathcing on collections is **not** the aim of this proposal, but such +**Pattern mathcing on collections is not the aim of this proposal**, but such a thing *could* be achieved through additional extension functions on some -interfaces with the sole purpose of matching them: +interfaces with the sole purpose of matching on them: ```kotlin -inline fun List destructFst() = - get(0) to if (size == 1) null else drop(1) +inline fun List destructFst() = when(size) { + 0 -> null + else -> first() to drop(1) + } val ls = listOf(1,2,3,4) fun mySum(l: List) = when(l.destructFst()) { - is (head, null) -> head + null -> 0 is (head, tail) -> head + mySum(tail) } - -// or: - -fun List.mySum2() = when(this.destructFst()) { - is (head, tail) -> head + tail?.mySum2()?:0 -} ``` +This proposal does not argue for including such extension functions in the standard library. ## Beyond the proposal The discussion and specification of the actual construct this proposal aims to introduce into the language ends here. But this section covers some possible additions that could be interesting to discuss if they are popular, and are in the spirit of Kotlin's idioms. -### Membership matching - -Consider: -```kotlin -data class Point(val x: Double, val y: Double) -val p: Point = /... -val max = Double.MAX_VALUE -val min = Double.MIN_VALUE -val location = when(p) { - is (in 0.0..max, in 0.0..max) -> "Top right quadrant of the graph" - is (in min..0.0, in 0.0..max) -> "Top left" - is (in min..0.0, in min..0.0) -> "Bottom left" - is (in 0.0..max, in min..00) -> "Bottom right" -} -``` -...where a destructured `componentN()` in `Point` is called as an argument to `in`, using the operator function `contains()`. This would allow to use pattern matching to test for membership of collections, ranges, and anything that might implement `contains()`. Swift has this idiom through the `~=` operator. - -### Guards - -A guard is an additional boolean constraint on a match, widely used in Haskell or Scala pattern matching. Consider a variation of the initial customers example: -```kotlin -when(elem) { - is Customer(name, age, addr) where age > 18 -> Mail.send(addr, "Thanks for choosing us, $name!") - is Prospect(addr, true) -> Mail.send(addr, "Please consider our product...") -} -``` -...where the additional guard allows us to avoid a nested `if` if we only wish to contact customers that are not underage. It would also cover most cases [membership matching](#in-match) covers, and makes for very readable matching. - -Additionally, guards would solve the problem of matching existing identifiers. Consider this example: -``` kotlin -val expected : String = // ... - -val result = when(download) { - is App(name, Person(author, _)) where author == expected -> "$expected's app $name" - is Movie(title, Person(author, _)) where author == expected-> "$expected's movie $title" - is App, Movie -> "Not by $expected" -} -``` - - #### Component Guards Guards could be extended to allow guards on destructured components, instead of just the entire case. @@ -607,26 +495,33 @@ An example: data class Person(val name: String, val age: Int, val contacts: List) val p: Person = // ... when(p) { - is Person(name where { "-" !in it.substringAfterLast(" ") }, age where {it >= 18}, _) -> // ... - is Person(_, _, _ where {it.size >= 5}) -> // ... + is Person(name where { "-" !in it }, age where {it >= 18}, _) -> // ... + is Person(_, _, _ where { it.size >= 5 }) -> // ... } ``` -The biggest benefit of this is allowing custom match comparisons instead of just equality, including inequalities, regex, case-insensitive equality, etc. -It also allows for some matching on collections or other types that don't destructure well. -A user could define their guards like `is Person(name where isLastNameNotHyphenated, _, _)` through named lambdas or function references, for more complex guards. +The biggest benefit of this is allowing custom matches on any of the destructured arguments +It also allows for some matching on collections or other types that don't destructure well: +```kotlin +val ls = listOf(1,2,3,4) + +when("SomeList" to ls) { + is (_, list where Collection::isNotEmpty) -> // ... use list with the sweet relief of knowing it is not empty +} +``` +A user could define their guards like `is Person(name where isLastNameNotHyphenated, _, _)` through named lambdas or function references, for more complex matching. Because the guards are named, they stay readable. A large drawback is the verbosity. Large classes with lots of guards quickly become very long. This could cleaned up some by using a different, shorter syntax, but extra sytax will still always be added to compoment declarations. However, assuming normal guards are implemented, a guard would be just as verbose, it would just cover all checks at once rather than doing the check where the component is declared, which may reduce readability. -Doing checks at the component declaration also allows for checks on non-assigned (`_`) components, as in the last case in the example. +Doing checks at the component declaration also allows for checks on non-assigned (`_`) components, as in the last case in one of the examples. ### Named components In the existing proposal, components must be identified through order and accessed using the `componentN` functions. -This is very limiting. Ideally, we would be able to identify components by name as well as order, like with function parameters. +This is limiting. Ideally, we would be able to identify components by name as well as order, like with function parameters. Order-based components would be limited to coming before named components, again like function parameters. Similairly, any unused components wouldn't have to be specified with `_`. @@ -644,9 +539,18 @@ when(p){ } ``` -The way we envision it, this would mean there is a variable `n` with the value of `p.name` in scope for the case's body (similar to Rust's syntax). -However, it could be interpreted in the opposite manner and re-uses `:`, which is used for defining types. -An arrow like operator would make this clearer (e.g. `name -> n`), but is still requires adding a new operator or reusing an existing one. +The way we envision it, this would mean there is a variable `n` with the +value of `p.name` in scope for the case's body (similar to Rust's syntax). + + +However, it could be interpreted in the opposite manner and re-uses `:`, +which is used for a 'is-of-type' relationship. +An arrow-like operator would make this clearer (e.g. `name -> n`), but is +still requires adding a new operator or reusing an existing one. + +While this +is a very desirable feature, this proposal lacks good syntax for it, so +contributions are welcome. ## Implementation > Disclaimer: contributions are welcome as the author has no background on the specifics of the Kotlin compiler, and only some on JVM bytecode. @@ -674,12 +578,22 @@ when (Some(1) to Some(2)) { } ``` ... where `Option` is a sealed class which is either `Some(a)` or `None`. -- In case 1, the right `Some` case has been matched, whereas on the left no case has been matched -- In case 2, we finally match the right for `Some`. There are only 2 possible cases for `Option`, so we are waiting to match `None` for both left and right. -- In case 3, we can make progress on matching `None` for the left, but not for the right. -- In case 4, `None` is finally matched for both left and right, so we can infer that an `else` branch is not necessary. +- In case 1, the right `Some` case has been matched, whereas on the left no +case has been matched +- In case 2, we finally match the right for `Some`. There are only 2 possible +cases for `Option`, so we are waiting to match `None` for both left and +right. +- In case 3, we can make progress on matching `None` for the left, but not +for the right. +- In case 4, `None` is finally matched for both left and right, so we can +infer that an `else` branch is not necessary. -This example uses `Some(1) to Some(2)` for the sake of briefness, but ideally, the compiler can infer that the matches on `None` can't ever succeed, because we are matching a `Pair`. +This example uses `Some(1) to Some(2)` for the sake of briefness, but +ideally, the compiler can infer that the matches on `None` can't ever +succeed, because we are matching a `Pair`. + +Note that no progress can be made with `when` clauses that use guards, unless +contracts used by guards are taken into account by the compiler. ## Alternatives @@ -701,7 +615,7 @@ widely popular. ## Comparison to other languages -- Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375)) +- Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375)) without guards - [C# supports this](https://docs.microsoft.com/en-us/dotnet/csharp/pattern-matching) with a more verbose syntax through `case` .. `when` .. - In [Haskell](https://www.haskell.org/tutorial/patterns.html) pattern matching (along with guards) is a core language feature extensively used to traverse data structures and to define functions, mathcing on their arguments - [Rust](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html) supports pattern matching through `match` expressions From d68d57d07f3380cd51e11923baad5178e15fd51f Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Mon, 11 May 2020 08:18:52 +0200 Subject: [PATCH 37/46] Add JEP draft of java pattern matching to comparisons --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 3460060e9..88bcfc0e2 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -615,7 +615,7 @@ widely popular. ## Comparison to other languages -- Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375)) without guards +- Java is considering this (see [JEP 375](https://openjdk.java.net/jeps/375) and [JEP draft 8213076](https://openjdk.java.net/jeps/8213076)). Without guards, but the draft JEP mentions plans to add them in the future - [C# supports this](https://docs.microsoft.com/en-us/dotnet/csharp/pattern-matching) with a more verbose syntax through `case` .. `when` .. - In [Haskell](https://www.haskell.org/tutorial/patterns.html) pattern matching (along with guards) is a core language feature extensively used to traverse data structures and to define functions, mathcing on their arguments - [Rust](https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html) supports pattern matching through `match` expressions From 49df109a86df72047ec857ab3bea7bbe5a28e7bf Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Mon, 11 May 2020 12:13:47 +0200 Subject: [PATCH 38/46] Fix bad reference name of named components and mistake in implementation snippet --- proposals/pattern-matching.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 88bcfc0e2..1eb93d6a7 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -534,7 +534,7 @@ data class Person(val name: String, val age: Int) val p: Person = // ... -when(p){ +when(p) { is Person(name: n) -> //... } ``` @@ -567,12 +567,12 @@ arguments, assuming all these function calls are O(1). Note this is not a safe assumption (the calls are user defined) but it should be by far the common case. -While destructuring and checking for equality (with or without [guards](#guards) or [identifier matching](#match-existing-id)) should be mostly trivial, checking for exhaustiveness in nested patterns is not. The proposal suggests a naive implementation where a table is used for each level of nesting for each destructured element. For example, in order to call `when` on a `Pair` of `Option`s: +While destructuring and checking for equality (with or without [guards](#guards) or [identifier matching](#named-components)) should be mostly trivial, checking for exhaustiveness in nested patterns is not. The proposal suggests a naive implementation where a table is used for each level of nesting for each destructured element. For example, in order to call `when` on a `Pair` of `Option`s: ```kotlin when (Some(1) to Some(2)) { is (Some(4), Some(y)) -> ... // case 1 - is (Some(x), None) -> ... // case 2 + is (Some(x), Some(y)) -> ... // case 2 is (None, Some(3)) -> ... // case 3 is (_, None) -> ... // case 4 } @@ -580,7 +580,7 @@ when (Some(1) to Some(2)) { ... where `Option` is a sealed class which is either `Some(a)` or `None`. - In case 1, the right `Some` case has been matched, whereas on the left no case has been matched -- In case 2, we finally match the right for `Some`. There are only 2 possible +- In case 2, we finally match the left for any `Some`. There are only 2 possible cases for `Option`, so we are waiting to match `None` for both left and right. - In case 3, we can make progress on matching `None` for the left, but not From d5c160e67fbe7f16036cb70bc1aac13bba758605 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sun, 17 May 2020 10:51:02 +0200 Subject: [PATCH 39/46] Refrase case for less verbose syntax and add example under Desing Decisions --- proposals/pattern-matching.md | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 1eb93d6a7..cb810af5e 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -427,13 +427,40 @@ val result = when(download) { is App, Movie -> "Not by Alice" } ``` -The argument in favour of this syntax is that the added keywords clarify any ambiguities. This proposal argues **against** this more verbose syntax for the following reasons: - - It lifts the [literals only](#literals-only) restriction, which would allow the user to inline things inside a pattern match (making for harder to read equality checks) - - It is extremely cumbersome to write for nested patterns of more than one level, or where there is a lot of inspection of destructured elements. Consider how using `is` and `val` every time would look for the [Arrow example](#arrow-example) +The argument in favour of this syntax is that the added keywords clarify any +ambiguities. This proposal argues **against** this more verbose syntax for +the following reasons: + - It lifts the [literals only](#literals-only) restriction, which would + allow the user to inline things inside a pattern match (making for harder to + read equality checks) + - It is much more verbose to write for nested patterns of more than one + level, or where there is a lot of inspection of destructured elements. + Consider how using `is` and `val` every time would look for the + [Arrow example](#arrow-example), which does not even make use of +more than 1 level of nesting nor guards: + +```kotlin +fun Kind.eqK(other: Kind, EQ: Eq) = + when(this.fix() to other.fix()) { + is (is Some(val a), is Some(val b)) -> EQ.run { a.eqv(b) } + is (is None, is None) -> true + else -> false + } + + +fun Ior.eqv(other: Ior): Boolean = when (this to other) { + is (is Ior.Left(val a), is Ior.Left(val b)) -> EQL().run { a.eqv(b) } + is (is Ior.Both(val al, val ar), is Ior.Both(val bl, val br)) -> + EQL().run { al.eqv(bl) } && EQR().run { ar.eqv(br) } + is (is Ior.Right(val a), is Ior.Right(val b)) -> EQR().run { a.eqv(b) } + else -> false +} +``` #### Conclusion -There are two competing alternative syntax possibilites for pattern matching in Kotlin. While this proposal argues for one that +There are two competing alternative syntax possibilites for pattern matching +in Kotlin. While this proposal argues for one that - is slightly more restrictive (no nested `when` conditions) - is less verbose - encourages guards From 7108c931b80161706b70bf20c3740cc7b25c9cd2 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sun, 17 May 2020 13:32:02 +0200 Subject: [PATCH 40/46] Add TornadoFX and dokka examples --- proposals/pattern-matching.md | 56 +++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index cb810af5e..8a4c6344e 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -75,6 +75,8 @@ for (p in list) { } ``` ## Comparisons + +### Existing code Below some examples from existing, open source Kotlin projects are listed, along with what they would look like if this KEEP was implemented. The aim of using real-world examples is to show the immediate benefit of adding the @@ -182,7 +184,6 @@ infix fun Expression.and(op: Expression): Op = when { }) else -> AndOp(listOf(this, op)) } - ``` With pattern matching: @@ -196,9 +197,60 @@ infix fun Expression.and(op: Expression): Op = when(t }) else -> AndOp(listOf(this, op)) } +``` + +#### From [TornadoFX](https://github.com/edvin/tornadofx/blob/facf7e8dd904e66a5516f2283538c12da55a085e/src/main/java/tornadofx/Rest.kt): +(in `fun one()`) + +Without pattern matching: +```kotlin +return when (val json = Json.createReader(StringReader(content)).use { it.read() }) { + is JsonArray -> { + if (json.isEmpty()) + return Json.createObjectBuilder().build() + else + return json.getJsonObject(0) + } + is JsonObject -> json + else -> throw IllegalArgumentException("Unknown json result value") +``` + +With pattern matching: + +```kotlin +return when (val json = Json.createReader(StringReader(content)).use { it.read() }) { + is JsonArray where { it.isEmpty() } -> Json.createObjectBuilder().build() + is JsonArray -> json.getJsonObject(0) + is JsonObject -> json + else -> throw IllegalArgumentException("Unknown json result value") ``` +#### From [dokka](https://github.com/Kotlin/dokka/blob/de2f32d91fb6f564826ddd7644940452356e2080/core/src/main/kotlin/Samples/DefaultSampleProcessingService.kt): + +Without pattern matching: + +```kotlin +fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { + is KtDeclarationWithBody -> { + val bodyExpression = psiElement.bodyExpression + when (bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + else -> psiElement.text +} +``` +With pattern matching: + +```kotlin +fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { + is KtDeclarationWithBody(KtBlockExpression(text)) -> text.removeSurrounding("{", "}") + is KtDeclarationWithBody(body) -> body!!.text + else -> psiElement.text +} +```` ### More textbook comparisons #### From [Jake Wharton at KotlinConf '19](https://youtu.be/te3OU9fxC8U?t=2528) @@ -218,7 +270,7 @@ val result = when(download) { val (name, dev) = download when(dev) { is Person -> - if(dev.name == "Alice") "Alice's app $name" else "Not by Alice" + if (dev.name == "Alice") "Alice's app $name" else "Not by Alice" else -> "Not by Alice" } } From 22700cbd131b632a36a8121fa3fb54c8ce4081db Mon Sep 17 00:00:00 2001 From: Nico D'Cotta Date: Sun, 17 May 2020 13:51:05 +0200 Subject: [PATCH 41/46] Add missing curly braces in snippet under Design Decisions section --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 8a4c6344e..3dbe0d93c 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -409,7 +409,7 @@ when ("Bob" to 4) { // valid code val expected = DB.getBobsName() when ("Bob" to 4) { - is Pair(name, num) where name == expected -> // ... + is Pair(name, num) where { name == expected } -> // ... } ``` This is for the following reasons: From 7f07bdb5e1576dff75a81c99fc7b2526de36109d Mon Sep 17 00:00:00 2001 From: Nico D'Cotta <45274424+Cottand@users.noreply.github.com> Date: Sat, 23 May 2020 17:21:33 +0200 Subject: [PATCH 42/46] Fix typo in proposals/pattern-matching.md Co-authored-by: Nicola Corti --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 3dbe0d93c..4b8a30fdd 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -50,7 +50,7 @@ when(elem) { The syntax proposed uses the already existing `is` construct to check the type of the subject, but adds what semantically looks a lot like a -destructuring delcaration with added equality checks. This approach is +destructuring declaration with added equality checks. This approach is intuitive in that the `componentN()` operator functions are used to destructure a class. From 0a74355bc2ce07740e4f179594d8606e46e563a8 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta <45274424+Cottand@users.noreply.github.com> Date: Sat, 23 May 2020 17:21:58 +0200 Subject: [PATCH 43/46] Fix typo in proposals/pattern-matching.md Co-authored-by: Nicola Corti --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 4b8a30fdd..9cd6a4274 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -517,7 +517,7 @@ in Kotlin. While this proposal argues for one that - is less verbose - encourages guards -...using `val` and `is` would make for a pattern mathcing that +...using `val` and `is` would make for a pattern matching that - is more verbose (but offers some clarity) - allows inlining for equality checking - allows recursive `when` conditions (like `in 0..18`) From e5eac476f5d4be89f79cbed39fb99a2ea531d762 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta <45274424+Cottand@users.noreply.github.com> Date: Sat, 23 May 2020 17:22:15 +0200 Subject: [PATCH 44/46] Fix typo in proposals/pattern-matching.md Co-authored-by: Nicola Corti --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 9cd6a4274..55a526cc3 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -524,7 +524,7 @@ in Kotlin. While this proposal argues for one that ### Restricting matching to data classes only -A possibilty suggested during the conception of this proposal was to restrict pattern matching to data classes. The argument would be to start with a samller size of 'matchable' elements in order to keep the inital proposal and feature as simple as possible, as it could be extended later down the line. +A possibility suggested during the conception of this proposal was to restrict pattern matching to data classes. The argument would be to start with a smaller size of 'matchable' elements in order to keep the initial proposal and feature as simple as possible, as it could be extended later down the line. This proposal argues **against** this restriction. Matching anything that implements `componentN()` has the important benefit of being able to match on 3rd party classes or interfaces that are not data classes, and to extend them for the sole purpose of matching. A notable example is `Map.Entry`, which is a Java interface. From 003ac6256ef8fa657dd2e6f887cc393e210c48ef Mon Sep 17 00:00:00 2001 From: Nico D'Cotta <45274424+Cottand@users.noreply.github.com> Date: Sat, 23 May 2020 17:22:28 +0200 Subject: [PATCH 45/46] Fix typo in proposals/pattern-matching.md Co-authored-by: Nicola Corti --- proposals/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 55a526cc3..80e36bc60 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -542,7 +542,7 @@ This limitation is due to the fact that in Haskell, a list is represented more similarly to how sealed classes work in Kotlin (and we can match on those). -**Pattern mathcing on collections is not the aim of this proposal**, but such +**Pattern matching on collections is not the aim of this proposal**, but such a thing *could* be achieved through additional extension functions on some interfaces with the sole purpose of matching on them: ```kotlin From 6e7ef6540a20bc0c66d4e9dd125771dc705858d8 Mon Sep 17 00:00:00 2001 From: Nico D'Cotta <45274424+Cottand@users.noreply.github.com> Date: Wed, 17 Jun 2020 16:35:36 +0200 Subject: [PATCH 46/46] Add subsection on 'if' guards as proposed by @Kroppeb (#5) * Add subsection on 'if' guards as proposed by @Kroppeb * Remove additional else in chained guards * Add haskell source and format Also make the arrow Option example exhaustive --- proposals/pattern-matching.md | 59 ++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/proposals/pattern-matching.md b/proposals/pattern-matching.md index 80e36bc60..848a0cef9 100644 --- a/proposals/pattern-matching.md +++ b/proposals/pattern-matching.md @@ -130,7 +130,8 @@ fun Kind.eqK(other: Kind, EQ: Eq) = when(this.fix() to other.fix()) { is (Some(a), Some(b)) -> EQ.run { a.eqv(b) } is (None, None) -> true - else -> false + is (Some, None), is (None, Some) -> false + // Note all cases are checked } @@ -388,7 +389,54 @@ using a lambda literal (`{autor == expected}` in the example) destructured components are also in scope. This is of course not encoded in the type of the predicate. -Guards make for very powerful matching, and more possibilities are discussed in the [Component guards](#component-guards) subsection. +#### Alternative `if` syntax + +The syntax for guards discussed so far focuses in expecting a function, in +order to easily compose and define custom guards. A possible alternative +could be the more familiar `if` syntax, which instead uses an +expression: + +``` kotlin +val expected : String = // ... +fun movieTitleIsValid(m: Movie) = '%' !in m.title + +val result = when(download) { + is App(name, Person(author, _)) if (author == expected) -> "$expected's app $name" + is Movie(title, Person("Alice", _)) + if (movieTitleIsValid(download)) -> "Alice's movie $title" + is App, Movie -> "Not by $expected" +} +``` + > Note that indentation and the choice of line breaks are merely a suggestion + +Additionally, this could be combined with the already common `else if` +construct in order to chain guards: + +```kotlin +sealed class Elem +data class Customer(val name: String, val age: Int, val email: String) : Elem() +data class Prospect(val homeAddress: Location, val email: String, active: Boolean) : Elem() +// ... + +val text = when(elem) { + is Customer(name, age, _) + if (age >= 18) -> "Thanks for choosing us, $name!" + else -> error("We should not have underage customers") + is Prospect(addr, _, _) + if (addr in Countries.Spanish) -> "Considere la compra de nuestro producto..." + // maybe `else if` instead? + if (addr in Countries.French) -> "Veulliez considérer l'achat de notre produit"... + else -> "Please consider buying our product..." +} +``` + +This is a common idiom in +[Haskell](https://www.futurelearn.com/courses/functional-programming-haskell/0/steps/27226). While this form of guards may look very similar to a nested `if` expression, note that it is different as an `else` entry is not necessarily required if exhaustiveness is achieved. + +
+ +Guards make for very powerful matching, and more possibilities are discussed +in the [Component guards](#component-guards) subsection. ## Design decisions @@ -475,7 +523,7 @@ val result = when(download) { ```kotlin val result = when(download) { is App(name, is Person("Alice", in 0..18)) -> "Alice's app $name" - is Movie(val title, Person("Alice", _)) -> "Alice's movie $title" + is Movie(val title, is Person("Alice", _)) -> "Alice's movie $title" is App, Movie -> "Not by Alice" } ``` @@ -585,7 +633,8 @@ It also allows for some matching on collections or other types that don't destru val ls = listOf(1,2,3,4) when("SomeList" to ls) { - is (_, list where Collection::isNotEmpty) -> // ... use list with the sweet relief of knowing it is not empty + is (_, list where Collection::isNotEmpty) -> + // ... use list with the sweet relief of knowing it is not empty } ``` A user could define their guards like `is Person(name where isLastNameNotHyphenated, _, _)` through named lambdas or function references, for more complex matching. Because the guards are named, they stay readable. @@ -712,3 +761,5 @@ The author has experience with only some of these languages so additional commen [JEP 375](https://openjdk.java.net/jeps/375) [Scala specification on pattern matching](https://www.scala-lang.org/files/archive/spec/2.11/08-pattern-matching.html) + +[Haskell.org pattern atching tutorial](https://www.haskell.org/tutorial/patterns.html)