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 Kleisli arrow with operation fusion for imported (impure) functions #1857

Merged
merged 13 commits into from
Jun 7, 2018

Conversation

jdegoes
Copy link
Member

@jdegoes jdegoes commented Jun 5, 2018

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 of KleisliIO is identical to Kleisli on the IO type, a separate constructor allows impure functions to be imported as A => B, without any IO 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 that IO provides.

Current major limitations include:

  • Lack of support for recursive combinator
  • Lack of support for infinite / lazy composition
  • Lack of support for asynchronous
  • There are virtually no error combinators

Because of the support for IO, these limitations do not actually hinder expressibility. Anything that cannot currently be done in KleisliIO directly can be done in IO and lifted into KleisliIO.

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.

@jdegoes jdegoes added the scalaz8 label Jun 5, 2018
try f(a)
catch {
case t: Throwable if (catcher.isDefinedAt(t)) =>
throw new KleisliIOError(catcher(t))
Copy link
Member

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.

Copy link
Member Author

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()
Copy link

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.

Copy link
Member Author

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 =>
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Why?

Copy link
Member

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?

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 the (tiny) benefit is that you can use it wherever A => IO[E, B] is required.

Why prefer (run: A => IO[E, B])?

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

Copy link
Member Author

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. 👍

@puffnfresh
Copy link
Member

What's the benchmarks look like?

@jdegoes
Copy link
Member Author

jdegoes commented Jun 7, 2018

@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

@jdegoes jdegoes merged commit af74d19 into scalaz:series/8.0.x Jun 7, 2018
@jdegoes jdegoes deleted the kleisli_io branch June 7, 2018 01:46
@edmundnoble
Copy link
Contributor

This needs to have review before it goes in, @jdegoes.

@jdegoes
Copy link
Member Author

jdegoes commented Jun 7, 2018

@edmundnoble It had 2 reviews and two thumbs up. Or do you mean Github review?

@edmundnoble
Copy link
Contributor

@jdegoes Github approval, specifically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants