-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
* {{{ | ||
* 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above :)
I find it confusing to introduce a |
@rossabaker that's a valid point. We had the same issue with |
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
My argument is diminished by the fact I've never noticed the |
}.value | ||
|
||
/** | ||
* alias for collectFirst |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done! closing this one.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
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. |
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. |
collectFirst
that takes A => Option[B]
instead of PartialFunction
collectFirst
and collectFirstSome
to Foldable
@@ -214,6 +214,31 @@ import simulacrum.typeclass | |||
case Right(_) => None | |||
} | |||
|
|||
def collectFirst[A, B](fa: F[A])(pf: PartialFunction[A, B]): Option[B] = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
daa1a0d
to
0ee9906
Compare
There was a problem hiding this 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) |
There was a problem hiding this comment.
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]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops. fixed.
There was a problem hiding this 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.
@rossabaker I didnt move it because it was parameterized to |
There was a problem hiding this 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!
No description provided.