getify / monio

The most powerful IO monad implementation in JS, possibly in any language!
http://monio.run
MIT License
1.05k stars 58 forks source link

AsyncEither example #16

Closed Djordjenp closed 2 years ago

Djordjenp commented 2 years ago

Hello Kyle,

I am having trouble understanding and using AsyncEither, can you give some kind of example of usage?

getify commented 2 years ago

The best way to think about AsyncEither is that it's like a Future monad, which is sorta like a Promise. With a Future/Promise, you have an asynchronous operation that, when it completes, will have one of two final states: success or failure.

The Either monad models this sort of synchronous outcome, so AsyncEither extends all the same concepts to asynchronicity.

As a simple example:

function request(url) {
   return AsyncEither( fetch(url) )
      .chain(resp => (
         !resp.ok ?
            AsyncEither.Left("Request failed.") :
            AsyncEither.Right(resp.json())
      ));
}

// later
var records = await (
   request("/some-api")
   .map(data => data.records)
   .fold(
      (err) => { logError(err); return []; },
      v => v
   )
);

Like promises (and Maybe and Either monads), the subsequent chain(..) and map(..) calls get skipped if the AsyncEither becomes an AsyncEither:Left (which will just bubble forward the held exception message). Any promise rejection or uncaught JS exception will end up lifted to an AsyncEither:Left. The final fold(..) call extracts either the Left exception or the Right final success value. The whole chain operates as a promise and resolves with the final result, hence the single await call.


I've included AsyncEither because I think a monads library, for complete'ness sake if nothing else, needs a Future monad.

But in truth, I think all I/O (like a fetch(..)) should be modeled in IO instances, and that goes for pretty much any form of asynchrony (timers, animations, etc). Everything AsyncEither can do, you can do with Monio's IO, including even treating Either:Left values as catchable exceptions in IO.doEither(..) routines. So I personally wouldn't use AsyncEither, and would instead just use IO (and Either) together.

But you can think of AsyncEither as a lighter-weight approach if you didn't want or care to use IO. Or you can think of IO as a superset of AsyncEither -- essentially IO is sorta a Task monad based on the Future (aka AsyncEither) wired into it internally. You can just use normal JS promises with IO and it opaquely transforms over them, without you needing to mess around with Future / AsyncEither wrappings yourself.

Hope that's helpful!

Djordjenp commented 2 years ago

Ty for you answer Kyle, I understand it now, I will definitely look at IO monad next. Btw I tried your example with AsyncEither:

function request(url) {
   return AsyncEither(
       fetch(url).then(resp => {
          if (!resp.ok) throw "Request failed.";
          return resp;
       })
   )
       .chain(toJSON);
}

function toJSON(resp) {
   return AsyncEither( resp.json() );
}

// later
const records = await (
    request("https://swapi.dev/api/planets")
        .map(data => data.count)
        .fold(
            (err) => { console.log(err); return []; },
            v => v
        )
);

console.log(records)

But it seems to report some internal error:

Uncaught ReferenceError: identity is not defined
  at async-either.mjs:5:1348
    at Object.fold2 [as fold] (either.mjs:5:724)
    at _doChain (async-either.mjs:5:1265)
    at handle (async-either.mjs:5:1432)
getify commented 2 years ago

For posterity, request(..) above could alternately have been implemented like this:

function request(url) {
   return AsyncEither( fetch(url) )
      .map(resp => {
         if ( !resp.ok ) throw "Request failed.";
         else return resp.json();
      });
}

This style is a little more familiar/ergonomic to JS devs, but it relies on conveniences that AsyncEither provides, which is that it's automatically lifting thrown exceptions into AsyncEither:Left. I think the former version is a little more monadic-canonical.

getify commented 2 years ago

But it seems to report some internal error:

Are you using Monio off npm, or from github? I'm not sure, but... I think this may be a bug that I've fixed in the code that's on github, but hasn't yet been released to npm. That release is coming shortly I believe.

Djordjenp commented 2 years ago

Yeah I am using it of npm probably still isn't fixed, I thought I was doing something wrong. Ty for your help

Djordjenp commented 2 years ago

Hey sorry to bother u again Kyle,

I used "IO.do" syntax and there I could use try/catch to catch an exception, but I don't know how i can catch exception when I am using more "functional" syntax. This is the example I tried:

const sendRequest = url => IO.of(fetch(url).catch(err => {
    console.log("ERROR: " + err); 
    return err}));
const getJson = response => IO.of(response.json());

sendRequest('https://swapi.dev/api/WrongUrl')
    .chain(getJson)
    .map(x => console.log('SUCCESS: ' + x))
    .run();
getify commented 2 years ago

Try this:

const sendRequest = url => IO.of(fetch(url));
const getJson = response => IO.of(response.json());

sendRequest('https://swapi.dev/api/WrongUrl')
    .chain(getJson)
    .map(x => console.log('SUCCESS: ' + x))
    .run()
    // we get a promise back here, so we can call `catch(..)` on it
    .catch(err => console.log("ERROR:",err));
Djordjenp commented 2 years ago

Yeah that works, And one final question, how can we throw an Error in IO chaining, for instance if "response.ok" is false.

getify commented 2 years ago

Just use throw inside any IO method (map(..), chain(..), etc), and that will bubble out.