Prolog: Writing a Server-side App in WebAssembly using Spin
Matt Butcher
Posted on October 31, 2023
This post walks through creating a simple Prolog serverless function. Prolog is a logic-oriented language with several implementations. We’ll be using Trealla Prolog.
I am new to Prolog, so this is an exploration for me as well. For those more seasoned in the language, feel free to leave corrections and so on in the comments below. I probably used incorrect terminology and so on.
Prerequisites:
Configuring Spin for Prolog Support
To install Prolog support in Spin, you can add the template:
$ spin templates install --git https://github.com/guregu/trealla-spin --update
This uses the amazing Prolog Spin template and the Traella Prolog interpreter. I am a huge fan of this template because, as a newcomer to Prolog, it got me started right away.
Building an App
First, we’ll create a new app using the newly installed http-prolog
template. We’ll name the project hello-prolog
:
$ spin new http-prolog hello-prolog --accept-defaults
The option —accept-defaults
just tells Spin not to walk us through the creation wizard and instead use the default settings.
Now we have a directory named hello-prolog
with just a few files:
$ tree hello-prolog/
hello-prolog/
├── spin.toml
└── src
└── init.pl
1 directory, 2 files
Since Prolog is an interpreted language, we don’t necessarily need to build our source into a Wasm binary. All we need is a copy of the interpreter that is itself compiled to WebAssembly. If we take a look at spin.toml
(the Spin configuration file), we’ll see exactly that:
spin_version = "1"
authors = ["Matt Butcher <matt.butcher@fermyon.com>"]
description = ""
name = "hello-prolog"
trigger = { type = "http", base = "/" }
version = "0.1.0"
[[component]]
id = "hello-prolog"
files = [ { source = "./src", destination = "/" } ]
# for outgoing HTTP (see spin:http_fetch/3)
allowed_http_hosts = [] # "insecure:allow-all" is a special unsafe value to allow any host
# access to key-value stores (see spin:store_* predicates)
key_value_stores = ["default"]
[component.source]
url = "https://github.com/guregu/trealla/releases/download/v0.14.4/libtpl-spin.wasm"
digest = "sha256:6adb31903bc55e2b5ef3db1619727596f0b08bb789ff6c42df458d0209228677"
[component.trigger]
route = "/..."
Note that the url
line tells Spin to download the libtpl-spin.wasm
file.
While many of the basic Spin templates produce boilerplate code, the Prolog template produces a great example that generates HTML, uses Key Value Store, serializing to JSON, and illustrates some cool features of Prolog. The program is in the src/init.pl
:
:- use_module(library(spin)).
% See library/spin.pl for all the predicates built-in
% https://github.com/guregu/trealla/blob/main/library/spin.pl
%% http_handler(+Spec, +Headers, +Body, -Status)
http_handler(get("/", _QueryParams), _RequestHeaders, _RequestBody, 200) :-
html_content,
setup_call_cleanup(
store_open(default, Store),
(
( store_get(Store, counter, N0)
-> true
; N0 = 0
),
succ(N0, N),
store_set(Store, counter, N)
),
store_close(Store)
),
http_header_set("x-powered-by", "memes"),
current_prolog_flag(dialect, Dialect),
% stream alias http_body is the response body
write(http_body, '<!doctype html><html>'),
format(http_body, "<h1>Hello, ~a prolog!</h1>", [Dialect]),
format(http_body, "Welcome, visitor #<b>~d!</b>", [N]),
write(http_body, '</html>').
http_handler(get("/json", _), _, _, 200) :-
wall_time(Time),
% json_content({"time": Time}) works too
json_content(pairs([string("time")-number(Time)])).
The sample above implements two HTTP routes:
- Requests to
/
will get an HTML page with a view counter - Requests to
/json
will get the server’s current time serialized into a JSON document
The JSON endpoint
Let’s look at the /json
one first:
http_handler(get("/json", _), _, _, 200) :-
wall_time(Time),
% json_content({"time": Time}) works too
json_content(pairs([string("time")-number(Time)])).
This applies the http_handler
rule. First it fetches the time (wall_time()
), then it transforms the time the JSON object {“time”: :1698343563}
(where that long number is the UNIX timestamp).
The HTML endpoint
We already looked at the second http_handler()
rule, which generates JSON. Let’s take a quick high-level look at the first.
This rule does two things:
- It manages a page view counter using Spin’s key value storage
- It generates some HTML to return to the user
The key value storage logic is this part:
store_open(default, Store),
(
( store_get(Store, counter, N0)
-> true
; N0 = 0
),
succ(N0, N),
store_set(Store, counter, N)
),
store_close(Store)
Essentially, with an open store, we check to see if the counter is already present. If not, it is initialized to 0. Then the counter is incremented and stored. At the end, the store is closed.
So each time this rule is invoked, the counter will be incremented.
The second part of this handler creates the HTTP response:
http_header_set("x-powered-by", "memes"),
current_prolog_flag(dialect, Dialect),
% stream alias http_body is the response body
write(http_body, '<!doctype html><html>'),
format(http_body, "<h1>Hello, ~a prolog!</h1>", [Dialect]),
format(http_body, "Welcome, visitor #<b>~d!</b>", [N]),
write(http_body, '</html>').
The HTTP header X-Powered-By
is set to memes
. This is just a fun illustration of how to set a header.
Afterward, the next four terms write out HTML. The two terms that use format
are substituting terms (the Prolog Dialect
and the counter N
from key value storage) into the string.
When we access the endpoint, this is what we’ll see:
$ curl localhost:3000/
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>1!</b></html>
And running it again, we’ll see the counter increment:
curl localhost:3000/
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>2!</b></html>
Deploying to Fermyon Cloud
The notes on the spin-prolog
README suggest that the demo will not work on Fermyon Cloud. However, I tested it out, and it works great!
I did a spin deploy
(after doing a spin login
using GitHub integration):
$ spin deploy
Uploading hello-prolog version 0.1.0-rb0b520ce to Fermyon Cloud...
Deploying...
Waiting for application to become ready....... ready
Available Routes:
hello-prolog: https://hello-prolog-grl101su.fermyon.app (wildcard)
This produced a public endpoint (which you can test. I left it running).
Testing with curl
a few times, I see:
$ curl https://hello-prolog-grl101su.fermyon.app
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>1!</b></html>
$ curl https://hello-prolog-grl101su.fermyon.app
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>2!</b></html>
$ curl https://hello-prolog-grl101su.fermyon.app
<!doctype html><html><h1>Hello, trealla prolog!</h1>Welcome, visitor #<b>3!</b></html>
As the notes on the Spin Prolog page note, most of the Fermyon Cloud services are accessible through the SDK, though the serverless AI LLM inferencing is not yet available.
Wrap-Up
Prolog is a new language for me. I have a background in formal logic, though, and it is such an enticing language from that perspective.
I'm super grateful to Guregu for making my first forays into the language so pleasant.
Posted on October 31, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.