Compare commits

..

8 Commits
main ... loops

11 changed files with 586 additions and 155 deletions

View File

@ -1,11 +1,5 @@
# Revision history for UnitJS
## 0.1.1 -- 2022-08-09
* Package UnitJS for [guix](https://guix.gnu.org/)
* Fix state bug in Async's sequences and bindings
* Generalize support for events in Dom element maker
## 0.1.0 -- 2020-05-17
* Release UnitJS as a [SJW](https://git.marvid.fr/Tissevert/SJW) package still useable as a standalone script.

View File

@ -1,49 +1,43 @@
# UnitJS
UnitJS is a tiny Javascript framework to write web applications. It provides
four essential modules to deal with asynchronous functions (technically, it's a
monad obtained by currifying the CPS functions of Javascript like
`setTimeout`), value caching, DOM manipulation and basic functional needs like
composition or object projections.
UnitJS is a tiny Javascript framework to write web applications. It provides four essential modules to deal with asynchronous functions (technically, it's a monad obtained by currifying the CPS functions of Javascript like `setTimeout`), value caching, DOM manipulation and basic functional needs like composition or object projections.
## Usage
## How to use it in your project
### With [SJW](https://git.marvid.fr/Tissevert/SJW)
### As a [`SJW`](https://git.marvid.fr/Tissevert/SJW) package.
UnitJS comes as a SJW package. You can install it by copying the `src/` directory of this repository to your package database by issuing the following command from the current directory :
#### With [`guix`](https://guix.gnu.org/)
```
$ cp -r src/ ~/.sjw/unitJS
```
The easiest way to use it in your web projects is to simply [package them with
`guix`](https://git.marvid.fr/Tissevert/SJW#how-not-to-install).
Then, using unitJS in your projects is as simple as writing :
#### Otherwise
```
import * as Async from UnitJS.Async;
```
You can follow [these
instructions](https://git.marvid.fr/Tissevert/SJW#when-guix-is-not-an-option)
to use it without `guix`.
in any module if you want to use the `Async` module for instance and compile your code using the `--include` option of `sjw` with package `unitJS` :
### Without `SJW`
```
$ sjw -I unitJS your/code/src -o your/code/main.js
```
If you don't want to or can't use SJW, it's still possible to use UnitJS by
generating a single script that can then be loaded from your web page like this
:
Note that all modules have their path prefixed by a common `UnitJS` component and that SJW will only include the modules actually used in your code, not all of UnitJS.
### As a standalone JS script
If you don't want to or can't use SJW, it's still possible to use UnitJS by generating a single script that can then be loaded from your web page like this :
```
<script src="/path/to/unit.js"></script>
```
The file `unit.js` can be easily generated with `make`, which actually just
calls the `unit.js.tpl` script. Note that you can still generate custom
«partial» versions of `unit.js` if you don't use all of it by overriding the
`SRC` variable of the `Makefile` like so :
The file `unit.js` can be easily generated with `make`, which actually just calls the `unit.js.tpl` script. Note that you can still generate custom «partial» versions of `unit.js` if you don't use all of it by overriding the `SRC` variable of the `Makefile` like so :
```
make SRC="src/UnitJS/Dom.js src/UnitJS/Cache.js"
```
or by manually calling `./unit.js.tpl` with the files you want, like the
`Makefile` does.
When used that way, the `UnitJS` library will be available to your Javascript
code as a global variable `unitJS` containing each of the modules (`Async`,
`Dom`, etc.) as an attribute.
or by manually calling `./unit.js.tpl` with the files you want, like the `Makefile` does.

View File

@ -1,24 +0,0 @@
(use-modules (guix build-system copy)
(guix gexp)
(guix git-download)
(guix licenses)
(guix packages))
(let ((%source-dir (dirname (current-filename))))
(package
(name "sjw-unitJS")
(version "devel")
(source (local-file %source-dir
#:recursive? #t
#:select? (git-predicate %source-dir)))
(build-system copy-build-system)
(arguments
'(#:install-plan '(("src" "lib/SJW/unitJS"))))
(home-page "https://git.marvid.fr/Tissevert/UnitJS")
(synopsis "The Simple Javascript Wrench.")
(description
"A collection of JS modules to write simple web applications. It covers
the basics, providing asynchronous operations without any need for
promises-support from the browser as well as primitives to create DOM
elements and basic functional-programming tooling.")
(license gpl3+)))

View File

@ -1,48 +1,164 @@
import compose from UnitJS.Fun;
import curry from UnitJS.Fun.Curry;
return {
apply: apply,
pure: pure,
wrap: pure, // kept for backwards compatibility
tuple: pure, // an alias to insist on the fact that pure can be used to build tuples
fmap: fmap,
liftUncurry: liftUncurry,
lift: lift,
liftA2: liftA2,
mApply: mApply,
bind: bind,
fail: fail,
http: http,
apply: apply,
map: map,
parallel: parallel,
run: run,
sequence: sequence,
parallel: parallel,
loopWhile: loopWhile,
loopUntil: loopUntil,
loopForEach: loopForEach,
wait: wait,
wrap: wrap
http: http,
run: run
};
function apply(f, x) {
return function(g) {
g(f(x));
/*
* The Async monad obtained by curryfying Continuation Passing Style
*
* Async a = (a -> void) -> void
*
*/
/*
* pure :: a -> Async a
*
* Since all values only exist as arguments to the next computation, this allows to represent tuples and indeed, pure is variadic and allows to build tuples by passing as many arguments as you want
*
* tuple :: (a0, a1, , an) -> Async (a0, a1, , an)
*/
function pure() {
var args = arguments;
return function(f) {
f.apply(null, args);
};
}
/*
* Async is a functor:
*
* fmap :: (a -> b) -> Async a -> Async b
*
*/
function fmap(f) {
return function(mA) {
return function(g) {
return mA(compose(g, f));
};
};
}
/*
* Async is also an Applicative:
*
* When considered for tuples, fmap is almost liftAN for all N for uncurryied functions, all we need to do is to collect the various Async arguments with `parallel`
*
* liftUncurry :: ((a0, a1, , an) -> b) -> (Async a0, Async a1, , Async an) -> Async b
*/
function liftUncurry(f) {
return function() {
return fmap(f)(parallel.apply(null, arguments));
}
}
/*
* A version for curryied functions (still taking the Async parameters in an uncurryied way)
*
* lift :: (a0 -> a1 -> -> an -> b) -> (Async a0, Async a1, , Async an) -> Async b
*/
function lift(f) {
return function() {
return Array.prototype.reduce.call(arguments, function(acc, v) {
return mApply(acc)(v);
}, pure(f));
};
}
/*
* The fully curryied version of lift for each arity can then be easily derived following this example for arity 2
*
* liftA2 :: (a -> b -> c) -> Async a -> Async b -> Async c
*/
function liftA2(f) {
return curry(lift(f), 2);
}
/*
* Since we defined `lift` in term of mApply, we must implement this part of Applicative:
*
* mApply :: Async (a -> b) -> Async a -> Async b
*/
function mApply(mF) {
return function(mA) {
return function(g) {
mF(function(f) {fmap(f)(mA)(g)});
};
};
}
/*
* The bind operator needed to use the monad as such
*
* bind :: Async a -> (a -> Async b) -> Async b
*
* Actually, this has been generalized to allow an arbitrarily long call chain
*
* bind :: Async a0 -> (a0 -> Async a1) -> (a1 -> Async a2) -> -> Async an
*/
function bind() {
var m, steps;
if(arguments.length < 1) {
return wrap();
return pure();
}
m = arguments[0];
steps = arguments;
return function(f) {
var i = 1;
var step = function(x) {
var step = function() {
if(i < steps.length) {
steps[i++](x)(step);
steps[i++].apply(null, arguments)(step);
} else {
return f(x);
return f.apply(null, arguments);
}
}
m(step);
};
}
/*
* A primitive to stop a computation in Async and display a log message in the console
*
* fail :: string -> Async a
*/
function fail(message) {
return function(f) {
console.log(message);
}
}
/*
* apply :: (a -> b, a) -> Async b
*/
function apply(f, x) {
return function(g) {
g(f(x));
};
}
/*
* map :: (a -> b) -> a -> Async b
*/
function map(mapper) {
return function(x) {
return function(f) {
@ -51,12 +167,37 @@ function map(mapper) {
};
}
/*
* A sequence of void computations
*
* sequence :: (Async void, Async void, , Async a) -> Async a
*/
function sequence() {
var steps = arguments;
return function(f) {
var i = 0;
var step = function() {
if(i < steps.length) {
steps[i++](step);
} else {
f.apply(null, arguments);
}
}
step();
};
}
/*
* Parallel computations which results are collected and passed on to the next computation as a tuple
*
* parallel :: (Async a0, Async a1, , Async an) -> Async (a0, a1, , an)
*/
function parallel() {
var threads = arguments;
var pending = threads.length;
var results = [];
var returned = [];
return function(f) {
var pending = threads.length;
var results = [];
var returned = [];
var useResult = function(i) {
return function(x) {
if(!returned[i]) {
@ -64,7 +205,7 @@ function parallel() {
returned[i] = true;
pending--;
if(pending < 1) {
f(results);
f.apply(null, results);
}
}
};
@ -75,43 +216,63 @@ function parallel() {
};
}
function run() {
var m;
if(arguments.length == 1) {
m = arguments[0];
} else {
m = sequence.apply(null, arguments);
}
m(function() {});
}
function sequence() {
var steps = arguments;
return function(f) {
var i = 0;
var step = function(x) {
if(i < steps.length) {
steps[i++](step);
} else {
f(x);
}
/*
* Three async loops:
*
* loopWhile performs the test before entering the loop and ends when the condition becomes false
*
* loopWhile :: (Async boolean, Async void) -> Async void
*/
function loopWhile(predicate, body) {
return bind(predicate, function(keepOn) {
if(keepOn) {
return sequence.apply(null, [].concat(body, loopWhile(predicate, body)));
} else {
return pure();
}
step();
};
});
}
/*
* A variant that check the condition at the end of the loop that, hence, always runs at least once. Also note that this loop stops when the condition becomes true
*
* loopUntil :: (Async void, Async boolean) -> Async void
*/
function loopUntil(body, predicate) {
return sequence.apply(null, [].concat(body, bind(predicate, function(atEnd) {
if(atEnd) {
return pure();
} else {
return loopUntil(body, predicate);
}
})));
}
/*
* A loop that iterates over an array; the body is a function that accesses the iteration context as its arguments just like Array.prototype.forEach: current element, its index in the array, the whole array
*
* loopForEach :: ([a], ((a, number, [a]) -> Async void)) -> Async void
*/
function loopForEach(array, bodyF) {
return sequence.apply(null, array.reduce(function(acc, v, k, a) {
return acc.concat(bodyF(v, k, a));
}, []));
}
/*
* wait :: number -> Async void
*/
function wait(delay) {
return function(f) {
setTimeout(f, delay);
};
}
function wrap(x) {
return function(f) {
f(x);
};
}
/*
* Perform an Async HTTP(S) query
*
* http :: query -> Async XMLHttpRequest
*/
function http(query) {
return function(f) {
var xhr = new XMLHttpRequest();
@ -123,6 +284,20 @@ function http(query) {
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
}
xhr.send(query.body);
};
}
/*
* The only «way-out» of Async: running the computation from without
*
* run :: Async a -> void
*/
function run() {
var m;
if(arguments.length == 1) {
m = arguments[0];
} else {
m = sequence.apply(null, arguments);
}
m(function() {});
}

42
src/UnitJS/Async/Box.js Normal file
View File

@ -0,0 +1,42 @@
import * as Async from UnitJS.Async;
function Box(initValue) {
this.value = initValue;
}
Box.prototype.get = function(f) {
f(this.value);
};
Box.prototype.set = function(newValue) {
return function(f) {
this.value = newValue;
f();
}.bind(this);
};
Box.prototype.bind = function(f) {
return function(g) {
f(this.value)(g);
}.bind(this);
};
Box.prototype.update = function(modifier) {
return function(f) {
modifier(this.value)(function (newValue) {this.value = newValue; f()}.bind(this));
}.bind(this);
};
return {
make: make
};
function make(initValue) {
var box = new Box(initValue);
return {
get: box.get.bind(box),
set: box.set.bind(box),
bind: box.bind.bind(box),
update: box.update.bind(box),
};
}

52
src/UnitJS/Async/Cache.js Normal file
View File

@ -0,0 +1,52 @@
function Cache(loader) {
this.loader = loader;
this.loaded = {};
this.loading = {};
}
Cache.prototype.get = function(key) {
return function(f) {
if(this.loaded[key] != undefined) {
f(this.loaded[key]);
} else {
this.startLoading(key);
this.loading[key].push(f);
this.loader(key)(this.store(key));
}
}.bind(this);
};
Cache.prototype.warmUp = function(key) {
if(this.loaded[key] == undefined) {
this.startLoading(key);
this.loader(key)(this.store(key));
}
};
Cache.prototype.startLoading = function(key) {
if(this.loading[key] == undefined) {
this.loading[key] = [];
}
};
Cache.prototype.store = function(key) {
return function(value) {
this.loaded[key] = value;
for(var i = 0; i < this.loading[key].length; i++) {
this.loading[key][i](value);
}
this.loading[key] = null;
}.bind(this);
};
return {
make: make
};
function make(loader) {
var cache = new Cache(loader);
return {
get: cache.get.bind(cache),
warmUp: cache.warmUp.bind(cache)
};
}

View File

@ -1,52 +1,3 @@
function Cache(loader) {
this.loader = loader;
this.loaded = {};
this.loading = {};
}
import * as Cache from UnitJS.Async.Cache;
Cache.prototype.get = function(key) {
return function(f) {
if(this.loaded[key] != undefined) {
f(this.loaded[key]);
} else {
this.startLoading(key);
this.loading[key].push(f);
this.loader(key)(this.store(key));
}
}.bind(this);
};
Cache.prototype.warmUp = function(key) {
if(this.loaded[key] == undefined) {
this.startLoading(key);
this.loader(key)(this.store(key));
}
};
Cache.prototype.startLoading = function(key) {
if(this.loading[key] == undefined) {
this.loading[key] = [];
}
};
Cache.prototype.store = function(key) {
return function(value) {
this.loaded[key] = value;
for(var i = 0; i < this.loading[key].length; i++) {
this.loading[key][i](value);
}
this.loading[key] = null;
}.bind(this);
};
return {
make: make
};
function make(loader) {
var cache = new Cache(loader);
return {
get: cache.get.bind(cache),
warmUp: cache.warmUp.bind(cache)
};
}
return Cache; //Re-export for compatibility reasons

View File

@ -3,6 +3,7 @@ return {
compose: compose,
defined: defined,
id: id,
not: not,
insert: insert,
map: map,
mapFilter: mapFilter,
@ -32,6 +33,10 @@ function id(x) {
return x;
}
function not(x) {
return !x;
}
function insert(obj, t, comparer, min, max) {
min = defined(min) ? min : 0;
max = defined(max) ? max : t.length;

137
src/UnitJS/Fun/Curry.js Normal file
View File

@ -0,0 +1,137 @@
return {
curry: curry,
flip: flip,
plus: plus,
minus: minus,
substractTo: substractTo,
times: times,
dividedBy: dividedBy,
divide: divide,
gt: gt,
ge: ge,
eq: eq,
neq: neq,
teq: teq,
tneq: tneq,
lt: lt,
le: le,
and: and,
or: or
};
function curry(f, n, t) {
n = n == undefined ? f.length : n;
t = t == undefined ? [] : t;
if(n < 1) {
return f.apply(null, t);
} else {
return function(x) {
var args = Array.prototype.slice.call(arguments);
return curry(f, n - args.length, t.concat(args));
};
}
}
function flip(f) {
return function(a) {
return function(b) {
return f(b)(a);
}
}
}
function plus(a) {
return function(b) {
return a+b;
}
}
function minus(a) {
return function(b) {
return b-a; // reversed to match the expected infix feel : «minus(4)» should substract 4 to whatever's passed to it, not the other way round — which is implemented by substractTo
}
}
function substractTo(a) {
return function(b) {
return a-b;
}
}
function times(a) {
return function(b) {
return a*b;
}
}
function dividedBy(a) {
return function(b) {
return b/a; // same natural order as for minus — the other way round equivalent is divide
}
}
function divide(a) {
return function(b) {
return a/b;
}
}
function gt(a) {
return function(b) {
return b > a;
};
}
function ge(a) {
return function(b) {
return b >= a;
};
}
function eq(a) {
return function(b) {
return b == a;
};
}
function neq(a) {
return function(b) {
return b != a;
};
}
function teq(a) {
return function(b) {
return b === a;
};
}
function tneq(a) {
return function(b) {
return b !== a;
};
}
function lt(a) {
return function(b) {
return b < a;
};
}
function le(a) {
return function(b) {
return b <= a;
};
}
function and(a) {
return function(b) {
return a && b;
};
}
function or(a) {
return function(b) {
return a || b;
};
}

105
src/UnitJS/Fun/Uncurry.js Normal file
View File

@ -0,0 +1,105 @@
return {
uncurry: uncurry,
flip: flip,
plus: plus,
minus: minus,
times: times,
divide: divide,
gt: gt,
ge: ge,
eq: eq,
neq: neq,
teq: teq,
tneq: tneq,
lt: lt,
le: le,
and: and,
or: or
};
function uncurry(f) {
return function() {
var args = Array.prototype.slice.call(arguments);
var result = f;
while(args.length > 0 && typeof result == 'function') {
result = result.apply(null, args.splice(0, result.length));
}
return result;
}
}
function flip(f) {
return function(a, b) {
return f(b)(a);
}
}
function plus(a, b) { // keeping two arguments so that it can be curryied to Curry.plus by default
return Array.prototype.reduce.call(arguments, function(acc, v) {
return acc+v;
}, 0);
}
function minus(a, b) {
return a-b;
}
function times(a, b) {
return Array.prototype.reduce.call(arguments, function(acc, v) {
return acc*v;
}, 1);
}
function divide(a, b) {
return a/b;
}
function gt(a, b) {
return a > b;
}
function ge(a, b) {
return a >= b;
}
function eq(a, b) {
return a == b;
}
function neq(a, b) {
return a != b;
}
function teq(a, b) {
return a === b;
}
function tneq(a, b) {
return a !== b;
}
function lt(a, b) {
return a < b;
}
function le(a, b) {
return a <= b;
}
function and(a, b) {
for(var i = 0; i < arguments.length; i++) {
if(arguments[i] === false) {
return false;
}
}
return true;
}
function or(a, b) {
for(var i = 0; i < arguments.length; i++) {
if(arguments[i] === true) {
return true;
}
}
return false;
}

View File

@ -30,7 +30,7 @@ EOF
cat <<EOF
var unitJS = (function() {
return {
$(printf "${MODULES}" | sed -e 's|:|,\n|g' -e 's|[^,\n]\+|&: &()|g' | indent | indent)
$(printf "${MODULES}" | sed -e 's|:|,\n|g' -e 's|[^,\n]\+|&: &|g' | indent | indent)
};
$(for file in "${@}"; do includeModule "${file}"; done | indent)
})();