adamhaile / s Goto Github PK
View Code? Open in Web Editor NEWS.js - Simple, Clean, Fast Reactive Programming in Javascript
License: MIT License
S.js - Simple, Clean, Fast Reactive Programming in Javascript
License: MIT License
Hi Adam! This is a small thing but it tripped me up for a while.
In the S readme file, the example code provided for integrating localStorage is as follows:
if (localStorage.todos) // load stored todos on start
todos(JSON.parse(localStorage.todos).map(Todo));
S(() => // store todos whenever they change
localStorage.todos = JSON.stringify(todos()));
However, JSON.stringify(todos())
results in an array of empty objects.
The correct code taken from the functioning CodePen example is:
if (localStorage.todos) // load stored todos on start
todos(JSON.parse(localStorage.todos).map(Todo));
S(() => // store todos whenever they change
localStorage.todos = JSON.stringify(todos().map(t =>
({ title: t.title(), done: t.done() }))
));
It would be super handy if the first syntax just worked automagically, but I don't know whether that's possible/feasible.
I am using s-js with nativescript and i have a problem. Importing default exported S from s-js is undefined.
NativeScript app need to comply with the CommonJS specification
import S from 's-js';
console.log(S) //undefined !!!!!!!!
Changing es2015 to commonjs in https://github.com/adamhaile/S/blob/master/tsconfig.json#L8 fixes this.
I noticed all static methods are put on the S function.
I think it would pretty easy to make these separate exports instead and your consumers would get the benefit of being able to tree-shake some of the lesser used functions.
Hi, absolutely love the speed, robustness, and elegance of S.js.
I am building something with S.js that requires me to keep a large data structure that looks something like a Map<string, DataSignal<any>>
. I would like the insertions and deletions to the Map to be reactive because some of the computations in the map rebuild their computed values based on other values in the map being available or not available .
I don't need Mobx in this application, or else I would use it's reactive Map, because it's a very low level app, where the performance of S.js is desired. Any idea for a simple way to achieve observing some kind of keyed data structure with S.js? It doesn't have to a true ES6 Map, just some way to achieve the desired effect.
If you create an S.value
and update it multiple times before attaching any listeners, the Value erroneously reports conflicting values.
var foo = S.value(100);
foo(200); // Succeeds
foo(300);
// Error: conflicting values: 300 is not the same as 200
I believe this happens, because on line 267 the DataNode
does not cause to RootClock
to tick if it has no listeners. So S.value's age
is equal to the RootClock.time
causing the fault.
A fix would be to preserve the age
of the S.value
when RunningClock == null
and no listeners.
if (arguments.length === 0) {
return node.current();
}
else if(RunningClock === null && node.log === null) {
return current = node.next(update!);
} else {
var same = eq ? eq(current, update!) : current === update;
if (!same) {
var time = RootClock.time;
if (age === time)
throw new Error("conflicting values: " + update + " is not the same as " + current);
age = time;
current = update!;
node.next(update!);
}
return update!;
}
I'm looking to create a graph of computed data where there may exist dependencies. I had previously begun working on my own library, but came across this one and it seems to handle most of my requirements that other libraries don't, in particular the "no redundant computations" and its ability to handle cycles.
From my experimentation though, the only way to hook up a cycle is if the cycle loops by emitting onto a data signal. This has a few undesirable properties
Boilerplate of what I'm trying to achieve: https://jsfiddle.net/uto837uf/
Example of a not-working cycle (by creating dependent computations): https://jsfiddle.net/uto837uf/3/
One solution that I can think of to this is to have a way to create a dummy node up front so that you have a reference to use for dependency tracking, and then "replace" the dummy node with the real thing later. I haven't found a way to do this from user-space though.
How can one cleanup a single computation (with or without a root)?
When using the CKEditor5 API, creating a new editor returns a Promise which must be resolved to obtain the newly created editor object: ClassicEditor.create(textarea).then(editor => { ... })
. I needed to bind a data signal to the editors change event, unfortunately by the time the promise is resolved, Owner information is no longer available so any computations created will never be disposed.
A simple solution would be to resolve the promise into a DataSignal and create a computation which depends thereby. Unfortunately, this means that the dependent computation will be run twice, and the DataSignal will remain in the graph until the parent is disposed.
I've implemented a solution S.resolve(promise, onfulfilled, onrejected)
which behaves with the same semantics as Promise.then, but restores the Owner information when onfulfilled or onrejected is executed.
S.resolve = function <T, TResult1 = never, TResult2 = never>(promise: PromiseLike<T>, onfulfilled: ((value: T) => TResult1 | PromiseLike<TResult1>), onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2> {
const owner = Owner;
const _onfulfilled = function (value: T): TResult1 | PromiseLike<TResult1> {
Owner = owner;
const result = onfulfilled(value);
Owner = null;
return result;
}
const _onrejected = onrejected ? function (reason: any): TResult2 | PromiseLike<TResult2> {
Owner = owner;
const result = onrejected(reason);
Owner = null;
return result;
} : undefined;
return promise.then(_onfulfilled, _onrejected);
}
This way any computations created when resolving the Promise will be owned by the correct ComputationNode.
I tried to implement a wrapper for Promises, so that async/await could be used while preserving Owner. But await has to resolve the Promise fully before continuing execution of the async function, so I'm fairly confident it's impossible to preserve the Owner inside async without exposing Owner as part of the API or through a proxy object.
Hey Adam!
I've been using your projects for a while now and found myself in a similar situation as the one @ryansolid described about conditionally rendering HTML elements (#21).
I just wanted to let you know that I've created a fork of S 1, and when I hit Ryans issue I added a new function to S, S.track
.
What it does is that it adds the same capacity to a ComputationNode to behave like S.value
, e.g. downstream computations are only reevaluated once the value of their source changes. I still need to add more tests to ensure that it actually works, and it would be great to hear your thoughts about this functionality (I'm still pretty new to reactive programming so if you have some edge cases where you think you can break it that would be really helpful, I'm struggling a bit to write proper specs).
Here is a simple example (I've changed some syntax in my own version, but I use yours here to illustrate how it works):
S.root(() => {
let count = S.data(6);
let onCountChange = S.track(() => {
return count() > 5;
})
S(() => {
console.log('Evaluating');
if (onCountChange()) {
S(() => {
console.log('Tracked ' + count() + ' times.');
});
} else {
console.log('Too low');
}
});
count(7); // Tracked 7 times
count(4); // Evaluating. Too low.
});
Basically how it works is that it adds another state to ComputationNodes: PENDING, and then for tracking, instead of traversing through Log marking nodes STALE, we traverse nodes incrementing a pending counter, then after evaluating their sourced Stale node, either setting them as NotPending, or flagging them as Stale. This way, changes still propagate in correct order as every Computation is flagged for incoming changes, but not always evaluated.
I guess you could get this functionality wrapping computations in another DataSignal, but then they wouldn't respond until next tick.
I thought maybe this could be useful for Enumerable computations in S-array, as you could wrap computations in S.track, only triggering chained computations once the sequence changes.
1: I started out forking it to add type information for Closure Compiler, then found it so useful I've started extending it.
Hi Adam, I know the project isn't active, but I'm tinkering with it while implementing a variation on the same idea and I think I found a bug in S
. I'll leave it here if only for documentation:
const {data, freeze, cleanup, root} = S
root(()=>{
const s1 = data(1)
const s2 = data(2)
const a = S(()=> {
return "a:"+s1()
})
const b = S(()=> {
console.log("b running", a())
cleanup(()=>console.log("b cleanup", a()))
})
s1(10)
})
Expected:
b running a:1
b cleanup a:1
b running a:10
Actual output:
b running a:1
b cleanup a:10
b running a:10
Edit: This used to mention freeze and nested computations for accidental reasons, I had failed to reduce the test case. It looks like the behavior is by design, but I find it surprising, given the emphasis on atomic instants.
I wanted to ask two questions, coming from a little mobx experience and having attempted to write my own observable state lib I was wondering if making a Proxy wrapper had been considered so that use of the s-js lib begins to look like POJO?
const S = require("s-js");
const SArray = require("s-array");
function isData(fn) {
return fn.toString().startsWith("function data(value)");
}
function isComputation(fn) {
return fn.toString().startsWith("function computation()");
}
// maybe this should be called s-pojo or something
function Store(state, actions) {
const store = {};
const proxy = new Proxy(store, {
get: function(target, name) {
if (name in target) {
if (
typeof target[name] === "function" &&
(isData(target[name]) || isComputation(target[name]))
) {
return target[name]();
} else {
return target[name];
}
} else {
if (name in actions) {
return actions[name];
} else {
return undefined;
}
}
},
set: function(target, name, value) {
if (name in target) {
if (typeof target[name] === "function") {
if (isData(target[name])) {
target[name](value);
} else if (isComputation(target[name])) {
return false;
}
} else {
target[name] = value;
}
return true;
} else {
if (Array.isArray(value)) {
target[name] = SArray(value);
} else if (typeof value === "function") {
let fn = value.bind(proxy);
target[name] = S(fn);
} else if (typeof value === "object") {
// write logic here to recursively create new proxy for this obect?
return false;
} else {
target[name] = S.data(value);
}
return true;
}
}
});
// attach data and computations to proxy...
Object.keys(state).forEach(key => {
proxy[key] = state[key];
});
return proxy;
}
// I prefer some kind of wrapper so that I can just pass POJO & functions...
const store = Store(
{
counter: 0,
first: "Andy",
last: "Johnson",
/*nested: {
foo: "BAR"
},*/
fullName: function() {
return `${this.first} ${this.last}`;
},
fullCount: function() {
return `${this.fullName}: ${this.counter}`;
}
},
{
updateData(firstName, lastName, count) {
S.freeze(() => {
this.counter = count;
this.first = firstName;
this.last = lastName;
});
}
}
);
// basically mobx autorun
S.root(() => {
S(() => {
console.log(store.fullName);
});
S(() => {
console.log(store.fullCount);
});
});
store.updateData("Jon", "Doe", 10);
I love the library I just dislike the set/get via function calls personally.
Is there anyway we could make the warning: "computations created without a root or parent will never be disposed" optional? I understand what it's complaining about since my computations are outside of the S.root thunk and thus won't be disposed, but I couldn't really find a better way to structure this to emulate what essentially is mobx lol (other than handling the nested object case...).
First day messing with the library so maybe I have overlooked how to avoid that error message for this kind of use case or maybe I'm completely off in left field.
Here's a small recursive example:
const word = S.data('John')
const number = S.data(123)
S.root(() => {
S(() => {
if (word() == 'mary') {
number(number() + 1)
}
})
S(() => {
console.log(number())
})
})
word('blah')
word('mary')
word('foo')
It causes the first computation to run infinitely (until the error) because it reads and sets the number
in the same computation.
What's you advice for these sorts of situations in general?
In this case, maybe the hypothetical user meant:
const word = S.data('John')
const number = S.data(123)
S.root(() => {
S(() => {
console.log(number())
})
})
S.on(word, () => {
if (word() === 'mary') {
number(number() + 1)
}
})
word('blah')
word('mary') // logs the new number
word('foo')
Curious to know in what sorts of cases we might run into the infinite recursion, and how to deal with it.
Is anybody using subclocks in production? Are they considered ok and bug free enough to use? If so, can anybody explain a use case in which they are using them?
Hi there, me again! This time with a question, the one in the subject... 😉
Anyway, if I have something like:
const pending = S.data();
S.root(() => {
S.on(pending, () => {
console.log("should not have ran...");
});
});
I would like the computation to not happen until a define something for pending!
Something like https://mithril.js.org/stream.html#stream-states
Reason: I'm trying to migrate a small ui from mithril so I can test/compare it with Surplus...
Thanks!
Grieb.
I have a question. It's a bad idea for S to create something like a local state for effects (hooks?)?
For example
const $source = S.value(0);
const $target = S.value(0);
$.effect(() => {
const $throttler = S.localValue(0);
const throttler = S.sample($throttler);
const source = $source();
$throttler(throttler + 1);
if (throttler % 3 === 0) {
$target(source);
}
})
$source($source() + 1)
$source($source() + 1)
$source($source() + 1)
I hit an interesting problem that I'm gathering I'm just attacking wrong but I wanted to see what you think. This goes back to issues around trying to do conditionals with boolean statements that can run too many times without changing their results. Obviously breaking synchronicity is another option. But as you may remember I'd been creating Roots to handle disposal. I came across an issue there recently that I hadn't noticed before, I'm gathering due to manual disposal getting queued up (ie it's frozen like the rest).
Here is the simplest reproduction I have. You can understand why you wouldn't expect data to be re-evaluated again after the ternary evaluates the other way. Thoughts?
https://codesandbox.io/s/objective-tdd-dif57
The issue is when the condition turned from true to false and value is set to undefined it still evaluates the nested child and fails to find a value with length.
EDIT
I was wrong this even happens when you break synchronicity:
import S from "s-js";
S.root(() => {
const trigger = S.data(false),
data = S.data(null),
cache = S.value(S.sample(trigger)),
child = data => {
S(() => console.log("nested", data().length));
return "Hi";
};
S(() => cache(trigger()));
const memo = S(() => (cache() ? child(data) : undefined));
S(() => console.log("view", memo()));
S.freeze(() => {
trigger(true);
data("name");
});
S.freeze(() => {
trigger(false);
data(undefined);
});
});
This produces the same error.
EDIT 2
https://codesandbox.io/s/vigorous-cherry-vo79c shows an even simpler re-production using only a single condition and no freezing.
import S from "s-js";
S.root(() => {
const data = S.data(null),
cache = S.value(S.sample(() => !!data())),
child = data => {
S(() => console.log("nested", data().length));
return "Hi";
};
S(() => cache(!!data()));
const memo = S(() => (cache() ? child(data) : undefined));
S(() => console.log("view", memo()));
console.log("ON");
data("name");
console.log("OFF");
data(undefined);
});
Is this just by design and we should expect these states to exist at the same time?
Hey Adam.
const S = require('s-js');
const i = S.data(0);
const j = S.data(0);
const k = S.data(0);
const l = S.data(0);
function show() {
console.error({i:i(), j:j(), k:k(), l:l()});
}
console.log('-- INIT --');
S.root(() => {
S(() => {
console.error('start i->j');
j(i());
console.error({i:S.sample(i), j:S.sample(j)});
S(() => {
console.error('start j->k');
k(j());
console.error({j:S.sample(j), k:S.sample(k)});
S(() => {
console.error('start k->l');
l(k());
console.error({k:S.sample(k), l:S.sample(l)});
console.error('end k->l');
});
console.error('end j->k');
});
console.error('end i->j');
});
});
console.log('-- i=10 --');
i(10);
show();
console.log('-- i=30 --');
i(30);
show();
Apologies for all of the debug output.
The above computations are just j = i
, k = j
, l = k
, but nested within one another. I log when the function is entered and when it completes, and show the samples of each of the associated variables at each step.
However, the output really surprised me.
-- INIT --
start i->j
{ i: 0, j: 0 }
start j->k
{ j: 0, k: 0 }
start k->l
{ k: 0, l: 0 }
end k->l
end j->k
end i->j
start j->k
{ j: 0, k: 0 }
start k->l
{ k: 0, l: 0 }
end k->l
end j->k
start k->l
{ k: 0, l: 0 }
end k->l
-- i=10 --
start i->j
{ i: 10, j: 0 }
start j->k
{ j: 0, k: 0 }
start k->l
{ k: 0, l: 0 }
end k->l
end j->k
end i->j
start j->k
{ j: 10, k: 0 }
start k->l
{ k: 0, l: 0 }
end k->l
end j->k
start k->l
{ k: 10, l: 0 }
end k->l
{ i: 10, j: 10, k: 10, l: 10 }
-- i=30 --
start i->j
{ i: 30, j: 10 }
start j->k
{ j: 10, k: 10 }
start k->l
{ k: 10, l: 10 }
end k->l
end j->k
end i->j
start j->k
{ j: 30, k: 10 }
start k->l
{ k: 10, l: 10 }
end k->l
end j->k
start k->l
{ k: 30, l: 10 }
end k->l
{ i: 30, j: 30, k: 30, l: 30 }
It appears that the inner computations are run multiple times, even though they're declared once.
I see that this is required, as otherwise the order of scheduled writes vs outcome wouldn't make sense; for example, since the i(10)
would immediately set i=10
, then it makes sense that j(i())
would work fine, but on the next nested computation, j
would still be the previous value (0) since it was only scheduled and not yet swapped. Therefore, without running the inner computations again, the result would be {i:10, j:10, k:0, l:0}
.
This coincides with the test cases for the port I discussed with you:
│ ok: i == 0 ../test.cc:420
│ ok: j == 0 ../test.cc:421
│ ok: k == 0 ../test.cc:422
│ ok: l == 0 ../test.cc:423
│ ok: outer_calls == 1 ../test.cc:424
│ ok: middle_calls == 1 ../test.cc:425
│ ok: inner_calls == 1 ../test.cc:426
│ ok: i == 10 ../test.cc:429
│ ok: j == 10 ../test.cc:430
│ not ok: k == 10 ../test.cc:431
│ not ok: l == 10 ../test.cc:432
However, what's the justification for running the inner computations again other than this? The re-run of the inner computation surprised me. Are cleanup functions run as well?
Beginner question, awesome library, thanks! If I have a DataSignal<T[]>
and I want to cause it's dependent computations to trigger/change, I assume I have to do the following:
const arr = dataSignal()
arr.push(newValue)
dataSignal(arr)
Is there another or better way to do it?
S.js looks very promising. I've been playing around with the todo sample, and while it's easy to add a reactive computation that counts the number of completed tasks by calling reduce on the todos SArray, trying to incrementalize by avoiding iterating over all todo entries it yields surprising behaviour.
For instance, defining the completed count as todos.reduce((acc,e) => e.done() ? 1 : acc) works, but it's not incremental because in general there's no inverse for any function you may pass in. So instead I can make the completed count a variable that's updated upon completing a task:
completed = S.data(0),
...
toggleTask = (task) => {
console.log(task.done());
var delta = task.done() ? 1 : -1;
completed(delta + completed());
},
...
{todos.map(todo =>
<div>
<input type="checkbox" fn={data(todo.done)} onChange={() => toggleTask(todo)} />
<input type="text" fn={data(todo.title)}/>
<a onClick={() => todos.remove(todo)}>×</a>
</div>)}
But todo.done() returns the opposite value, presumably because it's not yet updated from the checkbox value. You can see it in action here: https://codepen.io/anon/pen/bLaVwP
Is there an obvious or canonical way to solve this that I'm missing?
So, does S.js automatically watches for changes on S objects inside DOM?
For example, I believe something like this should work:
var icon = 'fas fa-check'
var iconData = S.data(icon)
const loggedInLabel = S.root(() =>
<label class={iconData()}/>
)
document.body.appendChild(loggedInLabel)
setTimeout(() => {icon = 'fas fa-cross'}, 1000)
But even if I assign the value of iconData
again after changing the value of icon
the DOM doesn't update. What am I doing wrong?
If a computation computes new signals, i.e. new state e.g. by computing a new S.data
signal, are there any rules of which to be aware? I haven't been successful in exactly understanding the rules. In a lot of cases it works, but in some rare cases I have to create a new S.root
and dispose that root manually in the cleanup method of the computation.
Most of the high level state that drives the rest of the computation of an app can be created in the main S.root but how to handle cases where you need to create new signals?
Is there a good way to gracefully handle disposals where I have to build up values or data signals from within async/await code? E.g. loading an image asynchronously and then attaching some styles to the element afterward that create and use signals.
It would be nice to have something akin to Objective-C's non-ARC autopool that acted similar to S.root()
but as a bit more manual - for example, something like:
const sroot = S.root();
try {
await myCode();
} finally {
sroot.release();
}
Hi, I'm having a hard time setting Karma or Jest to work with S.js.
Do you have any example projects to provide? It'd be awesome if they get included into this repository for future reference.
I'm not going to lie. Most times I hit this I just bypass it anyway by not storing an undefined value in the signal and then reading the value from elsewhere on a trigger. In fact, my Proxy implementation does this for a different reason and is likely subject to whatever negative that comes with setting pending values more than once. If values aren't seen until the next clock that processes the update, I want to understand the case, where the last value wins, fails us.
I'm coming from a KnockoutJS background so I'm more comfortable with this inconsistency perhaps, but I'm gathering this has to do with the core differences between S and say MobX. Since in MobX significant execution reordering occurs to ensure the order of execution is predictable. I'm gathering this has to do with S being simpler (and more performant) and in so doesn't reorder execution just ensures that things are consistent at each tick.
Perfect 😍
Hi, how can I create a computed value (with prev != next checking similar to S.value)?
const computed = S(() => compute());
And I need to run all relative computations only if value that compute
returns was changed, but code above run all relative computations even if compute
returns the same value.
Possible solution:
let computed;
S(() => {
if (!computed) {
computed = S.value(compute());
} else {
computed(compute());
}
});
It works as I expected, but it's too large and maybe S exists same functionality out of the box
I'm trying to marry s-js
with lit-html
, but I can't get the view to update. I've reduced the issue down to the smallest example I could.
This should output 123
, but outputs 12
.
import { html, render } from 'lit-html';
import S from 's-js';
const nums = S.data([1, 2]);
const view = S(() => html`<div>${nums()}</div>`);
const r = S.root(() => {
render(view(), document.getElementById('app'));
});
nums(nums().push(3));
The render function is only getting called once. As far as I understand it, that function should also be called as a result of nums(nums().push(3))
.
Can you enlighten me as to what I'm missing?
Edit: I also tried this with the same output result:
const nums = S.data([1, 2]);
const view = S.root(() => html`<div>${nums()}</div>`);
render(view, document.getElementById('app'));
nums(nums().push(3));
As of now, I do so believe that S is probably the most well thought through stream/reactivity library of all. Still, I find the documentation/readme somewhat lacking. I feel that because the way S works is so unique, it needs some more detailed explanation to let people understand how exactly S works, to see that S is not making horses*** claims. One of the points I feel might really need improvement is the part about:
Automatic Dependencies - No manual (un)subscription to change events. Dependencies in S are automatic and exact.
This really does sound too good/magical to be true, especially in this kind of library, if no further explanation is provided. I feel the documentation about:
When an S computation runs, S records what signals it references, thereby creating a live dependency graph of running code. When data changes, S uses that graph to figure out what needs to be updated and in what order.
is insufficient to convince a casual reader that this actually works, its not a crackpot claim. Another part of the reason why I'm asking here is that even I myself am somewhat confused, and want a confirmation from you that my understanding is correct:
If I am correct, I think it is important for the reader to understand the (reasonable) assumptions S has put on the code given to S computations, and how it actually works. If there was anything I could help with, I'd be glad to as well!
Hi @adamhaile
I am quite excited to investigate S as it has a clock system as do synchronous data flow languages (thinking about Lucid Synchrone for instance). I could not find an obvious way to replicate the behavior of the scan
operator of Rxjs. The semantics can be found in the official documentation with marble diagrams. In short, a stream emits a value, a reducing function takes that value, and uses its accumulator to compute another value that is both emitted and put back in the accumulator. The resulting stream is thus a stream of accumulator values.
The issue here is that you need access to values of the accumulator at time n-1 (+ the triggering stream) to produce the values at time n. I tried using reduce
but I got into running away clock messages.
To give you some background, I am trying to implement a chess game:
The chess application behaviour follows stream equations (pseudo code follows), and have an implementation in Rxjs using the scan operator):
const behaviours$ = boardClicks.pipe(
scan((acc, clickedSquare) => {
const {
gameOver,
playerTurn,
selectedPiece,
whitePiecesPos: wPP,
blackPiecesPos: bPP,
squareStyles,
position,
whiteMove,
blackMove,
chessEngine
} = acc;
let nextGameOver, nextPlayerTurn, nextSelectedPiece, nextWhitePiecesPos;
let nextBlackPiecesPos, nextSquareStyles, nextPosition;
let nextWhiteMove, nextBlackMove;
nextWhiteMove =
!gameOver &&
playerTurn === WHITE_TURN &&
selectedPiece !== NO_PIECE_SELECTED &&
wPP.indexOf(clickedSquare) === -1 &&
isValidMove(selectedPiece, clickedSquare, chessEngine)
? { from: selectedPiece, to: clickedSquare }
: NO_MOVE;
...
For information, the pseudo-code inspired from synchronous data-flow languages is as follows:
---- Main types
-- one possible position on the check board
Square :: ["a1", "a2", ..., "a8", "b1", ..., "h8"]
-- type synonym for Square, with the added implication that there is a piece on that square
Square => PiecePos
- record describing a move with an origin square and target square
Move :: {from :: Square, to :: Square} | ∅
---- Application logic
-- a move is either a white piece move or a black piece move
moves = whiteMoves || blackMoves
-- Whites move iff:
-- the game is not over,
-- it is Whites turn to play,
-- there is already a selected piece (origin square),
-- user clicks on a square which does not contain a white piece (target square),
-- the corresponding move (origin square to target square) is valid
whiteMoves = ∅ `then` map boardClick(square)
case !gameOver &
playerTurn == White &
selectedPiece &
!hasWhitePiece(square) &
isValidMove(selectedPiece, square): {from: selectedPiece, to: square}
case _ : ∅
-- Black moves is the symmetric version of White moves
blackMoves = ∅ `then` map boardClick(square)
case ...
-- a piece is selected if the game is on, the click does not trigger a valid move and
-- + when Whites play, and a white piece is clicked on
-- + or when Blacks play, and a black piece is clicked on
--
-- a piece is deselected if the game is on, and the click triggers a valid move
selectedPiece :: Maybe PiecePos
selectedPiece = ∅ `then` map boardClick(square)
case gameOver: ∅
case moves: ∅
case playerTurn == White & square in whitePiecesPos: square
case playerTurn == Black & square in blackPiecesPos: square
case _: rec last selectedPiece
-- the game is over if a valid move ends the game.
gameOver = false `then` map moves isWinningMove
-- Whites initially have the turn, then that alternates with Blacks after every move
playerTurn = White `then`
case whiteMoves: Black
case blackMoves: White
case _: rec last playerTurn
-- Board state
-- the position of white pieces is set initially per the chess game rules
-- then it changes with every move performed by the players
-- achtung! white pieces may be affected by a black piece move and vice versa
whitePiecesPos :: Array of PiecePos
whitePiecesPos = whitesStartPos `then`
-- remove old white piece position and add new one
case whiteMoves({from, to}):
rec last whitePiecesPos |> filter not(from) |> concat [to]
-- remove old black piece position if any - case when a white piece gobbles a black one
case blackMoves({from, to}):
rec last whitePiecesPos |> filter not(from)
-- blackPiecesPos is deduced from whitePiecesPos by symmetry
blackPiecesPos :: Array of PiecePos
...
-- The render uses the ChessBoard React component.
-- We specify some look and feel options for the component.
-- We essentially render a board position (`position`)
-- and possibly one highlighted piece (`squareStyle`).
renderProps = {
draggable: false,
width: 320,
boardStyle: {
borderRadius: "5px",
boxShadow: `0 5px 15px rgba(0, 0, 0, 0.5)`
},
onSquareClick: eventSourceFactory(boardClicks),
-- only the following two properties vary
squareStyles,
position
}
-- the position *prop* is [as defined by the ChessBoard React component](https://chessboardjsx.com/props) we are using
position = "start" `then` map moves getPositionAfterMove
-- the squareStyle prop allows us to manage the style for a highlighted piece
squareStyles = selectedPiece(square)
? { [square]: { backgroundColor: "rgba(255, 255, 0, 0.4)" }}
: {}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.