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 Defer typeclass, laws and implementations #2279

Merged
merged 9 commits into from
Jul 22, 2018
Merged

Conversation

johnynek
Copy link
Contributor

@johnynek johnynek commented Jun 4, 2018

This seems like a little thing, but I was able to use it to implement ContT[M, A, B], a wrapper for (B => M[A]) => M[A] and pass all the laws using the naive map/flatMap (with cats.data.AndThen) and the following tailRecM:

 def tailRecM[M[_], A, B, C](a: A)(fn: A => ContT[M, C, Either[A, B]])(implicit M: Defer[M]): ContT[M, C, B] =
  ContT[M, C, B] { cb: (B => M[C]) => 
     def go(a: A): M[C] =
        fn(a).run {
          case Left(a) => M.defer(go(a))
          case Right(b) => cb(b)
        }
       go(a)
    }

I think this is pretty clear that this is an interesting typeclass along with the fact that we have so many implementations.

closes #2273

@johnynek
Copy link
Contributor Author

johnynek commented Jun 4, 2018

I think this strategy can allow us to extend ContT to any cats.Monad: in the ContT.tailRecM loop, lift M to Free, use defer on Free, then finally run the Free to get back to M at the end.

@@ -462,6 +462,12 @@ private[data] sealed abstract class IRWSTInstances extends IRWSTInstances1 {
implicit def L: Monoid[L] = L0
}

implicit def catsDataDefer[F[_], E, L, SA, SB](implicit F: Defer[F]): Defer[IndexedReaderWriterStateT[F, E, L, SA, SB, ?]] =
Copy link
Contributor

Choose a reason for hiding this comment

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

totally trivial nit: the naming convention has a ForIRWST suffix. Although I don't think it matters much since there is no risk of naming conflict here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep, that was a goof up.

@@ -30,13 +30,35 @@ class FunctionSuite extends CatsSuite {
import Helpers._

checkAll("Function0[Int]", SemigroupalTests[Function0].semigroupal[Int, Int, Int])
// TODO: make an binary compatible way to do this
implicit val deferFunction0: Defer[Function0] =
Copy link
Contributor

@kailuowang kailuowang Jun 4, 2018

Choose a reason for hiding this comment

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

One way to do this that I can think of is to just write this in a new cats.instance.FunctionInstancesBinaryCompatible1 trait and then add a new cats.instances.AllInstancesBinaryCompatible1 trait to extend it. And let cats.instances.all and cats.implicits extend it. (basically the same way as how we extend syntax while reserving binary compatibility)

if (c <= 0) F.defer(fa(()))
else F.defer(loop(c - 1))

loop(1000000) <-> (fa(()))
Copy link
Contributor

Choose a reason for hiding this comment

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

this number might be so too big that all jobs in build ran OOM.

@codecov-io
Copy link

codecov-io commented Jun 5, 2018

Codecov Report

Merging #2279 into master will increase coverage by <.01%.
The diff coverage is 96.15%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2279      +/-   ##
==========================================
+ Coverage   95.08%   95.08%   +<.01%     
==========================================
  Files         343      345       +2     
  Lines        5935     5987      +52     
  Branches      218      217       -1     
==========================================
+ Hits         5643     5693      +50     
- Misses        292      294       +2
Impacted Files Coverage Δ
core/src/main/scala/cats/data/WriterT.scala 91.52% <100%> (+0.14%) ⬆️
...c/main/scala/cats/laws/discipline/DeferTests.scala 100% <100%> (ø)
core/src/main/scala/cats/data/Nested.scala 94.44% <100%> (+0.15%) ⬆️
core/src/main/scala/cats/data/Tuple2K.scala 91.11% <100%> (+0.41%) ⬆️
core/src/main/scala/cats/Eval.scala 98.78% <100%> (+0.03%) ⬆️
core/src/main/scala/cats/data/IndexedStateT.scala 89.47% <100%> (+0.22%) ⬆️
free/src/main/scala/cats/free/Free.scala 91.78% <100%> (+0.23%) ⬆️
core/src/main/scala/cats/data/OptionT.scala 97.67% <100%> (+0.05%) ⬆️
core/src/main/scala/cats/data/Kleisli.scala 97.11% <100%> (+0.05%) ⬆️
core/src/main/scala/cats/instances/function.scala 100% <100%> (ø) ⬆️
... and 6 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e15188b...c86ece0. Read the comment docs.

@@ -435,6 +435,12 @@ private[data] abstract class IorTInstances extends IorTInstances1 {
Monad[IorT[M, E, ?]]
}
}

implicit def catsDataDeferForIor[F[_], E](implicit F: Defer[F]): Defer[IorT[F, E, ?]] =
Copy link
Member

Choose a reason for hiding this comment

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

missing T after catsDataDeferForIor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

looks like we are still missing the T.

Copy link
Contributor

Choose a reason for hiding this comment

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

@kubukoz
Copy link
Member

kubukoz commented Jun 5, 2018

Why not call it Suspend to keep consistent with Sync's suspend method? You mentioned yourself that it could be an alias to it, but is there a reason why we can't just make Sync extend Defer/Suspend and provide delay?

EDIT: now I see that #2273's name originally mentioned a Suspend typeclass, so I guess the decision to rename was influenced by Suspend being too specific (side effects, not deferred computations)...

Can we rename suspend then? :D

@kailuowang
Copy link
Contributor

@johnynek do you want this in the coming 1.2 release?

@johnynek
Copy link
Contributor Author

@kailuowang I would.

@kailuowang kailuowang added this to the 1.2 milestone Jun 12, 2018
@kailuowang
Copy link
Contributor

#2284 adds a FunctionInstancesBinCompat0, shall we merge that one first so that you can add your Function Defer instance there?

@johnynek
Copy link
Contributor Author

@kailuowang sounds great.

LukaJCB
LukaJCB previously approved these changes Jun 26, 2018
Copy link
Member

@LukaJCB LukaJCB left a comment

Choose a reason for hiding this comment

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

LGTM, thanks! 👍

Copy link
Member

@alexandru alexandru left a comment

Choose a reason for hiding this comment

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

[deleted comment]

F.defer(fa(())) <-> fa(())

def deferDoesNotEvaluate[A](fa: Unit => F[A]): IsEq[Boolean] = {
var evaluated = false
Copy link
Member

Choose a reason for hiding this comment

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

Unsuspended vars in laws make me feel uneasy. First of all I fear that such laws will not have a long life.

To fix it, you can defer the whole thing:

  def deferDoesNotEvaluate[A](f: Boolean => F[A]): IsEq[Boolean] = {
    val lh = F.defer {
      var evaluated = false
      F.defer {
        evaluated = true
        f(evaluated)
      }
      f(evaluated)
    }
    lh <-> f(false)
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m nervous that if you pass a function that ignores the Boolean this is true trivially.

def deferIsStackSafe[A](fa: Unit => F[A]): IsEq[F[A]] = {
def loop(c: Int): F[A] =
if (c <= 0) F.defer(fa(()))
else F.defer(loop(c - 1))
Copy link
Member

Choose a reason for hiding this comment

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

This law is interesting, but in case an F[_] provides such a powerful defer, what about the behavior of flatMap? Shouldn't flatMap also be stack safe if defer is?

Also the question on my mind would be: is defer any relevant without a Monad restriction? Or in other words, do we have any data type that can implement Defer, but not Monad?

Note that we also have a StackSafeMonad.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good question about making the law stronger: if there is a monad, flatMap must be stack safe. The function cases wouldn't pass this law.

As to what might not be a monad: command line parsing is often done with an applicative, not a monad (see optparse-applicative library in haskell or https://github.com/bkirwi/decline for a cats version). Those types can still have a lawful Defer.

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 have Applicative examples without a Monad definition, I guess that's fine 👍

if (c <= 0) F.defer(fa(()))
else F.defer(loop(c - 1))

val cnt = if (Platform.isJvm) 20000 else 2000
Copy link
Member

Choose a reason for hiding this comment

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

In my experience these arbitrary values hurt for F[_] data types that have an expensive flatMap, for example streaming data types, because you're not talking of a single event being emitted, but multiple ones, per flatMap.

In Cats-Effect what I did was to introduce an iterations: Int in SyncLaws (see sample) and an implicit Parameters in SyncTests (see definition and usage). Otherwise those tests would choke for Monix's Iterant for example, especially when executed on Travis.

And in absence of that, I'd use smaller values:

val cnt = if (Platform.isJvm) 10000 else 1000

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don’t see a compelling reason why this is only 2x too large. Can you give an idea?

@kailuowang
Copy link
Contributor

@johnynek do you happen to have time to finish this one up? or can this wait until the next release?

@johnynek
Copy link
Contributor Author

@kailuowang what needs to happen to merge?

I didn't want to take the time to cut the counts in half suggested above.

@kailuowang
Copy link
Contributor

Do you want to respond to this feedback?

Also the instance of Function1 has a place now, thanks to the FunctionInstancesBinCompat0 added by #2284

@kailuowang
Copy link
Contributor

@johnynek I added a commit to move Function0 instance and added Function1 instance. would you review?

@johnynek
Copy link
Contributor Author

This is failing Mima:

[info] Cats core: found 2 potential binary incompatibilities while checking against org.typelevel:cats-core_2.12:1.0.0 
[error]  * abstract synthetic method cats$instances$Function0Instances$_setter_$catsSddDeferForFunction0_=(cats.Defer)Unit in interface cats.instances.Function0Instances is present only in current version
[error]    filter with: ProblemFilters.exclude[ReversedMissingMethodProblem]("cats.instances.Function0Instances.cats$instances$Function0Instances$_setter_$catsSddDeferForFunction0_=")
[error]  * abstract method catsSddDeferForFunction0()cats.Defer in interface cats.instances.Function0Instances is present only in current version
[error]    filter with: ProblemFilters.exclude[ReversedMissingMethodProblem]("cats.instances.Function0Instances.catsSddDeferForFunction0")

I'm not sure how badly we need the function instances anyway. :/ Sorry this is so much work. Unfortunately I am buried between work and travel. I'm sorry.

@kailuowang
Copy link
Contributor

@johnynek mima should've been fixed by the latest commit. It was me being dumb and totally forgot about it.

@kailuowang
Copy link
Contributor

@LukaJCB do you want to take another quick look at the commits I made?

@LukaJCB
Copy link
Member

LukaJCB commented Jul 21, 2018

LGTM 👍

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.

Proposal: adding a Defer typeclass
7 participants