Skip to content

Instantly share code, notes, and snippets.

@dtipson
Last active August 13, 2024 18:34
Show Gist options
  • Select an option

  • Save dtipson/01fba81f3ba19daa6da5bd809629990e to your computer and use it in GitHub Desktop.

Select an option

Save dtipson/01fba81f3ba19daa6da5bd809629990e to your computer and use it in GitHub Desktop.

Revisions

  1. dtipson revised this gist Jun 30, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion simple.Task.js
    Original file line number Diff line number Diff line change
    @@ -91,7 +91,7 @@ taskOfSix.fork(e => console.log(e), x => console.log(x))//synchronously logs 6
    // smartly flatten things out so that you just end up with another Task containing another eventual value.

    // If none of that made sense, well what I mean is just this sort of pattern, which we do with Promises all the time:
    fetch('/api1').then( result => fetch(`/api2?value=${result}`));
    // fetch('/api1').then( result => fetch(`/api2?value=${result}`));

    // The result of that operation isn't a Promise containing a Promise, it's instead just a Promise with the result
    // from the second api call. Promises auto-flatten, guessing that if the result of calling the function given to
  2. dtipson revised this gist Jun 30, 2019. 1 changed file with 113 additions and 47 deletions.
    160 changes: 113 additions & 47 deletions simple.Task.js
    Original file line number Diff line number Diff line change
    @@ -1,62 +1,118 @@
    // Finally wrapped your head around Promises? Time to toss out all that knowledge and learn the functional alternative!

    // This is a super simple implementation of a Task "type" (a real one would use a type-creation lib like daggy)
    // Here's a super simple implementation of a Task "type"
    const __Task = fork => ({fork})

    // soooo simple! At this point "fork" is just a cute name though:
    // what matters is how we use it: what makes Task a "Task" is that is that "fork" will be a higher-order function...
    // Absurdly simple! All we're doing is using a function that returns some unknown value, by name, in an object.
    // At this point "fork" is just a cute name though: what matters is how we use it.
    // What makes Task a "Task" is that is that the "fork" value here... will be a higher-order function.

    // Here's a usage example as just simple "continuation" where fork is
    // a function that takes, as its argument, a function
    // Here's a usage example, already fully functional, already nearly as powerful as Promise!
    const delayedFive = __Task(
    resolve => setTimeout(resolve, 500, 5)
    )
    delayedFive.fork(x => console.log(x));//returns cancelation id, logs 5
    delayedFive.fork(x => console.log(x));//returns cancelation id, but logs 5 after 500ms

    // Future-y/Task-y types generally handle an side-effect which succeed OR fail, so...
    // here's a more standardizable usage with an error function that's listed first (the standard FP way)
    // That's basically equivalent to:
    const eventuallyFive = new Promise(resolve => setTimeout(resolve, 500, 5)).then(x => console.log(x));

    // But here's a critical difference: that line above, all by itself, will log 5 after a delay
    // That's NOT the case with delayedFive: we had to separately run the .fork function to make it do anything.

    // Before we go further, let's also acknowledge that Future-y/Task-y/Promise-y types are generally built to
    // handle a side-effect which can succeed OR fail.
    // So here's a more complete usage of Task, wherein the fork function takes TWO arguments, first a function to
    // handle errors, the second to handle a success. In this example, we're creating an effect that,
    // When forked, with either log normally OR log an error, randomly, after the delay:
    const delayedBadFive = __Task(
    (reject, resolve) => Math.random() < 0.5 ? setTimeout(reject, 500, 5) : setTimeout(resolve, 500, 5)
    )
    delayedBadFive.fork(e=>console.log(`delayed error: ${e}`), x=>console.log(`delayed success: ${x}`));

    // The core of what makes this different from Promises is that it's pure & lazy
    // Brass tacks: that means we're defining the logic of the operation separately from
    // Again, creating delayedBadFive doesn't DO anything itself. It just stores an operation to be run later.
    // And so: the core of what makes Tasks different from Promises is that they're both pure & lazy.
    // What that means we're defining the logic of the operation separately from
    // running the impure effect (which Promises muddle, running the impure Promise constructor function immediately)
    // It's worth highlighting that this also separates out the result of running the constructor function
    // from the effect. With Promise you get back a value, and there's no distinction between creating
    // a Promise and running it. With Task you can create it at any point, and then when you run it,
    // you get back whatever value or function the constructor returns
    // That makes exposing interfaces over the effect (like cancelation of a http request) much more straightforward,
    // as the constructor's effect is exposed directly and synchronously at the call site.

    // now let's make a more usable version that's also a Functor
    // With Task you can create it at any point, save it for later, and then when you run it,
    // you'll get back whatever synchronous value or function the constructor returns and then LATER trigger behavior
    // based on some asynchronous effect.

    // This temporal bifrucation makes exposing control interfaces for the effect (like the cancelation of a http request)
    // much more straightforward, as the constructor's return value is exposed directly and synchronously
    // right where you called it. The final side-effect is separate and subsequent, happening in the future.
    // (In fact, Tasks are sometimes called "Futures," though some reserve those terms for different sorts of things).

    // Of course, if you've used Promises a lot, you're probably wondering what the equivalent to ".then" is. Well,
    // there are two answers to that question. One is that you've already seen it, in a sense. The functions you pass to
    // .fork ARE the equalivents to .then(success, error), just with the argument order reversed
    // (error handling function first, then a success handler).

    // But of course, the utility of Promises is in part that they can chain multiple .then operations, one after the other.
    // A Promise is in some sense a lot like an Array containing a single value, but smeared out over time. That is to say
    // you can Promise.resolve(5).then(fn1).then(f2).then(f3) in the same way you can do [5].map(fn1).map(fn2).map(fn3)
    // Are Tasks capable of this? As it happens, they are. They are because this ability to take some type and modify its
    // "inner" value in some way via a function is deeply connected. Promise.resolve(5).then(x=>x+1) is, in fact,
    // exactly the same sort of operation as [5].map(x=>x+1)! You're taking a value inside a type and transforming it into the
    // same type, but with a different value. Any type that can do this is, with certain restrictions, a Functor. In fact,
    // you could just define Promise.prototype.map as an alias of Promise.prototype.then and use it instead of .then
    // any time you have a callback that simply returns a new value.

    // So let's make a more usable version of Task that's also a Functor (which is to say, give it .map capability)
    const _Task = fork => ({
    fork,
    map: fn => _Task(
    (reject, resolve) => fork(reject, x=> resolve(fn(x)) )
    )
    });

    // Functors only work on the success route. If the original fork function rejects, .map here just passes
    // that along without modification. The success route allows you access to the eventual value for a
    // further calculation.
    // Task.map here takes a single argument function that, when called on a Task, returns a new Task.
    // But it does it in a particularly trisky way that exploits the original Task's fork function.
    // This .map operation on a Task quietly hooks into the original Task it's based on, hijacks its fork function,
    // and hooks the addition transformation function into the final result.

    // Functors of course, only work on the success route. If the original fork function rejects, then .map here just
    // returns that rejection along without any modification. The success route allows you access to the eventual value for
    // a further calculation. With the reject route, there's no point, since you got an error instead of a value, so
    // there's no point in continuing the logical chain: you just fast forward to whatever you set up to handle errors.

    // Before using .map, let's also give Tasks a simple way to lift a simple value into the minimal context...
    // Now, before using .map, let's also give Tasks a simple way to lift a simple value into the minimal context...
    _Task.of = x => _Task( (a, b) => b(x) );

    // usage (similar to doing Promise.resolve(4))
    // This method, .of, is basically similar to doing Promise.resolve(4)
    // It just gives you a cheap and easy way to put a value inside the type, just like Array.of does for Arrays.
    // For now, that mostly just makes it a lot easier to see how Task.map works, in action
    const taskOfSix = _Task.of(5).map( x => x+1 );// returns a forkable Task
    taskOfSix.fork(e => console.log(e), x => console.log(x))//synchronously logs 6

    // From here, it might start to be clear how to make a version that's a Monad.
    // If you don't know what a Monad is, no worries: for our purposes it's a method of Task that allows you
    // to return another Task, like requesting an api response, and then calling a second api using data from
    // the first.
    // We made Task into a Functor. From here, it might start to be clear how to make a version that's a Monad.
    // If you don't actually know what a Monad is, no worries: for our purposes it's a method of Task that allows you
    // to supply a function that will return another Task, like a request for a second api response,
    // calling that second api using data from the first. But instead of ending up with a Task within a Task, Monads
    // smartly flatten things out so that you just end up with another Task containing another eventual value.

    // If none of that made sense, well what I mean is just this sort of pattern, which we do with Promises all the time:
    fetch('/api1').then( result => fetch(`/api2?value=${result}`));

    // The result of that operation isn't a Promise containing a Promise, it's instead just a Promise with the result
    // from the second api call. Promises auto-flatten, guessing that if the result of calling the function given to
    // .then is another Promise, the new Promise you get back should just wait and return THAT new Promise's result.

    // How would this be constructed? Here's a hint: .fork is going to get called twice, similar to how
    // the implementation of .map uses fork once to "continue" the Task logic that passes along a value that
    // doesn't yet exist.
    // This is yet another thing that Promises muddle: by auto-detecting the result of the function and behaving
    // differently depending, they confuse .map with, well, .flatMap (which really should be called .mapFlat, but whatever).
    // Arrays don't behave this way. [5].map(x=>[x]) results in [[5]], not [5]. And Promises not behaving like Arrays do
    // makes them, well, weird. It prevents you from being able to ignore the difference between an Array and Promise
    // and have to invent special logic for each case.

    // Tasks won't do that. They'll have separate methods that match the behavior of Arrays, which on a deeper level means
    // that you can write a whole host of functions that are useful irregardless of whether they're using Tasks or Arrays.
    // In javascript, it seems like Arrays will soon get a native .flatMap method. If so, Tasks can as well. For the time being
    // though, we'll call this method .chain, since that's the convention in FP Javascript world. But .chain and .flatMap, if
    // defined properly, are just different names for the same thing: Task can just alias .chain to .flatMap or vice-versa.

    // So anyhow, how would this Monadic .chain method be constructed for a Task? Here's a hint: .fork is going to get
    // called twice. Why? Well, the implementation of .map uses fork once to "continue" the Task logic that passes along
    // a value that doesn't yet exist. Calling fork "unwraps" the inner value so we can transform it: so in the case of
    // transforming a nested Task, we'll have to "unwrap" things twice.

    const Task = fork => ({
    fork,
    @@ -72,9 +128,9 @@ const Task = fork => ({
    }
    )
    });
    Task.of = x => Task( (a, b) => b(x) );// adding this into the latest example version
    Task.of = x => Task( (a, b) => b(x) );// just adding this .of method into the latest example version

    // let's walk through how this works by building up some functional logic
    // Now let's walk through how this works by building up some functional logic

    const usernameTask = Task.of('Myusername');//just an example place to start with a value
    const fakeApi1 = username => Task((e, s) => setTimeout(s, 500, `${username}:4%2hSfds&5@h`));
    @@ -85,36 +141,46 @@ const getUser = usernameTask
    .chain(fakeApi1)
    .chain(fakeApi2);

    // nice: now we have a pure description of an operation composed of tiny bits, each of which is
    // testable on its own. Note that errors in these functions do NOT become rejections, they will just throw errors
    // That's a feature, not a bug. There's a critical difference between an error of logic/handling type signatures
    // safely, and errors thrown from side effects.
    // Task forces us to handle all errors AND those two types OF errors, differently (and appropriately, imo)

    // now let's use it
    // Nice: now we have a pure description of an operation that's composed of tiny bits, each of which is
    // testable on its own. Note that any errors in these functions directly do NOT become rejections,
    // they will JUST throw errors, breaking the program at the outer level instead of handling it internal to the
    // resolve/reject interface.
    // And that's a feature, not a bug. There's a critical difference between an error of logic/handling functional
    // type signatures safely and correctly, and errors thrown from side effects.
    // (That is, from an api failing vs. you writing a program that tries to .toUpperCase an Integer)
    // Task forces us to handle those two types of errors differently, one accounting for failures in side-effects, the
    // other accounting for testable, fixable errors in program logic. But we're getting too high level maybe.

    // Let's just let's use it and see what happens:
    getUser.fork( e => console.error(e), x => console.log(x) );// logs "Myusername:4%2hSfds&5@h, logged in"

    // Note that this operation returns a cancelation id which we could use to cancel the request before it completes
    // Let's note another cool thing going on here. getUser is actually a complete chain of functional logic all stored in a
    // single value. But we can always take that and extend it further, and assign THAT extension to a variable, and so on.
    // We can call either Task's fork any time we want OR we can modify that logic further and assign the modification
    // to a new value, and keep both chains of logic around for different purposes.

    // Also, let's still note that this entire operation, when forked, returns a cancelation id
    // which we could then use to cancel the requests before they ever even complete
    const cancelID = getUser.fork( e => console.error(e), x => console.log(x) ); clearTimeout(cancelID);

    // Of course, you might see a problem with the cancelation logic here: there are two cancelation IDs being
    // created in this process, and we're only getting one at the end, so depending on WHEN you call the cancelation
    // You might see a problem with the cancelation logic here: there are two cancelation IDs being
    // created in this whole process, and we're only getting one at the end, so depending on WHEN you call the cancelation
    // it might not be the right/currently active one. There's a way around this (and, surprise, it requires turning
    // the returned cancelation operation into a function) but it's just worth noting. The same problem exists, just
    // the returned cancelation operation into a function) but it's just worth noting for now. The same problem exists, just
    // in a worse and more obscure way, with Promises and their overly elaborate cancelationToken process

    // For now though, we've basically covered basically everything that Promises do in a (hopefully) pretty easy
    // to understand functional alternative way. It's worth noting that an actual implementation would define Task
    // itself as a function with a prototype. Here's just a taste of that:

    const Task = function(fork){
    const pTask = function(fork){
    if (!(this instanceof Task)) {
    return new Task(fork);
    }
    this.fork = fork
    };
    Task.of = Task.prototype.of = x => new Task((a, b) => b(x), `=> ${x}`);
    Task.prototype.map = function _map(f) {
    pTask.of = Task.prototype.of = x => new Task((a, b) => b(x), `=> ${x}`);
    pTask.prototype.map = function _map(f) {
    return new Task(
    (left, right) => this.fork(
    a => left(a),
    @@ -123,7 +189,7 @@ Task.prototype.map = function _map(f) {
    );
    };

    Task.prototype.chain = function _chain(f) {
    pTask.prototype.chain = function _chain(f) {
    return new Task(
    (left, right) => {
    let cancel;
  3. dtipson revised this gist Jun 29, 2019. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions simple.Task.js
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    // Finally wrapped your head around Promises? Time to toss out all that knowledge and learn the functional alternative!

    // This is a super simple implementation of a Task "type" (a real one would use a type-creation lib like daggy)
    const __Task = fork => ({fork})

  4. dtipson revised this gist Jun 27, 2019. 1 changed file with 43 additions and 3 deletions.
    46 changes: 43 additions & 3 deletions simple.Task.js
    Original file line number Diff line number Diff line change
    @@ -22,8 +22,11 @@ delayedBadFive.fork(e=>console.log(`delayed error: ${e}`), x=>console.log(`delay
    // Brass tacks: that means we're defining the logic of the operation separately from
    // running the impure effect (which Promises muddle, running the impure Promise constructor function immediately)
    // It's worth highlighting that this also separates out the result of running the constructor function
    // from the effect. With Promise you get back a value. With Task, you get back whatever the constructor returns
    // That makes exposing interfaces over the effect (like cancelation of a http request) straightforward.
    // from the effect. With Promise you get back a value, and there's no distinction between creating
    // a Promise and running it. With Task you can create it at any point, and then when you run it,
    // you get back whatever value or function the constructor returns
    // That makes exposing interfaces over the effect (like cancelation of a http request) much more straightforward,
    // as the constructor's effect is exposed directly and synchronously at the call site.

    // now let's make a more usable version that's also a Functor
    const _Task = fork => ({
    @@ -94,4 +97,41 @@ const cancelID = getUser.fork( e => console.error(e), x => console.log(x) ); cle

    // Of course, you might see a problem with the cancelation logic here: there are two cancelation IDs being
    // created in this process, and we're only getting one at the end, so depending on WHEN you call the cancelation
    // it might not be the right one
    // it might not be the right/currently active one. There's a way around this (and, surprise, it requires turning
    // the returned cancelation operation into a function) but it's just worth noting. The same problem exists, just
    // in a worse and more obscure way, with Promises and their overly elaborate cancelationToken process

    // For now though, we've basically covered basically everything that Promises do in a (hopefully) pretty easy
    // to understand functional alternative way. It's worth noting that an actual implementation would define Task
    // itself as a function with a prototype. Here's just a taste of that:

    const Task = function(fork){
    if (!(this instanceof Task)) {
    return new Task(fork);
    }
    this.fork = fork
    };
    Task.of = Task.prototype.of = x => new Task((a, b) => b(x), `=> ${x}`);
    Task.prototype.map = function _map(f) {
    return new Task(
    (left, right) => this.fork(
    a => left(a),
    b => right(f(b))
    )
    );
    };

    Task.prototype.chain = function _chain(f) {
    return new Task(
    (left, right) => {
    let cancel;
    let outerFork = this.fork(
    a => left(a),
    b => {
    cancel = f(b).fork(left, right);
    }
    );
    return cancel ? cancel : (cancel = outerFork, x => cancel());
    }
    );
    };
  5. dtipson revised this gist Jun 26, 2019. 1 changed file with 12 additions and 4 deletions.
    16 changes: 12 additions & 4 deletions simple.Task.js
    Original file line number Diff line number Diff line change
    @@ -72,8 +72,8 @@ Task.of = x => Task( (a, b) => b(x) );// adding this into the latest example ver
    // let's walk through how this works by building up some functional logic

    const usernameTask = Task.of('Myusername');//just an example place to start with a value
    const fakeApi1 = username => Task((e, s) => setTimeout(s, 500, `token for ${username}`));
    const fakeApi2 = tokenstring => Task((e, s) => setTimeout(s, 500, `${tokenstring}, is logged in`));
    const fakeApi1 = username => Task((e, s) => setTimeout(s, 500, `${username}:4%2hSfds&5@h`));
    const fakeApi2 = tokenstring => Task((e, s) => setTimeout(s, 500, `${tokenstring}, logged in`));

    const getUser = usernameTask
    .map(str => String(str).toLowerCase())
    @@ -83,7 +83,15 @@ const getUser = usernameTask
    // nice: now we have a pure description of an operation composed of tiny bits, each of which is
    // testable on its own. Note that errors in these functions do NOT become rejections, they will just throw errors
    // That's a feature, not a bug. There's a critical difference between an error of logic/handling type signatures
    // safely, and errors thrown from side effects. Task forces us to handle all errors and those two types OF errors, differently
    // safely, and errors thrown from side effects.
    // Task forces us to handle all errors AND those two types OF errors, differently (and appropriately, imo)

    // now let's use it
    getUser.fork( e => console.error(e), x => console.log(x) );//
    getUser.fork( e => console.error(e), x => console.log(x) );// logs "Myusername:4%2hSfds&5@h, logged in"

    // Note that this operation returns a cancelation id which we could use to cancel the request before it completes
    const cancelID = getUser.fork( e => console.error(e), x => console.log(x) ); clearTimeout(cancelID);

    // Of course, you might see a problem with the cancelation logic here: there are two cancelation IDs being
    // created in this process, and we're only getting one at the end, so depending on WHEN you call the cancelation
    // it might not be the right one
  6. dtipson revised this gist Jun 26, 2019. 1 changed file with 27 additions and 8 deletions.
    35 changes: 27 additions & 8 deletions simple.Task.js
    Original file line number Diff line number Diff line change
    @@ -1,19 +1,19 @@
    // This is a super simple implementation of a Task "type" (a real one would use a type-creation lib like daggy)
    const _Task = fork => ({fork})
    const __Task = fork => ({fork})

    // soooo simple! At this point "fork" is just a cute name though:
    // what matters is how we use it: what makes Task a "Task" is that is that "fork" will be a higher-order function...

    // Here's a usage example as just simple "continuation" where fork is
    // a function that takes, as its argument, a function
    const delayedFive = _Task(
    const delayedFive = __Task(
    resolve => setTimeout(resolve, 500, 5)
    )
    delayedFive.fork(x => console.log(x));//returns cancelation id, logs 5

    // Future-y/Task-y types generally handle an side-effect which succeed OR fail, so...
    // here's a more standardizable usage with an error function that's listed first (the standard FP way)
    const delayedBadFive = _Task(
    const delayedBadFive = __Task(
    (reject, resolve) => Math.random() < 0.5 ? setTimeout(reject, 500, 5) : setTimeout(resolve, 500, 5)
    )
    delayedBadFive.fork(e=>console.log(`delayed error: ${e}`), x=>console.log(`delayed success: ${x}`));
    @@ -26,9 +26,9 @@ delayedBadFive.fork(e=>console.log(`delayed error: ${e}`), x=>console.log(`delay
    // That makes exposing interfaces over the effect (like cancelation of a http request) straightforward.

    // now let's make a more usable version that's also a Functor
    const Task = fork => ({
    const _Task = fork => ({
    fork,
    map: fn => Task(
    map: fn => _Task(
    (reject, resolve) => fork(reject, x=> resolve(fn(x)) )
    )
    });
    @@ -38,10 +38,10 @@ const Task = fork => ({
    // further calculation.

    // Before using .map, let's also give Tasks a simple way to lift a simple value into the minimal context...
    Task.of = x => Task( (a, b) => b(x) );
    _Task.of = x => _Task( (a, b) => b(x) );

    // usage (similar to doing Promise.resolve(4))
    const taskOfSix = Task.of(5).map( x => x+1 );// returns a forkable Task
    const taskOfSix = _Task.of(5).map( x => x+1 );// returns a forkable Task
    taskOfSix.fork(e => console.log(e), x => console.log(x))//synchronously logs 6

    // From here, it might start to be clear how to make a version that's a Monad.
    @@ -52,7 +52,6 @@ taskOfSix.fork(e => console.log(e), x => console.log(x))//synchronously logs 6
    // How would this be constructed? Here's a hint: .fork is going to get called twice, similar to how
    // the implementation of .map uses fork once to "continue" the Task logic that passes along a value that
    // doesn't yet exist.
    // ( we also know that cancelation will get a bit trickier, though never as tricky it is w/ Promises! )

    const Task = fork => ({
    fork,
    @@ -68,3 +67,23 @@ const Task = fork => ({
    }
    )
    });
    Task.of = x => Task( (a, b) => b(x) );// adding this into the latest example version

    // let's walk through how this works by building up some functional logic

    const usernameTask = Task.of('Myusername');//just an example place to start with a value
    const fakeApi1 = username => Task((e, s) => setTimeout(s, 500, `token for ${username}`));
    const fakeApi2 = tokenstring => Task((e, s) => setTimeout(s, 500, `${tokenstring}, is logged in`));

    const getUser = usernameTask
    .map(str => String(str).toLowerCase())
    .chain(fakeApi1)
    .chain(fakeApi2);

    // nice: now we have a pure description of an operation composed of tiny bits, each of which is
    // testable on its own. Note that errors in these functions do NOT become rejections, they will just throw errors
    // That's a feature, not a bug. There's a critical difference between an error of logic/handling type signatures
    // safely, and errors thrown from side effects. Task forces us to handle all errors and those two types OF errors, differently

    // now let's use it
    getUser.fork( e => console.error(e), x => console.log(x) );//
  7. dtipson revised this gist Jun 26, 2019. 1 changed file with 53 additions and 20 deletions.
    73 changes: 53 additions & 20 deletions simple.Task.js
    Original file line number Diff line number Diff line change
    @@ -1,37 +1,70 @@
    // super simple implementation of a Task "type" (a real one would use something like daggy)
    Task = fork => ({fork})
    // This is a super simple implementation of a Task "type" (a real one would use a type-creation lib like daggy)
    const _Task = fork => ({fork})

    // soooo simple! At this point "fork" is just a cute name though
    // what matters is how we use it: what makes it a task is that "fork"
    // will be a function...
    // soooo simple! At this point "fork" is just a cute name though:
    // what matters is how we use it: what makes Task a "Task" is that is that "fork" will be a higher-order function...

    // usage as a simple "continuation" where fork is a function
    const delayedFive = Task(
    // Here's a usage example as just simple "continuation" where fork is
    // a function that takes, as its argument, a function
    const delayedFive = _Task(
    resolve => setTimeout(resolve, 500, 5)
    )
    delayedFive.fork(x => console.log(x));//returns cancelation id, logs 5

    // Future-y/Task-y types have two branches though, so...
    // here's a more standardizable usage with an error branch (the standard way)
    const delayedBadFive = Task(
    (reject, resolve) => setTimeout(reject, 500, 5)
    // Future-y/Task-y types generally handle an side-effect which succeed OR fail, so...
    // here's a more standardizable usage with an error function that's listed first (the standard FP way)
    const delayedBadFive = _Task(
    (reject, resolve) => Math.random() < 0.5 ? setTimeout(reject, 500, 5) : setTimeout(resolve, 500, 5)
    )
    delayedBadFive.fork(e=>console.log(e), x=>console.log(x));//returns cancelation id, errors 5
    delayedBadFive.fork(e=>console.log(`delayed error: ${e}`), x=>console.log(`delayed success: ${x}`));

    // now let's make a version that's also a Functor
    Task = fork => ({
    // The core of what makes this different from Promises is that it's pure & lazy
    // Brass tacks: that means we're defining the logic of the operation separately from
    // running the impure effect (which Promises muddle, running the impure Promise constructor function immediately)
    // It's worth highlighting that this also separates out the result of running the constructor function
    // from the effect. With Promise you get back a value. With Task, you get back whatever the constructor returns
    // That makes exposing interfaces over the effect (like cancelation of a http request) straightforward.

    // now let's make a more usable version that's also a Functor
    const Task = fork => ({
    fork,
    map: fn => Task(
    (reject, resolve) => fork(reject, x=> resolve(fn(x)) )
    )
    });

    // and give it a way to lift a simple value into the minimal context...
    Task.of = x => Task( (a,b) => b(x) )
    // Functors only work on the success route. If the original fork function rejects, .map here just passes
    // that along without modification. The success route allows you access to the eventual value for a
    // further calculation.

    // Before using .map, let's also give Tasks a simple way to lift a simple value into the minimal context...
    Task.of = x => Task( (a, b) => b(x) );

    // usage (similar to doing Promise.resolve(4))
    const taskOfSix = Task.of(5).map( x => x+1 );// returns a forkable Task
    taskOfSix.fork(e => console.log(e), x => console.log(x))//synchronously logs 6

    // usage
    Task.of(5).map( x => x+1 ).fork(e => console.log(e), x => console.log(x))//synchronously logs 6
    // From here, it might start to be clear how to make a version that's a Monad.
    // If you don't know what a Monad is, no worries: for our purposes it's a method of Task that allows you
    // to return another Task, like requesting an api response, and then calling a second api using data from
    // the first.

    // and from here, it should be clear how to make a version that's a Monad
    // ( hint: we know that .fork is going to get called twice )
    // How would this be constructed? Here's a hint: .fork is going to get called twice, similar to how
    // the implementation of .map uses fork once to "continue" the Task logic that passes along a value that
    // doesn't yet exist.
    // ( we also know that cancelation will get a bit trickier, though never as tricky it is w/ Promises! )

    const Task = fork => ({
    fork,
    map: fn => Task(
    (reject, resolve) => fork(reject, x=> resolve(fn(x)) )
    ),
    chain: fn => Task(
    (reject, resolve) => {
    return fork(
    a => reject(a),
    b => fn(b).fork(reject, resolve)
    )
    }
    )
    });
  8. dtipson revised this gist Jan 26, 2017. 2 changed files with 37 additions and 5 deletions.
    5 changes: 0 additions & 5 deletions Task.js
    Original file line number Diff line number Diff line change
    @@ -1,5 +0,0 @@
    //simple Task
    const Task = fork => ({fork})

    //continuation
    Task(res=>setTimeout(res,500,5))
    37 changes: 37 additions & 0 deletions simple.Task.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,37 @@
    // super simple implementation of a Task "type" (a real one would use something like daggy)
    Task = fork => ({fork})

    // soooo simple! At this point "fork" is just a cute name though
    // what matters is how we use it: what makes it a task is that "fork"
    // will be a function...

    // usage as a simple "continuation" where fork is a function
    const delayedFive = Task(
    resolve => setTimeout(resolve, 500, 5)
    )
    delayedFive.fork(x => console.log(x));//returns cancelation id, logs 5

    // Future-y/Task-y types have two branches though, so...
    // here's a more standardizable usage with an error branch (the standard way)
    const delayedBadFive = Task(
    (reject, resolve) => setTimeout(reject, 500, 5)
    )
    delayedBadFive.fork(e=>console.log(e), x=>console.log(x));//returns cancelation id, errors 5

    // now let's make a version that's also a Functor
    Task = fork => ({
    fork,
    map: fn => Task(
    (reject, resolve) => fork(reject, x=> resolve(fn(x)) )
    )
    });

    // and give it a way to lift a simple value into the minimal context...
    Task.of = x => Task( (a,b) => b(x) )

    // usage
    Task.of(5).map( x => x+1 ).fork(e => console.log(e), x => console.log(x))//synchronously logs 6

    // and from here, it should be clear how to make a version that's a Monad
    // ( hint: we know that .fork is going to get called twice )
    // ( we also know that cancelation will get a bit trickier, though never as tricky it is w/ Promises! )
  9. dtipson created this gist Jan 26, 2017.
    5 changes: 5 additions & 0 deletions Task.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    //simple Task
    const Task = fork => ({fork})

    //continuation
    Task(res=>setTimeout(res,500,5))