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

Monad defer laws #1150

Merged
merged 17 commits into from Nov 24, 2018
Merged

Monad defer laws #1150

merged 17 commits into from Nov 24, 2018

Conversation

1Jajen1
Copy link
Member

@1Jajen1 1Jajen1 commented Nov 21, 2018

Monad defer laws portet from cats-sync-laws.

Solves #590

Excluded laws are: (by function name from cats)

  • suspendConstantIsPureJoin (what is the suspend/flatten equivalent in arrow)
  • suspendThrowIsRaiseError (^)
  • unsequencedDelayIsNoop (what is this for? is that not covered by delaySuspendsEvaluation, is it the same? it is a confusing name either way...)
  • repeatedSyncEvaluationNotMemoized (is this a thing in arrow)
  • stack safety laws (all of them) (not sure what the case is here, I had them initially, and they passed for IO and deferred, but failed for all otheres, so I left them out)

Also there seems to be something wrong with DeferredK as it fails the test for delay suspends evaluation. I believe it is because it is using an async instance of DeferredK which overrides delay to invoke DeferredK.invoke which does not start a coroutine with CoroutineStart.LAZY but with CoroutineStart.DEFAULT.

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 21, 2018

Tests will fail btw because of the one DeferredK test (or they better should :D)

@pakoito
Copy link
Member

pakoito commented Nov 21, 2018

Good job so far! The anwers to your questions:

Flatten is in Monad, which means it's available in MonadDefer: https://github.com/arrow-kt/arrow/blob/master/modules/core/arrow-typeclasses/src/main/kotlin/arrow/typeclasses/Monad.kt#L22

Defer constant is a new function you can add in MonadDefer:
fun <A> defer(Kind<F, A>): Kind<F, A>

RaiseError is in ApplicativeError, of which MonadDefer inherits too: https://github.com/arrow-kt/arrow/blob/master/modules/core/arrow-typeclasses/src/main/kotlin/arrow/typeclasses/ApplicativeError.kt#L8

unsequencedDelayIsNoop

Yeah, it's likely a duplicated, but keep it just in case :D

repeatedSyncEvaluationNotMemoized

*> is a shorter version of flatMap, just use flatMap instead

stack safety laws

If the instances or the implementations are broken, we need to check the cause 🔍

DeferredK.invoke which does not start a coroutine with CoroutineStart.LAZY but with CoroutineStart.DEFAULT.

Seems like a bug to fix to to me caught by the tests then hahaha

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 21, 2018

So is a function deferConstant really needed? What would it look like? Simply returning its argument or would it invoke defer { arg }. I don't really get the use so I don't really know how to implement it 😕

unsequencedDelayIsNoop

Yeah, it's likely a duplicated, but keep it just in case :D

Well they don't have a delay/defer suspends eval law so that might it. So add it regardless?

DeferredK.invoke which does not start a coroutine with CoroutineStart.LAZY but with
CoroutineStart.DEFAULT.

Seems like a bug to fix to to me caught by the tests then hahaha

Well other instances (IO) override delay as well, to delegate it to invoke which in turn does exactly the same as delay in MonadDefer except it does not catch errors.
So how is delay in async supposed to work? Or does it need to be overridden at all?

And for async and invoke I am guessing it should just start lazy instead of default.

I am just asking all of these questions because there might have been a reason to implement it like that 🙈

@pakoito
Copy link
Member

pakoito commented Nov 21, 2018

would it invoke defer { arg }

Yeah, that's it, ez :D

So add it regardless?

yeah, go for it

And for async and invoke I am guessing it should just start lazy instead of default.

Yep!

Well other instances (IO) override delay as well, to delegate it to invoke which in turn does exactly the same as delay in MonadDefer except it does not catch errors. So how is delay in async supposed to work? Or does it need to be overridden at all?

So if I understand correctly, the cause of the test failing is that one of invoke/suspend/defer is not catching errors in the block when it's supposed to. Easy fix, try/catch the implementations in IO.kt then :D

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 21, 2018

So if I understand correctly, the cause of the test failing is that one of invoke/suspend/defer is not catching errors in the block when it's supposed to. Easy fix, try/catch the implementations in IO.kt then :D

Not quite, the cause of the test failing is that DeferredK's async instance overrides delay which calls invoke which starts default instead of lazy. I was just bringing this up to understand why delay was overriden there in the first place

@pakoito
Copy link
Member

pakoito commented Nov 21, 2018

Yes, it seems incorrect. Just change it to the original implementation in the datatype then.

Monad defer already provides a default for it. And if async override differs in any way this would produce unexpected results
@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 21, 2018

So I added repeatedSyncEvaluationNotMemoized not sure how useful it is tho. I skipped unsequencedDelayIsNoop because it ends up exactly like delaySuspendsEvaluation.

Regarding stack safety. Are all MonadDefer instances supposed to be stack safe?

@codecov
Copy link

codecov bot commented Nov 21, 2018

Codecov Report

Merging #1150 into master will increase coverage by 0.28%.
The diff coverage is 94.02%.

Impacted file tree graph

@@             Coverage Diff             @@
##             master   #1150      +/-   ##
===========================================
+ Coverage     42.62%   42.9%   +0.28%     
- Complexity      820     836      +16     
===========================================
  Files           404     404              
  Lines         11355   11417      +62     
  Branches       1272    1273       +1     
===========================================
+ Hits           4840    4899      +59     
- Misses         6125    6129       +4     
+ Partials        390     389       -1
Impacted Files Coverage Δ Complexity Δ
...outines/src/main/kotlin/arrow/effects/DeferredK.kt 57.35% <ø> (ø) 6 <0> (ø) ⬇️
...rc/main/kotlin/arrow/effects/DeferredKInstances.kt 69.56% <ø> (-1.27%) 0 <0> (ø)
...nces/src/main/kotlin/arrow/effects/instances/io.kt 50% <ø> (-1.17%) 0 <0> (ø)
...ain/kotlin/arrow/effects/typeclasses/MonadDefer.kt 76.47% <0%> (-4.78%) 0 <0> (ø)
...-test/src/main/kotlin/arrow/test/laws/AsyncLaws.kt 90.62% <100%> (ø) 8 <1> (ø) ⬇️
.../src/main/kotlin/arrow/test/laws/MonadDeferLaws.kt 67.72% <95.38%> (+18.24%) 29 <15> (+16) ⬆️
.../arrow-effects/src/main/kotlin/arrow/effects/IO.kt 82.5% <0%> (+1.25%) 16% <0%> (ø) ⬇️

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 e5c30a5...5dc5bf5. Read the comment docs.

@raulraja
Copy link
Member

raulraja commented Nov 21, 2018

Are all MonadDefer instances supposed to be stack safe?

yep.

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 21, 2018

Then there is a problem with reactor and rx2 instances of MonadDefer because they all fail the tests (I'll push them in a sec) for stacksafety while IO and DeferredK work just fine

Edit: Some more info. They each fail with a stackoverflow error (which gets swallowed by the eq instance) and since a stackOverflow error does not equal just(number) the test fails...

@raulraja
Copy link
Member

@1Jajen1 any way to see where the stack is recursing?

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 21, 2018

@raulraja For reactor (MonoK) the stacktrace points to methods from reactor.

This happens when ~5000 flatMaps get evaluated

StackTrace excerpt:
Exception in thread "main" java.lang.StackOverflowError at reactor.core.publisher.Operators$MonoSubscriber.actual(Operators.java:1103) at reactor.core.publisher.InnerOperator.currentContext(InnerOperator.java:32) at reactor.core.publisher.InnerOperator.currentContext(InnerOperator.java:32) at reactor.core.publisher.InnerOperator.currentContext(InnerOperator.java:32) at reactor.core.publisher.InnerOperator.currentContext(InnerOperator.java:32) at reactor.core.publisher.InnerOperator.currentContext(InnerOperator.java:32) at reactor.core.publisher.InnerOperator.currentContext(InnerOperator.java:32) at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:123) at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1083)

The last two lines get cycled over and over again.

FluxK:
Exception in thread "main" java.lang.StackOverflowError at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:331) at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:331) at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:331) at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:331)

And this as well cycles on and on...

I think rx2 will be similar. I don't really know what to do with this, or if something from inside arrow can/should be done.

@raulraja
Copy link
Member

I'm not familiar with those data types. Maybe @pakoito @Cotel can help, but it's bad. If they don't pass the laws we are gonna have to disable the integrations.

val sideEffect = SideEffect()
val df = delay { sideEffect.increment(); sideEffect.counter }

df.flatMap { df }.flatMap { df }.equalUnderTheLaw(just(3), EQ)
Copy link
Member

Choose a reason for hiding this comment

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

shouldBe true, else this is not checked

Copy link
Member Author

Choose a reason for hiding this comment

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

Can't believe I forgot that 😅. Added it and it works for io, reactor and rx2 but fails for DeferredK.

Copy link
Member

Choose a reason for hiding this comment

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

weep

Copy link
Member

Choose a reason for hiding this comment

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

No but seriously, why?

Copy link
Member Author

Choose a reason for hiding this comment

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

That is a good question and might be an issue or feature of coroutines. Will test this more thoroughly later on.
A possible scenario would be that coroutines simply after finishing return their result instead of rerunning when awaited again

Copy link
Member Author

@1Jajen1 1Jajen1 Nov 22, 2018

Choose a reason for hiding this comment

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

@pakoito
Well it makes sense now... Thats just how coroutines work:

  var sideEffect = 0
  val a = GlobalScope.async(start = CoroutineStart.LAZY) {
    ++sideEffect
  }

  println("Started a: ${a.start()}")

  runBlocking {
    println("Result a: ${a.await()}")
  }

  val b = GlobalScope.async(start = CoroutineStart.LAZY) {
    a.await()
  }

  println("Started b: ${b.start()}")

  runBlocking {
    println("Result b: ${b.await()}")
  }

  println(sideEffect)

Which prints:

Started a: true
Result a: 1
Started b: true
Result b: 1
1

So what should be done about this? This is not like the stack-safety issue, if someone were to try and build a retry/repeat scheduler that won't work with DeferredK. Also other code which reuses MonadDefer instances (and then gets a deferredK) simply won't work. And the more annoying problem here is that it would be very hard to track down to Coroutines in that case :/

I don't really know how to solve this, wrapping deferred in DeferredK into a function has other challenges (stack-safety etc) that might not work... Maybe someone at jb knows what to do here...

Copy link
Member

Choose a reason for hiding this comment

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

Right, I forgot they are memoized -.- This cannot be solved by JBs as it'd require moving the stack labels, which is the same issue we have with monad comprehensions, and it's just a design choice instead.

cc @raulraja for ideas

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok so how about I create a ticket for this. Comment the test out for now with a fixme linked to the ticket, add a copy to io's tests (also with a comment and link) and let that ticket deal with it then.

Copy link
Member

@pakoito pakoito Nov 22, 2018

Choose a reason for hiding this comment

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

let's wait for opinion, this one is somewhat different and may be solved with design changes by wrapping in functions

@pakoito
Copy link
Member

pakoito commented Nov 21, 2018

I've opened a ticket on the Project Reactor and Rx2 repos, let's wait for a reply.

@raulraja
Copy link
Member

@pakoito can you link them here?

@pakoito
Copy link
Member

pakoito commented Nov 22, 2018

It seems like it isn't happening. After talking to Raul, it's better to remove the tests rather than the instances. Maybe leave them commented out with a /* FIXME design limitation in RxJava2 https://github.com/ReactiveX/RxJava/issues/6322 */

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 22, 2018

Welp yeah will do. Should that be added to the docs somewhere? (better yet create a ticket for it since that is not really the scope of this pr...)
I mean for most it'll hopefully be a non-issue anyway...

@pakoito
Copy link
Member

pakoito commented Nov 22, 2018

Nah, leave it as the commented out tests with that FIXME. We have a couple of those in the codebase already.

As there'll be no fix on the Rx/Reactor department, not much we can do except wait for the first complaint then point at IO, Deferred or arrow-streams.

@pakoito
Copy link
Member

pakoito commented Nov 22, 2018

Not mandatory for this ticket, but if you could add the FIXME to the tailrecM implementations of Observable, Flowable, Mono, and Flux here we'd save ourselves a new PR :D

https://github.com/arrow-kt/arrow/blob/master/modules/effects/arrow-effects-rx2/src/main/kotlin/arrow/effects/ObservableK.kt#L113

https://github.com/arrow-kt/arrow/blob/master/modules/effects/arrow-effects-reactor/src/main/kotlin/arrow/effects/MonoK.kt#L90

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 22, 2018

Instead of commenting them out can't we use two lists of laws? Then at least io and deferred are well tested and any future changes that might break will be caught

@pakoito
Copy link
Member

pakoito commented Nov 22, 2018

In those cases what we normally do is add the tests directly over their files, i.e. IOTest, DeferredTest, etc. even if they're almost literal duplicates. I'm not sure if this is the best approach, but I'm now sure we want to have MonadDeferLaws split in two files/functions :/

@raulraja for second opinion

@raulraja
Copy link
Member

I think the docs on each one of the wrappers that are not stack-safe should state that, so users are aware of the reasons why it's not. Including proper links to the issues that @pakoito created in their repos. We can also display examples how we can make them stack-safe with bindingStackSafe examples that show how we have overcome the limitation with a classical example with over 5k iterations.

@raulraja
Copy link
Member

I'd rather have a way to tell the monad-suspend laws to skip the problematic tests so we can override in those data types and potentially emit a warning that they have been skipped because of [links here]

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 22, 2018

So something like a filter/map function that checks laws by name and either removes them or replaces them with an always true law that contains the warning message?
Or by adding a boolean switch to MonadDeferLaws.laws but that would have to be added to async as well (which might be fine if it gets a default value).

Should I do the docs right here or is that better suited in another issue/pr? I am not too familiar with bindingStackSafe tbh so the latter might make more sense.

Also regarding Deferred memoizing its result instead of rerunning: Any ideas? Or should that discussion be moved to a new ticket and the law filtered out similar to stack safe for rx2 and reactor?

@raulraja
Copy link
Member

@1Jajen1 you can split in PRs as you wish it's more manageable. The filtering in the laws can as simple as a Boolean that asks to skip stack-safety related laws and emits a warn for the given data type so that is known wart

bindingStackSafe is the same as binding but it automatically lifts binds into a Free monad which enables suspension and stack safety for any data type even those that are stack unsafe.

I was arguing with Paco that we can use Free or bindingStackSafe inside the instances implementations to build ourselves that stack safety where Rx and Reactor fall short.

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 22, 2018

Added an option to skip and warnings with the link to the tickets. Only test that fails now is the one that ensures MonadDefer instances don't memoize things for DeferredK. Don't really know what to do with it yet... (Might make sense to skip it as well for now and add a ticket/pr trying to fix it, because that may be some work...)

Using bindingStackSafe would only be stacksafe inside that binding right? I mean it'd be fine if well documented that without it its not supported, but a general stack safe integration would be better if it does not mean lots of added complexety or overhead.

I'll do docs later on or tomorrow morning. If its not too much I'll keep it in this pr.

@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 23, 2018

Added some documentation about rx2 and reactor not being stacksafe, with links to both tickets.
Last thing to do (hopefully 🙈) in this pr is dealing with DeferredK memoizing results. :/

@@ -181,6 +181,49 @@ disposable()
// Boom! caused by BindingCancellationException
```

### Stack safety
Copy link
Member

Choose a reason for hiding this comment

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

<3

@@ -148,6 +148,49 @@ disposable()
// Boom! caused by BindingCancellationException
```

### Stack safety
Copy link
Member

Choose a reason for hiding this comment

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

<3

@pakoito
Copy link
Member

pakoito commented Nov 23, 2018

is dealing with DeferredK memoizing results. :/

I'll take a look, I had some "shower" ideas last morning

@pakoito
Copy link
Member

pakoito commented Nov 23, 2018

We'll need to refactor Deferred for that test to pass, I've fixed it bit needs some cleanup. I say disable it for now, merge this PR and we'll make a ticket to work on deferred ASAP.

If no one creates a ticket the next few minutes I might not have to change the id :)
@1Jajen1
Copy link
Member Author

1Jajen1 commented Nov 23, 2018

Commented out and linket to #1156 (which will be the next ticket created if no one does anything the next few minutes 🙈)

I'll create the ticket and link back to this as well.

@@ -53,10 +55,15 @@ class FluxKTest : UnitSpec() {
}
}

override fun interceptSpec(context: Spec, spec: () -> Unit) {
Copy link
Member

Choose a reason for hiding this comment

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

Neat!

Copy link
Member

@pakoito pakoito left a comment

Choose a reason for hiding this comment

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

Awesome job here, really 😃 Worth getting a promotion to Contributor, congrats ^^

@pakoito pakoito merged commit 09d5f6f into arrow-kt:master Nov 24, 2018
@1Jajen1 1Jajen1 mentioned this pull request Nov 24, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants