Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-function": "off"
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "error"
}
};
43 changes: 28 additions & 15 deletions src/behavior.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { cons, DoubleLinkedList, Node, fromArray, nil } from "./datastructures";
import {
Cons,
cons,
DoubleLinkedList,
fromArray,
nil,
Node
} from "./datastructures";
import { combine, isPlaceholder } from "./index";
import { State, Reactive, Time, BListener, Parent, SListener } from "./common";
import { Future, BehaviorFuture } from "./future";
import { BListener, Parent, Reactive, SListener, State, Time } from "./common";
import * as F from "./future";
import { BehaviorFuture, Future } from "./future";
import {
Stream,
FlatFuturesOrdered,
FlatFutures,
FlatFuturesLatest,
FlatFutures
FlatFuturesOrdered,
Stream
} from "./stream";
import { tick, getTime } from "./clock";
import { sample, Now } from "./now";
import { getTime, tick } from "./clock";
import { Now, sample } from "./now";

export type MapBehaviorTuple<A> = { [K in keyof A]: Behavior<A[K]> };

Expand Down Expand Up @@ -88,7 +95,11 @@ export abstract class Behavior<A> extends Reactive<A, BListener>
for (const parent of this.parents) {
if (isBehavior(parent)) {
parent.pull(t);
shouldRefresh = shouldRefresh || this.changedAt < parent.changedAt;
shouldRefresh =
shouldRefresh ||
(this.changedAt !== undefined &&
parent.changedAt !== undefined &&
this.changedAt < parent.changedAt);
}
}
if (shouldRefresh) {
Expand Down Expand Up @@ -139,7 +150,7 @@ function refresh<A>(b: Behavior<A>, t: number) {

export function isBehavior<A>(b: unknown): b is Behavior<A> {
return (
(typeof b === "object" && "at" in b && !isPlaceholder(b)) ||
(typeof b === "object" && b !== null && "at" in b && !isPlaceholder(b)) ||
(isPlaceholder(b) && (b.source === undefined || isBehavior(b.source)))
);
}
Expand Down Expand Up @@ -302,7 +313,10 @@ class FlatMapBehavior<A, B> extends Behavior<B> {
this.parents = cons(this.outer);
}
update(t: number): B {
const outerChanged = this.outer.changedAt > this.changedAt;
const outerChanged =
this.outer.changedAt !== undefined &&
this.changedAt !== undefined &&
this.outer.changedAt > this.changedAt;
if (outerChanged || this.changedAt === undefined) {
if (this.innerB !== undefined) {
this.innerB.removeListener(this.innerNode);
Expand Down Expand Up @@ -703,9 +717,8 @@ class MomentBehavior<A> extends Behavior<A> {
parent.removeListener(node);
}
}
this.parents = undefined;
const value = this.f(this.sampleBound);
return value;
this.parents = nil;
return this.f(this.sampleBound);
}
sample<B>(b: Behavior<B>): B {
const node = new Node(this);
Expand All @@ -727,7 +740,7 @@ class FormatBehavior extends Behavior<string> {
private behaviors: Array<string | number | Behavior<string | number>>
) {
super();
let parents = undefined;
let parents: Cons<Behavior<string | number>> = nil;
for (const b of behaviors) {
if (isBehavior(b)) {
parents = cons(b, parents);
Expand Down
16 changes: 11 additions & 5 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { tick } from "./clock";
export type Time = number;

function isBehavior(b: unknown): b is Behavior<unknown> {
return typeof b === "object" && "at" in b;
return typeof b === "object" && b !== null && "at" in b;
}

export type PullHandler = (pull: (t?: number) => void) => () => void;
Expand Down Expand Up @@ -135,18 +135,22 @@ export abstract class Reactive<A, C extends Child> implements Child {
}

export class CbObserver<A> implements BListener, SListener<A> {
private endPulling: () => void;
private endPulling?: () => void;
node: Node<CbObserver<A>> = new Node(this);
constructor(
private callback: (a: A) => void,
readonly handlePulling: PullHandler,
private time: Time,
private time: Time | undefined,
readonly source: ParentBehavior<A>
) {
source.addListener(this.node, tick());
if (source.state === State.Pull) {
this.endPulling = handlePulling(this.pull.bind(this));
} else if (isBehavior(source) && source.state === State.Push) {
} else if (
isBehavior(source) &&
source.state === State.Push &&
source.last !== undefined
) {
callback(source.last);
}
this.time = undefined;
Expand All @@ -156,7 +160,9 @@ export class CbObserver<A> implements BListener, SListener<A> {
time !== undefined ? time : this.time !== undefined ? this.time : tick();
if (isBehavior(this.source) && this.source.state === State.Pull) {
this.source.pull(t);
this.callback(this.source.last);
if (this.source.last !== undefined) {
this.callback(this.source.last);
}
}
}
pushB(_t: number): void {
Expand Down
49 changes: 32 additions & 17 deletions src/datastructures.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
export class Cons<A> {
constructor(
public readonly value: A,
public readonly tail: Cons<A>,
public readonly isNil: boolean
) {}
*[Symbol.iterator](): IterableIterator<A> {
let head: Cons<A> = this;
while (head.isNil === false) {
const v = head.value;
head = head.tail;
yield v;
}
}
export interface ConsNil {
readonly isNil: true;
[Symbol.iterator](): Generator<never, void, undefined>;
}

export const nil: Cons<undefined> = new Cons(undefined, undefined, true);
export interface ConsValue<A> {
readonly isNil: false;
readonly value: A;
readonly tail: Cons<A>;
[Symbol.iterator](): Generator<A, void, undefined>;
}

export type Cons<A> = ConsNil | ConsValue<A>;

export function cons<A>(value: A, tail: Cons<A> = nil): Cons<A> {
return new Cons(value, tail, false);
function* generator<A>(this: Cons<A>) {
let head: Cons<A> = this;
while (!head.isNil) {
const v = head.value;
head = head.tail;
yield v;
}
}

export const nil: Cons<never> = {
isNil: true,
[Symbol.iterator]: generator
};

export const cons = <A>(value: A, tail: Cons<A> = nil): Cons<A> => {
return {
isNil: false,
value,
tail,
[Symbol.iterator]: generator
};
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this change, what about implementing the linked list as an abstract class called List which Cons and Nil extends? Then the iterator and the isNil property could sit on the abstract class. I also think that would fit better with the rest of the code where we use abstract classes and classes that extend them to represent something like algebraic data types/sum types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reply. Personally I just use tagged unions and simple constructors to encode ADT in TypeScript because it lets to check such unions exhaustively, sort of exhaustive pattern matching. But I'll check your suggestion, thanks!

Copy link
Contributor Author

@raveclassic raveclassic May 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paldepind Looks like it's impossible to declare Cons using abstract class, because it would require declaring value and tail fields on Nil with some "nully" values to make shared generator work. That would defeat the very purpose of this PR - get rid of such values.
Declaring Cons using tagged union OTOH allows to declare value and tail only when isNil === false (means using isNil as tag).

export function fromArray<A>(values: A[]): Cons<A> {
let list = cons(values[0]);
for (let i = 1; i < values.length; ++i) {
Expand Down
2 changes: 1 addition & 1 deletion src/future.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export abstract class Future<A> extends Reactive<A, SListener<A>>
}

export function isFuture(a: unknown): a is Future<unknown> {
return typeof a === "object" && "resolve" in a;
return typeof a === "object" && a !== null && "resolve" in a;
}

export class CombineFuture<A> extends Future<A> {
Expand Down
2 changes: 1 addition & 1 deletion src/placeholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class Placeholder<A> extends Behavior<A> {
}

export function isPlaceholder<A>(p: unknown): p is Placeholder<A> {
return typeof p === "object" && "replaceWith" in p;
return typeof p === "object" && p !== null && "replaceWith" in p;
}

class MapPlaceholder<A, B> extends MapBehavior<A, B> {
Expand Down
9 changes: 5 additions & 4 deletions src/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ export function selfie<A>(stream: Stream<Behavior<A>>): Stream<A> {
}

export function isStream(s: unknown): s is Stream<unknown> {
return typeof s === "object" && "scanFrom" in s;
return typeof s === "object" && s !== null && "scanFrom" in s;
}

class PerformCbStream<A, B> extends ActiveStream<B> implements SListener<A> {
Expand Down Expand Up @@ -509,11 +509,12 @@ export class FlatFuturesOrdered<A> extends Stream<A> {
});
}
pushFromBuffer(): void {
while (this.buffer[0] !== undefined) {
let a = this.buffer.shift();
while (a !== undefined) {
const t = tick();
const { value } = this.buffer.shift();
this.pushSToChildren(t, value);
this.pushSToChildren(t, a.value);
this.next++;
a = this.buffer.shift();
}
}
}
Expand Down
Loading