Prolog Projects Tips
As I’ve been writing about here for a while now, I quite like Prolog as a general-purpose programming language. It’s a lovely, very high-level language, quite comparable to Lisp (with M-Expressions instead of S-Expressions) and my implementation of choice, SWI-Prolog, has a very substantial standard library, is open-source, and very easy to contribute to. However, it is still quite a niche language, with a very small user-base, especially outside of academia.
That is something that I was recently talking to with another (the other?) Prolog coder in my city about. Both of us came to Prolog from being software developers and are interested primarily in using it for general coding (web development, TUI apps, etc). While, as mentioned about, SWI-Prolog does have a pretty good assortment of built-in libraries, to be really competitive with the more mainstream languages, it seems like it really just needs more developers using it. Otherwise, one ends up having to implement all sorts of fairly tedious things because no-one else has tried doing that particular task in the language before (for instance, I wrote an HTTP/2 client, a not-insignificant task, because I wanted to be able to send Apple push notifications). But how does one get more people using the language?
I don’t pretend to know the whole answer for that – obviously a lot about popularity comes down to luck – but something that I think would help is a tool or tools to better manage projects.
It seems like most uses of Prolog are for single, large projects that evolve over time, like in academia.
The default Emacs Prolog mode only supports a single top-level (REPL), so if one is trying to jump between multiple different projects, you have to switch all the state of the top-level (I have my own forked version that gives per-project top-levels).
Compare to Clojure, where the REPLs started are per-project.
Similarly, dependencies can be specified in the pack.pl, but not with specific versions, and they’re installed globally.
One of the projects I’d really like to make some day is the equivalent of Clojure’s Leiningen for Prolog, a tool that lets you easily manage projects in a hermetic way (you can make Prolog save packs in a different directory, so you can avoid global dependencies, but it’s not the default or particularly ergonomic).
In the mean time though, here are some handy little scripts I’ve written to help myself manage projects:
Testing
Prolog’s built-in plunit is a very nice unit-testing framework, but the way that it discovers tests is tied to which modules are loaded.
The “normal” way of running them is to load the toplevel, consult your source files, then consult the tests.
This is nice and it even hooks into the make predicate to automatically & incrementally re-run the appropriate tests when you compile (so you could use my automake pack to compile & re-test on change!), but it means that you need to remember to consult all the test files when loading things up.
Additionally, because it’s running in the top-level, it’s possible for there to be local state that makes the tests pass there, but not when starting from a clean slate.
Therefore, I like to have a script that automatically runs all the tests for the project; this also has the benefit of making it easier to set up CI, since it’s just a bash script!
#!/usr/bin/env bash set -euo pipefail declare -a load_test_files for f in test/*.plt; do load_test_files+=( "-l" ) load_test_files+=( "${f}" ) done exec swipl --quiet ${load_test_files[@]} -g plunit:run_tests -g nl -t halt
Straightforward enough; it assumes the test files all have the extension .plt and are in the test directory.
It loads them all, runs the tests, then prints out a newline (for formatting reasons) and quits.
$args = Get-ChildItem -Path test -Filter *.plt | ForEach-Object { @("-l", $_.FullName) } $args += @("-g", "run_tests", "-t", "halt", "--quiet", "-g", "nl") $proc = Start-Process -FilePath swipl_win/bin/swipl.exe -ArgumentList $args -NoNewWindow -Wait -PassThru if ($proc.ExitCode -ne 0) { exit $proc.ExitCode }
The equivalent for Windows Powershell.
I’ve never written Powershell before, so this may be terrible; I just wanted to be able to run tests against Windows in CI after having a number of Windows-specific bugs reported to my Prolog LSP project.
Releasing
This one is a little more involved, to the point it’s written in Prolog instead of bash. I run it to generate a new release for the library.
The first part is pretty simple: update_pack_version/2 parses the version number in the pack.pl file, increments it based on whether the new version is major, minor, or patch (using increment_version/3), then writes the new number back.
git_commit_and_tag/1 does what it says, plus pushes it up origin.
The final step, implemented by register_new_pack/1 is a bit more mysterious.
It fixes the somewhat annoying issue that after pushing a new version, I have to manually install that new version by URL to get the SWI-Prolog server to see the new version & register it (so normal users can just run pack_upgrade(Whatever)).
Previously I would just manually pack_remove/1 the package in a top-level and run pack_install/2, explicitly passing the URL of the bundle to install, but of course, we can automate that!
Knowing that’s why we’re doing this hopefully makes register_new_pack/1 less mysterious.
The download_pattern_format_string/2 helper predicate turns the pattern in pack.pl into a format string (e.g. 'https://example.com/release/*.zip' to "https://example.com/release/~w.zip") so we can use format/3 to download the new archive for the new version.
Unfortunately there’s some special-casing for repos that are still on Github, as for reasons I do not recall the actual download URL and the one specified in the pack file are different, hence the first clause of the predicate.
Finally, main/1 checks that a valid release type was given, prompts for confirmation, then does the thing.
I added the prompt after I accidentally ran the script from history when trying to re-run tests and had to force-push one too many times 😅
#!/usr/bin/env swipl :- module(make_release, []). :- use_module(library(readutil), [read_file_to_terms/3, read_line_to_string/2]). :- initialization(main, main). increment_version(major, [Major0, _Minor, _Patch], [Major1, 0, 0]) :- !, succ(Major0, Major1). increment_version(minor, [Major, Minor0, _Patch], [Major, Minor1, 0]) :- !, succ(Minor0, Minor1). increment_version(patch, [Major, Minor, Patch0], [Major, Minor, Patch1]) :- !, succ(Patch0, Patch1). update_pack_version(ReleaseType, NewVersion) :- read_file_to_terms('pack.pl', PackTerms, []), memberchk(version(OldVersion), PackTerms), atomic_list_concat([MajorS, MinorS, PatchS], '.', OldVersion), maplist(atom_number, [MajorS, MinorS, PatchS], VersionNums0), increment_version(ReleaseType, VersionNums0, VersionNums1), atomic_list_concat(VersionNums1, '.', NewVersion), once(select(version(OldVersion), PackTerms, version(NewVersion), NewPackTerms)), setup_call_cleanup(open('pack.pl', write, S, []), forall(member(T, NewPackTerms), write_term(S, T, [fullstop(true), nl(true), quoted(true), spacing(next_argument) ])), close(S)). git_commit_and_tag(NewVersion) :- shell('git add pack.pl'), shell('git commit -m "Bump version"'), format(atom(TagCmd), "git tag v~w", [NewVersion]), shell(TagCmd), format(atom(PushCmd), 'git push origin master v~w', [NewVersion]), shell(PushCmd). download_pattern_format_string(DownloadURLPat, FormatString) :- string_concat("https://github.com", _, DownloadURLPat), !, % Github download locations are special-cased string_concat(Prefix, "releases/*.zip", DownloadURLPat), string_concat(Prefix, "archive/refs/tags/v~w.zip", FormatString). download_pattern_format_string(DownloadURLPat, FormatString) :- file_name_extension(Base0, Ext, DownloadURLPat), string_concat(Base, "*", Base0), format(string(FormatString), "~s~s.~s", [Base, "v~w", Ext]). register_new_pack(NewVersion) :- read_file_to_terms('pack.pl', PackTerms, []), memberchk(name(ProjectName), PackTerms), memberchk(download(DownloadURLPattern), PackTerms), download_pattern_format_string(DownloadURLPattern, URLFormat), ( pack_remove(ProjectName) -> true ; true ), format(atom(Url), URLFormat, [NewVersion]), pack_install(ProjectName, [url(Url), interactive(false)]). main(Args) :- ( Args = [ReleaseType], increment_version(ReleaseType, [0, 0, 0], _) -> true ; ( format(user_error, "Usage: make_release.pl [major|minor|patch]~n", []), halt(1) ) ), ( stream_property(user_input, tty(true)) -> format("Make new release? [y/n]: ", []), read_line_to_string(user_input, Input), Input == "y" ; true ), update_pack_version(ReleaseType, NewVersion), format("Bumping to ~w~n", [NewVersion]), git_commit_and_tag(NewVersion), register_new_pack(NewVersion).
May these be of some use to you in using & learning Prolog! Some day soon I will try to stitch these together, along with some other helpful libraries, tools, and templates I habitually use, into a nice unified tool that will help bring Prolog to more of a mainstream developer audience. Until then, enjoy and use wisely!