From f25e7f8a23456654f5602e811acca0bf2c466497 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Thu, 3 Oct 2024 17:05:20 +0200 Subject: [PATCH 01/21] strict equality pattern matching --- content/strict-equality-pattern-matching.md | 102 ++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 content/strict-equality-pattern-matching.md diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md new file mode 100644 index 00000000..cad40da9 --- /dev/null +++ b/content/strict-equality-pattern-matching.md @@ -0,0 +1,102 @@ +--- +layout: sip +permalink: /sips/:title.html +stage: implementation +status: waiting-for-implementation +presip-thread: https://contributors.scala-lang.org/t/pre-sip-foo-bar/9999 +title: SIP-NN - Strict-Equality pattern matching +--- + +**By: Matthias Berndt** + +## History + +| Date | Version | +|---------------|--------------------| +| Oct 3rd 2024 | Initial Draft | + +## Summary + +This proposal aims to make the `strictEquality` feature easier to adopt by avoiding the need for a `CanEqual` instance +when matching a `sealed` or `enum` type against singleton cases (e. g. `Nil` or `None`). + +## Motivation + +The `strictEquality` feature is important to improve type safety. However due to the way that pattern matching in +Scala works, it often requires `CanEqual` instances where they conceptually don't really make sense, as evidenced +by the fact that in e. g. Haskell, an `Eq` instance is never required to perform a pattern matching. +It also seems arbitrary that a `CanEqual` instance is required to match on types such as `Option` or `List` but +not for e. g. `Either`. + + +A simple example is this code: + +```scala +import scala.language.strictEquality + +enum Nat: + case Zero + case Succ(n: Nat) + +extension(l: Nat) def +(r: Nat): Nat = + l match + case Nat.Zero => r + case Nat.Succ(x) => Nat.Succ(x + r) +``` +This fails to compile with the following error message: + +``` +[error] ./nat.scala:9:10 +[error] Values of types Nat and Nat cannot be compared with == or != +[error] case Nat.Zero => r +[error] ^^^^^^^^ +``` +### Possible fixes today + - add a `derives CanEqual` clause to the ADT definition. This is unsatisfactory for multiple reasons: + - it is additional boilerplate code that needs to be added in potentially many places when enabling this option, thus hindering adoption + - the ADT might not be under the user's control, e. g. defined in a 3rd party library + - one might not *want* a `CanEqual` instance to be available for this type because one doesn't want this type to be compared with the `==` + operator. For example, when one of the fields in the `enum` is a function, it actually isn't possible to perform a meaningful equality check. + - turn the no-argument-list cases into empty-argument-list cases: + ```scala + enum Nat: + case Zero() // notice the parens + case Succ(n: Nat) + ``` + The downsides are similar to the previous point: + - doesn't work for ADTs defined in a library + - hinders adoption in existing code bases by requiring new syntax (even more so, because now you not only need to change the `enum` definition but also every `match` and `PartialFunction` literal) + - uglier than before + - pointless overhead: can have more than one `Zero()` object at run-time + - perform a type check instead: + ```scala + l match + case _: Nat.Zero.type => r + case Nat.Succ(x) => Nat.Succ(x + r) + ``` + But like the previous solutions: + - hinders adoption in existing code bases by requiring new syntax + - looks uglier than before (even more so than the empty-argument-list thing) + +For these reasons the current state of affairs is unsatisfactory and needs to improve in order to encourage adoption of `strictEquality` in existing code bases. +## Proposed solution + +### Specification + +The proposed solution is to perform an equality check without requiring a `CanEqual` instance when pattern matching when: + - the scrutinee's type is a `sealed` type and the pattern is a `case object` that extends the scrutinee's type, or + - the scrutinee's type is an `enum` type and the pattern is one of the enum's cases without a parameter list (e. g. `Nat.Z`) + +### Compatibility + +This change creates no new compatibility issues and improves the compatibility of the `strictEquality` feature with existing code bases. + +## Alternatives + +It was proposed to instead change the `enum` feature so that it always includes an implicit `derives CanEqual` clause. This is unsatisfactory for many reasons: + - doesn't work for sealed types + - doesn't work for 3rd party libraries compiled with an older compiler + - `CanEqual` might be unwanted for that type – just because I want to perform pattern matching against an `enum` type doesn't mean I want to allow usage of `==` + +## FAQ + From a87d3ac04349c8e6ebf9e89a80ca1babd99b8932 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Thu, 3 Oct 2024 22:40:08 +0200 Subject: [PATCH 02/21] add related work section to strictEquality pattern matching proposal --- content/strict-equality-pattern-matching.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index cad40da9..aa3649bf 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -3,7 +3,7 @@ layout: sip permalink: /sips/:title.html stage: implementation status: waiting-for-implementation -presip-thread: https://contributors.scala-lang.org/t/pre-sip-foo-bar/9999 +presip-thread: https://contributors.scala-lang.org/t/pre-sip-better-strictequality-support-in-pattern-matching/6781 title: SIP-NN - Strict-Equality pattern matching --- @@ -98,5 +98,10 @@ It was proposed to instead change the `enum` feature so that it always includes - doesn't work for 3rd party libraries compiled with an older compiler - `CanEqual` might be unwanted for that type – just because I want to perform pattern matching against an `enum` type doesn't mean I want to allow usage of `==` +## Related Work + - https://contributors.scala-lang.org/t/pre-sip-better-strictequality-support-in-pattern-matching/6781 + - https://contributors.scala-lang.org/t/how-to-improve-strictequality/6722 + - https://contributors.scala-lang.org/t/enumeration-does-not-derive-canequal-for-strictequality/5280 + ## FAQ From 7fe25c63c1958bed710d96557ddc1aa162a8264a Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Fri, 4 Oct 2024 00:41:43 +0200 Subject: [PATCH 03/21] wordsmithing --- content/strict-equality-pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index aa3649bf..1ca842d2 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -83,7 +83,7 @@ For these reasons the current state of affairs is unsatisfactory and needs to im ### Specification -The proposed solution is to perform an equality check without requiring a `CanEqual` instance when pattern matching when: +The proposed solution is to not require a `CanEqual` instance during pattern matching when: - the scrutinee's type is a `sealed` type and the pattern is a `case object` that extends the scrutinee's type, or - the scrutinee's type is an `enum` type and the pattern is one of the enum's cases without a parameter list (e. g. `Nat.Z`) From 6a4956ec10e46bd3f150d1ba2b183f528293761a Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Fri, 4 Oct 2024 12:16:38 +0200 Subject: [PATCH 04/21] add paragraph about using a type check instead of equals --- content/strict-equality-pattern-matching.md | 22 ++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 1ca842d2..d5179d55 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -11,9 +11,11 @@ title: SIP-NN - Strict-Equality pattern matching ## History -| Date | Version | -|---------------|--------------------| -| Oct 3rd 2024 | Initial Draft | +| Date | Version | +|---------------|-----------------------------------------------------------| +| Oct 3rd 2024 | Initial Draft | +| Oct 3rd 2024 | Related Work | +| Oct 4th 2024 | Add paragraph about using a type check instead of equals | ## Summary @@ -93,10 +95,16 @@ This change creates no new compatibility issues and improves the compatibility o ## Alternatives -It was proposed to instead change the `enum` feature so that it always includes an implicit `derives CanEqual` clause. This is unsatisfactory for many reasons: - - doesn't work for sealed types - - doesn't work for 3rd party libraries compiled with an older compiler - - `CanEqual` might be unwanted for that type – just because I want to perform pattern matching against an `enum` type doesn't mean I want to allow usage of `==` + - It was proposed to instead change the `enum` feature so that it always includes an implicit `derives CanEqual` clause. This is unsatisfactory for many reasons: + - doesn't work for sealed types + - doesn't work for 3rd party libraries compiled with an older compiler + - `CanEqual` might be unwanted for that type – just because I want to perform pattern matching against an `enum` type doesn't mean I want to allow usage of `==` + + - It was proposed to change the behaviour of pattern matching from an `==` comparison to a type check, i. e. make `case Foo =>` equivalent to `case _: Foo.type =>`. + - pro: the behaviour would be more consistent between `case class` and `case object` matching as matching against a `case class` also does a type check + - contra: it is a backward incompatible change. A prominent example is `Nil`, whose `equals` method is overridden to return true for empty collections, even if these collections aren't of type `List`. Changing the behaviour would break such code + - we could mostly avoid this by only doing the type check behaviour in the cases outlined above (i. e. scrutinee is `sealed` or `enum` and pattern is one of the `case`s or `case object`s), while retaining the equality check behaviour for cases like matching a `Vector` against `Nil`. But then pattern matching behaviour would be inconsistent depending on the types involved and we would only replace one inconsistency with another + - the author's opinion is that, while this is the approach that he would have chosen in a new language, the practical benefits over the existing behaviour are marginal and that therefore the compatibility concerns outweigh them in this case ## Related Work - https://contributors.scala-lang.org/t/pre-sip-better-strictequality-support-in-pattern-matching/6781 From 460f3b2ced6ec929a17019b59c64d75d05ad8c4e Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Mon, 7 Oct 2024 22:28:25 +0200 Subject: [PATCH 05/21] Add paragraph about using `unapply` instead of equals --- content/strict-equality-pattern-matching.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index d5179d55..bbd3808d 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -16,7 +16,7 @@ title: SIP-NN - Strict-Equality pattern matching | Oct 3rd 2024 | Initial Draft | | Oct 3rd 2024 | Related Work | | Oct 4th 2024 | Add paragraph about using a type check instead of equals | - +| Oct 7th 2024 | Add paragraph about using `unapply` instead of equals | ## Summary This proposal aims to make the `strictEquality` feature easier to adopt by avoiding the need for a `CanEqual` instance @@ -95,16 +95,21 @@ This change creates no new compatibility issues and improves the compatibility o ## Alternatives - - It was proposed to instead change the `enum` feature so that it always includes an implicit `derives CanEqual` clause. This is unsatisfactory for many reasons: +1. It was proposed to instead change the `enum` feature so that it always includes an implicit `derives CanEqual` clause. This is unsatisfactory for many reasons: - doesn't work for sealed types - doesn't work for 3rd party libraries compiled with an older compiler - `CanEqual` might be unwanted for that type – just because I want to perform pattern matching against an `enum` type doesn't mean I want to allow usage of `==` - - It was proposed to change the behaviour of pattern matching from an `==` comparison to a type check, i. e. make `case Foo =>` equivalent to `case _: Foo.type =>`. +1. It was proposed to change the behaviour of pattern matching from an `==` comparison to a type check, i. e. make `case Foo =>` equivalent to `case _: Foo.type =>`. - pro: the behaviour would be more consistent between `case class` and `case object` matching as matching against a `case class` also does a type check - contra: it is a backward incompatible change. A prominent example is `Nil`, whose `equals` method is overridden to return true for empty collections, even if these collections aren't of type `List`. Changing the behaviour would break such code - we could mostly avoid this by only doing the type check behaviour in the cases outlined above (i. e. scrutinee is `sealed` or `enum` and pattern is one of the `case`s or `case object`s), while retaining the equality check behaviour for cases like matching a `Vector` against `Nil`. But then pattern matching behaviour would be inconsistent depending on the types involved and we would only replace one inconsistency with another - - the author's opinion is that, while this is the approach that he would have chosen in a new language, the practical benefits over the existing behaviour are marginal and that therefore the compatibility concerns outweigh them in this case + - the author's opinion is that, while this is an approach that he might have chosen in a new language, the practical benefits over the existing behaviour are marginal and that therefore the compatibility concerns outweigh them in this case +1. It was proposed to change the behaviour of `case object` so that it adds a suitable `def unapply(n: Nat): Boolean` method and to have `case Foo =>` invoke the `unapply` method (like `case Foo() =>` does today) if one exists, falling back to `==` otherwise + - pro: more consistent behaviour between `case object` and `case class` as `unapply` would be used in both cases + - contra: behaviour of `match` statements now depends on *both* the version of the compiler that you're using *and* the compiler used to compile the ADT. + - contra: incompatible change. If your `case object` has an overridden `equals` method (like e. g. `Nil` does), you now need to define an `unapply` method that delegates to `equals`, otherwise your code will break. + - authors opinion: same as for 2. Fine if this was a new language, but the benefits aren't huge and practical compatibility concerns matter more. ## Related Work - https://contributors.scala-lang.org/t/pre-sip-better-strictequality-support-in-pattern-matching/6781 From 193e6ccb2b2b16138b5485c97f9f802a4854059a Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Mon, 21 Oct 2024 11:12:18 +0200 Subject: [PATCH 06/21] remove unnecessary line --- content/strict-equality-pattern-matching.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index bbd3808d..6a87d8c4 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -115,6 +115,3 @@ This change creates no new compatibility issues and improves the compatibility o - https://contributors.scala-lang.org/t/pre-sip-better-strictequality-support-in-pattern-matching/6781 - https://contributors.scala-lang.org/t/how-to-improve-strictequality/6722 - https://contributors.scala-lang.org/t/enumeration-does-not-derive-canequal-for-strictequality/5280 - -## FAQ - From 7876767b0da98642bf7b3565e329f7f5568d2306 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Mon, 21 Oct 2024 11:15:00 +0200 Subject: [PATCH 07/21] simplify code example --- content/strict-equality-pattern-matching.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 6a87d8c4..e8a97bad 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -40,10 +40,10 @@ enum Nat: case Zero case Succ(n: Nat) -extension(l: Nat) def +(r: Nat): Nat = - l match - case Nat.Zero => r - case Nat.Succ(x) => Nat.Succ(x + r) + def +(that: Nat): Nat = + this match + case Nat.Zero => that + case Nat.Succ(x) => Nat.Succ(x + that) ``` This fails to compile with the following error message: From 0c099f220e5522b596e9a433843d0419a9342552 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Mon, 21 Oct 2024 11:17:43 +0200 Subject: [PATCH 08/21] update code example --- content/strict-equality-pattern-matching.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index e8a97bad..18f7c8c9 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -72,9 +72,9 @@ This fails to compile with the following error message: - pointless overhead: can have more than one `Zero()` object at run-time - perform a type check instead: ```scala - l match - case _: Nat.Zero.type => r - case Nat.Succ(x) => Nat.Succ(x + r) + this match + case _: Nat.Zero.type => that + case Nat.Succ(x) => Nat.Succ(x + that) ``` But like the previous solutions: - hinders adoption in existing code bases by requiring new syntax From 06070bbf2f0c049e449815e4d571b9db5779c8d8 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Mon, 21 Oct 2024 17:47:10 +0200 Subject: [PATCH 09/21] correct stage/status --- content/strict-equality-pattern-matching.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 18f7c8c9..3b8bf541 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -1,8 +1,8 @@ --- layout: sip permalink: /sips/:title.html -stage: implementation -status: waiting-for-implementation +stage: pre-sip +status: submitted presip-thread: https://contributors.scala-lang.org/t/pre-sip-better-strictequality-support-in-pattern-matching/6781 title: SIP-NN - Strict-Equality pattern matching --- From 6a7fb5809965a3a6c2e7ec1d682a6ee1c7d113c5 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Mon, 28 Oct 2024 15:00:01 +0100 Subject: [PATCH 10/21] Update strict-equality-pattern-matching.md fix indentation --- content/strict-equality-pattern-matching.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 3b8bf541..44a1b588 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -41,9 +41,9 @@ enum Nat: case Succ(n: Nat) def +(that: Nat): Nat = - this match - case Nat.Zero => that - case Nat.Succ(x) => Nat.Succ(x + that) + this match + case Nat.Zero => that + case Nat.Succ(x) => Nat.Succ(x + that) ``` This fails to compile with the following error message: From 92d13fa46cbdf6115630da923fb5f38cc0a8bba9 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Tue, 3 Dec 2024 04:00:00 +0100 Subject: [PATCH 11/21] use a magic `CanEqual` instance to solve the problem --- content/strict-equality-pattern-matching.md | 43 ++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 44a1b588..0c929dee 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -1,10 +1,10 @@ --- layout: sip permalink: /sips/:title.html -stage: pre-sip -status: submitted +stage: design +status: under-review presip-thread: https://contributors.scala-lang.org/t/pre-sip-better-strictequality-support-in-pattern-matching/6781 -title: SIP-NN - Strict-Equality pattern matching +title: SIP-67 - Strict-Equality pattern matching --- **By: Matthias Berndt** @@ -19,16 +19,17 @@ title: SIP-NN - Strict-Equality pattern matching | Oct 7th 2024 | Add paragraph about using `unapply` instead of equals | ## Summary -This proposal aims to make the `strictEquality` feature easier to adopt by avoiding the need for a `CanEqual` instance -when matching a `sealed` or `enum` type against singleton cases (e. g. `Nil` or `None`). +This proposal aims to make the `strictEquality` feature easier to adopt by making pattern matching +against singleton cases (e. g. `Nil` or `None`) work even when the relevant `sealed` or `enum` type +does not (or cannot) have a `derives CanEqual` clause. ## Motivation The `strictEquality` feature is important to improve type safety. However due to the way that pattern matching in -Scala works, it often requires `CanEqual` instances where they conceptually don't really make sense, as evidenced -by the fact that in e. g. Haskell, an `Eq` instance is never required to perform a pattern matching. -It also seems arbitrary that a `CanEqual` instance is required to match on types such as `Option` or `List` but -not for e. g. `Either`. +Scala works, it requires a `CanEqual` instance when matching against a `case object` or a singleton `enum` `case`. This is problematic because it means that pattern matching doesn't work in the expected +way for types where a `derives CanEqual` cause is not desired. +In languages like Haskell, an `Eq` instance is never required to perform a pattern matching. +It also seems arbitrary that a `CanEqual` instance is required to match on types such as `Option` or `List` but not for e. g. `Either`. A simple example is this code: @@ -59,6 +60,11 @@ This fails to compile with the following error message: - the ADT might not be under the user's control, e. g. defined in a 3rd party library - one might not *want* a `CanEqual` instance to be available for this type because one doesn't want this type to be compared with the `==` operator. For example, when one of the fields in the `enum` is a function, it actually isn't possible to perform a meaningful equality check. + Functions inside ADTs are not uncommon, examples can be found for example in + [ZIO](https://github.com/zio/zio/blob/65a35bcba47bdc1720fd86c612fc6573c84b460d/core/shared/src/main/scala/zio/ZIO.scala#L6075), + [cats](https://github.com/typelevel/cats/blob/1cc04eca9f2bc934c869a7c5054b15f6702866fb/free/src/main/scala/cats/free/Free.scala#L219) or + [cats-effect](https://github.com/typelevel/cats-effect/blob/eb918fa59f85543278eae3506fda84ccea68ad7c/core/shared/src/main/scala/cats/effect/IO.scala#L2235) + It should be possible to match on such types without requiring a `CanEqual` instance that couldn't possibly work correctly in the general case. - turn the no-argument-list cases into empty-argument-list cases: ```scala enum Nat: @@ -67,7 +73,8 @@ This fails to compile with the following error message: ``` The downsides are similar to the previous point: - doesn't work for ADTs defined in a library - - hinders adoption in existing code bases by requiring new syntax (even more so, because now you not only need to change the `enum` definition but also every `match` and `PartialFunction` literal) + - hinders adoption in existing code bases by requiring new syntax (even more so, because now you not only need to change the `enum` definition + but also every `match` and `PartialFunction` literal) - uglier than before - pointless overhead: can have more than one `Zero()` object at run-time - perform a type check instead: @@ -85,9 +92,19 @@ For these reasons the current state of affairs is unsatisfactory and needs to im ### Specification -The proposed solution is to not require a `CanEqual` instance during pattern matching when: - - the scrutinee's type is a `sealed` type and the pattern is a `case object` that extends the scrutinee's type, or - - the scrutinee's type is an `enum` type and the pattern is one of the enum's cases without a parameter list (e. g. `Nat.Z`) +The proposed solution consists of two changes: + 1. Tweak the behaviour of pattern matching with regards to singleton `enum` `case` patterns. When a singleton `enum` `case` (like `Nat.Zero` in the above example) + is used as a pattern, it should be considered to have that `case`'s singleton type, not the `enum` type. E. g. the above `def +` currently requires a `CanEqual[Nat, Nat]`, + and should require a `CanEqual[Nat.Zero.type, Nat]` in the future. This is already the case for `case object`s today. In expressions, singleton `enum` `case`s + should continue to have the `enum` type, not the singleton type. + 1. Add a magic `given` `CanEqual[A, B]` instance that is available when either of the following is true: + - `A` is the singleton type of a `case object`, and `B` is a supertype of `A`, and is a type that allows exhaustiveness checks + (e. g. a `sealed` type or a union or intersection of several `sealed` types) + - `A` is the singleton type of a singleton `enum` `case`, and `B` is a supertype of the corresponding `enum` type, and is a type that allows exhaustiveness + checks + +These rules ensure that pattern matching against singleton patterns continues to work in all cases that I would consider sane. + ### Compatibility From 5fb77acd7735587f780329ec5061e53af9aa07b6 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Tue, 3 Dec 2024 12:02:03 +0100 Subject: [PATCH 12/21] minor tweaks --- content/strict-equality-pattern-matching.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 0c929dee..eba703fc 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -115,12 +115,13 @@ This change creates no new compatibility issues and improves the compatibility o 1. It was proposed to instead change the `enum` feature so that it always includes an implicit `derives CanEqual` clause. This is unsatisfactory for many reasons: - doesn't work for sealed types - doesn't work for 3rd party libraries compiled with an older compiler - - `CanEqual` might be unwanted for that type – just because I want to perform pattern matching against an `enum` type doesn't mean I want to allow usage of `==` + - `CanEqual` might be undesirable for that type – doing general `==` comparisons is a more powerful operation than pattern matching, which can only compare + for equality with the singleton `case`s, and hence pattern matching is possible for many types where a general `==` operation can not be implemented correctly. 1. It was proposed to change the behaviour of pattern matching from an `==` comparison to a type check, i. e. make `case Foo =>` equivalent to `case _: Foo.type =>`. - pro: the behaviour would be more consistent between `case class` and `case object` matching as matching against a `case class` also does a type check - - contra: it is a backward incompatible change. A prominent example is `Nil`, whose `equals` method is overridden to return true for empty collections, even if these collections aren't of type `List`. Changing the behaviour would break such code - - we could mostly avoid this by only doing the type check behaviour in the cases outlined above (i. e. scrutinee is `sealed` or `enum` and pattern is one of the `case`s or `case object`s), while retaining the equality check behaviour for cases like matching a `Vector` against `Nil`. But then pattern matching behaviour would be inconsistent depending on the types involved and we would only replace one inconsistency with another + - contra: it is a backward incompatible change. A prominent example is `Nil`, whose `equals` method is overridden to return true for empty collections, even if these + collections aren't of type `List`. Changing the behaviour would break such code - the author's opinion is that, while this is an approach that he might have chosen in a new language, the practical benefits over the existing behaviour are marginal and that therefore the compatibility concerns outweigh them in this case 1. It was proposed to change the behaviour of `case object` so that it adds a suitable `def unapply(n: Nat): Boolean` method and to have `case Foo =>` invoke the `unapply` method (like `case Foo() =>` does today) if one exists, falling back to `==` otherwise - pro: more consistent behaviour between `case object` and `case class` as `unapply` would be used in both cases From 0b7d2bc5250d72f20bedc13ff17f41e9a2e81245 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Tue, 3 Dec 2024 12:03:44 +0100 Subject: [PATCH 13/21] update History --- content/strict-equality-pattern-matching.md | 1 + 1 file changed, 1 insertion(+) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index eba703fc..134f31cf 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -17,6 +17,7 @@ title: SIP-67 - Strict-Equality pattern matching | Oct 3rd 2024 | Related Work | | Oct 4th 2024 | Add paragraph about using a type check instead of equals | | Oct 7th 2024 | Add paragraph about using `unapply` instead of equals | +| Dec 3rd 2024 | Change the approach to a magic `CanEqual` instance | ## Summary This proposal aims to make the `strictEquality` feature easier to adopt by making pattern matching From aa7dc28405d513952ed51623eb05ad2667c73ee1 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Tue, 3 Dec 2024 12:05:12 +0100 Subject: [PATCH 14/21] minor tweak --- content/strict-equality-pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 134f31cf..f0c4def8 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -29,7 +29,7 @@ does not (or cannot) have a `derives CanEqual` clause. The `strictEquality` feature is important to improve type safety. However due to the way that pattern matching in Scala works, it requires a `CanEqual` instance when matching against a `case object` or a singleton `enum` `case`. This is problematic because it means that pattern matching doesn't work in the expected way for types where a `derives CanEqual` cause is not desired. -In languages like Haskell, an `Eq` instance is never required to perform a pattern matching. +By contrast, in languages like Haskell, an `Eq` instance is never required to perform a pattern matching. It also seems arbitrary that a `CanEqual` instance is required to match on types such as `Option` or `List` but not for e. g. `Either`. From 6d327ac87e233edfeb126ec6c3490eb043f74ae9 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Fri, 20 Dec 2024 02:50:43 +0100 Subject: [PATCH 15/21] more concrete specification --- content/strict-equality-pattern-matching.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index f0c4def8..a312437d 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -99,10 +99,8 @@ The proposed solution consists of two changes: and should require a `CanEqual[Nat.Zero.type, Nat]` in the future. This is already the case for `case object`s today. In expressions, singleton `enum` `case`s should continue to have the `enum` type, not the singleton type. 1. Add a magic `given` `CanEqual[A, B]` instance that is available when either of the following is true: - - `A` is the singleton type of a `case object`, and `B` is a supertype of `A`, and is a type that allows exhaustiveness checks - (e. g. a `sealed` type or a union or intersection of several `sealed` types) - - `A` is the singleton type of a singleton `enum` `case`, and `B` is a supertype of the corresponding `enum` type, and is a type that allows exhaustiveness - checks + - `A` is the singleton type of a `case object`, and `B` is a supertype of `A`, and is a union or intersection of one or more `sealed` or `enum` types + - `A` is the singleton type of a singleton `enum` `case`, and `B` is a supertype of the corresponding `enum` type, and is a union of one or more `sealed` or `enum` types These rules ensure that pattern matching against singleton patterns continues to work in all cases that I would consider sane. From 29215472e144d773420292133e3b608e28a3f4d1 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Fri, 3 Jan 2025 05:07:20 +0100 Subject: [PATCH 16/21] Undo previous change, "magic `CanEqual` has no benefits --- content/strict-equality-pattern-matching.md | 25 +++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index a312437d..7c1bb892 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -18,6 +18,7 @@ title: SIP-67 - Strict-Equality pattern matching | Oct 4th 2024 | Add paragraph about using a type check instead of equals | | Oct 7th 2024 | Add paragraph about using `unapply` instead of equals | | Dec 3rd 2024 | Change the approach to a magic `CanEqual` instance | +| Jan 3rd 2024 | Undo previous change, "magic `CanEqual` has no benefits | ## Summary This proposal aims to make the `strictEquality` feature easier to adopt by making pattern matching @@ -27,10 +28,12 @@ does not (or cannot) have a `derives CanEqual` clause. ## Motivation The `strictEquality` feature is important to improve type safety. However due to the way that pattern matching in -Scala works, it requires a `CanEqual` instance when matching against a `case object` or a singleton `enum` `case`. This is problematic because it means that pattern matching doesn't work in the expected -way for types where a `derives CanEqual` cause is not desired. -By contrast, in languages like Haskell, an `Eq` instance is never required to perform a pattern matching. -It also seems arbitrary that a `CanEqual` instance is required to match on types such as `Option` or `List` but not for e. g. `Either`. +Scala works, it requires a `CanEqual` instance when matching against a `case object` or a singleton `enum` `case`. +This is problematic because it means that pattern matching doesn't work in the expected way for types where a +`derives CanEqual` clause is not desired. +By contrast, in languages like Haskell, an `Eq` instance is never required to perform pattern matching. It also +seems arbitrary that a `CanEqual` instance is required to match on types such as `Option` or `List` but not on +others such as `Either`. A simple example is this code: @@ -93,17 +96,9 @@ For these reasons the current state of affairs is unsatisfactory and needs to im ### Specification -The proposed solution consists of two changes: - 1. Tweak the behaviour of pattern matching with regards to singleton `enum` `case` patterns. When a singleton `enum` `case` (like `Nat.Zero` in the above example) - is used as a pattern, it should be considered to have that `case`'s singleton type, not the `enum` type. E. g. the above `def +` currently requires a `CanEqual[Nat, Nat]`, - and should require a `CanEqual[Nat.Zero.type, Nat]` in the future. This is already the case for `case object`s today. In expressions, singleton `enum` `case`s - should continue to have the `enum` type, not the singleton type. - 1. Add a magic `given` `CanEqual[A, B]` instance that is available when either of the following is true: - - `A` is the singleton type of a `case object`, and `B` is a supertype of `A`, and is a union or intersection of one or more `sealed` or `enum` types - - `A` is the singleton type of a singleton `enum` `case`, and `B` is a supertype of the corresponding `enum` type, and is a union of one or more `sealed` or `enum` types - -These rules ensure that pattern matching against singleton patterns continues to work in all cases that I would consider sane. - +The proposed solution is to not require a `CanEqual` instance during pattern matching when: + - the scrutinee's type is a `sealed` type and the pattern is a `case object` that extends the scrutinee's type, or + - the scrutinee's type is an `enum` type and the pattern is one of the enum's cases without a parameter list (e. g. `Nat.Z`) ### Compatibility From be6d8bd663c4b60496cb4e359c4a2b5562745db3 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Fri, 3 Jan 2025 05:11:57 +0100 Subject: [PATCH 17/21] small fix --- content/strict-equality-pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 7c1bb892..a24ffc2d 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -98,7 +98,7 @@ For these reasons the current state of affairs is unsatisfactory and needs to im The proposed solution is to not require a `CanEqual` instance during pattern matching when: - the scrutinee's type is a `sealed` type and the pattern is a `case object` that extends the scrutinee's type, or - - the scrutinee's type is an `enum` type and the pattern is one of the enum's cases without a parameter list (e. g. `Nat.Z`) + - the scrutinee's type is an `enum` type and the pattern is one of the enum's cases without a parameter list (e. g. `Nat.Zero`) ### Compatibility From c779cbd7e958b101d9a25638d2d51941a603e2a7 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Fri, 24 Jan 2025 12:56:01 +0100 Subject: [PATCH 18/21] add a paragraph about the proposal to drop `CanEqual` requirement from pattern matching entirely --- content/strict-equality-pattern-matching.md | 27 +++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index a24ffc2d..d38cec0c 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -97,8 +97,8 @@ For these reasons the current state of affairs is unsatisfactory and needs to im ### Specification The proposed solution is to not require a `CanEqual` instance during pattern matching when: - - the scrutinee's type is a `sealed` type and the pattern is a `case object` that extends the scrutinee's type, or - - the scrutinee's type is an `enum` type and the pattern is one of the enum's cases without a parameter list (e. g. `Nat.Zero`) + - the pattern is a `case object` that extends the scrutinee's type, or + - the pattern is an `enum case` without a parameter list (e. g. `Nat.Zero`) and the scrutinee has that `enum` type (or a supertype thereof) ### Compatibility @@ -122,6 +122,29 @@ This change creates no new compatibility issues and improves the compatibility o - contra: behaviour of `match` statements now depends on *both* the version of the compiler that you're using *and* the compiler used to compile the ADT. - contra: incompatible change. If your `case object` has an overridden `equals` method (like e. g. `Nil` does), you now need to define an `unapply` method that delegates to `equals`, otherwise your code will break. - authors opinion: same as for 2. Fine if this was a new language, but the benefits aren't huge and practical compatibility concerns matter more. +1. It was proposed to drop the requirement for a `CanEqual` instance during pattern matching + entirely and rely solely on the unreachability warning that the compiler emits when the scrutinee + type isn't a supertype of the pattern type. + The author's opinion is that this does not provide sufficient safety around the `equals` method. + As was explained above, `CanEqual` is not supposed to be available for types that cannot be + meaningfully tested for equality, such as `Int => Int`. This proposal trivially allows equality + comparisons between such types: + ``` + val x: Int => Int = ??? + val y: Int => Int = ??? + x match + case `y` => true + case _ => false + ``` + `strictEquality` currently prevents this from compiling, and that is a feature, not a bug. + It should also be pointed out that adding a `: Any` type ascription will make all such + comparisons compile, regardless of the type of the pattern. This is unfortunate as type + ascriptions are normally a fairly safe and innocouous operation. + The "philosophical" justification for nevertheless allowing matching against `case object` + and singleton `enum case`s is that in an ideal world, we wouldn't even need to call `equals` + to test for equality with these – the only thing that is equal to a singleton is the + singleton itself, and hence we could in principle use reference equality for these cases + (the fact that we don't is a mere concession to backward compatibility). ## Related Work - https://contributors.scala-lang.org/t/pre-sip-better-strictequality-support-in-pattern-matching/6781 From a17ff35e38e269bb39a6b5f828b6c467a2c2e634 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Fri, 31 Jan 2025 01:24:10 +0100 Subject: [PATCH 19/21] fix typo (thanks to Seth Tisue) Co-authored-by: Seth Tisue --- content/strict-equality-pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index d38cec0c..2c9a2867 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -139,7 +139,7 @@ This change creates no new compatibility issues and improves the compatibility o `strictEquality` currently prevents this from compiling, and that is a feature, not a bug. It should also be pointed out that adding a `: Any` type ascription will make all such comparisons compile, regardless of the type of the pattern. This is unfortunate as type - ascriptions are normally a fairly safe and innocouous operation. + ascriptions are normally a fairly safe and innocuous operation. The "philosophical" justification for nevertheless allowing matching against `case object` and singleton `enum case`s is that in an ideal world, we wouldn't even need to call `equals` to test for equality with these – the only thing that is equal to a singleton is the From 786f7f1a926899c55b6749de8b11b15d087c7ece Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Thu, 6 Mar 2025 21:48:53 +0100 Subject: [PATCH 20/21] add examples --- content/strict-equality-pattern-matching.md | 60 ++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 2c9a2867..77f7cfda 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -96,10 +96,68 @@ For these reasons the current state of affairs is unsatisfactory and needs to im ### Specification -The proposed solution is to not require a `CanEqual` instance during pattern matching when: +The proposed solution is to not require a `CanEqual` instance during pattern matching when: - the pattern is a `case object` that extends the scrutinee's type, or - the pattern is an `enum case` without a parameter list (e. g. `Nat.Zero`) and the scrutinee has that `enum` type (or a supertype thereof) +The semantics of pattern matching against a constant are otherwise unchanged, that is, `equals` will continue +to be invoked. + +### Examples + +#### Example 1 + +```scala +def foo(vector: Vector[Int]) = + vector match + case Nil => 0 +``` +This example is not affected by this SIP: `Vector[Int]` is not a supertype of `Nil.type`. `CanEqual` is still required. +(Note: This code produces an unreachable code warning, but the branch is nevertheless taken (compiler bug)) + +#### Example 2 +```scala +def foo(list: List[Int]) = + list match + case Nil => 0 +``` +This example is affected by this SIP. `List[Int]` is a supertype of `Nil.type`, and `Nil` is a case object, hence no +`CanEqual` instance is required (Note: the example still compiles without this SIP because that `CanEqual` instance +is available. But with this SIP, pattern matching will no longer require it) + +#### Example 3 + +```scala +val TheAnswer = 42 + +def foo(i: Int) = + i match + case TheAnswer => 0 +``` +This example is not affected by this SIP: `TheAnswer` is not a `case object` or `enum case`, `CanEqual` is required like before. + +#### Example 4 +```scala +def foo(either: Either[String, Int]) = + either match + case Left(_) => "l" + case Right(_) => "r" +``` +This example is not affected by this SIP. It never required `CanEqual` as matching is performed using `unapply`. + + +#### Example 5 +```scala +enum Foo: + case Bar + case Baz + +def foo(x: Any) = + x match + case Foo.Bar => 0 +``` +This example is affected by this SIP: `Any` is a supertype of `Foo` and `Foo.Bar` is an `enum case` without a parameter list, hence no `CanEqual` instance is required. + ### Compatibility This change creates no new compatibility issues and improves the compatibility of the `strictEquality` feature with existing code bases. From 9affedd8f2275aa68e5816dda2e0b290c87c4ce2 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Thu, 6 Mar 2025 21:52:21 +0100 Subject: [PATCH 21/21] add history entry --- content/strict-equality-pattern-matching.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/content/strict-equality-pattern-matching.md b/content/strict-equality-pattern-matching.md index 77f7cfda..cdbc9c9c 100644 --- a/content/strict-equality-pattern-matching.md +++ b/content/strict-equality-pattern-matching.md @@ -18,7 +18,8 @@ title: SIP-67 - Strict-Equality pattern matching | Oct 4th 2024 | Add paragraph about using a type check instead of equals | | Oct 7th 2024 | Add paragraph about using `unapply` instead of equals | | Dec 3rd 2024 | Change the approach to a magic `CanEqual` instance | -| Jan 3rd 2024 | Undo previous change, "magic `CanEqual` has no benefits | +| Jan 3rd 2025 | Undo previous change, "magic `CanEqual`" has no benefits | +| Mar 6th 2025 | Add examples | ## Summary This proposal aims to make the `strictEquality` feature easier to adopt by making pattern matching