E~wee~ctor: writing tiny Effector from scratch #2 — Maps and Filters
Tags: effector, graphs, javascript
Published: 2020-04-02 19:07:29

Hi, all!

In the [previous article](https://dev.to/yumauri/e-wee-ctor-writing-tiny-effector-from-scratch-1-1kap) we've made minimal implementation of our new **E<i>~wee~</i>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_:

```javascript
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.

```diff
// 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:

1. Kernel should be able to do different actions depending on a step type
2. 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:

```javascript
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

```javascript
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.map](https://dev-to-uploads.s3.amazonaws.com/i/m2ho2aj1lh9hqwez8huj.png)

# Event.prepend

_Prepend_ is kind of like _reverse map_ – it prepends event with new event.

```javascript
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:

![Event.prepend](https://dev-to-uploads.s3.amazonaws.com/i/j2ez6i4fc4p8f6xfbdu6.png)

# Store.map

```javascript
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.

![Store.map](https://dev-to-uploads.s3.amazonaws.com/i/tgrj98dss6l5hxv0stzw.png)

# Event.filter

_Filter_ functionality is a bit different beast. This is the first place, where we need new _step type_:

```javascript
export const filter = step('filter')
```

We also need to teach our kernel to support this new step `filter`:

```diff
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](https://developer.mozilla.org/en-US/docs/Glossary/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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label).

Next let's add `.filter` method to event:

```javascript
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.filter](https://dev-to-uploads.s3.amazonaws.com/i/vonhne2ap3i6gr5ucu64.png)

# Event.filterMap

```javascript
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.

![Event.filterMap](https://dev-to-uploads.s3.amazonaws.com/i/u5pyp4n0f78h6ijue714.png)

- - -

And that's it for today!
You can see all this chapter changes in [this commit](https://github.com/yumauri/eweector/commit/0aa744ff9151cbb6cc2f72c6e0c2b82a716c9627).
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...