Conversation
|
Nice! asXxx methods asNumber(): number;
asString(): string;
asList(): string[];Are these just to doo Or, otherwise, if this is to improve caller-side ergnomics I wonder if we can't go further: signal.asToken() // <-- would return A, only valid if A is one of string | number | string[]Possible implementation: class Signal<A> {
public asToken(): A extends string | number | string[] ? A : any {
const v = this.get(); // This is a bit unfortunate but we need a value to work with
if (typeof v === 'string') {
return Token.asString(this);
} else if (typeof v === 'number') {
return Token.asNumber(this);
} else if (Array.isArray(v) && (v[0] === undefined || typeof v[0] === 'string')) {
return Token.asList(this);
} else {
return this;
}
}
}But also, this is a function that does not depend on the specific implementation of the signal (it's the same for every implementation), so does it really need to be on the interface? (Not quite sure how to solve that in general) CachingYeah, I suppose we can do without for a bit. Seems like it wouldn't complicate things very much though, and we're likely to need it anyway? Computed signalsI'm not super keen on the I don't understand this comment:
Seems to me that the only difference between: const b = Signal.computed(() => a.get() * 2);
const b = a.map((a) => a * 2);Is how we find the dependencies of |
| public readonly propagateTags?: boolean; | ||
|
|
||
| private readonly resource: CfnJobDefinition; | ||
| private _containers: Signal<Array<MultiNodeContainer>>; |
There was a problem hiding this comment.
I wonder if Signal is the most descriptive name to use for us. It certainly calls out to other initiatives that people might be familiar with, so that is a plus. But the name "signal" sounds ephemeral to me, whereas our thing definitely holds state.
Observable perhaps?
There was a problem hiding this comment.
Yeah, the name is just a placeholder for now.
| nodeRangeProperties: this._containers.map(containers => | ||
| containers.map((container) => ({ |
There was a problem hiding this comment.
Double .map() here. I know this is correct, but I wonder if people are going to get tripped up on this? Especially since mapping over a list in a signal (to render it to CFN) is going to be very common. Maybe worth some syntactic sugar?
Perhaps (silly name):
interface Signal<A> {
mapmap: A extends Array<any> ? <B>(callback: (item: A[number]) => B) => Signal<Array<B>> : never;
}| }); | ||
|
|
||
| this.node.addValidation({ validate: () => validateContainers(this.containers) }); | ||
| this.node.addValidation({ validate: () => validateContainers(this._containers.get()) }); |
There was a problem hiding this comment.
Did strictly speaking not need to get refactored 😉
| const containers = this._containers.get(); | ||
| containers.push(container); | ||
| this._containers.set(containers); |
There was a problem hiding this comment.
Ohhh. This also speaks for a special signal that holds an array, no? So that we can keep arraySignal.push()?
(Although now arraySignal.map() will be a naming conflict which will be awkward, we'll need to rename our signal-map function)
| } | ||
|
|
||
| abstract class BaseSignal<A> implements Signal<A> { | ||
| public readonly creationStack: string[] = []; |
There was a problem hiding this comment.
Ugh. I hate this property.
| } | ||
| } | ||
|
|
||
| class Zipped<A, B> extends BaseSignal<[A, B]> { |
There was a problem hiding this comment.
More extensible if we do this with an object, so we don't have to make 2-ary, 3-ary, etc versions of this utility.
Signals.zip({
a: signalA,
b: signalB,
}, ({ a, b }) => a * b);| return [this.a.get(), this.b.get()]; | ||
| } | ||
|
|
||
| public set(_: [A, B]): void { |
There was a problem hiding this comment.
Put set on State, not Signal ?
| } | ||
|
|
||
| public getStackTraces(): Array<StackTrace> { | ||
| return this.a.getStackTraces().concat(this.b.getStackTraces()); |
There was a problem hiding this comment.
We should probably maintain order?
a.set(...); // 1
b.set(...); // 2
a.set(...); // 3Should return [1, 2, 3], not [1, 3, 2].
| } | ||
|
|
||
| public get(): B { | ||
| this.stackTraces = this.source.getStackTraces(); |
| if (debugModeEnabled()) { | ||
| this.stackTraces.push(captureStackTrace(this.set.bind(this))); | ||
| } | ||
| this.value = value; |
There was a problem hiding this comment.
Can we do some sort of comparison here, to make sure we only record a stack trace update if the value actually changed?
(May need custom equality comparison functions, depending on the type of A, since JS doesn't have the built-in notion of course...)
Can do:
const x = Signal.state(complexObject(), {
eq: equalityChecker,
});We can pre-deliver an equality checker that does (old, new) => JSON.stringify(old) === JSON.stringify(new) which will be slow but good enough for many cases?
Add a new API to replace
Lazy, so that we can capture the stack trace at the right places.The problem
If you have an L2 that creates L1s with properties derived from other values:
and it is used like this:
The property assignment metadata will record the stack trace leading to line 123, which is where the constructor was called, which, in turn assigned the lazy value to the L1. This is correct, but we are also missing the fact that adding a new container (line 456) also affects the
numNodesproperty.New API
The main interface is:
A simple example of usage:
To solve the construct problem above:
This API is loosely inspired by the Signals proposal for JavaScript, but differs from it in some significant ways:
No caching
This is a lazy API that behaves pretty much the same as the current
Lazy. It computes the value as needed, every timeresolve(orget) is called. Real signal algorithms, such as push-pull, keep track of which nodes in the graph are dirty and need re-computation. This is not a feature we need, but it's possible to add later, if necessary.Explicitly input parameters and types
To declare a computed signal, you would do something like:
Notice that the function passed to
computedtakes no arguments. Calls to other signals can be made inside the body of that function, and, in the process, the algorithm will keep track of the dependency graph, and the necessary cache invalidations. But apart from this "magic", the values flow through the graph via the output of these functions. There is no magic here. This does not fit very well our requirement to also pass the stack traces as part of a context attached to the value.Instead, this API has a
mapfunction, scoped to a signal, that expects a function with input and output explicitly stated:This is certainly less flexible than having the input parameters accessed ad-hoc, from the body of the function. In particular, if you need a function with more than one parameter, you can't use it as it is. To mitigate this, you can use the
Signals.zipfactory:By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license