0

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.

Aaron
  • 3,249
  • 4
  • 35
  • 51
  • Removed the Haskell tag. Just because you are using monads doesn't make this related to Haskell. – chepner Dec 15 '19 at 13:56
  • 1
    You would use the [`chain`](https://github.com/fantasyland/fantasy-land#chain) method. For example, `getDataFromServer.chain(doSomethingWithData).run()`. I'm assuming that `doSomethingWithData` returns a `Task`. If it doesn't then you can use [`map`](https://github.com/fantasyland/fantasy-land#functor) instead of `chain`. – Aadit M Shah Dec 15 '19 at 14:10

2 Answers2

1

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.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
0

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.

  • 1
    "*A fundamental trait of applicatives and monads is that you can't escape their context*" - no, that's a trait of `IO` or, in javascript, asynchronous execution. But you can trivially unwrap list, maybe, state or defer monads. – Bergi Dec 15 '19 at 14:35
  • 1
    OK, but than it isn't an applicative/monad anymore, but specific to this type. –  Dec 15 '19 at 14:36
  • 1
    Yes, this is totally unrelated to applicative/monad, whether you can escape their context depends only on the specific data structure. – Bergi Dec 15 '19 at 14:40
  • But how would you escape from the non-deterministic context of lists? Or do you just mean taking the value out of its `Object` wrapper? –  Dec 15 '19 at 14:42
  • I think I get it. However, I often read with monads you can't freely get values out of them and with Comonads you can't freely add values to them. This confuses me. –  Dec 15 '19 at 14:45
  • 2
    I mean folding the list into something else (taking its length, concatenating its elements, etc). About every data structure offers functions to work with it that are specific to this structure (and which make it useful in the first place) - the `IO` type and asynchronous computations are the exceptions here. Sure, if you write a *generic* function and all you know about a type is that it is a monad, then you cannot get the values out of it. – Bergi Dec 15 '19 at 14:53
  • OK, there seems to be a suited elimination rule for almost all types except for `IO`/`Task`. The monad API just doesn't provide the right tool to extract results of monadic computations, because there are monad instances of data structures that doesn't allow such an operation. If monads had an `extract` function they would be less general. Enlightening! –  Dec 15 '19 at 15:04
  • @Bergi Actually, since the `IO` type in JavaScript is just a function (i.e. a `Cont`), therefore its elimination rule is just function application. Hence, the `runTask` function is the elimination rule for tasks. – Aadit M Shah Dec 15 '19 at 16:19
  • 1
    @AaditMShah `runTask` is impure though, that's why I didn't consider it. I'd treat `IO` as an opaque value that can only be executed by the runtime, ignoring things like `unsafePerformIO`. Sure, in JS we have to provide the runtime ourselves, so the separation is less clear. – Bergi Dec 15 '19 at 18:05
  • @Bergi I think the distinction between the data structure and its corresponding monad is artificial provided you agree that monads also exist on the term level. The monadic portion of [1,2,3] on the term level is []. When you eliminate this structure you may escape from the monad, but you also lose the information that was stored in the monadic portion of the data structure. 6 is not equivalent with [1,2,3]. It is the same with control monads. In order to eliminate Maybe you have to provide a default value, since you lose the monadic effect of computation that may fail. –  Jan 28 '20 at 19:14
  • ...With running a Task you lose purity, namely serial evaluation order without race conditions. Hence it is valid to state you cannot escape a monad. –  Jan 28 '20 at 19:14