-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Introduce andThen overload which combines two partial functions
#7263
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
Conversation
|
is this in reference to scala/bug#9968 ? |
|
Not really, I just needed it by myself. I widely use this |
|
Will also be glad to backport it to 2.12.x if there is a possibility to be merged to |
|
we couldn't accept it for 2.12.x, because bincompat (https://docs.scala-lang.org/overviews/core/binary-compatibility-of-scala-releases.html) |
| /** Composite function produced by `PartialFunction#andThen` method | ||
| */ | ||
| private class Combined[-A, B, +C] (pf: PartialFunction[A, B], k: PartialFunction[B, C]) extends PartialFunction[A, C] with Serializable { | ||
| def isDefinedAt(x: A) = pf.isDefinedAt(x) && k.isDefinedAt(pf(x)) |
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.
Should we use checkFallback and fallbackOccurred? 🤔
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.
Seems reasonable
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
3087b3a to
0614541
Compare
|
This looks good to me in principle, but it should come with tests cases. In particular we have to see how well overloading resolution behaves for the various possible arguments (selections of functions, partial functions, function literals, partial function literals). |
4bcac10 to
bf1d9fd
Compare
|
Added some test cases for partial function combinations.
|
|
Great, thanks for adding tests! I have to ask for more though :-) We should also test type inference. The following work in 2.13.0-M5: ( Type inference needs to figure out the type of |
|
@lrytz You cases work as expected |
|
Thank you, glad that everything works out! Can we also test type inference for compose? |
|
Hmm, got errors for /Users/danslapman/Projects/scala/test/junit/scala/PartialFunctionCompositionTests.scala:144: missing parameter type
[error] val pf2 = pf compose (x => x + 1)
[error] ^
[error] /Users/danslapman/Projects/scala/test/junit/scala/PartialFunctionCompositionTests.scala:149: missing parameter type for expanded function
[error] The argument types of an anonymous function must be fully known. (SLS 8.5)
[error] Expected type was: ?
[error] val pf3 = pf compose { case x if x > 20 => x + 1 }Test is pretty identical: val fb = (_: Int) => 42
val pf: PartialFunction[Int, Int] = { case x if x > 10 => x + 1 }
val pf2 = pf compose (x => x + 1)
assertEquals(pf2.applyOrElse(11, fb), 13)
assertFalse(pf2.isDefinedAt(10))
val pf3 = pf compose { case x if x > 20 => x + 1 }
assertEquals(pf3.applyOrElse(15, fb), 42)
assertEquals(pf3.applyOrElse(21, fb), 23)
assertFalse(pf3.isDefinedAt(15))
assertTrue(pf3.isDefinedAt(21)) |
|
@danslapman totally my bad, I'm sorry. Of course the compiler cannot infer the parameter type for compose, there's no constraint on the function's parameter type in So this PR LGTM! |
|
Asking for a review by @NthPortal and @Jasper-M who commented on the related ticket (scala/bug#9968). |
|
LGTM. Pretty cool that type inference can handle this now. |
NthPortal
left a comment
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.
LGTM
I'm still a little bit concerned about the performance (see scala/bug#9968 (comment)) of having to apply the first pf in order to check if it's defined for the second -- thus a call to isDefinedAt followed by apply will evaluate the first function twice -- but I'm not sure what a good solution is. I don't think it should hold up this PR regardless, but I do think it's worth looking into.
|
|
||
| /** | ||
| * Composes this partial function with other partial function that | ||
| * gets applied to results of this partial function. |
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.
Something about this description (as well as the one for compose seems confusing to me; I think it could use a clearer wording. I can't think of anything off the top of my head, but I will think on it.
| */ | ||
| private class Combined[-A, B, +C] (pf: PartialFunction[A, B], k: PartialFunction[B, C]) extends PartialFunction[A, C] with Serializable { | ||
| def isDefinedAt(x: A) = { | ||
| val b: B = pf.applyOrElse(x, checkFallback[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.
@NthPortal Maybe caching pf.applyOrElse into a lazy val class variable can achieve better performance?
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.
Also since this method calls the pf.applyOrElse, in case that pf is impure (modifies external variable), this can mess up the external variable 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.
- For the best perfomance
applyOrElseshould be used - caching can be done outside
isDefinedAtif it really needed, caching inside can lead to unexpected memory usage and other undesired effects
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.
Memory and performance concerns aside, correct me if I am wrong, any side effect will be evaluated twice, which will result in undesired behavior.
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.
If You need the results of combined function, call applyOrElse, both partial functions will be called at most once. It is impossible to check if k is defined on pf's result, so if pf is side-effectful You should avoid calling isDefinedAt. For me the most appropriate way is not to implement isDefinedAt for combined funcion, but PartialFunction trait forced to do this.
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.
In the above snippet, c1 and c2 confusingly do different things
This PR makes andThen consistent in that way, that's why I think we should name this method andThen and nothing else.
caching the result of the first evaluation would prevent double evaluation in most cases
it's only one additional field - I'm not personally super concerned about memory usage
- in fact it would lead to memoizing results into a
mutable.Map, which can lead to significant memory consumption and, even, to OOM - any cashing assumes that function is pure
applyOrElseis existing and documented way to callPartialFunctioneffectively, and as I said earlier, PartialFunction provides no contract of non-evaluating body while checking if it is defined at some argument. I think there are no excuses to keep current inconsistent and confusing behaviour.
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 PR makes andThen consistent in that way
I'm not understanding how this PR makes it consistent - my example is what would happen according to this PR. Unless the implementation of andThen(Function1) is changed to check for PartialFunction, the behavior depends on the static type of a function, rather than its actual type. At least to me, this is confusing and inconsistent.
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.
what would happen according to this PR
That's wrong, c1.applyOrElse(2, 8) currently throws an exception, but would not with new andThen implementation: https://scastie.scala-lang.org/UFazJnXSSpyxAgCp8dDRPQ
The only aim of the PR was to make this thing consisten.
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.
While caching would avoid executing the side effect twice, we're still in a weird situation where isDefinedAt of a combined PF executes parts of its side-effect. There's no way around this, isDefinedAt is part of the interface and needs to be implemented.
Caching the value can also be surprising because a call to apply (after isDefinedAt has been called somewhere) has no side-effect.
We have to pick one or the other evil, so it's a documentation issue. I think we should go ahead with the current design. The documentation should say that the resulting PF may apply the first PF and execute its side effect. (We could do an early return by checking pf.isDefinedAt first, and avoid evaluating pf in this case).
Then I think this PR is good to go.
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.
@lrytz added notes on behavior of Combined.
Reconsidered if this is the best course of action, based on subsequent discussion
0c5b228 to
e0702c3
Compare
e0702c3 to
5050915
Compare
lrytz
left a comment
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, @danslapman, also for not giving up in 3 months :-)
|
I'm still not happy with how the behaviour depends on the statically known type of the argument to Welcome to Scala 2.13.0-20181218-090557-f3c0b0f (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_191).
Type in expressions for evaluation. Or try :help.
scala> val f: PartialFunction[Int, Int] = { case x if x % 2 == 0 => x + 2 }
f: PartialFunction[Int,Int] = <function1>
scala> val g: PartialFunction[Int, Int] = { case x if x % 2 == 1 => x - 2 }
g: PartialFunction[Int,Int] = <function1>
scala> val c1 = f andThen g
c1: PartialFunction[Int,Int] = <function1>
scala> val c2 = f andThen (g: Int => Int)
c2: PartialFunction[Int,Int] = <function1>
scala> c1.applyOrElse(2, (_: Int) => 8)
res0: Int = 8
scala> c2.applyOrElse(2, (_: Int) => 8)
scala.MatchError: 4 (of class java.lang.Integer)
at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:308)
at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:306)
at $anonfun$1.applyOrElse(<console>:1)
at $anonfun$1.applyOrElse(<console>:1)
at scala.runtime.AbstractPartialFunction$mcII$sp.apply$mcII$sp(AbstractPartialFunction.scala:38)
at scala.runtime.AbstractPartialFunction$mcII$sp.apply(AbstractPartialFunction.scala:38)
at scala.runtime.AbstractPartialFunction$mcII$sp.apply(AbstractPartialFunction.scala:30)
at scala.PartialFunction$AndThen.applyOrElse(PartialFunction.scala:227)
... 29 elided |
needed in recent 2.13 nightlies because of the combination of scala/scala#7660 and scala/scala#7263
|
reviewers here might like to weigh in on scala/bug#12426 |
|
pls note that this breaks |
Combineduses thefallbackOccurredtrick to combine two partial funtions with domain of pf1 narrowed with domain of pf2