597 lines
14 KiB
Racket
597 lines
14 KiB
Racket
#lang scribble/manual
|
|
|
|
@(require scribble/core
|
|
(for-label racket/base
|
|
"../main.rkt"))
|
|
|
|
@(define (code-box title source)
|
|
(tabular #:style 'boxed
|
|
(list (list (bold title))
|
|
(list (verbatim source)))))
|
|
|
|
@(define (code-pair racket-source js-source)
|
|
(list
|
|
(code-box "Racket / js-maker" racket-source)
|
|
(code-box "Generated JavaScript" js-source)))
|
|
|
|
@(define side-by-side code-pair)
|
|
|
|
@(define (tested s)
|
|
(nested #:style 'inset (bold "Tested behavior: ") s))
|
|
|
|
@title{jsmaker JavaScript Use Cases}
|
|
@author+email["Hans Dijkema" "hans@dijkewijk.nl"]
|
|
|
|
@defmodule[js-maker/demo/js-usecases]
|
|
|
|
This document describes the practical JavaScript use cases in
|
|
@filepath{demo/js-usecases.rkt}. Each implementation is written as a Racket
|
|
snippet using @racket[js] and is tested by compiling it to JavaScript and
|
|
executing that JavaScript with the configured test executor. The corresponding
|
|
tests are in @filepath{testing/jsmaker-usecases.rkt}.
|
|
|
|
The examples are shown vertically: first the Racket/js-maker source, then the
|
|
generated JavaScript. Each code fragment is shown in a boxed documentation
|
|
cell, preserving the light shaded background of the earlier side-by-side
|
|
layout while avoiding narrow, wrapped code columns.
|
|
|
|
The tests intentionally use @racket[js/expression] for their calls wherever
|
|
possible. Raw JavaScript remains only in small harness preambles, such as fake
|
|
@tt{setInterval}, fake DOM objects, and fake @tt{fetch}.
|
|
|
|
@section{Running the examples}
|
|
|
|
Generate the JavaScript examples with:
|
|
|
|
@codeblock{
|
|
racket demo/js-usecases.rkt
|
|
}
|
|
|
|
Run the use-case regression tests with:
|
|
|
|
@codeblock{
|
|
racket testing/jsmaker-usecases.rkt
|
|
}
|
|
|
|
@section{Use cases}
|
|
|
|
@subsection{1. Random number between 1 and 5}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (randomBetween1And5)
|
|
(return (+ (send Math floor (* (send Math random) 5)) 1))))
|
|
RKT
|
|
#<<JS
|
|
function randomBetween1And5() {
|
|
return (Math.floor((Math.random() * 5)) + 1);
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "With Math.random fixed at 0.80, the generated function returns 5.")
|
|
|
|
@subsection{2. Unique values with Set}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (uniqueValues xs)
|
|
(return (send Array from (new Set xs)))))
|
|
RKT
|
|
#<<JS
|
|
function uniqueValues(xs) {
|
|
return Array.from(new Set(xs));
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The array [1, 2, 2, 3, 1] becomes [1, 2, 3].")
|
|
|
|
@subsection{3. Six falsey JavaScript values}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (falseyValues)
|
|
(return (array #f 0 "" js-null js-undefined js-NaN))))
|
|
RKT
|
|
#<<JS
|
|
function falseyValues() {
|
|
return [false, 0, "", null, undefined, NaN];
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "Mapping JavaScript Boolean over the returned values yields six false values.")
|
|
|
|
@subsection{4. Currying}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (add x)
|
|
(return (lambda (y)
|
|
(return (+ x y))))))
|
|
RKT
|
|
#<<JS
|
|
function add(x) {
|
|
return function(y) {
|
|
return (x + y);
|
|
};
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The generated call add(2)(3) returns 5; the test call itself is produced with js/expression.")
|
|
|
|
@subsection{5. Object destructuring}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (describePerson person)
|
|
(let-object ([name 'name]
|
|
[age 'age 0])
|
|
person
|
|
(return (string-append name ":" (number->string age))))))
|
|
RKT
|
|
#<<JS
|
|
function describePerson(person) {
|
|
return (() => {
|
|
const { name: name, age: age = 0 } = person;
|
|
return (name + ":" + String(age));
|
|
})();
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The object { name: \"Ada\", age: 37 } is rendered as \"Ada:37\".")
|
|
|
|
@subsection{6. Escaping a timer interval}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (startTimer)
|
|
(let* ([ticks 0]
|
|
[intervalId #f])
|
|
(set! intervalId
|
|
(setInterval (lambda ()
|
|
(set! ticks (+ ticks 1))
|
|
(when (= ticks 3)
|
|
(clearInterval intervalId)))
|
|
10))
|
|
(return (object 'id intervalId
|
|
'getTicks (lambda () (return ticks)))))))
|
|
RKT
|
|
#<<JS
|
|
function startTimer() {
|
|
{
|
|
let ticks = 0;
|
|
let intervalId = false;
|
|
intervalId = setInterval(function() {
|
|
ticks = (ticks + 1);
|
|
if ((ticks === 3)) clearInterval(intervalId);
|
|
return undefined;
|
|
}, 10);
|
|
return {id: intervalId, getTicks: function() { return ticks; }};
|
|
}
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "A fake timer runs the callback five times, but clearInterval stops it at tick 3.")
|
|
|
|
@subsection{7. Get, set, and delete object properties}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (objectProps)
|
|
(let* ([obj (object 'a 1)]
|
|
[a1 obj.a]
|
|
[a2 (js-ref obj "a")])
|
|
(let-object ([a3 'a]) obj
|
|
(set! obj.b 2)
|
|
(set-prop! obj "c" 3)
|
|
(delete-prop! obj "a")
|
|
(return (array a1 a2 a3 obj.b (js-ref obj "c")
|
|
(send Object hasOwn obj "a")))))))
|
|
RKT
|
|
#<<JS
|
|
function objectProps() {
|
|
let obj = {a: 1};
|
|
let a1 = obj.a;
|
|
let a2 = obj["a"];
|
|
return (() => {
|
|
const { a: a3 } = obj;
|
|
obj.b = 2;
|
|
obj["c"] = 3;
|
|
delete obj["a"];
|
|
return [a1, a2, a3, obj.b, obj["c"], Object.hasOwn(obj, "a")];
|
|
})();
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The three reads produce 1, the two writes produce 2 and 3, and the deleted property is absent.")
|
|
|
|
@subsection{8. String concatenation order}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (concatOrder)
|
|
(return (array (+ 1 2 "3")
|
|
(+ "1" 2 3)))))
|
|
RKT
|
|
#<<JS
|
|
function concatOrder() {
|
|
return [(1 + 2 + "3"), ("1" + 2 + 3)];
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The generated function returns [\"33\", \"123\"].")
|
|
|
|
@subsection{9. Object.freeze versus Object.seal}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (freezeVsSeal)
|
|
(let* ([frozen (send Object freeze (object 'a 1))]
|
|
[sealed (send Object seal (object 'a 1))])
|
|
(set! frozen.a 9)
|
|
(set! sealed.a 9)
|
|
(delete-prop! sealed "a")
|
|
(return (array frozen.a sealed.a
|
|
(send Object isFrozen frozen)
|
|
(send Object isSealed sealed)
|
|
(send Object hasOwn sealed "a"))))))
|
|
RKT
|
|
#<<JS
|
|
function freezeVsSeal() {
|
|
let frozen = Object.freeze({a: 1});
|
|
let sealed = Object.seal({a: 1});
|
|
frozen.a = 9;
|
|
sealed.a = 9;
|
|
delete sealed["a"];
|
|
return [frozen.a, sealed.a, Object.isFrozen(frozen),
|
|
Object.isSealed(sealed), Object.hasOwn(sealed, "a")];
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The frozen value stays 1; the sealed value changes to 9 but remains present.")
|
|
|
|
@subsection{10. Switch example}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (switchExample n)
|
|
(case n
|
|
[(1) (return "one")]
|
|
[(2 3) (return "two-or-three")]
|
|
[else (return "other")]))))
|
|
RKT
|
|
#<<JS
|
|
function switchExample(n) {
|
|
switch (n) {
|
|
case 1: return "one";
|
|
case 2:
|
|
case 3: return "two-or-three";
|
|
default: return "other";
|
|
}
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The generated function maps 1, 2, and 9 to the three expected branches.")
|
|
|
|
@subsection{11. Class constructor with a default value}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define-class Greeter
|
|
(constructor ([name "world"])
|
|
(set! this.name name))
|
|
(method greet ()
|
|
(return (string-append "Hello " this.name))))
|
|
|
|
(define (classExample)
|
|
(let* ([a (new Greeter)]
|
|
[b (new Greeter "Ada")])
|
|
(return (array (send a greet) (send b greet))))))
|
|
RKT
|
|
#<<JS
|
|
class Greeter {
|
|
constructor(name = "world") {
|
|
this.name = name;
|
|
}
|
|
greet() {
|
|
return ("Hello " + this.name);
|
|
}
|
|
}
|
|
function classExample() {
|
|
let a = new Greeter();
|
|
let b = new Greeter("Ada");
|
|
return [a.greet(), b.greet()];
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The default constructor value and explicit constructor argument both work.")
|
|
|
|
@subsection{12. Sort objects by a property}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (sortByProperty xs prop)
|
|
(return (send (send xs slice)
|
|
sort
|
|
(lambda (a b)
|
|
(return (- (js-ref a prop) (js-ref b prop))))))))
|
|
RKT
|
|
#<<JS
|
|
function sortByProperty(xs, prop) {
|
|
return xs.slice().sort(function(a, b) {
|
|
return (a[prop] - b[prop]);
|
|
});
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "Sorting age objects by age yields [20, 25, 30].")
|
|
|
|
@subsection{13. Four ways to delete or remove an array element}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (deleteArrayWays xs)
|
|
(let* ([a1 (send xs slice)]
|
|
[a2 (send xs slice)]
|
|
[a3 (send xs slice)]
|
|
[a4 (send xs slice)])
|
|
(send a1 splice 1 1)
|
|
(set! a2 (send a2 filter (lambda (x i) (return (not (= i 1))))))
|
|
(set! a3 (send (send a3 slice 0 1) concat (send a3 slice 2)))
|
|
(delete-prop! a4 1)
|
|
(return (array a1 a2 a3
|
|
(array (send Object hasOwn a4 "1") (length a4)))))))
|
|
RKT
|
|
#<<JS
|
|
function deleteArrayWays(xs) {
|
|
let a1 = xs.slice(), a2 = xs.slice(), a3 = xs.slice(), a4 = xs.slice();
|
|
a1.splice(1, 1);
|
|
a2 = a2.filter(function(x, i) { return !(i === 1); });
|
|
a3 = a3.slice(0, 1).concat(a3.slice(2));
|
|
delete a4[1];
|
|
return [a1, a2, a3, [Object.hasOwn(a4, "1"), a4.length]];
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "splice, filter, and slice/concat remove the item; delete leaves a hole and preserves length.")
|
|
|
|
@subsection{14. Bubble sort}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (bubbleSort xs)
|
|
(let* ([a (send xs slice)]
|
|
[n (length a)])
|
|
(while (> n 1)
|
|
(let* ([i 1])
|
|
(while (< i n)
|
|
(when (> (list-ref a (- i 1)) (list-ref a i))
|
|
(let* ([tmp (list-ref a (- i 1))])
|
|
(vector-set! a (- i 1) (list-ref a i))
|
|
(vector-set! a i tmp)))
|
|
(set! i (+ i 1))))
|
|
(set! n (- n 1)))
|
|
(return a))))
|
|
RKT
|
|
#<<JS
|
|
function bubbleSort(xs) {
|
|
let a = xs.slice();
|
|
let n = a.length;
|
|
while ((n > 1)) {
|
|
let i = 1;
|
|
while ((i < n)) {
|
|
if ((a[(i - 1)] > a[i])) {
|
|
let tmp = a[(i - 1)];
|
|
a[(i - 1)] = a[i];
|
|
a[i] = tmp;
|
|
}
|
|
i = (i + 1);
|
|
}
|
|
n = (n - 1);
|
|
}
|
|
return a;
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "Sorting [5, 1, 4, 2, 8] yields [1, 2, 4, 5, 8].")
|
|
|
|
@subsection{15. Recursive binary search}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (binarySearch xs target low high)
|
|
(if (> low high)
|
|
(return -1)
|
|
(let* ([mid (send Math floor (/ (+ low high) 2))]
|
|
[value (list-ref xs mid)])
|
|
(cond
|
|
[(= value target) (return mid)]
|
|
[(< value target) (return (binarySearch xs target (+ mid 1) high))]
|
|
[else (return (binarySearch xs target low (- mid 1)))])))))
|
|
RKT
|
|
#<<JS
|
|
function binarySearch(xs, target, low, high) {
|
|
if ((low > high)) return -1;
|
|
let mid = Math.floor(((low + high) / 2));
|
|
let value = xs[mid];
|
|
if ((value === target)) return mid;
|
|
if ((value < target)) return binarySearch(xs, target, (mid + 1), high);
|
|
return binarySearch(xs, target, low, (mid - 1));
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "Searching 7 returns index 3; searching 4 returns -1.")
|
|
|
|
@subsection{16. Count occurrences with Map}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (countOccurrences xs)
|
|
(let* ([counts (new Map)])
|
|
(for ([x (in-list xs)])
|
|
(if (send counts has x)
|
|
(send counts set x (+ (send counts get x) 1))
|
|
(send counts set x 1)))
|
|
(return (send Array from (send counts entries))))))
|
|
RKT
|
|
#<<JS
|
|
function countOccurrences(xs) {
|
|
let counts = new Map();
|
|
for (const x of xs) {
|
|
if (counts.has(x)) counts.set(x, (counts.get(x) + 1));
|
|
else counts.set(x, 1);
|
|
}
|
|
return Array.from(counts.entries());
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "The array [\"a\", \"b\", \"a\", \"c\", \"b\", \"a\"] yields [[\"a\",3],[\"b\",2],[\"c\",1]].")
|
|
|
|
@subsection{17. Get HTML in three ways}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (getHtmlThreeWays)
|
|
(return (array document.body.innerHTML
|
|
(js-dot (send document querySelector "body") innerHTML)
|
|
(js-ref (send document getElementById "root") "innerHTML")))))
|
|
RKT
|
|
#<<JS
|
|
function getHtmlThreeWays() {
|
|
return [document.body.innerHTML,
|
|
document.querySelector("body").innerHTML,
|
|
document.getElementById("root")["innerHTML"]];
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "A fake DOM returns the same HTML string through all three access forms.")
|
|
|
|
@subsection{18. Anagram / rearrangement check}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (sortChars s)
|
|
(return (send (send (send s split "") sort) join "")))
|
|
|
|
(define (canArrange stringA stringB)
|
|
(return (string=? (sortChars stringA) (sortChars stringB)))))
|
|
RKT
|
|
#<<JS
|
|
function sortChars(s) {
|
|
return s.split("").sort().join("");
|
|
}
|
|
function canArrange(stringA, stringB) {
|
|
return (sortChars(stringA) === sortChars(stringB));
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "listen/silent returns true; abc/abd returns false.")
|
|
|
|
@subsection{19. Pairs equal to a target, without repeats}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (pairsEqualTarget xs target)
|
|
(let* ([seen (new Set)]
|
|
[used (new Set)]
|
|
[out (array)])
|
|
(for ([x (in-list xs)])
|
|
(let* ([y (- target x)])
|
|
(if (and (send seen has y)
|
|
(not (send used has x))
|
|
(not (send used has y)))
|
|
(begin
|
|
(send out push (array y x))
|
|
(send used add x)
|
|
(send used add y))
|
|
(send seen add x))))
|
|
(return out))))
|
|
RKT
|
|
#<<JS
|
|
function pairsEqualTarget(xs, target) {
|
|
let seen = new Set();
|
|
let used = new Set();
|
|
let out = [];
|
|
for (const x of xs) {
|
|
let y = (target - x);
|
|
if (seen.has(y) && !(used.has(x)) && !(used.has(y))) {
|
|
out.push([y, x]); used.add(x); used.add(y);
|
|
} else {
|
|
seen.add(x);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "For [1, 2, 3, 4, 3, 5] and target 6 the returned pairs are [[2,4],[3,3],[1,5]].")
|
|
|
|
@subsection{20. Fetch API with result and error handling}
|
|
|
|
@(side-by-side
|
|
#<<RKT
|
|
(js
|
|
(define (loadTitle url)
|
|
(return
|
|
(send
|
|
(send
|
|
(send (fetch url)
|
|
then
|
|
(lambda (response)
|
|
(return (send response json))))
|
|
then
|
|
(lambda (data)
|
|
(return (object 'ok #t 'title data.title))))
|
|
catch
|
|
(lambda (err)
|
|
(return (object 'ok #f 'message err.message)))))))
|
|
RKT
|
|
#<<JS
|
|
function loadTitle(url) {
|
|
return fetch(url)
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) { return {ok: true, title: data.title}; })
|
|
.catch(function(err) { return {ok: false, message: err.message}; });
|
|
}
|
|
JS
|
|
)
|
|
|
|
@(tested "A fake fetch resolves /ok to {ok:true,title:\"Done\"} and rejects /fail to {ok:false,message:\"network\"}.")
|