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 ifElseM #3553

Merged
merged 2 commits into from
Aug 12, 2020
Merged

add ifElseM #3553

merged 2 commits into from
Aug 12, 2020

Conversation

mtomko
Copy link
Contributor

@mtomko mtomko commented Aug 6, 2020

The actual implementation is from @djspiewak:

https://gist.github.com/djspiewak/1b92a6e338f4e1537692e748c54b9743

We discussed this function briefly on the cats-effect gitter channel:

https://gitter.im/typelevel/cats-effect?at=5f297e4314c413356f56d230

As indicated in the discussion there, it wasn't totally clear where it belonged. There is no package object in cats.syntax.flatMap and I'm not sure adding a package object now makes sense, with Scala 3 on the horizon. This is my first code contribution to cats and I'm still learning the code layout so I'm happy to move things around to suit people's preferences.

Also, I did see some failures locally in the alleycats JS tests, but a clean checkout of the master branch also had those, and I don't see how I could have broken them or made the situation any worse with the changes I've made here. I'm curious to see what the CI build does - if it has problems I'll investigate further.

@mtomko mtomko mentioned this pull request Aug 6, 2020
@codecov-commenter
Copy link

codecov-commenter commented Aug 6, 2020

Codecov Report

Merging #3553 into master will decrease coverage by 0.25%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##           master    #3553      +/-   ##
==========================================
- Coverage   91.30%   91.04%   -0.26%     
==========================================
  Files         386      385       -1     
  Lines        8565     8554      -11     
  Branches      248      251       +3     
==========================================
- Hits         7820     7788      -32     
- Misses        745      766      +21     

barambani
barambani previously approved these changes Aug 7, 2020
Copy link
Contributor

@barambani barambani left a comment

Choose a reason for hiding this comment

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

I see from the discussion that calling it from the object (FlatMap.ifElseM(...)) is the preferred solution from the ergonomics point of view when compared to the class/syntax alternative (fa.ifElseM(p1 -> a1, p2 -> a2, ...)(else)) so the location seems ok to me. I don't use this pattern much so I don't have preferences. 👍

LukaJCB
LukaJCB previously approved these changes Aug 10, 2020
Copy link
Contributor

@johnynek johnynek left a comment

Choose a reason for hiding this comment

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

I'd rather see this directly on the Monad (or FlatMap) trait since that is binary compatible on 2.12 and later.

@johnynek
Copy link
Contributor

PS: I can see why the the ergonomics is nicer on the object (in fact, we explored this pattern many years ago, but cats never seemed to like it, it makes type inference better. See e.g.:

abstract class MonoidFunctions[M[T] <: Monoid[T]] extends SemigroupFunctions[M] {
which we copied from algebird. Being able to do Monoid.combine(a, b) is great for type inference, but copying each method to the companion object is a bit of a pain, so I eventually buckled and felt that Monoid[Int].combine was a fine compromise in general. When the type is really ugly, that isn't so great of course).

@mtomko
Copy link
Contributor Author

mtomko commented Aug 10, 2020

Will do, running tests now...

* @see See [[https://gitter.im/typelevel/cats-effect?at=5f297e4314c413356f56d230]] for the discussion.
*/
def ifElseM[F[_]: FlatMap, A](branches: (F[Boolean], F[A])*)(els: F[A]): F[A] = {
def loop(branches: List[(F[Boolean], F[A])]): F[A] =
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought I left this comment but I don't see it now.

If we have a long list of branches, this will stack overflow. We can use tailRecM to address that if we work on Monad:

type Branches = List[(F[Boolean], F[A])]
def step(branches: Branches): F[Either[Branches, A]] =
  branches match {
    case (cond, conseq) :: tail =>
      cond.flatMap { b => if (b) conseq.map(Right(_)) else pure(Left(tail)) }
    case Nil => els.map(Right(_))
}

tailRecM(branches)(step(_))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing that out. I have a couple of questions. I'm not sure how to spot when a flatMap isn't stack-safe - is it dependent on the underlying monad's own stack-safety or is there something I can see in the chain of calls to flatMap itself?

Also, if simply using tailRecM is the solution, there is a tailRecM for FlatMap. I'm not averse to moving this code over to Monad but if we don't need the full power of Monad for stack-safety, then I guess I'd like to save the effort.

Thanks again for your comments and suggestions!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I just reread the cats FAQ on tailRecM. So it's nothing particular to this implementation, it depends on the underlying Monad, and using tailRecM avoids that problem. I'm going to reframe the implementation in terms of tailRecM.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And, now I think I see why it has to be on Monad...

@mtomko mtomko dismissed stale reviews from LukaJCB and barambani via b04afb3 August 10, 2020 21:16
@mtomko
Copy link
Contributor Author

mtomko commented Aug 11, 2020

Just a wild thought, too, we could call this condM as a nod to our FP roots (Lisp's cond form)... It almost has that feel.

Copy link
Contributor

@johnynek johnynek left a comment

Choose a reason for hiding this comment

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

This looks good to me. Thanks for making the stack safety changes.

BTW: I don't have a strong opinion about the name. Cond is a cute reference. But personally I'd lean to what you have which I think will be more intuitive for newcomers.

@mtomko
Copy link
Contributor Author

mtomko commented Aug 11, 2020

@johnynek thanks so much for your help. I'm digging into some more resources on stack safety now - I need to learn a few things before I can properly understand the Phil Freeman paper but I think it'll be good for me to build that background.

I think ultimately the shape of this method isn't quite the same as cond because the els has to be in a separate argument list. I'll just stick with ifElseM for that reason and for the reason you suggested (approachability).

@LukaJCB LukaJCB merged commit 94eb6e2 into typelevel:master Aug 12, 2020
@mtomko mtomko deleted the ifElseM2 branch August 12, 2020 15:52
@travisbrown travisbrown added this to the 2.2.0-RC3 milestone Aug 15, 2020
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.

6 participants