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

added collectFirst and collectFirstSome to Foldable #2030

Merged
merged 12 commits into from
Nov 21, 2017

Conversation

kailuowang
Copy link
Contributor

No description provided.

LukaJCB
LukaJCB previously approved these changes Nov 16, 2017
* {{{
* scala> import cats.implicits._
* scala> val numbers = List(2,4,5,6,8,10)
* scala> numbers.collectFst(i => if(i % 2 == 1) Some(i.toDouble / 2d) else None)
Copy link
Member

Choose a reason for hiding this comment

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

scalastyle wants a space between if and the parantheses.

* scala> val numbers = List(2,4,5,6,8,10)
* scala> numbers.collectFst(i => if(i % 2 == 1) Some(i.toDouble / 2d) else None)
* res0: Option[Double] = Some(2.5)
* scala> List(2, 4, 6).collectFst(i => if(i % 2 == 1) Some(i.toDouble / 2d) else None)
Copy link
Member

Choose a reason for hiding this comment

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

Same as above :)

@rossabaker
Copy link
Member

I find it confusing to introduce a collectFirst that is different from the standard collectFirst that will shadow it on so many Foldables. I imagine only wanting to call this via the alias.

@kailuowang
Copy link
Contributor Author

kailuowang commented Nov 16, 2017

@rossabaker that's a valid point. We had the same issue with foldRight as well, in which case we still keep the foldRight name and provide a foldR alias. I don't feel strongly about it, just try to be consistent. If you still prefer I can remove the collectFirst and just keep collectFst

@codecov-io
Copy link

codecov-io commented Nov 16, 2017

Codecov Report

Merging #2030 into master will decrease coverage by 0.04%.
The diff coverage is 88.57%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2030      +/-   ##
==========================================
- Coverage   95.16%   95.11%   -0.05%     
==========================================
  Files         304      304              
  Lines        5027     5061      +34     
  Branches      121      129       +8     
==========================================
+ Hits         4784     4814      +30     
- Misses        243      247       +4
Impacted Files Coverage Δ
...eycats-core/src/main/scala/alleycats/std/set.scala 100% <ø> (ø) ⬆️
...eycats-core/src/main/scala/alleycats/std/map.scala 4.76% <0%> (-1.13%) ⬇️
core/src/main/scala/cats/instances/sortedMap.scala 95.23% <100%> (+0.32%) ⬆️
core/src/main/scala/cats/Foldable.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/instances/queue.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/instances/stream.scala 96.61% <100%> (+0.11%) ⬆️
laws/src/main/scala/cats/laws/FoldableLaws.scala 100% <100%> (ø) ⬆️
...ain/scala/cats/laws/discipline/FoldableTests.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/instances/vector.scala 72.72% <100%> (+1.29%) ⬆️
core/src/main/scala/cats/instances/option.scala 100% <100%> (ø) ⬆️
... and 2 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 ef64ff8...3d9c390. Read the comment docs.

@rossabaker
Copy link
Member

My argument is diminished by the fact I've never noticed the foldRight clash. I still think it's confusing, but I feel less strongly than I did two comments ago in light of that.

}.value

/**
* alias for collectFirst
Copy link
Contributor

Choose a reason for hiding this comment

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

can we explain why this is here also (not just in the above comment).

@@ -215,6 +215,33 @@ import simulacrum.typeclass
}

/**
* Like `collectFirst` from `scala.collection.Traversable` but takes `A => Option[B]`
* instead of `PartialFunction`s.
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 partial functions are very nice for this case. why not use them?

Copy link
Contributor Author

@kailuowang kailuowang Nov 16, 2017

Choose a reason for hiding this comment

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

For a PartialFunction, when isDefinedAt and apply relies on a shared computation, that computation will have to be repeated for the hit case? For example, giving a list of keys, you want to find the first key (value) with which you can find a record in a Map. A PartialFunction would course one extra Map query. Also Map.get is just very nature, making it a PartialFunction would be awkward right?

Copy link
Member

Choose a reason for hiding this comment

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

👍 Reviewing some old code, I've implemented it this way on a couple pre-cats projects. I'd rather this suggestion than dropping it altogether.

Copy link
Member

Choose a reason for hiding this comment

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

TraversableOnce does an unspeakable thing to avoid the double evaluation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rossabaker wow, that is unspeakable. That certainly removed one of the incentives of adding this method. Just need a opposite version of PartialFunction#lift, maybe in mouse.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done! closing this one.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I thought you were talking about removing the incentive for collectFst. What about Foldable without collectFirst? I've needed it on NonEmptyList before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My mistake, my only incentive is for a A=>Option[B] collectFirst. I am reopening it. I wonder if we should add collectFirst: PartialFunction[A, B] and collectFirstO: A => Option[B]
For the default collectFirst implementation (which is going to be overriden for actual TraversableOnce data types) shall we do the same trick as TraversableOnce?

@johnynek
Copy link
Contributor

I’m +1 on two methods. I’d be more verbose. collectFirstSome or something. Which is what we are doing in the Option returning case.

I’m also +1 on ugly hacks in libraries that speed things up for many users if they are safe. I haven’t studied this particular hack in detail however.

@kailuowang
Copy link
Contributor Author

Just made the change, I can't use the hacks because I couldn't get applyOrElse API to work with foldRight, however I did override it in our instances.

@kailuowang kailuowang changed the title added a collectFirst that takes A => Option[B] instead of PartialFunction added collectFirst and collectFirstSome to Foldable Nov 18, 2017
@@ -214,6 +214,31 @@ import simulacrum.typeclass
case Right(_) => None
}

def collectFirst[A, B](fa: F[A])(pf: PartialFunction[A, B]): Option[B] =
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth mentioning that a more efficient implementation is often possible? We do this on size and foldM, but I suppose it would be easy to get carried away with this. I don't feel strongly.

Copy link
Member

Choose a reason for hiding this comment

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

My battery is going to die before I can run the tests, but I think this might be right:

    foldRight(fa, Eval.now(Option.empty[B])) { (a, lb) =>
      var result: Eval[Option[B]] = lb
      pf.runWith(b => result = Eval.now(Some(b)))(a)
      result
    }.value

Copy link
Member

Choose a reason for hiding this comment

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

This is the TraversableOnce trick in foldRight form. TraversableOnce claims it's faster than runWith, but I have not benchmarked it in this context. Tests pass even after I remove the overrides. sentinel could be moved to a private member of the companion.

    val sentinel: Function1[A, Any] = new scala.runtime.AbstractFunction1[A, Any]{ def apply(a: A) = this }
    foldRight(fa, Eval.now(Option.empty[B])) { (a, lb) =>
      val x = pf.applyOrElse(a, sentinel)
      if (x.asInstanceOf[AnyRef] ne sentinel) Eval.now(Some(x.asInstanceOf[B]))
      else lb
    }.value

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 a lot @rossabaker , I made the change.

LukaJCB
LukaJCB previously approved these changes Nov 20, 2017
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 great to me! Thanks.

*/
def collectFirstSome[A, B](fa: F[A])(f: A => Option[B]): Option[B] =
foldRight(fa, Eval.now(Option.empty[B])) { (a, lb) =>
val fa = 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.

nit: can we not shadow fa with f(a) which is actually Option[B] and not a F[A]?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops. fixed.

johnynek
johnynek previously approved these changes Nov 20, 2017
rossabaker
rossabaker previously approved these changes Nov 20, 2017
Copy link
Member

@rossabaker rossabaker left a comment

Choose a reason for hiding this comment

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

Moving the sentinel to the companion would save an allocation per invocation of collectFirst, but also makes the hack a little less localized. I'm happy either way.

@kailuowang
Copy link
Contributor Author

@rossabaker I didnt move it because it was parameterized to A, I just realized that it doesn't have to. So I moved it.

Copy link
Member

@rossabaker rossabaker left a comment

Choose a reason for hiding this comment

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

I like the solution, and that the git blame of AbstractFunction1[Any, Any] won't be me! :trollface:

@kailuowang kailuowang merged commit 9325184 into typelevel:master Nov 21, 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.

5 participants