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 syntax extension #12

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
82 changes: 40 additions & 42 deletions core/src/main/scala/cats/effect/bi/BiIO.scala
Original file line number Diff line number Diff line change
@@ -1,51 +1,44 @@
package cats.effect.bi

import cats.implicits._
import cats.effect.{IO => BaseIO}
import scala.util.Success
import scala.util.Failure
import scala.util.Try
import scala.concurrent.Future
import cats.effect.ContextShift
import scala.concurrent.ExecutionContext
import cats.Eval
import cats.Now
import cats.effect.Concurrent
import cats.effect.ExitCase
import cats.effect.Fiber
import cats.Monad
import cats.data.EitherT
import cats.effect.Bracket
import cats.effect.concurrent.Ref
import cats.effect.Async
import cats.Bifunctor
import cats.SemigroupK
import cats.MonadError
import cats.effect.{Async, Concurrent, ContextShift, ExitCase, Fiber, IO => BaseIO}
import cats.implicits._
import cats.{Bifunctor, Eval, Monad, MonadError, Now, SemigroupK}

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

object BiIO extends BiIOInstances {

implicit def biIOOps[F[_], E, A](e: BiIO[E, A]): BiIOOps[E, A] =
new BiIOOps[E, A](e) {}

def raiseError[E](e: E): BiIO[E, INothing] = create(BaseIO.raiseError(new CustomException(e)))
def raiseError[E](e: E): BiIO[E, INothing] = create(BaseIO.raiseError(CustomException(e)))

def terminate(e: Throwable): IO[INothing] = create(BaseIO.raiseError(e))
def terminate(e: Throwable): BiIO[INothing, INothing] = create(BaseIO.raiseError(e))

def pure[A](a: A): IO[A] = create(BaseIO.pure(a))

def delay[A](a: => A): IO[A] = create(BaseIO.delay(a))

def suspend[A](fa: => IO[A]): IO[A] = create(BaseIO.suspend(embed(fa)))
def suspend[A](fa: => IO[A]): IO[A] = create(BaseIO.suspend(embed[INothing, A](fa)))

def pureBi[E, A](a: A): BiIO[E, A] = create(BaseIO.pure(a))

def suspendBi[E, A](fa: => BiIO[E, A]) = create(BaseIO.suspend(embed(fa)))
def delayBi[E, A](a: => A): BiIO[E, A] = create(BaseIO.delay(a))

def suspendBi[E, A](fa: => BiIO[E, A]): BiIO[E, A] = create(BaseIO.suspend(embed(fa)))

def fromIO[A](fa: BaseIO[A]): IO[A] = create(fa)

def toEitherIO[E, A](fa: BiIO[E, A]): BaseIO[Either[E, A]] =
embed(attemptBi(fa))
embed[INothing, Either[E, A]](attemptBi(fa))

def async[A](k: (Either[Throwable, A] => Unit) => Unit): IO[A] = create(BaseIO.async(k))
def async[A](k: (Either[Throwable, A] => Unit) => Unit): BiIO[INothing, A] = create(BaseIO.async(k))

def asyncF[A](k: (Either[Throwable, A] => Unit) => IO[Unit]): IO[A] = create(BaseIO.asyncF(cb => embed(k(cb))))
def asyncF[A](k: (Either[Throwable, A] => Unit) => IO[Unit]): IO[A] =
create(BaseIO.asyncF(cb => embed[INothing, Unit](k(cb))))

def asyncBi[E, A](k: (Either[E, A] => Unit) => Unit): BiIO[E, A] =
create(
Expand Down Expand Up @@ -82,7 +75,7 @@ object BiIO extends BiIOInstances {

def fromEither[E, A](e: Either[E, A]): BiIO[E, A] =
e match {
case Right(a) => pure(a)
case Right(a) => pureBi(a)
case Left(e) => raiseError(e)
}

Expand Down Expand Up @@ -116,7 +109,7 @@ object BiIO extends BiIOInstances {

val unit: IO[Unit] = pure(())

def none[A]: IO[Option[A]] = pure(None)
def none[A]: IO[Option[A]] = pureBi(None)

/**
* Lifts an `Eval` into `IO`.
Expand Down Expand Up @@ -149,16 +142,16 @@ object BiIO extends BiIOInstances {
private[bi] trait Tag extends Any
type Type[+E, +A] <: Base with Tag

private[cats] def create[E, A](s: BaseIO[A]): Type[E, A] =
private[cats] def create[E, A](s: BaseIO[A]): BiIO[E, A] =
s.asInstanceOf[Type[E, A]]

private[cats] def embed[E, A](e: Type[E, A]): BaseIO[A] =
private[cats] def embed[E, A](e: BiIO[E, A]): BaseIO[A] =
e.asInstanceOf[BaseIO[A]]

private[bi] case class CustomException[+E](e: E) extends Exception
}

sealed abstract private[bi] class BiIOOps[+E, +A](val bio: BiIO[E, A]) {
sealed abstract private[bi] class BiIOOps[E, A](val bio: BiIO[E, A]) {

def attempt: BiIO[E, Either[E, A]] =
BiIO.create(
Expand All @@ -173,7 +166,7 @@ sealed abstract private[bi] class BiIOOps[+E, +A](val bio: BiIO[E, A]) {
)
)

def attemptBi[EE >: E, AA >: A]: IO[Either[EE, AA]] =
def attemptBi: IO[Either[E, A]] =
BiIO.create(
BiIO
.embed(bio)
Expand All @@ -193,18 +186,23 @@ sealed abstract private[bi] class BiIOOps[+E, +A](val bio: BiIO[E, A]) {

def toBaseIO: BaseIO[A] = BiIO.embed(bio)

def map[B](f: A => B): BiIO[E, B] = BiIO.create(BiIO.embed(bio).map(f))

def toEitherT[EE >: E, AA >: A]: EitherT[BaseIO, EE, AA] = EitherT(BiIO.embed(attemptBi))
def toEitherT: EitherT[BaseIO, E, A] = EitherT(BiIO.embed(attemptBi))

def unsafeRunToEither(): Either[E, A] = BiIO.embed(attempt).unsafeRunSync()

def unsafeRunSync(): A = BiIO.embed(bio).unsafeRunSync()

private def asyncBiIO: AsyncBiIO[E] = new AsyncBiIO[E] {}

def map[B](f: A => B): BiIO[E, B] = asyncBiIO.map(bio)(f)

def flatMap[B](f: A => BiIO[E, B]): BiIO[E, B] = asyncBiIO.flatMap(bio)(f)

def map2[B, Z](second: BiIO[E, B])(f: (A, B) => Z): BiIO[E, Z] = asyncBiIO.map2(bio, second)(f)
}

private[bi] trait MonadBiIO[E] extends Monad[BiIO[E, *]] {
def pure[A](x: A): BiIO[E, A] = BiIO.pure(x)
def pure[A](a: A): BiIO[E, A] = BiIO.pureBi(a)

def flatMap[A, B](fa: BiIO[E, A])(f: A => BiIO[E, B]): BiIO[E, B] =
BiIO.create(BiIO.embed(fa).flatMap(a => BiIO.embed(f(a))))
Expand All @@ -229,18 +227,18 @@ private[bi] trait AsyncBiIO[E] extends Async[BiIO[E, *]] with MonadBiIO[E] {
BiIO.create(
Ref.of[BaseIO, Option[E]](None).flatMap { ref =>
BiIO
.embed(BiIO.attemptBi(acquire))
.embed(BiIO.attemptBi(acquire): BiIO[INothing, Either[E, A]])
.bracketCase {
case Right(a) => BiIO.embed(BiIO.attemptBi(use(a)))
case Right(a) => BiIO.embed(BiIO.attemptBi(use(a)): BiIO[INothing, Either[E, B]])
case l @ Left(_) => BaseIO.pure(l.rightCast[B])
} {
case (Left(_), _) => BaseIO.unit
case (Right(a), ExitCase.Completed) =>
BiIO.embed(BiIO.attemptBi(release(a, ExitCase.Completed))).flatMap {
BiIO.embed(BiIO.attemptBi(release(a, ExitCase.Completed)): BiIO[INothing, Either[E, Unit]]).flatMap {
case Left(l) => ref.set(Some(l))
case Right(_) => BaseIO.unit
}
case (Right(a), res) => BiIO.embed(BiIO.attemptBi(release(a, res))).void
case (Right(a), res) => BiIO.embed(BiIO.attemptBi(release(a, res)): BiIO[INothing, Either[E, Unit]]).void
}
.flatMap {
case Right(b) =>
Expand Down Expand Up @@ -280,9 +278,9 @@ abstract private[bi] class BiIOInstances extends BiIOInstancesLowPriority {

}

implicit val catsEffectBiBifunctorForBiIO: Bifunctor[BiIO] = new Bifunctor[BiIO] {
implicit val catsEffectBifunctorForBiIO: Bifunctor[BiIO] = new Bifunctor[BiIO] {
def bimap[A, B, C, D](fab: BiIO[A, B])(f: A => C, g: B => D): BiIO[C, D] =
fab.attemptBi.map(_.bimap(f, g)).rethrowBi
BiIO.biIOOps(fab.attemptBi.map(_.bimap(f, g)): BiIO[INothing, Either[C, D]]).rethrowBi
}

implicit def catsEffectBiSemigroupKForBiIO[E]: SemigroupK[BiIO[E, *]] =
Expand Down
88 changes: 67 additions & 21 deletions core/src/test/scala/cats/effect/bi/BiIOSuite.scala
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
package cats.effect.bi

import cats.effect.laws.ConcurrentLaws
import cats.effect.laws.util.TestContext
import org.typelevel.discipline.Laws
import org.scalacheck.Prop.forAll
import org.scalacheck.Prop
import cats.effect.ContextShift
import cats.effect.bi.BiIO.{pure, raiseError, terminate}
import cats.effect.laws.discipline.ConcurrentTests
import cats.implicits._
import org.scalacheck.Arbitrary
import org.scalacheck.Cogen
import cats.effect.laws.discipline.arbitrary.catsEffectLawsArbitraryForIO
import org.scalacheck.Gen
import cats.effect.laws.util.{TestContext, TestInstances}
import cats.effect.laws.util.TestInstances.eqThrowable
import cats.implicits._
import cats.kernel.Eq
import cats.effect.laws.util.TestInstances
import TestInstances.eqThrowable
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import scala.util.control.NonFatal
import cats.laws.discipline.BifunctorTests
import cats.laws.discipline.MonadErrorTests
import cats.laws.discipline.SemigroupKTests
import cats.laws.discipline.{BifunctorTests, MonadErrorTests, SemigroupKTests}
import org.scalacheck.{Arbitrary, Cogen, Gen}
import org.typelevel.discipline.Laws

class BiIOSuite extends munit.DisciplineSuite {
def checkAllAsync(name: String, f: TestContext => Laws#RuleSet): Unit = {
Expand All @@ -40,10 +30,7 @@ class BiIOSuite extends munit.DisciplineSuite {
)

implicit def eqBiIO[E: Eq, A: Eq](implicit ec: TestContext): Eq[BiIO[E, A]] =
new Eq[BiIO[E, A]] {
def eqv(x: BiIO[E, A], y: BiIO[E, A]): Boolean =
TestInstances.eqIO[Either[E, A]].eqv(BiIO.toEitherIO(x), BiIO.toEitherIO(y))
}
(x: BiIO[E, A], y: BiIO[E, A]) => TestInstances.eqIO[Either[E, A]].eqv(BiIO.toEitherIO(x), BiIO.toEitherIO(y))

checkAllAsync(
"BiIO[String, *]",
Expand Down Expand Up @@ -73,4 +60,63 @@ class BiIOSuite extends munit.DisciplineSuite {
SemigroupKTests[BiIO[String, *]].semigroupK[Int]
}
)

test("IO <-> BiIO type inference") {
val x: BiIO[INothing, Int] = pure(5)
val y: IO[Int] = x

val xx: IO[Int] = pure(5)
val yy: BiIO[INothing, Int] = xx
}

test("Left Right combined inference") {
val pureValue: IO[Int] = pure(5)
val errValue: BiIO[RuntimeException, INothing] = raiseError(new RuntimeException)

val combined1: BiIO[RuntimeException, Int] = pureValue
val combined2: BiIO[RuntimeException, Int] = errValue
}

test("Recovery") {
val ioa: BiIO[INothing, Int] = terminate(new RuntimeException())

// todo: need companion object
import BiIO._
assertEquals(ioa.recover { case _ => 5 }.unsafeRunSync(), 5)
}

test("map and flatMap") {
val bio: BiIO[INothing, Int] = BiIO.pure(5)

val exp = for {
a <- bio
b <- bio
} yield a + b

assertEquals(10, exp.unsafeRunSync())
}

test("map2") {
val bio: BiIO[INothing, Int] = BiIO.pure(5)

val exp = bio.map2(bio)(_ + _)

assertEquals(10, exp.unsafeRunSync())
}

test("flatMap is unambiguous with cats syntax import") {
import cats.syntax.flatMap._

val bio: BiIO[INothing, Int] = BiIO.pure(5)

bio.flatMap(_ => bio)
}

test("map2 is unambiguous with cats syntax import") {
import cats.syntax.applicative._

val bio: BiIO[INothing, Int] = BiIO.pure(5)

bio.map2(bio)(_ + _)
}
}