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 NonEmptyList#partitionE #1858

Merged
merged 18 commits into from
Sep 6, 2017
Merged

Conversation

LukaJCB
Copy link
Member

@LukaJCB LukaJCB commented Aug 26, 2017

As per #1743 (now closed). Also open for alternative names (partitionIor maybe?)

@codecov-io
Copy link

codecov-io commented Aug 26, 2017

Codecov Report

Merging #1858 into master will increase coverage by 0.02%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #1858      +/-   ##
=========================================
+ Coverage   95.17%   95.2%   +0.02%     
=========================================
  Files         248     248              
  Lines        4352    4379      +27     
  Branches      121     125       +4     
=========================================
+ Hits         4142    4169      +27     
  Misses        210     210
Impacted Files Coverage Δ
core/src/main/scala/cats/Reducible.scala 95.38% <100%> (+0.46%) ⬆️
core/src/main/scala/cats/instances/list.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/Foldable.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/data/NonEmptyList.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/data/NonEmptyVector.scala 100% <100%> (ø) ⬆️

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 ef28e24...e1d39b1. Read the comment docs.

@kailuowang
Copy link
Contributor

Would it be more useful to add it to NonEmptyTraverse with a slightly different signature?

def partitionIor[B, C](f: A => Ior[B, C]): Ior[NonEmptyList[B], NonEmptyList[C]] = {

@LukaJCB
Copy link
Member Author

LukaJCB commented Aug 28, 2017

You mean generalizing it to NonEmptyTraverse?

def partitionIor[F[_]: NonEmptyTraverse, A, B, C](fa: F[A])(f: A => Ior[B, C]): Ior[F[B], F[C]]

Not sure, if that works, but would be cool :)

@kailuowang
Copy link
Contributor

kailuowang commented Aug 28, 2017

@LukaJCB i didn't mean to generalize the result NonEmptyList part (not sure if that's doable). I.e.
for NonEmptyVector it would still return Ior[NonEmptyList[B], NonEmptyList[C]], but at least it will have this method.

@johnynek
Copy link
Contributor

You don't need traverse do you? Reducible will do, no?

@LukaJCB
Copy link
Member Author

LukaJCB commented Aug 29, 2017

@johnynek is correct! I was able to generalize it to Reducible :)

@kailuowang
Copy link
Contributor

looks good. is it just me or does anyone else feel a bit weird that we have a partitionE on Reducible but not a partition on Foldable (on which it would probably be returning (List[A], List[B])

like how about this

  def partitionEither[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit F: MonoidK[F], A: Applicative[F]): (F[B], F[C]) =
    foldRight(fa, Eval.later((F.empty[B], F.empty[C]))) { (a, e) =>
      e.map {
        case (fbs, fcs) => f(a).fold(
          b => (F.combineK(A.pure(b), fbs), fcs),
          c => (fbs, F.combineK(A.pure(c), fcs))
        )
      }
    }.value
}

Note that it requires Applicative and MonoidK on F, maybe we can reduce that requirements?

@LukaJCB
Copy link
Member Author

LukaJCB commented Aug 29, 2017

I don't think we can reduce the requirements, because either side of the tuple might be empty, so MonoidK is a must, and for Applicative to go, we'd need a way to go from Either[B,C] to (F[B], F[C]), so either we'd keep those two, or we'd just return (List[A], List[B]) as you said.

@kailuowang
Copy link
Contributor

I am fine with keeping those requirements. My guess is that for most of the use cases they are going to have those two instances anyway.

@peterneyens
Copy link
Collaborator

That Foldable#partitionEither would be the same as a map followed by Alternative#separate, right?
Maybe we can call it Alternative#mapSeparate ?

@LukaJCB
Copy link
Member Author

LukaJCB commented Aug 29, 2017

That's a good idea! It also just dawned on me that MonoidK + Applicative is Alternative 🙈. I think the implementation could be even easier:

def mapSeparate[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit A: Alternative[F]): (F[B], F[C]) =
  foldMap(fa)(a => f(a) match {
    case Right(c) => (A.empty[B], A.pure(c))
    case Left(b) => (A.pure(b), A.empty[C])
  })

@kailuowang
Copy link
Contributor

Right I keep forgetting Alternative

@LukaJCB
Copy link
Member Author

LukaJCB commented Aug 29, 2017

It seems the above didn't work, because there's no Monoid instance only MonoidK. It'd need to be foldMapK.

  def mapSeparate[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit A: Alternative[F]): (F[B], F[C]) = {

    implicit val mFbFC: Monoid[(F[B], F[C])] = new Monoid[(F[B], F[C])] {
      def empty: (F[B], F[C]) = (A.empty[B], A.empty[C])
      def combine(x: (F[B], F[C]), y: (F[B], F[C])): (F[B], F[C]) = (x, y) match {
        case ((xfb, xfc), (yfb, yfc)) => (MonoidK[F].combineK(xfb, yfb), MonoidK[F].combineK(xfc, yfc))
      }
    }

    foldMap(fa)(a => f(a) match {
      case Right(c) => (A.empty[B], A.pure(c))
      case Left(b) => (A.pure(b), A.empty[C])
    })
  }

@kailuowang
Copy link
Contributor

can MonoidK#algebra help?

@LukaJCB
Copy link
Member Author

LukaJCB commented Aug 29, 2017

algebra works like a charm, thanks! Should I add this to the current PR, or make a new one? :)

@kailuowang
Copy link
Contributor

current PR works for me.

case Left(b) => (pure(b), empty[C])
})
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I think @peterneyens was suggesting that
you can just write

  import cats.instances.either._
  def mapSeparate[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit FM: Monad[F]): (F[B], F[C]) =
    separate(map(fa)(f))

So instead of requiring F to be Foldable, it requires to be a Monad.
Or we can use your implementation, which requires Foldable, which might be more performant? (traverse once instead three times?)

I still think we should name it partitionEither or something, the idea is to provide a familiar api to users. map and then separate probably don't justify a special delegate method also named mapSeparate.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I prefer the Foldable way, for the reasons you mentioned. I'm not married to the name, but it makes sense in the same way flatMap makes sense, I guess.

I think I'd call the method on Foldable partitionEither as you suggested and renamed the one on Reducible to partitionIor Maybe?
partitionE isn't a great name IMO, because there's no real precedence for the "E" and I think if we want to stay a beginner friendly library we should have as little of these one-letter-abbreviations as possible.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The current implementation is better than map + separate indeed. Just wanted to point out the similarity.

separate is available for all Bifoldables while we currently have to use Either with mapSeparate:

  • With separate you could also split Ior and Tuple2 (similar to unzip).
  • mapSeparate + toEither will probably be faster than map(toEither).separate as you fold once instead of twice.

I agree that partitionE isn't a great name.

Copy link
Contributor

Choose a reason for hiding this comment

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

my 2 cents

  1. since we are going to use Foldable, it seems to make more sense to be on Foldable than Alternative. Having a pair of partition methods on Foldable and Reducible is what I was looking for.
  2. both methods should be named partitionEitherXX since they both use a function that returns an Either for the actual partition logic. Maybe we can call Reducible#partitionEitherNE and Foldable#partitionEither
  3. maybe we should mention the map + separate option in the scaladoc in Foldable#partitionEither, I think it might worth some promotion.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm +1 on renaming to Foldable#partitionEither and mentioning the map + separate option in the scaladoc. Disagree on partitionEitherNE though, for similar reasons as partitionE.
How about nonEmptyPartition? Would go in line with nonEmptyIntercalate etc. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I am fine with nonEmptyPartition

Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed to Foldable#partitionEither and Reducible#nonEmptyPartition :)

val h: Int => Either[String, Double] = f andThen Left.apply

val withG = fi.partitionE(g).fold(_ => NonEmptyList.one(""), identity, (l,r) => r)
withG should === (Reducible[F].toNonEmptyList((fi.map(f))))
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this map is the reason we add the Functor constraint to ReducibleCheck we could write Reducible[F].toNonEmptyList(fi).map(f), right?

val g: Int => Either[Double, String] = f andThen Right.apply
val h: Int => Either[String, Double] = f andThen Left.apply

val withG = fi.partitionE(g).fold(_ => NonEmptyList.one(""), identity, (l,r) => r)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The fold could be .right.getOrElse(NonEmptyList.one("")).

case Left(b) => (pure(b), empty[C])
})
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

The current implementation is better than map + separate indeed. Just wanted to point out the similarity.

separate is available for all Bifoldables while we currently have to use Either with mapSeparate:

  • With separate you could also split Ior and Tuple2 (similar to unzip).
  • mapSeparate + toEither will probably be faster than map(toEither).separate as you fold once instead of twice.

I agree that partitionE isn't a great name.

import cats.syntax.either._

def g(a: A, eval: Eval[Ior[NonEmptyList[B], NonEmptyList[C]]]): Eval[Ior[NonEmptyList[B], NonEmptyList[C]]] = {
val ior = eval.value
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should probably do:

eval.map { ior =>
  (f(a), ior) match {
    // ...
  }
}

@LukaJCB
Copy link
Member Author

LukaJCB commented Aug 30, 2017

Thanks for your review @peterneyens! I addressed your comments :)

@@ -25,6 +25,40 @@ abstract class FoldableCheck[F[_]: Foldable](name: String)(implicit ArbFInt: Arb
}
}

test("Alternative#partitionEither retains size") {
Copy link
Contributor

Choose a reason for hiding this comment

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

titles of the tests should be Foldable#partitionEither xxxx

@@ -407,6 +407,21 @@ import simulacrum.typeclass
}.toList

/**
* Separate this Foldable into a Tuple by a separating function `A => Either[B, C]`
*/
def partitionEither[A, B, C](fa: F[A])(f: A => Either[B, C])(implicit F: Foldable[F], A: Alternative[F]): (F[B], F[C]) = {
Copy link
Contributor

@kailuowang kailuowang Aug 30, 2017

Choose a reason for hiding this comment

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

you are already in Foldable, so no need for the implicit F: Foldable[F] here

}
}

test("Foldable#mapSeparate remains sorted") {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You forgot to rename it here.

@@ -407,6 +407,31 @@ import simulacrum.typeclass
}.toList

/**
* Separate this Foldable into a Tuple by a separating function `A => Either[B, C]`
* Equivalent to `Functor#map` and then `Alternative#separate`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we did define it in Alternative, we could add a consistency law checking this equality, but hey ... you can't always get what you want 😃

Copy link
Member Author

Choose a reason for hiding this comment

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

True, I don't really mind either way tbh.
We could add a test that checks if it's consistent with List#partition though 😄

@fommil
Copy link

fommil commented Sep 3, 2017

@LukaJCB since I wrote this, I'd appreciate a mention in the method 😉 recall that my original implementation carried no license with it.

@fommil
Copy link

fommil commented Sep 3, 2017

hmm, on second thoughts, your impl is sufficiently different that I don't consider it a derivative works, no attribution required.

@LukaJCB
Copy link
Member Author

LukaJCB commented Sep 3, 2017

Hey @fommil sorry for any confusion, didn't mean any harm!

@kailuowang
Copy link
Contributor

kailuowang commented Sep 4, 2017

@peterneyens any other feedback?

* res1: cats.data.Ior[cats.data.NonEmptyList[Nothing],cats.data.NonEmptyList[Int]] = Right(NonEmptyList(4, 8, 12, 16))
* }}}
*/
def nonEmptyPartition[A, B, C](fa: F[A])(f: A => Either[B, C]): Ior[NonEmptyList[B], NonEmptyList[C]] = {
Copy link
Contributor

Choose a reason for hiding this comment

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

couldn't we do this considerably more efficiently for NonEmptyList and NonEmptyVector? Can we add methods to those datatypes that does so? Eval is kind of a perf killer, sadly, which this method hides.

Copy link
Member Author

Choose a reason for hiding this comment

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

I just had the same idea :D I already implemented a O(n) solution for partitionEither on List and will override nonemptyPartition as well!

Copy link
Member Author

Choose a reason for hiding this comment

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

Added overrides for NEL and NEV :)

@LukaJCB LukaJCB force-pushed the add-partitione-nel branch 2 times, most recently from dcb2852 to 505475d Compare September 6, 2017 08:11
}
}

test("Reducible#nonEmptyPartition remains sorted") {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is a bit weird. The only F[_] part of it toNonEmptyList. After that, we are testing nonEmptyPartition on NonEmptyList, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes that's true, I wrote this test when originally nonEmptyPartition was part of NonEmptyList. I could move the test there, if you'd like :) Otherwise, I'm not sure it's possible to sort an F[_] with just Reducible

@johnynek
Copy link
Contributor

johnynek commented Sep 6, 2017

👍

@kailuowang kailuowang merged commit 7be0e64 into typelevel:master Sep 6, 2017
@kailuowang
Copy link
Contributor

Great contribution. Thanks @LukaJCB !

@LukaJCB LukaJCB deleted the add-partitione-nel branch September 7, 2017 08:07
@kailuowang kailuowang added this to the 1.0.0-RC1 milestone Oct 13, 2017
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