DEV Community

Matt Butcher
Matt Butcher

Posted on

Prolog: Writing a Server-side App in WebAssembly using Spin

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 = "/..."
Enter fullscreen mode Exit fullscreen mode

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)])).


Enter fullscreen mode Exit fullscreen mode

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)])).
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>').
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)