Seque: What It Is & How It Works

What even is this?

Seque is a library of chainable control flow functions — in other words, it allows you to write code like this:

[1, 2, 3]
  .if(useEvens)
  .filter(x => x % 2 === 0)
  .else()
  .filter(x => x % 2 === 1)
  .endif()
  .forEach(doSomething)
  // ...

So far there are three control flow functions:

  • if(<boolean>) …​ [else() …​] endif()

  • while(<Function>) …​ endwhile() (continuously applies functions as long as the function returns true)

  • loop(<number>) …​ endloop() (applies functions <number> times)

How does it work? First, a quick recap of what’s really going on in a chain:

See the Pen Basic Chain by Jonathan Skeate (@skeate) on CodePen.

Each step returns a value, which the next function in the chain is then invoked upon. Chained functions have no knowledge of each other.

Wrappers Galore

To get around this, Seque uses a wrap function. Essentially, it returns an object which will intercept any function you try to call on it. It stashes these away until some endpoint (e.g. endif()), and then executes a callback with the entire list of functions that had been "called", along with their arguments. The callback can then decide to apply them or not apply them as it sees fit.

Technically, it will only intercept any function if the platform has ES6 Proxy support. More on that in a bit.

All of Seque’s control flow methods use this wrap method, with differing callbacks. if 's callback, for example, splits the chain where the else call is (if it exists), and then applies the stack before or after the else based on the value it was passed.

An attempt at illustrating this point (press arrow buttons at the top to change slides):

See the Pen Seque chain by Jonathan Skeate (@skeate) on CodePen.

Caveat

Without ES6’s new Proxy feature, doing this in a completely seamless way is (as far as I can tell) impossible. Without it, the wrap function can only track those functions it has some way of knowing about. By default, it will track any functions available on the original object it was called upon — if an array, it’ll watch for calls to map or forEach, for example. But if some link in the chain returns a different type, then you could have problems if you try to call a function that didn’t exist (at least by name) on the original object.

For example, something like this will work:

['a', 'b', 'c', ...]
  .if(someCondition)
  .join('')
  .slice(1)
  .endif()

Because slice is available on both arrays and strings. However, this will fail:

['a', 'b', 'c', ...]
  .if(someCondition)
  .join('')
  .toUpperCase()
  .endif()

Because toUpperCase() does not exist on arrays. The Seque-wrapped version of the original array will not know that it should be looking for some function named toUpperCase, and thus when it encounters that in the chain, it will say undefined is not a function (or something along those lines).

There is a way around this issue: pass an array of the extra function names as a second argument to the control flow function. Thus, the above failing example would become

['a', 'b', 'c', ...]
  .if(someCondition, ['toUpperCase'])
  .join('')
  .toUpperCase()
  .endif()

Admittedly, this is a bit gross, but I can think of no other way around a lack of proper Proxy support, at least in the code itself. One possible way would be to make a Seque preprocessor, so the array of function names would be generated for you. Of course, if what you’re chaining always returns the same type (e.g. arrays or promises), then this is unnecessary. Speaking of promises…​

Chaining Asynchrony with Promises

One thing you might have noticed is that these all resolve immediately. That can be useful with a chain of promises (e.g. if you want to do this one asynchronous step conditionally, but you know the condition ahead of time). But what if you want to branch or loop based on the resolved value of a promise? Seque includes some Async versions of its functions for this purpose.

$.get('/api/posts')
  .ifAsync(posts => posts.length === 1)
  .then(handleSinglePost)
  .else()
  .then(handlePostList)
  .endif()

It takes a function which will get passed the resolved value of the promise immediately preceding the ifAsync.

There is also a similar whileAsync, but its function will get the last resolved value in the enclosed chain on each iteration. For example, imagine you are dealing with some awful API that has a "nextPage" endpoint which returns page info (including a pageNumber property), and no way to get many pages at once. You want to load up and process 50 pages. You could do something like

$.get('/api/page')
  .whileAsync(page => {
    processPage(page);
    return page.pageNumber !== 50;
  })
  .then(() => $.get('/api/nextPage'))
  .endWhile();

Next

Have ideas for other functions? Find bugs with the current ones? Please post an issue to Github, and I’ll see what I can do!

comments powered by Disqus