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 MVar #217

Merged
merged 17 commits into from May 15, 2018
216 changes: 216 additions & 0 deletions core/shared/src/main/scala/cats/effect/concurrent/MVar.scala
@@ -0,0 +1,216 @@
/*
* Copyright (c) 2017-2018 The Typelevel Cats-effect Project Developers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cats.effect
package concurrent

import cats.effect.internals.{MVarAsync, MVarConcurrent}

/**
* A mutable location, that is either empty or contains
* a value of type `A`.
*
* It has 3 fundamental atomic operations:
*
* - [[put]] which fills the var if empty, or blocks
* (asynchronously) until the var is empty again
* - [[take]] which empties the var if full, returning the contained
* value, or blocks (asynchronously) otherwise until there is
* a value to pull
* - [[read]] which reads the current value without touching it,
* assuming there is one, or otherwise it waits until a value
* is made available via `put`
*
* The `MVar` is appropriate for building synchronization
* primitives and performing simple inter-thread communications.
* If it helps, it's similar with a `BlockingQueue(capacity = 1)`,
* except that it doesn't block any threads, all waiting being
* done asynchronously (via [[Async]] or [[Concurrent]] data types,
* such as [[IO]]).
*
* Given its asynchronous, non-blocking nature, it can be used on
* top of Javascript as well.
*
* Inspired by `Control.Concurrent.MVar` from Haskell and
* by `scalaz.concurrent.MVar`.
*/
abstract class MVar[F[_], A] {
/**
* Fills the `MVar` if it is empty, or blocks (asynchronously)
* if the `MVar` is full, until the given value is next in
* line to be consumed on [[take]].
*
* This operation is atomic.
*
* @return a task that on evaluation will complete when the
* `put` operation succeeds in filling the `MVar`,
* with the given value being next in line to
* be consumed
*/
def put(a: A): F[Unit]

/**
* Empties the `MVar` if full, returning the contained value,
* or blocks (asynchronously) until a value is available.
*
* This operation is atomic.
*
* @return a task that on evaluation will be completed after
* a value was retrieved
*/
def take: F[A]

/**
* Tries reading the current value, or blocks (asynchronously)
* until there is a value available.
*
* This operation is atomic.
*
* @return a task that on evaluation will be completed after
* a value has been read
*/
def read: F[A]
}

/** Builders for [[MVar]]. */
object MVar {
/**
* Builds an [[MVar]] value for `F` data types that are [[Concurrent]].
*
* Due to `Concurrent`'s capabilities, the yielded values by [[MVar.take]]
* and [[MVar.put]] are cancelable.
*
* This builder uses the
* [[https://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially-Applied Type]]
* technique.
*
* For creating an empty `MVar`:
* {{{
* MVar[IO].empty[Int] <-> MVar.empty[IO, Int]
* }}}
*
* For creating an `MVar` with an initial value:
* {{{
* MVar[IO].init("hello") <-> MVar.init[IO, String]("hello")
* }}}
*
* @see [[init]], [[initF]] and [[empty]]
*/
def apply[F[_]](implicit F: Concurrent[F]): ApplyBuilders[F] =
Copy link
Member

Choose a reason for hiding this comment

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

I like this pattern - can we use it in Ref too? Specifically the notion of having both the regular method on companion and then ApplyBuilders for the type curried type params.

new ApplyBuilders[F](F)

/**
* Creates a cancelable `MVar` that starts as empty.
*
* @see [[emptyAsync]] for non-cancelable MVars
*
* @param F is a [[Concurrent]] constraint, needed in order to
* describe cancelable operations
*/
def empty[F[_], A](implicit F: Concurrent[F]): F[MVar[F, A]] =
F.delay(MVarConcurrent.empty)

/**
* Creates a non-cancelable `MVar` that starts as empty.
*
* The resulting `MVar` has non-cancelable operations.
*
* @see [[empty]] for creating cancelable MVars
*/
def emptyAsync[F[_], A](implicit F: Async[F]): F[MVar[F, A]] =
F.delay(MVarAsync.empty)

/**
* Creates a cancelable `MVar` that's initialized to an `initial`
* value.
*
* @see [[initAsync]] for non-cancelable MVars
*
* @param initial is a value that will be immediately available
* for the first `read` or `take` operation
*
* @param F is a [[Concurrent]] constraint, needed in order to
* describe cancelable operations
*/
def init[F[_], A](initial: A)(implicit F: Concurrent[F]): F[MVar[F, A]] =
F.delay(MVarConcurrent(initial))

/**
* Creates a non-cancelable `MVar` that's initialized to an `initial`
* value.
*
* The resulting `MVar` has non-cancelable operations.
*
* @see [[init]] for creating cancelable MVars
*/
def initAsync[F[_], A](initial: A)(implicit F: Async[F]): F[MVar[F, A]] =
F.delay(MVarAsync(initial))

/**
* Creates a cancelable `MVar` initialized with a value given
* in the `F[A]` context, thus the initial value being lazily evaluated.
*
* @see [[init]] for creating MVars initialized with strict values
* @see [[initAsyncF]] for building non-cancelable MVars
* @param fa is the value that's going to be used as this MVar's
* initial value, available then for the first `take` or `read`
* @param F is a [[Concurrent]] constraint, needed in order to
* describe cancelable operations
*/
def initF[F[_], A](fa: F[A])(implicit F: Concurrent[F]): F[MVar[F, A]] =
F.map(fa)(MVarConcurrent.apply(_))

/**
* Creates a non-cancelable `MVar` initialized with a value given
* in the `F[A]` context, thus the initial value being lazily evaluated.
*
* @see [[initAsync]] for creating MVars initialized with strict values
* @see [[initF]] for building cancelable MVars
* @param fa is the value that's going to be used as this MVar's
* initial value, available then for the first `take` or `read`
*/
def initAsyncF[F[_], A](fa: F[A])(implicit F: Async[F]): F[MVar[F, A]] =
F.map(fa)(MVarAsync.apply(_))

/**
* Returned by the [[apply]] builder.
*/
final class ApplyBuilders[F[_]](val F: Concurrent[F]) extends AnyVal {
/**
* Builds an `MVar` with an initial value.
*
* @see documentation for [[MVar.init]]
*/
def init[A](a: A): F[MVar[F, A]] =
MVar.init(a)(F)

/**
* Builds an `MVar` with an initial value that's lazily evaluated.
*
* @see documentation for [[MVar.initF]]
*/
def initF[A](fa: F[A]): F[MVar[F, A]] =
MVar.initF(fa)(F)

/**
* Builds an empty `MVar`.
*
* @see documentation for [[MVar.empty]]
*/
def empty[A]: F[MVar[F, A]] =
MVar.empty(F)
}
}
Expand Up @@ -41,10 +41,10 @@ private[effect] object Callback {
}

/** Reusable `Right(())` reference. */
final val rightUnit = Right(())

Choose a reason for hiding this comment

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

why are these not final now?

Copy link
Member Author

Choose a reason for hiding this comment

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

AFAIK final val in companion objects will make the compiler treat that as a constant and inline it. Which would defeat the point of declaring it in the first place, as we're doing such shenanigans in order to not allocate memory every time we need a Right(()).

Choose a reason for hiding this comment

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

@non I thought told me that was only if there is no type annotation, so we could add that but keep the final. But in any case probably not a big deal.

Copy link

Choose a reason for hiding this comment

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

Yes, final val x = ... will inline but final val x: SomeType = ... will not. It's a weird rule.

Copy link
Member Author

Choose a reason for hiding this comment

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

Is there any difference between final val x: SomeType = ... and val x: SomeType?

From the JVM's point of view both will be final, so I'd rather go with the later.

val rightUnit = Right(())

/** Reusable no-op, side-effectful `Function1` reference. */
final val dummy1: Any => Unit = _ => ()
val dummy1: Any => Unit = _ => ()

/** Builds a callback with async execution. */
def async[A](cb: Type[A]): Type[A] =
Expand Down
18 changes: 16 additions & 2 deletions core/shared/src/main/scala/cats/effect/internals/LinkedMap.scala
Expand Up @@ -30,6 +30,10 @@ private[effect] class LinkedMap[K, +V](
private[this] val insertionOrder: LongMap[K],
private[this] val nextId: Long) {

/** Returns `true` if this map is empty, or `false` otherwise. */
def isEmpty: Boolean =
entries.isEmpty

/** Returns a new map with the supplied key/value added. */
def updated[V2 >: V](k: K, v: V2): LinkedMap[K, V2] = {
val insertionOrderOldRemoved = entries.get(k).fold(insertionOrder) { case (_, id) => insertionOrder - id }
Expand All @@ -51,10 +55,20 @@ private[effect] class LinkedMap[K, +V](
/** The values in this map, in the order they were added. */
def values: Iterable[V] = keys.flatMap(k => entries.get(k).toList.map(_._1))

override def toString = keys.zip(values).mkString("LinkedMap(", ", ", ")")
/** Pulls the first value from this `LinkedMap`, in FIFO order. */
def dequeue: (V, LinkedMap[K, V]) = {
val k = insertionOrder.head._2
(entries(k)._1, this - k)
}

override def toString: String =
keys.zip(values).mkString("LinkedMap(", ", ", ")")
}

private[effect] object LinkedMap {
def empty[K, V]: LinkedMap[K, V] =
new LinkedMap[K, V](Map.empty, LongMap.empty, 0)
emptyRef.asInstanceOf[LinkedMap[K, V]]

private val emptyRef =
new LinkedMap[Nothing, Nothing](Map.empty, LongMap.empty, 0)
}