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 |
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!