From f25e7f8a23456654f5602e811acca0bf2c466497 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Thu, 3 Oct 2024 17:05:20 +0200 Subject: [PATCH 01/17] 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 0000000..cad40da --- /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/17] 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 cad40da..aa3649b 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/17] 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 aa3649b..1ca842d 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/17] 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 1ca842d..d5179d5 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/17] 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 d5179d5..bbd3808 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/17] 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 bbd3808..6a87d8c 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/17] 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 6a87d8c..e8a97ba 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/17] 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 e8a97ba..18f7c8c 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/17] 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 18f7c8c..3b8bf54 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/17] 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 3b8bf54..44a1b58 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/17] 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 44a1b58..0c929de 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/17] 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 0c929de..eba703f 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/17] 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 eba703f..134f31c 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/17] 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 134f31c..f0c4def 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/17] 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 f0c4def..a312437 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/17] 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 a312437..7c1bb89 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/17] 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 7c1bb89..a24ffc2 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