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

Incorporate MapRef into Std #2464

Merged
merged 13 commits into from
Jun 19, 2022
Merged

Conversation

ChristopherDavenport
Copy link
Member

@ChristopherDavenport ChristopherDavenport commented Oct 25, 2021

Fixes #2444

Integrates MapRef.

Features: ShardedMapRef, ConcurrentHashMap, and Scala Concurrent Map Implementations, and Defaulted Ref/MapRef.

Why Care? This enables keyed concurrent state machines that don't space leak. It basically allows almost any state machine with a default state to be lifted to a keyed version of the same state machine that is removed entirely when it enters some zero state.

* Also useful for anytime a shared storage location is used for a ref, i.e. DB or Redis to not waste
* space.
**/
def defaultedRef[F[_]: Functor, A: Eq](ref: Ref[F, Option[A]], default: A): Ref[F, A] =
Copy link
Member Author

Choose a reason for hiding this comment

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

This makes sense here as this is where it necessary. But might be useful elsewhere. We could also rely on global equality if we like being risky or offer a global equality version.

@@ -169,4 +169,40 @@ class MapRefTrieSpec extends BaseSpec {

}

"MapRef" should { // Requires unsafeRunSync so doesn't work with JS
Copy link
Member

Choose a reason for hiding this comment

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

Interesting note, it's not just that JS doesn't have unsafeRunSync so this test doesn't work, it's actually that this test is meaningless on JS anyway because concurrent modifications are impossible :)

@armanbilge
Copy link
Member

armanbilge commented Nov 1, 2021

Just a thought, could we abstract the current RefSpec in tests and reuse it for the various MapRef implementations?

https://github.com/typelevel/cats-effect/blob/series/3.x/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala

Copy link
Member

@djspiewak djspiewak left a comment

Choose a reason for hiding this comment

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

I really like this as an addition to the stdlib. My concerns basically boil down to the following:

  • Confusing and IMO unnecessary of/in constructor duplication
  • Inaccurate typeclass constraints faked by using unit.map(_ => ...) rather than delay(...)
  • Syntax requires unnecessary imports
  • Minor bikeshedding on test structure
  • Lots of unnecessary and likely-inefficient use of lazy val rather than def

Comment on lines 30 to 32
Concurrent[F]
.unit
.map(_ => scala.collection.concurrent.TrieMap.empty[K, V])
Copy link
Member

Choose a reason for hiding this comment

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

Very leery of something like this. :-)

class HandleRef(k: K) extends Ref[F, Option[V]] {

def access: F[(Option[V], Option[V] => F[Boolean])] = for {
hasBeenCalled <- Applicative[F].unit.map(_ => new AtomicBoolean(false))
Copy link
Member

Choose a reason for hiding this comment

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

…and here too. Why not simply embrace the fact that this is really forcing Sync[F]?

Copy link
Contributor

@SystemFw SystemFw Dec 12, 2021

Choose a reason for hiding this comment

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

I'm going to open an issue for what I'm about to say, and I also want to give a general review of MapRef (tl;dr mostly amazing, some api bikeshedding), but I'm going to drop it here in case I forget: we should relax the semantics on access to not need hasBeenCalled. It was in "broken" form in fs2 for ages, the only reason this was introduced was to conform to the docs, and it's a pain in the arse for things that want to implement Ref, like SignallingRef or MapRef.

* equally distributed Hash, the contention should allow for interaction like a general
* datastructure. Created in G, operates in F.
*/
def inShardedImmutableMap[G[_]: Sync, F[_]: Async, K, V](
Copy link
Member

Choose a reason for hiding this comment

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

If we more fully embrace Sync, then we don't need to have the of/in split in the constructors.

import cats.syntax.all._

trait MapRefSyntax {
implicit def mapRefOptionSyntax[F[_], K, V](
Copy link
Member

Choose a reason for hiding this comment

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

You can actually stick this into the MapRef companion and it won't require the import.


"MapRef" should {

"MapRef.ofSingleImmutableMapRef - concurrent modifications" in real {
Copy link
Member

Choose a reason for hiding this comment

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

Uh… we're in Specs2, so this kind of test name is pretty confusing. :-) I would prefer if we did "ofSingleImmutableMapRef" >> { ... } and then inside that block have "concurrent modifications" >> real { etc etc.

@SystemFw SystemFw self-requested a review December 12, 2021 21:03
@durban
Copy link
Contributor

durban commented Dec 12, 2021

This seems pretty useful. What I find very strange is that it uses universal equality/hash for the keys, while there is a Hash typeclass in Cats.

@ChristopherDavenport
Copy link
Member Author

ChristopherDavenport commented Mar 22, 2022

Proposal:

  1. Remove the access requirement on replay so concurrent constraint can get met by Ref implementation. (or allow the concurrent.map unit case for this single location)
  2. Make a single default implementation that matches on the effect type like the Queue implementation does that delegates to CHM on Async and Ref on Concurrent
  3. All the current constructors live on in MapRef library but the trait and default lives here. That way people can still leverage the interface, but the advanced uses still have the library for those more advanced use cases that would bulk up the binary interface in cats-effect too much.

Thoughts?

Questions with this: Def we want configurable CHM initialCapacity, loadFactor and concurrencyLevel? For The Ref of Map Implementation should there be a default striping we leverage?

@SystemFw
Copy link
Contributor

I'm in favour of the proposal above, and I think it would be good to get this into CE :)

@durban
Copy link
Contributor

durban commented Mar 23, 2022

I would recommend basing the default implementation on TrieMap instead, since that can support using the Hash typeclass for the keys.

@djspiewak djspiewak merged commit 8f905b8 into typelevel:series/3.x Jun 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Integration of MapRef into cats-effect
5 participants