E~wee~ctor: writing tiny Effector from scratch #2 — Maps and Filters
Victor Didenko
Posted on April 2, 2020
Hi, all!
In the previous article we've made minimal implementation of our new E~wee~ctor library, which could run "counter" example from Effector website. But, honestly, this example is all it could do, nothing more. So, let's add some more features.
In this chapter I want to add maps and filters.
Steps
Last time we decided to use functions as steps. That was good and simple for the start, but unfortunately we can't go further with this approach. In some cases kernel needs to make different decisions depending on steps. Like filter functionality – in case filter function returns false
, kernel should stop execution for the current graph branch.
So we need to introduce step types:
const step = type => fn => ({
type,
fn,
})
export const compute = step('compute')
Function step
creates step object, containing fields type
and fn
. Let's begin with single step compute and change our existing code.
// change `watch` node
export const watch = unit => fn => {
const node = createNode({
- seq: [fn],
+ seq: [compute(fn)],
})
unit.graphite.next.push(node)
}
// --8<--
// change `store` unit
store.graphite = createNode({
- seq: [value => (currentState = value)],
+ seq: [compute(value => (currentState = value))],
})
store.on = (event, fn) => {
const node = createNode({
next: [store.graphite],
- seq: [value => fn(currentState, value)],
+ seq: [compute(value => fn(currentState, value))],
})
event.graphite.next.push(node)
return store
}
We also need to change kernel with following requirements:
- Kernel should be able to do different actions depending on a step type
- For the filter functionality we should be able to stop execution of current branch
In the first version we've used .forEach
to traverse through all node steps. But it is impossible to stop and quit .forEach
, so we have to rewrite it with good old for
cycle:
const exec = () => {
while (queue.length) {
let { node, value } = queue.shift()
for (let i = 0; i < node.seq.length; i++) {
const step = node.seq[i]
switch (step.type) {
case 'compute':
value = step.fn(value)
break
}
}
node.next.forEach(node => queue.push({ node, value }))
}
}
Now our steps preparations are done, let's go with maps first.
Event.map
export const createEvent = () => {
// --8<--
event.map = fn => {
const mapped = createEvent()
const node = createNode({
next: [mapped.graphite],
seq: [compute(fn)],
})
event.graphite.next.push(node)
return mapped
}
// --8<--
}
.map
method accepts map function. It creates new event unit, and ties two events, old and new one, with new auxiliary node map
. And given map function is executed inside this auxiliary node, to modify data.
Event.prepend
Prepend is kind of like reverse map – it prepends event with new event.
export const createEvent = () => {
// --8<--
event.prepend = fn => {
const prepended = createEvent()
const node = createNode({
next: [event.graphite],
seq: [compute(fn)],
})
prepended.graphite.next.push(node)
return prepended
}
// --8<--
}
.prepend
method behaves almost exactly like .map
, just in an opposite direction:
Store.map
export const createStore = defaultState => {
// --8<--
store.map = fn => {
const mapped = createStore(fn(currentState))
const node = createNode({
next: [mapped.graphite],
seq: [compute(fn)],
})
store.graphite.next.push(node)
return mapped
}
// --8<--
}
.map
method accepts map function. It creates new store unit, and ties two stores, old and new one, with new auxiliary node map
. And given map function is executed inside this auxiliary node, to modify data.
Additionally, to compute new store initial state, this method calls map function once with current store state.
⚠️ It should be noted, that this implementation doesn't follow Effector API completely – map function doesn't receive mapped store state as a second argument. We will fix this in later chapters.
Event.filter
Filter functionality is a bit different beast. This is the first place, where we need new step type:
export const filter = step('filter')
We also need to teach our kernel to support this new step filter
:
const exec = () => {
- while (queue.length) {
+ cycle: while (queue.length) {
let { node, value } = queue.shift()
for (let i = 0; i < node.seq.length; i++) {
const step = node.seq[i]
switch (step.type) {
case 'compute':
value = step.fn(value)
break
+ case 'filter':
+ if (!step.fn(value)) continue cycle
+ break
}
}
node.next.forEach(node => queue.push({ node, value }))
}
}
If we meet a step with type filter
, and its filter function returns falsy value – we just skip all other execution in this branch.
If you are unfamiliar with this strange syntax continue cycle
– this is called label, you can read about it here.
Next let's add .filter
method to event:
export const createEvent = () => {
// --8<--
event.filter = fn => {
const filtered = createEvent()
const node = createNode({
next: [filtered.graphite],
seq: [filter(fn)],
})
event.graphite.next.push(node)
return filtered
}
// --8<--
}
As you can see, it looks exactly like .map
method, with only difference – instead of step compute
we use step filter
.
⚠️ This implementation doesn't follow Effector API also – due to historical reasons Effector's Event.filter accepts not function, but object {fn}
.
Event.filterMap
export const createEvent = () => {
// --8<--
event.filterMap = fn => {
const filtered = createEvent()
const node = createNode({
next: [filtered.graphite],
seq: [compute(fn), filter(value => value !== undefined)],
})
event.graphite.next.push(node)
return filtered
}
// --8<--
}
.filterMap
method is like .map
and .filter
combined together. This is the first place, where we've created auxiliary node filterMap
, containing two steps – compute
, to execute given function, and filter
, to check, if we have undefined
or not value.
And that's it for today!
You can see all this chapter changes in this commit.
I've also added automated testing, so we can be sure, that we will not break old functionality with new one.
Thank you for reading!
To be continued...
Posted on April 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.