13.3 Accessing JavaScript from Prolog
This section describes how we can interact with JavaScript from Prolog. The interface is captured in a single predicate :=/2/.
- Left := Right
- Depending on Left, this predicate implements two different
actions. If Left is a Prolog variable, it evaluates the
expression
Right in JavaScript and unifies the result to Left.
If
Left is a term Obj[Key], where Key
is an atom, it accesses a JavaScript setter. The general form
of an expression is Expression[Callable] or simply Callable.
If Callable is compound it expresses a function (or method)
call. Otherwise we call JavaScript eval(), except for these
special values:
- window
- The main browser window itself (
undefined
when not in a browser). - prolog
- The
Prolog
instance.
Prolog values are translated according to the rules in section 13.2.2.2 and the result is translated back to Prolog according to the rules in section 13.2.2.1. Because callables are translated to function calls, object properties or global variables we need an escape to pass them as data. This is achieved using the prefix operator
. Note that lists are passed as JavaScript arrays rather than calls to the list functor. For convenience Prolog strings are by default translated to JavaScript#
String
objects rather thanProlog.String
instances. Below are some examples:?- Res := myfunc([1,2,3]). ?- Max := 'Math'.max(10, 20). ?- Out := document.getElementById('output'). ?- Par := document.createElement(p), Par.textContent := #Text. ?- Par.textContent := "aap" + " " + "noot".
Some JavaScript expressions are not implemented as functions. The following “functions” are handled directly by the implementation.
- instanceof
- Returns the name of the class to which the object belongs. Same as
Obj.constructor.name
. - instanceof(ClassName)
- Returns a
Boolean
indicating whether the object is an instance of ClassName. Note that the class name must be an atom and as JavaScript class names normally start with a capital, the names dypically need to be quoted using single quotes. For example:?- W := window, T := W.instanceof('Window'). W = <js_Window>(1), T = true.
-
(Any)- Numerical negation
!
(Any)- Logical negation.
+
(Any, Any)-
(Any, Any)*
(Any, Any)/
(Any, Any)&
(Any, Any)|
(Any, Any)&&
(Any, Any)||
(Any, Any)- Binary operators. Note that some are not defined as Prolog operators and
thus one must write e.g.
A := &&(true,false)
.||
is not a Prolog atom, so logical disjunction getsA := '||'(false,false)
.
- [semidet]is_object(@Term)
- True if Term is a reference to a JavaScript object.
- [semidet]is_object(@Term, ?Class)
- True when Term is an instance of Class. If Class
is unbound it is unified with the name of the constructor,
otherwise a JavaScript
Term instanceof Class
is executed. - js_script(+String, +Options)
- Evaluate String as JavaScript. This is designed to cooperate
with string quasi quotations, so we can write e.g.,
:- use_module(library(strings)). :- js_script({|string|| function myfunc(a) ... |}).
The implementation uses =:/2, calling the JavaScript function eval().
- fetch(+URL, +Type, -Data)
- Wrapper around JavaScript fetch(), conversion of the
Response
object and waiting for thePromise
. Type is an atom or string that is used as method on theResponse
object. Examples aretext
,json
,html
orblob
. Theblob
type returns the Data as a string of bytes, i.e., character codes in the range 0 ... 255.
13.3.1 Asynchronous access to JavaScript from Prolog
While section 13.3
describes synchronous calls from Prolog to JavaScript, we also need
asynchronous calling to implement sleep/1,
wait for user input, downloading documents from the web, etc.
Asynchronous calling is achieved by yielding from the Prolog
virtual machine. This can only be done when Prolog is told to expect
that the VM may yield. This is implemented by Prolog.forEach()
as described in section
13.2.
- [det]await(+Promise, -Result)
- Yield the Prolog VM, returning control back to JavaScript. When this is
called from Prolog invoked using
Prolog.forEach()
, execution of await/2 completes when the Promise resolves and Result is unified with the value passed to thePromise.then()
method. As an exception to the normal conversion rules, if the result is a singleString
, it is returned as a Prolog string rather than an atom. When the Promise is rejected await/2 throws an exception. Note that await/2 allows, for example, downloading a URL from Prolog:?- FP := fetch("test.pl"), await(FP, Response), TP := Response.text(), await(TP, T). FP = <js_Promise>(4), Response = <js_Response>(5), TP = <js_Promise>(6), T = "% :- debug(js) ...".
Calls to await/2 may be asynchronously aborted by calling
Prolog.abort()
if Promise implements.abort()
. See section 13.3.2 for implementing such a promise. - [semidet]is_async
- True when we can call await/2 in the current state. This implies Prolog has been called from JavaScript code that is prepared to deal with Prolog yielding and Prolog is not inside a callback from C (WASM).
13.3.2 JavaScript Promise that can be aborted
A Promise
resolves or is rejected. As Prolog waits for a
specific promise on a call to await/2
we may want to abort long running operations. This may be achieved using
the class
Prolog.Promise
which extends Promise
. To make
the promise abortable the executor function must have an
abort
property. Below is the code for
Prolog.promise_sleep()
that implements this schema.
First we create the executor and use properties on the function
itself to represent the necessary state information (here, the running
timer). Next, we add an abort
property the clears the timer
and runs the reject
callback of the Promise
.
Finally we return an instance of Prolog.Promise
which
implements .abort()
.
promise_sleep(time) { const f = function(resolve, reject) { f.reject = reject; f.timer = setTimeout(() => { f.timer = undefined; resolve(true); }, time*1000); }; f.abort = function() { if ( f.timer ) { clearTimeout(f.timer); f.timer = undefined; f.reject("abort"); } } return new Prolog.Promise(f); }