Capture Side Effects in a Task

Brian Lonsdorf
InstructorBrian Lonsdorf
Share this video with your friends

Social Share Links

Send Tweet
Published 7 years ago
Updated 5 years ago

We examine the data structure Task, see some constructors, familiar methods, and finally how it captures side effects through laziness.

[00:00] Let's have a look at task. Now, we're going to use this data.task on NPM here from Folktale, but you can use whatever implementation you like. It might slightly vary. Here we are. We have a task, and this has an of method.

[00:10] We can throw anything in there. I could say, "Hello," or the number two, or true. Let's stick with just the number one to be simple here. It acts just like a box. We have a task with the number one in it there. Now, to actually witness this number one, we have to call a function called fork.

[00:24] This is takes an error case -- I'll console.log(error), the error, e -- and the actual success case, too -- console.log(success) with the x here. Now let's run this.

[00:40] I have success of one. If I wanted to hit the error case, I can make a rejected task with the rejected method here. We run that, and we have an error of one.

[00:50] Now, something to interest to note is if we map over this, just like the other containery types here, we could add one to x. On the rejected case, it ignores it altogether. It just returns error one, just like I left on the either type. Now, if we have of, this is a successful task, and it will actually add one here in the map.

[01:06] We could also chain over it. If we were to return another task, we call chain if we're returning a task within the task here. This will give us the success of three. Again, if we return the rejected version, it will just ignore both the map and the chain, and short circuit, and go right down to the error.

[01:22] This, at this point, is not that interesting, but there's a different constructor. This will allow us to capture side effects.

[01:28] Now, let's go ahead and make ourselves a function, called launchMissiles. That will take nothing, and simply it's going to just console.log(launchMissiles()). Now, the interesting this is, this will just run immediately.

[01:42] Now imagine, we're just faking this with the console.log, but if we're actually launching the missiles, we want to be able to capture this in a task so that it will be lazy, and we can compose with it. Let's use the task constructor here that takes a rejection function and a result function.

[01:55] These will correspond to the two functions that I've given to fork. What we can do here is go ahead and launch the missiles, and then call a result with, let's just say, missile. That will be our return value for this function.

[02:09] Here we have launchMissiles. That will be passed into this map. This is the result here. We don't want to add one to it anymore. How about an exclamation point here? Then we can run this, and it will, indeed, launch the missiles. It gives us a success here with our result.

[02:26] Now, if we don't fork it all here, it will just not run. That is very good, because if our application was just this, then the caller of our application has to fork it. They're in charge of all of the side effects, and the problems that come with that.

[02:40] Also, they can perhaps extend the computation before it runs. Let's add a second exclamation point here. Now, when they fork it here, it will have two exclamation points. That's very good, because it means that, if this was some library code, or somewhere else, that they can keep extending things, and composing along, and our whole application remains pure.

Cristian Gabbanini
Cristian Gabbanini
~ 6 years ago

Hi Brian (or Prof. Frisby depending on who answers), I'd like to ask you a question. I'm using a Task to capture a js library that I'm using to drag and drop elements, and I would like to chain my event handlers as per the following snippet (which of course doesn't work):

const dragNDrop = lib =>
    Task.of( lib )
    .map( initLib=> initLib() )
    .map( l => l.on("drag", dragEventHandler) )
    .map( l => l.on("drop", dropEventHandler) )
    .map( l => l.on("cancel", cancelDragEventHandler) )

The map method, as per its definition, works only the first and the second time it is called and, from the third time on, it stops working as it receives undefined as its argument (calling lib.on("event", handler) does return undefined). This behavior makes me question my choice: is Task the right data type to use in these situations? Maybe is mapping the wrong choice?

Before I go, I'd like to thank you for this whole course: it has been really usefull (even If I think I'll need to watch it a few more times to sink in more concepts). Thank you!

Cristian Gabbanini
Cristian Gabbanini
~ 6 years ago
Jan Hesters
Jan Hesters
~ 5 years ago

The API for folktale has changed drastically. Here is the code for the updated API:

const { of, rejected, task } = require("folktale/concurrency/task");

of(1)
  .run()
  .listen({
    onRejected: e => console.log("err", e),
    onResolved: x => console.log("success", x)
  });

rejected(1)
  .map(x => x + 1)
  .run()
  .listen({
    onRejected: e => console.log("err", e),
    onResolved: x => console.log("success", x)
  });

of(1)
  .map(x => x + 1)
  .chain(x => of(x + 1))
  .run()
  .listen({
    onRejected: e => console.log("err", e),
    onResolved: x => console.log("success", x)
  });

const launchMissiles = () =>
  task(resolver => {
    console.log("launch missiles!");
    resolver.resolve("missile");
    // resolver.cleanup(() => {
    //   //
    // });
  });

const app = launchMissiles().map(x => x + "!");

app
  .map(x => x + "!")
  .run()
  .listen({
    onRejected: e => console.log("err", e),
    onResolved: x => console.log("success", x)
  });
Rajesh
Rajesh
~ 4 years ago

Hi Brain,

I was converting your todo app with fp-ts library(https://gcanti.github.io/fp-ts/). Below are some code whcih i want to clear.

const saveInStorage = (todos: Todo[]) => { return task .of(todos) .map(todos => JSON.stringify(todos)) .chain(todos => { return new Task(() => Promise.resolve(sessionStorage.setItem("Todos", todos)) ); }) .map(() => todos); };

export const saveTodo = (todoName: string, todos: Todo[]) => { return fromNullable("")(todoName) .map(name => new Todo(uuid4(), name)) .map(todo => todos.concat(todo)) .map(saveInStorage); }; when calling saveTodo(...) without .map(saveInStorage); then saveTodo return type is Either<string, Todo[]> but calling with .map(saveInStorage); then saveTodo return type is Either<string, Task<Todo[]>>

Please help me how my funtion saveTodo always return Either<string, Todo[]>.

Markdown supported.
Become a member to join the discussionEnroll Today