Skip to content

Commit

Permalink
Add operators/tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ryansolid committed Jul 20, 2019
1 parent 93759ac commit f10c98f
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 99 deletions.
23 changes: 18 additions & 5 deletions documentation/signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ function useTick(delay) {
}
```

### Accessors & Context
## Accessors & Context

Signals are special functions that when executed return their value. Accessors are just functions that "access", or read a value from one or more Signals. At the time of reading the Signal the current execution context (a computation) has the ability to track Signals that have been read, building out a dependency tree that can automatically trigger recalculations as their values are updated. This can be as nested as desired and each new nested context tracks it's own dependencies. Since Accessors by nature of being composed of Signal reads are too reactive we don't need to wrap Signals at every level just at the top level where they are used and around any place that is computationally expensive where you may want to memoize or store intermediate values.

### Computations
## Computations

An computation is calculation over a function execution that automatically dynamically tracks any dependent signals. A computation goes through a cycle on execution where it releases its previous execution's dependencies, then executes grabbing the current dependencies.

Expand Down Expand Up @@ -59,7 +59,7 @@ dispatch({type: 'LIST/ADD', payload: {id: 1, title: 'New Value'}});
```
That being said there are plenty of reasons to use actual Redux.

### Rendering
## Rendering

You can also use signals directly. As an example, the following will show a count of ticking seconds:

Expand All @@ -75,7 +75,7 @@ createRoot(() => {
})
```

### Composition
## Composition

State and Signals combine wonderfully as wrapping a state selector in a function instantly makes it reactive accessor. They encourage composing more sophisticated patterns to fit developer need.

Expand All @@ -95,7 +95,20 @@ const useReducer = (reducer, init) => {
}
```

### Observable
## Operators

Solid provides a couple simple operators to help construct more complicated behaviors. They are in Functional Programming form, where they are functions that return a function that takes the input accessor. They are not computations themselves and are designed to be passed into `createMemo`. The possibilities of operators are endless. Solid only ships with 3 basic ones:

### `pipe(...operators): (signal) => any`
The pipe operator is used to combine other operators.

### `map(iterator: (item, index) => any, fallback: () => any): (signal) => any[]`
Memoized array map operator with optional fallback. This operator does not re-map items if already in the list.

### `reduce(accumulator: (memo, item, index) => any, seed): (signal) => any`
Array reduce operator useful for combining or filtering lists.

## Observables

Signals and Observable are similar concepts that can work together but there are a few key differences. Observables are as defined by the [TC39 Proposal](https://github.com/tc39/proposal-observable). These are a standard way of representing streams, and follow a few key conventions. Mostly that they are cold, unicast, and push-based by default. What this means is that they do not do anything until subscribed to at which point they create the source, and do so for each subscription. So if you had an Observable from a DOM Event, subscribing would add an event listener for each function you pass. In so being unicast they aren't managing a list of subscribers. Finally being push you don't ask for the latest value, they tell you.

Expand Down
2 changes: 1 addition & 1 deletion src/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function render(code: () => any, element: Node): () => void {
}

export function For<T, U>(props: {each: T[], fallback?: any, transform?: (fn: () => U[]) => () => U[], children: (item: T) => U }) {
const mapped = map<T, U>(() => props.each, props.children, 'fallback' in props ? () => props.fallback : undefined);
const mapped = map<T, U>(props.children, 'fallback' in props ? () => props.fallback : undefined)(() => props.each);
return props.transform ? props.transform(mapped) : mapped;
}

Expand Down
219 changes: 126 additions & 93 deletions src/operator.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,142 @@
import { onCleanup, createRoot, sample } from './signal';

const FALLBACK = '@@FALLBACK'
const FALLBACK = Symbol('fallback');

type Operator<T, U> = (seq: () => T) => () => U

export function pipe<T>(): Operator<T, T>;
export function pipe<T, A>(fn1: Operator<T, A>): Operator<T, A>;
export function pipe<T, A, B>(fn1: Operator<T, A>, fn2: Operator<A, B>): Operator<T, B>;
export function pipe<T, A, B, C>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>): Operator<T, C>;
export function pipe<T, A, B, C, D>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>, fn4: Operator<C, D>): Operator<T, D>;
export function pipe<T, A, B, C, D, E>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>, fn4: Operator<C, D>, fn5: Operator<D, E>): Operator<T, E>;
export function pipe<T, A, B, C, D, E, F>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>, fn4: Operator<C, D>, fn5: Operator<D, E>, fn6: Operator<E, F>): Operator<T, F>;
export function pipe<T, A, B, C, D, E, F, G>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>, fn4: Operator<C, D>, fn5: Operator<D, E>, fn6: Operator<E, F>, fn7: Operator<F, G>): Operator<T, G>;
export function pipe<T, A, B, C, D, E, F, G, H>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>, fn4: Operator<C, D>, fn5: Operator<D, E>, fn6: Operator<E, F>, fn7: Operator<F, G>, fn8: Operator<G, H>): Operator<T, H>;
export function pipe<T, A, B, C, D, E, F, G, H, I>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>, fn4: Operator<C, D>, fn5: Operator<D, E>, fn6: Operator<E, F>, fn7: Operator<F, G>, fn8: Operator<G, H>, fn9: Operator<H, I>): Operator<T, I>;
export function pipe<T, A, B, C, D, E, F, G, H, I>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>, fn4: Operator<C, D>, fn5: Operator<D, E>, fn6: Operator<E, F>, fn7: Operator<F, G>, fn8: Operator<G, H>, fn9: Operator<H, I>, ...fns: Operator<any, any>[]): Operator<T, {}>;
export function pipe(...fns: Array<Operator<any, any>>): Operator<any, any> {
if (!fns) return i => i;
if (fns.length === 1) return fns[0];
return input => fns.reduce(((prev, fn) => fn(prev)), input);
}

// Modified version of mapSample from S-array[https://github.com/adamhaile/S-array] by Adam Haile
export function map<T, U>(
list: () => T[],
mapFn: (v: T, i: number) => U,
fallback?: () => U
mapFn: (v: T, i: number) => U | any,
fallback?: () => any
) {
let items = [] as T[],
mapped = [] as U[],
disposers = [] as (() => void)[],
len = 0;
onCleanup(() => {
for (let i = 0, length = disposers.length; i < length; i++) disposers[i]();
})
return function() {
let newItems = list(),
i: number,
j: number;
return sample(() => {
let newLen = newItems.length,
newIndices: Map<T, number>,
newIndicesNext: number[],
temp: U[],
tempdisposers: (() => void)[],
start: number,
end: number,
newEnd: number,
item: T;
return (list: () => T[]) => {
let items = [] as T[],
mapped = [] as U[],
disposers = [] as (() => void)[],
len = 0;
onCleanup(() => {
for (let i = 0, length = disposers.length; i < length; i++) disposers[i]();
})
return () => {
let newItems = list(),
i: number,
j: number;
return sample(() => {
let newLen = newItems.length,
newIndices: Map<T, number>,
newIndicesNext: number[],
temp: U[],
tempdisposers: (() => void)[],
start: number,
end: number,
newEnd: number,
item: T;

// fast path for empty arrays
if (newLen === 0) {
if (len !== 0) {
for (i = 0; i < len; i++) disposers[i]();
disposers = [];
items = [];
mapped = [];
len = 0;
}
if (fallback) {
items = [FALLBACK as unknown as any];
mapped[0] = createRoot(disposer => {
disposers[0] = disposer;
return fallback();
});
len = 1;
}
}
else if (len === 0) {
for (j = 0; j < newLen; j++) {
items[j] = newItems[j];
mapped[j] = createRoot(mapper);
}
len = newLen;
}
else {
newIndices = new Map<T, number>();
temp = new Array(newLen);
tempdisposers = new Array(newLen);
// skip common prefix and suffix
for (start = 0, end = Math.min(len, newLen); start < end && items[start] === newItems[start]; start++)
;
for (end = len - 1, newEnd = newLen - 1; end >= 0 && newEnd >= 0 && items[end] === newItems[newEnd]; end-- , newEnd--) {
temp[newEnd] = mapped[end];
tempdisposers[newEnd] = disposers[end];
// fast path for empty arrays
if (newLen === 0) {
if (len !== 0) {
for (i = 0; i < len; i++) disposers[i]();
disposers = [];
items = [];
mapped = [];
len = 0;
}
if (fallback) {
items = [FALLBACK as unknown as any];
mapped[0] = createRoot(disposer => {
disposers[0] = disposer;
return fallback();
});
len = 1;
}
}
// 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order
newIndicesNext = new Array(newEnd + 1);
for (j = newEnd; j >= start; j--) {
item = newItems[j];
i = newIndices.get(item)!;
newIndicesNext[j] = i === undefined ? -1 : i;
newIndices.set(item, j);
else if (len === 0) {
for (j = 0; j < newLen; j++) {
items[j] = newItems[j];
mapped[j] = createRoot(mapper);
}
len = newLen;
}
// 1) step through all old items and see if they can be found in the new set; if so, save them in a temp array and mark them moved; if not, exit them
for (i = start; i <= end; i++) {
item = items[i];
j = newIndices.get(item)!;
if (j !== undefined && j !== -1) {
temp[j] = mapped[i];
tempdisposers[j] = disposers[i];
j = newIndicesNext[j];
else {
newIndices = new Map<T, number>();
temp = new Array(newLen);
tempdisposers = new Array(newLen);
// skip common prefix and suffix
for (start = 0, end = Math.min(len, newLen); start < end && items[start] === newItems[start]; start++)
;
for (end = len - 1, newEnd = newLen - 1; end >= 0 && newEnd >= 0 && items[end] === newItems[newEnd]; end-- , newEnd--) {
temp[newEnd] = mapped[end];
tempdisposers[newEnd] = disposers[end];
}
// 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order
newIndicesNext = new Array(newEnd + 1);
for (j = newEnd; j >= start; j--) {
item = newItems[j];
i = newIndices.get(item)!;
newIndicesNext[j] = i === undefined ? -1 : i;
newIndices.set(item, j);
}
else disposers[i]();
}
// 2) set all the new values, pulling from the temp array if copied, otherwise entering the new value
for (j = start; j < newLen; j++) {
if (temp.hasOwnProperty(j)) {
mapped[j] = temp[j];
disposers[j] = tempdisposers[j];
// 1) step through all old items and see if they can be found in the new set; if so, save them in a temp array and mark them moved; if not, exit them
for (i = start; i <= end; i++) {
item = items[i];
j = newIndices.get(item)!;
if (j !== undefined && j !== -1) {
temp[j] = mapped[i];
tempdisposers[j] = disposers[i];
j = newIndicesNext[j];
newIndices.set(item, j);
}
else disposers[i]();
}
else mapped[j] = createRoot(mapper);
// 2) set all the new values, pulling from the temp array if copied, otherwise entering the new value
for (j = start; j < newLen; j++) {
if (temp.hasOwnProperty(j)) {
mapped[j] = temp[j];
disposers[j] = tempdisposers[j];
}
else mapped[j] = createRoot(mapper);
}
// 3) in case the new set is shorter than the old, set the length of the mapped array
len = mapped.length = newLen;
// 4) save a copy of the mapped items for the next update
items = newItems.slice(0);
}
// 3) in case the new set is shorter than the old, set the length of the mapped array
len = mapped.length = newLen;
// 4) save a copy of the mapped items for the next update
items = newItems.slice(0);
return mapped;
});
function mapper(disposer: () => void) {
disposers[j] = disposer;
return mapFn(newItems[j], j);
}
};
}
}

export function reduce<T, U>(fn: (memo: U | undefined, value: T, i: number) => U, seed?: U) {
return (list: () => T[]) => () => {
let newList = list(),
result = seed;
return sample(() => {
for (let i = 0; i < newList.length; i++) {
result = fn(result, newList[i], i);
}
return mapped;
});
function mapper(disposer: () => void) {
disposers[j] = disposer;
return mapFn(newItems[j], j);
}
return result;
})
};
}
84 changes: 84 additions & 0 deletions test/operator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { pipe, map, reduce, createSignal, createMemo, createRoot } from '../dist';

describe('Pipe operator', () => {
const multiply = (m) => (s) => () => s() * m;
test('no ops', () => {
createRoot(() => {
const [s, set] = createSignal(0),
r = createMemo(pipe()(s));
expect(r()).toBe(0);
set(2);
expect(r()).toBe(2);
});
});

test('single op', () => {
createRoot(() => {
const [s, set] = createSignal(1),
r = createMemo(pipe(multiply(2))(s));
expect(r()).toBe(2);
set(2);
expect(r()).toBe(4);
});
});

test('multiple ops', () => {
createRoot(() => {
const [s, set] = createSignal(1),
r = createMemo(pipe(multiply(2), multiply(3))(s));
expect(r()).toBe(6);
set(2);
expect(r()).toBe(12);
});
});
});

describe('Reduce operator', () => {
test('simple addition', () => {
createRoot(() => {
const [s, set] = createSignal([1, 2, 3, 4]),
sum = reduce((m, v) => m + v, 0),
r = createMemo(sum(s));
expect(r()).toBe(10);
set([3, 4, 5]);
expect(r()).toBe(12);
});
});

test('filter list', () => {
createRoot(() => {
const [s, set] = createSignal([1, 2, 3, 4]),
filterOdd = reduce((m, v) => v % 2 ? [...m, v] : m, []),
r = createMemo(filterOdd(s));
expect(r()).toEqual([1, 3]);
set([3, 4, 5]);
expect(r()).toEqual([3, 5]);
});
});
});

describe('Map operator', () => {
test('simple map', () => {
createRoot(() => {
const [s, set] = createSignal([1, 2, 3, 4]),
double = map(v => v * 2),
r = createMemo(double(s));
expect(r()).toEqual([2, 4, 6, 8]);
set([3, 4, 5]);
expect(r()).toEqual([6, 8, 10]);
});
});

test('show fallback', () => {
createRoot(() => {
const [s, set] = createSignal([1, 2, 3, 4]),
double = map(v => v * 2, () => 'Empty'),
r = createMemo(double(s));
expect(r()).toEqual([2, 4, 6, 8]);
set([]);
expect(r()).toEqual(['Empty']);
set([3, 4, 5]);
expect(r()).toEqual([6, 8, 10]);
});
});
});

0 comments on commit f10c98f

Please sign in to comment.