-
Notifications
You must be signed in to change notification settings - Fork 705
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 Kleisli arrow with operation fusion for imported (impure) functions #1857
Conversation
try f(a) | ||
catch { | ||
case t: Throwable if (catcher.isDefinedAt(t)) => | ||
throw new KleisliIOError(catcher(t)) |
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 can be
try f(a) catch catcher.andThen(new KleisliIOError(_))
to avoid executing catcher
twice.
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 doesn't type check, and I'm pretty sure even if it did, andThen
would do the same thing behind the scenes that is being done here.
(for { | ||
array <- Task.eval(createTestArray) | ||
_ <- arrayFill(array)(0) | ||
} yield ()).toIO.unsafeRunSync() |
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.
Calling toIO
on Task
doesn't seem fair. You could use runSyncUnsafe
instead.
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.
Fixed, although it has zero impact on performance because it's dominated by the individual operations themselves.
* In both of these examples, the `KleisliIO` program is faster because it is | ||
* able to perform fusion of effectful functions. | ||
*/ | ||
sealed trait KleisliIO[E, A, B] extends (A => IO[E, B]) { self => |
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.
Extending functions makes me worried
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.
Why?
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 like containing a value of this type is just as useful. What's the benefit of extension here?
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 the (tiny) benefit is that you can use it wherever A => IO[E, B]
is required.
Why prefer (run: A => IO[E, 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.
At the very least, subtyping can interact with partial unification in surprising ways. Consider:
val foo: KleisliIO[Foo, Bar, Baz] = ...
def bar[F[_], G[_], A](x: F[G[A]]) = x
val baz = foo.map(f)
val qux = bar(foo).map(f)
Quiz: Are baz
and qux
the same?
Answer: It depends on the definition of Baz
!
Revised Quiz: What are the semantics of baz
and qux
for each of the following definitions of Baz
?:
trait Baz
type Baz = Qux[Int]
Now seemingly innocuous changes like refactoring trait Baz
to type Baz = Qux[Int]
can break code, and with sufficient polymorphism can even cause silent semantic changes rather than type errors.
There may be other problems as well, but I'd have to think about it, and I'd rather just write .run
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.
@aaronvargo Sounds good. I'll submit a PR. 👍
What's the benchmarks look like? |
@puffnfresh It's an average of 2x as fast as the fastest monadic approaches across the example benchmarks: https://www.slideshare.net/jdegoes/blazing-fast-pure-effects-without-monads-lambdaconf-2018 |
This needs to have review before it goes in, @jdegoes. |
@edmundnoble It had 2 reviews and two thumbs up. Or do you mean Github review? |
@jdegoes Github approval, specifically. |
Based on the work of John Hughes in Programming with Arrows and similar in spirit to TraneIO's Arrows, this pull request introduces an arrow abstraction for importing and composing effectful code in a purely-functional way.
In this work, Kleisli arrows are specialized to
IO
. While the behavior and interface ofKleisliIO
is identical toKleisli
on theIO
type, a separate constructor allows impure functions to be imported asA => B
, without anyIO
overhead. This constructor, in turn, allows fusion of core arrow operations when both left and right side are impure functions. The result is a dramatic reduction in overhead for performance-critical sections of code composed entirely with impure functions.Although designed and optimized for fusion of impure functions,
KleisliIO
works seamlessly with pure functions lifted into the structure. Sections that can be optimized are optimized, while still allowing incorporation of the greater flexibility and expressiveness thatIO
provides.Current major limitations include:
Because of the support for
IO
, these limitations do not actually hinder expressibility. Anything that cannot currently be done inKleisliIO
directly can be done inIO
and lifted intoKleisliIO
.While almost universally faster for every class of problem it can be used to solve, there is definitely overhead associated with arrow-based programming—mainly all the tupling/eithering that occurs as information and conditions are propagated forward. Future work should be able to eliminate this overhead and show greater performance increases on very complex code written in the arrow style.
This pull request does not include a test suite but @wi101, who assisted with implementing and optimizing this code, has developed a test suite and will contribute it separately.