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

Good example of using IO? #18

Closed MalQaniss closed 2 years ago

MalQaniss commented 2 years ago

I used IO monad in combination with Ramda, it all works as expected but I just wanna get your opinion should have I used something differently to Improve my using of IO in this situation:

const sendHttp = url => IO.of(fetch(url))
const checkResponse = response => response.ok ? IO.of(response) : IO(() => {throw "Response Error"})
const getJson = response => IO.of(response.json())
const checkPopulation = data => data[0].population > 50_000_000 ? IO.of(data[0]) : IO(() => {throw 'Not Enough Population';})
const getCapital = data => data.capital;

const setBodyText = text => IO(() => document.body.textContent = text)

const fetchCapital = pipe(
    IO.of,
    chain(sendHttp),
    chain(checkResponse),
    chain(getJson),
    chain(checkPopulation),
    map(getCapital),
)

const text = await
    fetchCapital('https://restcountries.com/v3.1/name/germany').run()
        .catch(err => console.log("ERROR!!!"));

setBodyText(text).run()
getify commented 2 years ago

Some thoughts (I'll probably string these out in several comments)...

const sendHttp = url => IO.of(fetch(url))

This feels right, but it actually isn't. The reason is, IO.of(..) is not deferring the fetch(..)... the fetch(..) is being performed, and the promise for its result is being put inside an IO instance. The whole point of IO is for side-effect operations to be lazy/deferred. Rule of thumb: don't use IO.of(..) to encapsulate a value that comes from a side effect operation, unless you are sure you're already in the context of an IO.

That line should be:

const sendHttp = url => IO(() => fetch(url))

That small difference means that when sendHttp(..) is called, with a url, what comes back is an IO that will perform fetch(..) when told to do so, but where fetch(..) has not happened yet.

getify commented 2 years ago
const checkResponse = response => response.ok ? IO.of(response) : IO(() => {throw "Response Error"})

That line is OK, but I'd prefer doing it this way:

const checkResponse = response => IO(() => {
   if (!response.ok) throw "Response Error";
   return response;
});
getify commented 2 years ago
const getJson = response => IO.of(response.json())

Ditto to my first comment:

const getJson = response => IO(() => response.json())
getify commented 2 years ago
const checkPopulation = data => data[0].population > 50_000_000 ? IO.of(data[0]) : IO(() => {throw 'Not Enough Population';})

Ditto to my second comment:

const checkPopulation = data => IO.of(() => {
   if (data[0].population <= 50_000_000) throw 'Not Enough Population';
   return data[0];
});
getify commented 2 years ago
const fetchCapital = pipe(
    IO.of,
    chain(sendHttp),
    chain(checkResponse),
    chain(getJson),
    chain(checkPopulation),
    map(getCapital),
)

Monio just recently (in v0.50.0) added a .pipe(..) helper (as a sub-method on methods like chain(..) and map(..)) that cleans this up nicely:

const fetchCapital = url => (
    sendHttp(url)
    .chain.pipe(
      checkResponse,
      getJson,
      checkPopulation
   )
   .map(getCapital)
);
getify commented 2 years ago
const text = await
    fetchCapital('https://restcountries.com/v3.1/name/germany').run()
        .catch(err => console.log("ERROR!!!"));

setBodyText(text).run()

That works, but I'd do it this way:

function main*() {
   try {
      const text = yield fetchCapital('https://restcountries.com/v3.1/name/germany');
      yield setBodyText(text);
   }
   catch (err) {
      console.log("ERROR!!!",err);
   }
}

IO.do(main).run();

Side note: the console.log(..) statement is, itself, a side-effect. Since we're going to the trouble to put all our side-effects in IOs, in my opinion that should be no different.

catch (err) {
   yield IO(() => console.log("ERROR!!!!",err));
}

I know that probably feels unnecessary, but it's a question of discipline. We don't want to get lax in our habits and start cheating the way IO is supposed to work.

BTW, Monio's IOHelper module ships with a log(..) helper for this purpose, to make it a little easier to adhere. So you if you've imported log from IOHelpers:

catch (err) {
   yield log("ERROR!!!!",err);
}
getify commented 2 years ago

So... to bring it all together:

const prop = propName => obj => obj[propName];

const sendHttp = url => IO(() => fetch(url));
const checkResponse = response => IO(() => {
   if (!response.ok) throw "Response Error";
   return response;
});
const getJson = response => IO(() => response.json());
const checkPopulation = data => IO.of(() => {
   if (data[0].population <= 50_000_000) throw 'Not Enough Population';
   return data[0];
});

const fetchCapital = url => (
    sendHttp(url)
    .chain.pipe(
      checkResponse,
      getJson,
      checkPopulation
   )
   .map(prop('capital'))
);

function main*() {
   try {
      const text = yield fetchCapital('https://restcountries.com/v3.1/name/germany');
      yield setBodyText(text);
   }
   catch (err) {
      yield log("ERROR!!!",err);
   }
}

IO.do(main).run();
MalQaniss commented 2 years ago

I got a lot of insights, thank you for your help.