Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CanEqual typeclass instances of Option, Either and Tuple for strictEquality #12419

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions library/src/scala/CanEqual.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@ object CanEqual {
given canEqualNumber: CanEqual[Number, Number] = derived
given canEqualString: CanEqual[String, String] = derived

// The next three definitions can go into the companion objects of classes
// Seq and Set. For now they are here in order not to have to touch the
// The next five definitions can go into the companion objects of their corresponding
// classes. For now they are here in order not to have to touch the
// source code of these classes
given canEqualSeq[T, U](using eq: CanEqual[T, U]): CanEqual[Seq[T], Seq[U]] = derived
given canEqualSet[T, U](using eq: CanEqual[T, U]): CanEqual[Set[T], Set[U]] = derived

given canEqualOptions[T, U](using eq: CanEqual[T, U]): CanEqual[Option[T], Option[U]] = derived
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can only be added in 3.1. Not sure if we can use @experimnetal here because the users will no have the choice to use it or not.

@sjrd @smarter WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. This must be for 3.1.

Copy link
Contributor Author

@kevin-lee kevin-lee May 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @nicolasstucki and @sjrd for your answers. I have a few questions. I'm so sorry to ask it when everyone is busy with finalizing Scala 3 for release and I really appreciate your answer.

  1. Regarding the users having no choice once it's added, could you please think about who are affected by this?
    They are the ones who actually need strictEquality and most likely need this typeclass instance. The ones don't use strictEquality are not affected at all. Besides, the users do still have contol of CanEqual[T, U] since it's not auto-magically give them CanEqual[T, U] as well.

    Can there be another use case than this one? I think it would be hard to find any other use case of CanEqual for Option but I know that I could be easily wrong so I'm happy to take any advice.

  2. If what I mentioned above is not so important, that's fine. Could you tell me about the rest please?
    a. Can we have the other CanEqual for Option to solve cannot be compared with == or != on None case in pattern matching?
    b. Can we have CanEqual for Either?
    c. Can we have CanEqual for Tuple?

It will be so nice if at least the ones for Tuple are accepted for 3.0 because it's so common use case.

Finally, could you please think about what would make strictEquality usable and more useful. Whenever users use Option, Either and Tuple, they need to import their own CanEqual typeclass instances seprately and it would be so annoying.

As far as I know, wildcard import like import my.typeclasses._ doesn't work and it has to be done seprately like.

import my.typeclasses.{canEqualOptions, canEqualOption, canEqualEither, canEqualEmptyTuple, canEqualTuple}

Thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just found that 3.0.0 has been tagged already. 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding,

As far as I know, wildcard import like import my.typeclasses._ doesn't work and it has to be done seprately like.

I didn't know that I could do import my.typeclasses.given for that. Sorry about lack of my Scala 3 knowledge. 😅

given canEqualOption[T](using eq: CanEqual[T, T]): CanEqual[Option[T], Option[T]] = derived // for `case None` in pattern matching

given canEqualEither[L1, R1, L2, R2](
using eqL: CanEqual[L1, L2], eqR: CanEqual[R1, R2]
): CanEqual[Either[L1, R1], Either[L2, R2]] = derived
}
5 changes: 5 additions & 0 deletions library/src/scala/Tuple.scala
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ object Tuple {

def fromProductTyped[P <: Product](p: P)(using m: scala.deriving.Mirror.ProductOf[P]): m.MirroredElemTypes =
runtime.Tuples.fromProduct(p).asInstanceOf[m.MirroredElemTypes]

given canEqualEmptyTuple: CanEqual[EmptyTuple, EmptyTuple] = CanEqual.derived
given canEqualTuple[H1, T1 <: Tuple, H2, T2 <: Tuple](
using eqHead: CanEqual[H1, H2], eqTail: CanEqual[T1, T2]
): CanEqual[H1 *: T1, H2 *: T2] = CanEqual.derived
}

/** A tuple of 0 elements */
Expand Down
114 changes: 114 additions & 0 deletions tests/neg/equality1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,118 @@ object equality1 {
class A
class B
new A == new B // error: cannot compare

case class Foo(n: Int) derives CanEqual

sealed trait Status derives CanEqual
object Status {
case class Active(since: Int) extends Status
case object Pending extends Status
case object Inactive extends Status
}

enum Color derives CanEqual {
case Red
case Green
case Blue
}

val option1a: Option[Int] = Some(1)
val option1b: Option[Int] = Some(1)
option1a == option1b

option1a match {
case Some(1) =>
println("1")
case Some(n) =>
println("Not 1")
case None => // This None case doesn't work without CanEqual.canEqualOption[T]
println("None")
}

1 == '1'
val option2a: Option[Int] = Some(1)
val option2b: Option[Char] = Some('1')
option2a == option2b

val option3a: Option[Foo] = Some(Foo(1))
val option3b: Option[Foo] = Some(Foo(1))
option3a == option3b

val option4a: Option[Status] = Some(Status.Active(2020))
val option4b: Option[Status] = Some(Status.Pending)
val option4c: Option[Status] = Some(Status.Inactive)
option4a == option4b
option4b == option4c

val option5a: Option[Color] = Some(Color.Red)
val option5b: Option[Color] = Some(Color.Green)
val option5c: Option[Color] = Some(Color.Blue)
option5a == option5b
option5b == option5c

val optionError1a: Option[Int] = Some(1)
val optionError1b: Option[String] = Some("1")
optionError1a == optionError1b // error: cannot compare

val optionError2a: Option[Char] = Some('a')
val optionError2b: Option[String] = Some("a")
optionError2a == optionError2b // error: cannot compare

val optionTuple1a: Option[(Int, String)] = Some((1, "OK"))
val optionTuple1b: Option[(Int, String)] = Some((1, "OK"))
optionTuple1a == optionTuple1b

'a' == 97
val optionTuple2a: Option[(Int, Char)] = Some((1, 'a'))
val optionTuple2b: Option[(Int, Int)] = Some((1, 97))
optionTuple2a == optionTuple2b

val optionTupleError1a: Option[(Int, String)] = Some((1, "OK"))
val optionTupleError1b: Option[(String, Int)] = Some(("OK", 1))
optionTupleError1a == optionTupleError1b // error: cannot compare

val eitherL1a: Either[String, Int] = Left("Error")
val eitherL1b: Either[String, Int] = Left("Error")
eitherL1a == eitherL1b

val eitherR1a: Either[String, Int] = Right(999)
val eitherR1b: Either[String, Int] = Right(999)
eitherR1a == eitherR1b

val eitherErrorL1a: Either[String, Int] = Left("Error")
val eitherErrorL1b: Either[Char, Int] = Left('E')
eitherErrorL1a == eitherErrorL1b // error: cannot compare

val eitherErrorR1a: Either[String, Int] = Right(999)
val eitherErrorR1b: Either[String, String] = Right("999")
eitherErrorR1a == eitherErrorR1b // error: cannot compare


val eitherTupleL1a: Either[(String, Long), (Int, Boolean)] = Left(("Error", 123L))
val eitherTupleL1b: Either[(String, Long), (Int, Boolean)] = Left(("Error", 123L))
eitherTupleL1a == eitherTupleL1b

val eitherTupleR1a: Either[(String, Long), (Int, Boolean)] = Right((999, true))
val eitherTupleR1b: Either[(String, Long), (Int, Boolean)] = Right((999, true))
eitherTupleR1a == eitherTupleR1b

val eitherTupleErrorL1a: Either[(String, Long), (Int, Boolean)] = Left(("Error", 123L))
val eitherTupleErrorL1b: Either[(Long, String), (Int, Boolean)] = Left((123L, "Error"))
eitherTupleErrorL1a == eitherTupleErrorL1b // error: cannot compare

val eitherTupleErrorR1a: Either[(String, Long), (Int, Boolean)] = Right((999, true))
val eitherTupleErrorR1b: Either[(String, Long), (Boolean, Int)] = Right((true, 999))
eitherTupleErrorR1a == eitherTupleErrorR1b // error: cannot compare

(1, "a") == (1, "a")
(1, "a", true) == (1, "a", true)
(1, "a", true, 't') == (1, "a", true, 't')
(1, "a", true, 't', 10L) == (1, "a", true, 't', 10L)

(1, "a") == (1, 'a') // error: cannot compare
(1, "a") == ("a", 1) // error: cannot compare
(1, "a") == (1, "a", true) // error: cannot compare
(1, "a", true, 't', 10L) == (1, "a", 1.5D, 't', 10L) // error: cannot compare

}