Skip to content

Add the Empty[Map[A, B]] instance in alleycats #4735

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

Merged
merged 4 commits into from
Apr 13, 2025

Conversation

danicheg
Copy link
Member

Adding yet another instance of Empty that might be commonly used (at the very least, I need it). I thought of adding this:

implicit def alleycatsEmptyForMap[CC[_, _] <: Iterable[(_, _)], A, B](implicit
  factory: Factory[(A, B), CC[A, B]]
): Empty[CC[A, B]] = Empty(factory.newBuilder.result())

but as @johnynek pointed out in #4733, it will also add support for mutable.Map, which isn't a desirable outcome.
The existing derivation capabilities fail to derive Empty[Map[A, B]] when the key/value types are case classes or more sophisticated than primitives. See https://scastie.scala-lang.org/ggnoSWYGTFiGGqD6wA0VuQ

@danicheg danicheg requested a review from a team April 4, 2025 08:41
johnynek
johnynek previously approved these changes Apr 4, 2025
@johnynek
Copy link
Contributor

johnynek commented Apr 4, 2025

Do you want to add SortedMap at the same time? That will require the ordering on the key and is a bit less trivial.

@satorg
Copy link
Contributor

satorg commented Apr 4, 2025

@johnynek
To be honest, I'm not sure that a separate implementation for SortedMap is necessary, if we agree that this one is good to go. I mean, since there's a singleton instance used:

  private[this] val emptyMapSingleton: Empty[Map[Nothing, Nothing]] = Empty(Map.empty)
  implicit def alleycatsEmptyForMap[A, B]: Empty[Map[A, B]] = emptyMapSingleton.asInstanceOf[Empty[Map[A, B]]]

then the methods of Empty have to be consistent with the universal equality, even though they are supposed to use Eq.

Given that, we can use the same singleton instance for SortedMap just as well, because according to the universal equality Map.empty and SortedMap.empty are equal:

scala> val m = Map.empty[Nothing, Nothing]
val m: Map[Nothing, Nothing] = Map()

scala> val hm = immutable.HashMap.empty[Nothing, Nothing]
val hm: scala.collection.immutable.HashMap[Nothing, Nothing] = HashMap()

scala> val sm = immutable.SortedMap.empty[Nothing, Nothing]
val sm: scala.collection.immutable.SortedMap[Nothing, Nothing] = TreeMap()

scala> m == hm
val res0: Boolean = true

scala> m == sm
val res1: Boolean = true
                                                                                                                                                                                                        
scala> hm == sm
val res2: Boolean = true

UPD. Disregard please: as @danicheg pointed out, emptyMapSingleton cannot be reused for SortedMap since Empty[SortedMap[...]] will be failing with ClassCastException.

Comment on lines 90 to 93
private[this] object OrderingAny extends Ordering[Any] {
override def compare(x: Any, y: Any): Int =
x.hashCode().compareTo(y.hashCode())
}
Copy link
Member Author

Choose a reason for hiding this comment

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

It’s a bit controversial, but:

  • it’s consistent with object equality;
  • using Ordering[Nothing] isn’t a choice, as SortedMap API exposes Ordering instance to end-users;
  • this lets us enable the zero-allocation trick when creating an Empty[SortedMap[A, B]]. Otherwise, we’d need to require an Ordering[A] in alleycatsSortedEmptyForMap and pass it along to the SortedMap.empty constructor — which would mean extra allocations;
  • FWIW, we also can’t cast a Map[Nothing, Nothing] to SortedMap[Nothing, Nothing]. cc @satorg

Copy link
Contributor

@satorg satorg Apr 5, 2025

Choose a reason for hiding this comment

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

Yes, you're right – since the empty map is exposed, its ordering is exposed too. Maybe that zero-allocation trick is not exactly suitable for SortedMap then. Because someone may decide to get Empty.empty map and start building a non-empty map from it.

@danicheg danicheg requested a review from johnynek April 6, 2025 10:01

private[this] val emptySortedMapSingleton: Empty[SortedMap[Any, Any]] =
Empty(SortedMap.empty[Any, Any](OrderingAny))
implicit def alleycatsSortedEmptyForMap[A, B]: Empty[SortedMap[A, B]] =
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we are being too clever here and it will lead to big problems.

What do you expect Empty[SortedMap[A, B]].empty.ordering to return? It will return an ordering on A but which one? I certainly wouldn't expect it to order on hashcode.

I think we need to simply do:

implicit def alleycatsSortedEmptyForMap[A: Ordering, B]: Empty[SortedMap[A, B]] =
  Empty(SortedMap.empty[A, B])

That said... I'd love to see an example of using the Empty typeclass generically. Since it is lawless, I just don't see how you can write code with it that makes sense. It seems like what you really want is Alternative.empty, Monoid.empty or Applicative.unit or something depending on what you are trying to do.

In any case, IMO we definitely shouldn't add the casting/hashcode version in the PR currently.

Copy link
Member Author

Choose a reason for hiding this comment

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

it will lead to big problems

I can't judge the magnitude, but it could definitely introduce behavioural discrepancies in certain cases. So I agree — the safer approach would be what I proposed above (which is the same as what you suggested).

That said... I'd love to see an example of using the Empty typeclass generically. ... It seems like what you really want is Alternative.empty, Monoid.empty or Applicative.unit

I just need a handy way to create empty entities (which are often just collections) without requiring them to form a Monoid.

@danicheg danicheg force-pushed the empty-map-instance branch from 4f2a283 to d5a7e2f Compare April 12, 2025 12:16
@danicheg danicheg requested a review from johnynek April 12, 2025 12:30
@danicheg danicheg merged commit 44ff278 into typelevel:main Apr 13, 2025
16 checks passed
@danicheg danicheg deleted the empty-map-instance branch April 13, 2025 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants