Extending IPython to run Prolog from a Jupyter Notebook

This post will walk through creating an IPython magic command to run code like this from a Jupyter notebook.

%%prolog
sandwich(caprese).
sandwich(reuben).

run:-
  findall(X, sandwich(X), Y),
  write(Y), nl.

:- initialization run, halt.

I don’t write enough Prolog to dive into writing a new Jupyter kernel just yet, but when I’m writing about Prolog I’ve ended up having code in one directory, runninging the code from the swipl CLI, and copying the code and outputs over to a markdown file with surrounding text. It’s kind of a pain.

Prior to this I’d used “magic” commands like %ls, %pdb, and %edit in IPython notebooks before and seen that commands like %%ruby exist in the documentation so as I was suffering from some copy-paste errors, I found myself wishing for a %%prolog.

For background, the %%ruby command “is a shortcut for %%script ruby” and “[runs] cells with ruby in a subprocess.”, so the first thing I tried was just %%script swipl (-q to silence the welcome message).

%%script swipl -q
A = 1, B = A + 1, write(B).
1+1
A = 1,
B = 1+1.

It works! … well not really. It breaks down quickly once you start defining your own predicates

%%script swipl -q
sandwich(caprese).
sandwich(reuben).

run:-
  findall(X, sandwich(X), Y),
  write(Y), nl.

:- initialization run, halt.
ERROR: Undefined procedure: sandwich/1 (DWIM could not correct goal)
ERROR: Undefined procedure: sandwich/1 (DWIM could not correct goal)
ERROR: Undefined procedure: (:-)/2
ERROR:   Rules must be loaded from a file
ERROR:   See FAQ at http://www.swi-prolog.org/FAQ/ToplevelMode.txt
ERROR: In:
ERROR:    [9] throw(error(existence_error(procedure,...),_4488))
ERROR:    [6] '$dwim':correct_goal((run:- ...,...),user,['X'=_4554,...],_4530) at /usr/lib/swi-prolog/boot/dwim.pl:85

What that error tells me is that we’re in the REPL environment contrary to the documentation of %%script which says it “is like the #! line of script.” The REPL environment is really useful to ask questions of a knowledge base or module of code, but can be a little limitting since it doesn’t like to define new atoms or predicates.

The code right near that comment shows how things really work though: the IPython core kicks off a process running the header line (swipl -q) and sends the contents of the cell as stdin, so something like

swipl -q cell.pl    # documentation says
swipl -q < cell.pl  # actual execution

Though the next step was a little tough to Google for and I got mired in solutions using load_files, but eventually I stumbled upon a built in way to ”[add] rules from the console” which I distilled into

%%script swipl -q -t '[user].'
sandwich(caprese).
sandwich(reuben).

run:-
  findall(X, sandwich(X), Y),
  write(Y), nl.

:- initialization run, halt.
[caprese,reuben]

From %%script to %%prolog

That’s already pretty awesome, but I woke up the next morning knowing my mission wasn’t done yet. The finally step is givinng the header an alia

%alias_magic prolog script -p "swipl -q -t '[user].'"
Created `%%prolog` as an alias for `%%script swipl -q -t '[user].'`.

And now

%%prolog
sandwich(caprese).
sandwich(reuben).

run:-
  findall(X, sandwich(X), Y),
  write(Y), nl.

:- initialization run, halt.
[caprese,reuben]

Updated: