Skip to content

Commit f10c98f

Browse files
committed
Add operators/tests
1 parent 93759ac commit f10c98f

File tree

4 files changed

+229
-99
lines changed

4 files changed

+229
-99
lines changed

documentation/signals.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ function useTick(delay) {
1515
}
1616
```
1717

18-
### Accessors & Context
18+
## Accessors & Context
1919

2020
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.
2121

22-
### Computations
22+
## Computations
2323

2424
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.
2525

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

62-
### Rendering
62+
## Rendering
6363

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

@@ -75,7 +75,7 @@ createRoot(() => {
7575
})
7676
```
7777

78-
### Composition
78+
## Composition
7979

8080
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.
8181

@@ -95,7 +95,20 @@ const useReducer = (reducer, init) => {
9595
}
9696
```
9797

98-
### Observable
98+
## Operators
99+
100+
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:
101+
102+
### `pipe(...operators): (signal) => any`
103+
The pipe operator is used to combine other operators.
104+
105+
### `map(iterator: (item, index) => any, fallback: () => any): (signal) => any[]`
106+
Memoized array map operator with optional fallback. This operator does not re-map items if already in the list.
107+
108+
### `reduce(accumulator: (memo, item, index) => any, seed): (signal) => any`
109+
Array reduce operator useful for combining or filtering lists.
110+
111+
## Observables
99112

100113
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.
101114

src/dom/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function render(code: () => any, element: Node): () => void {
1414
}
1515

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

src/operator.ts

Lines changed: 126 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,142 @@
11
import { onCleanup, createRoot, sample } from './signal';
22

3-
const FALLBACK = '@@FALLBACK'
3+
const FALLBACK = Symbol('fallback');
4+
5+
type Operator<T, U> = (seq: () => T) => () => U
6+
7+
export function pipe<T>(): Operator<T, T>;
8+
export function pipe<T, A>(fn1: Operator<T, A>): Operator<T, A>;
9+
export function pipe<T, A, B>(fn1: Operator<T, A>, fn2: Operator<A, B>): Operator<T, B>;
10+
export function pipe<T, A, B, C>(fn1: Operator<T, A>, fn2: Operator<A, B>, fn3: Operator<B, C>): Operator<T, C>;
11+
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>;
12+
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>;
13+
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>;
14+
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>;
15+
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>;
16+
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>;
17+
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, {}>;
18+
export function pipe(...fns: Array<Operator<any, any>>): Operator<any, any> {
19+
if (!fns) return i => i;
20+
if (fns.length === 1) return fns[0];
21+
return input => fns.reduce(((prev, fn) => fn(prev)), input);
22+
}
423

524
// Modified version of mapSample from S-array[https://github.com/adamhaile/S-array] by Adam Haile
625
export function map<T, U>(
7-
list: () => T[],
8-
mapFn: (v: T, i: number) => U,
9-
fallback?: () => U
26+
mapFn: (v: T, i: number) => U | any,
27+
fallback?: () => any
1028
) {
11-
let items = [] as T[],
12-
mapped = [] as U[],
13-
disposers = [] as (() => void)[],
14-
len = 0;
15-
onCleanup(() => {
16-
for (let i = 0, length = disposers.length; i < length; i++) disposers[i]();
17-
})
18-
return function() {
19-
let newItems = list(),
20-
i: number,
21-
j: number;
22-
return sample(() => {
23-
let newLen = newItems.length,
24-
newIndices: Map<T, number>,
25-
newIndicesNext: number[],
26-
temp: U[],
27-
tempdisposers: (() => void)[],
28-
start: number,
29-
end: number,
30-
newEnd: number,
31-
item: T;
29+
return (list: () => T[]) => {
30+
let items = [] as T[],
31+
mapped = [] as U[],
32+
disposers = [] as (() => void)[],
33+
len = 0;
34+
onCleanup(() => {
35+
for (let i = 0, length = disposers.length; i < length; i++) disposers[i]();
36+
})
37+
return () => {
38+
let newItems = list(),
39+
i: number,
40+
j: number;
41+
return sample(() => {
42+
let newLen = newItems.length,
43+
newIndices: Map<T, number>,
44+
newIndicesNext: number[],
45+
temp: U[],
46+
tempdisposers: (() => void)[],
47+
start: number,
48+
end: number,
49+
newEnd: number,
50+
item: T;
3251

33-
// fast path for empty arrays
34-
if (newLen === 0) {
35-
if (len !== 0) {
36-
for (i = 0; i < len; i++) disposers[i]();
37-
disposers = [];
38-
items = [];
39-
mapped = [];
40-
len = 0;
41-
}
42-
if (fallback) {
43-
items = [FALLBACK as unknown as any];
44-
mapped[0] = createRoot(disposer => {
45-
disposers[0] = disposer;
46-
return fallback();
47-
});
48-
len = 1;
49-
}
50-
}
51-
else if (len === 0) {
52-
for (j = 0; j < newLen; j++) {
53-
items[j] = newItems[j];
54-
mapped[j] = createRoot(mapper);
55-
}
56-
len = newLen;
57-
}
58-
else {
59-
newIndices = new Map<T, number>();
60-
temp = new Array(newLen);
61-
tempdisposers = new Array(newLen);
62-
// skip common prefix and suffix
63-
for (start = 0, end = Math.min(len, newLen); start < end && items[start] === newItems[start]; start++)
64-
;
65-
for (end = len - 1, newEnd = newLen - 1; end >= 0 && newEnd >= 0 && items[end] === newItems[newEnd]; end-- , newEnd--) {
66-
temp[newEnd] = mapped[end];
67-
tempdisposers[newEnd] = disposers[end];
52+
// fast path for empty arrays
53+
if (newLen === 0) {
54+
if (len !== 0) {
55+
for (i = 0; i < len; i++) disposers[i]();
56+
disposers = [];
57+
items = [];
58+
mapped = [];
59+
len = 0;
60+
}
61+
if (fallback) {
62+
items = [FALLBACK as unknown as any];
63+
mapped[0] = createRoot(disposer => {
64+
disposers[0] = disposer;
65+
return fallback();
66+
});
67+
len = 1;
68+
}
6869
}
69-
// 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order
70-
newIndicesNext = new Array(newEnd + 1);
71-
for (j = newEnd; j >= start; j--) {
72-
item = newItems[j];
73-
i = newIndices.get(item)!;
74-
newIndicesNext[j] = i === undefined ? -1 : i;
75-
newIndices.set(item, j);
70+
else if (len === 0) {
71+
for (j = 0; j < newLen; j++) {
72+
items[j] = newItems[j];
73+
mapped[j] = createRoot(mapper);
74+
}
75+
len = newLen;
7676
}
77-
// 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
78-
for (i = start; i <= end; i++) {
79-
item = items[i];
80-
j = newIndices.get(item)!;
81-
if (j !== undefined && j !== -1) {
82-
temp[j] = mapped[i];
83-
tempdisposers[j] = disposers[i];
84-
j = newIndicesNext[j];
77+
else {
78+
newIndices = new Map<T, number>();
79+
temp = new Array(newLen);
80+
tempdisposers = new Array(newLen);
81+
// skip common prefix and suffix
82+
for (start = 0, end = Math.min(len, newLen); start < end && items[start] === newItems[start]; start++)
83+
;
84+
for (end = len - 1, newEnd = newLen - 1; end >= 0 && newEnd >= 0 && items[end] === newItems[newEnd]; end-- , newEnd--) {
85+
temp[newEnd] = mapped[end];
86+
tempdisposers[newEnd] = disposers[end];
87+
}
88+
// 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order
89+
newIndicesNext = new Array(newEnd + 1);
90+
for (j = newEnd; j >= start; j--) {
91+
item = newItems[j];
92+
i = newIndices.get(item)!;
93+
newIndicesNext[j] = i === undefined ? -1 : i;
8594
newIndices.set(item, j);
8695
}
87-
else disposers[i]();
88-
}
89-
// 2) set all the new values, pulling from the temp array if copied, otherwise entering the new value
90-
for (j = start; j < newLen; j++) {
91-
if (temp.hasOwnProperty(j)) {
92-
mapped[j] = temp[j];
93-
disposers[j] = tempdisposers[j];
96+
// 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
97+
for (i = start; i <= end; i++) {
98+
item = items[i];
99+
j = newIndices.get(item)!;
100+
if (j !== undefined && j !== -1) {
101+
temp[j] = mapped[i];
102+
tempdisposers[j] = disposers[i];
103+
j = newIndicesNext[j];
104+
newIndices.set(item, j);
105+
}
106+
else disposers[i]();
94107
}
95-
else mapped[j] = createRoot(mapper);
108+
// 2) set all the new values, pulling from the temp array if copied, otherwise entering the new value
109+
for (j = start; j < newLen; j++) {
110+
if (temp.hasOwnProperty(j)) {
111+
mapped[j] = temp[j];
112+
disposers[j] = tempdisposers[j];
113+
}
114+
else mapped[j] = createRoot(mapper);
115+
}
116+
// 3) in case the new set is shorter than the old, set the length of the mapped array
117+
len = mapped.length = newLen;
118+
// 4) save a copy of the mapped items for the next update
119+
items = newItems.slice(0);
96120
}
97-
// 3) in case the new set is shorter than the old, set the length of the mapped array
98-
len = mapped.length = newLen;
99-
// 4) save a copy of the mapped items for the next update
100-
items = newItems.slice(0);
121+
return mapped;
122+
});
123+
function mapper(disposer: () => void) {
124+
disposers[j] = disposer;
125+
return mapFn(newItems[j], j);
126+
}
127+
};
128+
}
129+
}
130+
131+
export function reduce<T, U>(fn: (memo: U | undefined, value: T, i: number) => U, seed?: U) {
132+
return (list: () => T[]) => () => {
133+
let newList = list(),
134+
result = seed;
135+
return sample(() => {
136+
for (let i = 0; i < newList.length; i++) {
137+
result = fn(result, newList[i], i);
101138
}
102-
return mapped;
103-
});
104-
function mapper(disposer: () => void) {
105-
disposers[j] = disposer;
106-
return mapFn(newItems[j], j);
107-
}
139+
return result;
140+
})
108141
};
109142
}

test/operator.spec.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { pipe, map, reduce, createSignal, createMemo, createRoot } from '../dist';
2+
3+
describe('Pipe operator', () => {
4+
const multiply = (m) => (s) => () => s() * m;
5+
test('no ops', () => {
6+
createRoot(() => {
7+
const [s, set] = createSignal(0),
8+
r = createMemo(pipe()(s));
9+
expect(r()).toBe(0);
10+
set(2);
11+
expect(r()).toBe(2);
12+
});
13+
});
14+
15+
test('single op', () => {
16+
createRoot(() => {
17+
const [s, set] = createSignal(1),
18+
r = createMemo(pipe(multiply(2))(s));
19+
expect(r()).toBe(2);
20+
set(2);
21+
expect(r()).toBe(4);
22+
});
23+
});
24+
25+
test('multiple ops', () => {
26+
createRoot(() => {
27+
const [s, set] = createSignal(1),
28+
r = createMemo(pipe(multiply(2), multiply(3))(s));
29+
expect(r()).toBe(6);
30+
set(2);
31+
expect(r()).toBe(12);
32+
});
33+
});
34+
});
35+
36+
describe('Reduce operator', () => {
37+
test('simple addition', () => {
38+
createRoot(() => {
39+
const [s, set] = createSignal([1, 2, 3, 4]),
40+
sum = reduce((m, v) => m + v, 0),
41+
r = createMemo(sum(s));
42+
expect(r()).toBe(10);
43+
set([3, 4, 5]);
44+
expect(r()).toBe(12);
45+
});
46+
});
47+
48+
test('filter list', () => {
49+
createRoot(() => {
50+
const [s, set] = createSignal([1, 2, 3, 4]),
51+
filterOdd = reduce((m, v) => v % 2 ? [...m, v] : m, []),
52+
r = createMemo(filterOdd(s));
53+
expect(r()).toEqual([1, 3]);
54+
set([3, 4, 5]);
55+
expect(r()).toEqual([3, 5]);
56+
});
57+
});
58+
});
59+
60+
describe('Map operator', () => {
61+
test('simple map', () => {
62+
createRoot(() => {
63+
const [s, set] = createSignal([1, 2, 3, 4]),
64+
double = map(v => v * 2),
65+
r = createMemo(double(s));
66+
expect(r()).toEqual([2, 4, 6, 8]);
67+
set([3, 4, 5]);
68+
expect(r()).toEqual([6, 8, 10]);
69+
});
70+
});
71+
72+
test('show fallback', () => {
73+
createRoot(() => {
74+
const [s, set] = createSignal([1, 2, 3, 4]),
75+
double = map(v => v * 2, () => 'Empty'),
76+
r = createMemo(double(s));
77+
expect(r()).toEqual([2, 4, 6, 8]);
78+
set([]);
79+
expect(r()).toEqual(['Empty']);
80+
set([3, 4, 5]);
81+
expect(r()).toEqual([6, 8, 10]);
82+
});
83+
});
84+
});

0 commit comments

Comments
 (0)