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

Effect Monad Instance #559

Open
reem opened this issue Sep 27, 2016 · 15 comments
Open

Effect Monad Instance #559

reem opened this issue Sep 27, 2016 · 15 comments

Comments

@reem
Copy link

reem commented Sep 27, 2016

After a long discussion with @WuTheFWasThat about what this should look like, I think I have something coherent to share:

We would like to be able to think about sagas using an Effect monad. Let's start by giving some example types:

put(action) :: Effect<undefined>
take(pattern) :: Effect<Action>
call(function, args) :: Effect<FunctionReturnValue>
fork(function, args) :: Effect<Task<FunctionReturnValue>>

given these we can try to write a Monad instance for Effect:

// A -> Effect<A>
function pure(value) {
    return call(x => x, value);
}

// Effect<A>, (A -> Effect<B>) -> Effect<B>
function bind(effect, transform) {
    // We use call(funtion* () {}) just as a form of do-notation
    return call(function* () { 
        const result = yield effect;
        return yield transform(result);
    });
}

so close! These functions appear to work, and have the correct type signatures, but their behavior is wrong in the presence of fork. One of the monad laws is that bind(instance, pure) === instance. It's important to follow these laws so that the behavior of monad combinators is clear and consistent.

First lets write fork in a way more friendly to thinking about things as just pure Effect creators:

// Effect<A> -> Effect<Task<A>>
function forkEffect(effect) {
   return fork(function* () { yield effect; });
}

Now lets try to apply our combinators to it:

const runSomeEffect = forkEffect(someEffect);
const shouldBeTheSame = bind(runSomeEffect, pure);

We now expect that runSomeEffect and shouldBeTheSame are the same, but they aren't! runSomeEffect has the expected behavior of running someEffect in the background, but shouldBeTheSame will run it in the foreground (or at least wait for it to complete).

One way to solve this issue is to introduce a new effect, run, which is like call except that it runs the given saga in the context of the current saga. Normally we use yield* to accomplish this, but you can't return yield* something as a pure value the same way you can return call(something) as a pure value. With run, we'd be able to return run(something) as a pure value.

Then we can write our Monad instance like so:

// A -> Effect<A>
function pure(value) {
    return run(x => x, value);
}

// Effect<A>, (A -> Effect<B>) -> Effect<B>
function bind(effect, transform) {
    // We could have used yield* but then `bind` would have to return a generator not an effect
    // run lets us get the right behavior *and* the right type
    return run(function* () { 
        const result = yield effect;
        return yield transform(result);
    });
}

and when we try to use them like before, we now get the correct behavior.

In conclusion I think the addition of run would allow more useful saga combinators to be written clearly and consistently by allowing us to write a correct monad instance for Effect.

@reem
Copy link
Author

reem commented Sep 27, 2016

Another case where run would be useful:

Let's say we have some function like this:

export function* doBackground(saga, config) {
   const handle = yield fork(backgroundTask(saga, config));
   return handle;
}

right now there is no convenient way for me to "call" this saga and get its result. If I call it to get the return value it never terminates because the fork never terminates, if I yield* it then I lose the return value.

I can manually create and iterate over the generator to collect the return value, but this is a pretty bad solution to the problem.

EDIT: Upon further research I discovered that yield* does give you the return value, so this case is just the same as the other one where you want run so you can do the equivalent of return yield* something as a pure effect.

@reem
Copy link
Author

reem commented Oct 16, 2016

Any thoughts here? Really all this issue is asking for is the run effect. Does anyone have thoughts on how difficult this would be to implement? I would be happy to tackle it myself if someone gave me some pointers on where to start and what parts of the code are most relevant.

@Andarist
Copy link
Member

Andarist commented Oct 16, 2016

Every effect is handled by runEffect function -

function runEffect(effect, parentEffectId, label = '', cb) {
. Just look what is returned. You would need to implement runRunEffect in similar way that other 'runners' are implemented.

Also you would have need to implement run effect creator here -

export function call(fn, ...args) {
which effectively just wraps your yielded effect in an object so it can be interpreted later by runEffect in proc.js.

Although I dont quite grasp this - "run, which is like call except that it runs the given saga in the context of the current saga.". Could you write a full example of usage of this run effect? What is exactly returned in case of run and in case of call?

@reem
Copy link
Author

reem commented Oct 16, 2016

run does the same thing as call except that if the saga it runs forks, the forks are attached to the saga that yielded the run effect, not the sub-saga.

Example:

function forkingSaga() {
     yield fork(function* () { while (true) { ... });
}

function calling() {
      yield call(forkingSaga);
      // never get here
}

function running() {
     yield run(forkingSaga); // "equivalent" to yield *forkingSaga(); except pure
     // we do get here
}

It looks like it might not be so easy given that runCallEffect just calls resolveIterator which is just proc, which appears to be the main "loop". I'd have to change proc to be able to "inherit" the forking context and call that from runRunEffect.

@Andarist
Copy link
Member

Ok, I guess I see the difference with to which saga forks are attached, however I have no idea whats the purpose, beside monad wizardry ;) not saying that its worthless, just dont see a personal attraction to it right now.

function calling() {
      yield call(forkingSaga);
      // never get here
}

Still aint sure though what did u mean with 'never get here', forkingSaga will terminate after yielding fork effect and calling will get resumed.

@reem
Copy link
Author

reem commented Oct 16, 2016

calling will never be resumed because a call effect only ends when all the sagas forks end.

@Andarist
Copy link
Member

Andarist commented Oct 16, 2016

Cant find that behavior nor in the docs nor in the source code, im on
mobile though.

Such behavior seems to me at least weird. Did u check if it rly behaves
like that? Do u think it should?

@reem
Copy link
Author

reem commented Oct 17, 2016

It looks like it is covered here https://redux-saga.github.io/redux-saga/docs/advanced/ForkModel.html.

@Andarist
Copy link
Member

Thanks for pointing that out, had no idea this works like that, fortunately never had urge to call forking task :)

In that case your run effect is indeed a little bit more complicated, but not impossible I guess.

@Andarist
Copy link
Member

Reopening this as I would like to spend some more thought on it and explore the idea, which truth to be told is not so easy for me to grasp as thinking about monads is really a brainburner when one is not accustomed to it.

However your example of:

export function* doBackground(saga, config) {
   const handle = yield fork(backgroundTask(saga, config));
   return handle;
}

Seems like a legitimate use case. One should be able to call it somehow in non-blocking manner and get the handle in the caller saga. It is possible now by forking instead of calling + join on the resulting task, but it will be joined only after doBackground finishes here so only after backgroundTask finishes, so effectively - pointless.

Maybe some call.nonblocking / call.spread (better naming should be used of course :) ) could be introduced. Would that somehow solve your issue? Or only a part of it? Wild idea - maybe call should behave like call.nonblocking and call.block should be introduced to behave like the call now?

@yelouafi what were your thoughts about this? you probably get a lot more about this as you are more familiar with functional concepts

Your requested run which would act like yield* would be possible to achieve, just wondering if there is other use case for that than a monad wizardry.

@Andarist Andarist reopened this Jan 14, 2017
@reem
Copy link
Author

reem commented Jan 23, 2017

So originally I phrased this all in Monad language because I thought it would be helpful to have some context to refer to, but it seems to have caused more confusion than clarity.

This issue is mostly a feature request for run/call.nonblocking/whatever that implements the same behavior as yield * and runs the given saga transparently (without it adding any additional effects). run is a necessary counterpart to fork and call. Without it you can't write any sagas that fork without joining, which is a helpful thing to do.

The "Monad wizardry" is mostly just recognizing that using yield in the way we are here is almost the same as Haskell's do notation which is used for desugaring Monads. Sagas (call/fork/join/take) are just another Monad (or they would be if we had run :P).

@Andarist
Copy link
Member

So, I would like to implement this one way or another, but aint sure when yet. Still I won't merge it in until @yelouafi comes back and have time to comment on the matter.

Dont take me wrong - 'monad wizardry' isnt something bad in mi eyes and I think that it was certainly helpful from your side to include this reasoning etc. Its just me who is not too familiar with these concepts (yet :P)

Sagas (call/fork/join/take) are just another Monad (or they would be if we had run :P).

why they would be then and are not now?

@reem
Copy link
Author

reem commented Jan 25, 2017

A Monad is just a type that fulfills a few criteria, mainly that there exists a set of functions pure and bind which operate on the type and whose implementation fulfills some monad laws, in particular:

  • bind(pure(x), f) == f(x)
  • bind(instance, pure) == instance
  • bind(instance, compose(f, g)) == bind(bind(instance, g), f)

If you refer back to my original post you will see my attempts at providing implementations of bind and pure above. pure is easy but bind requires run so it doesn't introduce additional effects (which is necessary to fulfill for all three laws).

For more info (in haskell though): https://en.wikipedia.org/wiki/Monad_(functional_programming)#Monad_laws

@ericelliott
Copy link

ericelliott commented Mar 22, 2017

In JavaScript, a lot of FP types are implemented according to the Fantasyland spec, which essentially just gives us standard names for common operations and provides chainable APIs.

Conforming to Fantasyland would allow for interoperability with a whole bunch of JS libraries.

See the Fantasyland Monad spec.

@Andarist
Copy link
Member

@ericelliott Yeah, I know about the spec and ofc would like to follow it if possible, but for now there are more pressing issues in the library than this and also this is a little over my head at the moment. Would need to study a little bit more about monads to understand it and implement properly.

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

No branches or pull requests

4 participants