IO as First In Composition Chain

114 views Asked by At

I am interested to experiment with Haskell-like IO monads in my JavaScript function compositions.

Something like Folktale has Task seems similar to Haskell's IO in that it's lazy, and thus technically pure. It represents an action that can occur in the future. But I have several questions.

How does one form a composition of functions when all the latter functions depend on the return value of the initial impure function in the composition? One has to run the actual Task first, implicitly passing the returned data to the functions further down the line. One can't just pass an unresolved Task around to do anything useful, or can one? It would look something like.

compose(doSomethingWithData, getDataFromServer.run());

I'm probably missing something critical, but what's the advantage of that?

A related question is what specific advantage does lazy evaluation of an impure function have? Sure, it provides referential transparency, but the core of understanding the problem is the data structure that's returned by the impure function. All the latter functions that are piped the data depend on the data. So how does the referential transparency of impure functions benefit us?

EDIT: So after looking at some answers, I was able to easily compose tasks by chaining, but I prefer the ergonomics of using a compose function. This works, but am wondering if it's at all idiomatic for functional programmers:

const getNames = () =>
  task(res =>
    setTimeout(() => {
      return res.resolve([{ last: "cohen" }, { last: "kustanowitz" }]);
    }, 1500)
);

const addName = tsk => {
  return tsk.chain(names =>
    task(resolver => {
      const nms = [...names];
      nms.push({ last: "bar" });
      resolver.resolve(nms);
    })
  );
};
const f = compose(
  addName,
  getNames
);

const data = await f()
  .run()
  .promise();
// [ { last: 'cohen' }, { last: 'kustanowitz' }, { last: 'bar' } ]

Then, another question, perhaps more related to style, is now we have to have composed functions that all deal with tasks, which seems less elegant and less general than those that deal with arrays/objects.

2

There are 2 answers

11
AudioBubble On

How can we express Haskell's IO type in Javascript? Actually we can't, because in Haskell IO is a very special type deeply intertwined with the runtime. The only property we can mimic in Javascript is lazily evaluation with explicit thunks:

const Defer = thunk => ({
  get runDefer() {return thunk()}
}));

Usually lazy evaluation is accompanied by sharing but for the sake of convenience I leave this detail out.

Now how would you compose such a type? Well, you need to compose it in the context of thunks. The only way to compose thunks lazily is to nest them instead of calling them right away. As a result you can't use function composition, which merely provides the functorial instance of functions. You need the applicative (ap/of) and monad (chain) instances of Defer to chain or rather nest them.

A fundamental trait of applicatives and monads is that you can't escape their context, i.e. once a result of your computation is inside an applicative/monad you can't just unwrap it again.* All subsequent computations have to take place within the respective context. As I already have mentioned with Defer the context are thunks.

So ultimately when you compose thunks with ap/chain you build a nested, deferred function call tree, which is only evaluated when you call runDefer of the outer thunk.

This means that your thunk composition or chaining remains pure until the first runDefer invocation. This is a pretty useful property we all should aspire to.


*you can escape a monad in Javascript, of course, but than it isn't a monad anymore and you lose all the predictable behavior.

0
Aadit M Shah On

How does one form a composition of functions when all the latter functions depend on the return value of the initial impure function in the composition?

The chain method is used to compose monads. Consider the following bare bones Task example.

// Task :: ((a -> Unit) -> Unit) -> Task a
const Task = runTask => ({
    constructor: Task, runTask,
    chain: next => Task(callback => runTask(value => next(value).runTask(callback)))
});

// sleep :: Int -> Task Int
const sleep = ms => Task(callback => {
    setTimeout(start => {
        callback(Date.now() - start);
    }, ms, Date.now());
});

// main :: Task Int
const main = sleep(5000).chain(delay => {
    console.log("%d seconds later....", delay / 1000);
    return sleep(5000);
});

// main is only executed when we call runTask
main.runTask(delay => {
    console.log("%d more seconds later....", delay / 1000);
});

One has to run the actual Task first, implicitly passing the returned data to the functions further down the line.

Correct. However, the execution of the task can be deferred.

One can't just pass an unresolved Task around to do anything useful, or can one?

As I demonstrated above, you can indeed compose tasks which haven't started yet using the chain method.

A related question is what specific advantage does lazy evaluation of an impure function have?

That's a really broad question. Perhaps the following SO question might interest you.

What's so bad about Lazy I/O?

So how does the referential transparency of impure functions benefit us?

To quote Wikipedia[1].

The importance of referential transparency is that it allows the programmer and the compiler to reason about program behavior as a rewrite system. This can help in proving correctness, simplifying an algorithm, assisting in modifying code without breaking it, or optimizing code by means of memoization, common subexpression elimination, lazy evaluation, or parallelization.