opinionated-logic-development/skills/swi-prolog-programmer/SKILL.md
SWI-Prolog-specific tooling, standards, and idioms. Use when working with SWI-Prolog code. Emphasizes relational thinking, steadfastness, DCGs, constraints, and mandatory testing with PlUnit.
npx skillsauth add pyroxin/opinionated-claude-skills swi-prolog-programmerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert-level SWI-Prolog development centers on thinking in relations not procedures, writing steadfast predicates that work in multiple directions, and embracing the declarative paradigm. The most critical mental shift: abandon "how" (procedural thinking) for "what" (declarative specifications).
Related skills:
logic-programmer - Logic programming fundamentals (relations, unification, search)software-engineer - System design principles and architecturetest-driven-development - Testing philosophy (PlUnit section below covers SWI-specific practices)<logic_programming_fundamentals> Core logic programming principles:
p(X, Y) :- q(X), r(Y) as "X is related to Y when q(X) holds and r(Y) holds"
</logic_programming_fundamentals>This skill focuses on SWI-Prolog-specific tools, idioms, and practices that distinguish veteran developers from beginners.
Target version: Latest development release (9.3.x series as of November 2025) Current stable: 9.2.9.1 (April 2025) Current development: 9.3.34 (November 2025)
Aggressive adoption philosophy: Most developers should use development releases even for production. The development track is released every 2-4 weeks, typically robust, provides latest features, and issues are resolved quickly. The stable track (even minor versions) only receives critical patches and is intended for conservative deployments requiring predictable installations.
SWI-Prolog tries to minimize breaking changes and stay close to the ISO standard. From the SWI-Prolog documentation:[^1] "We try to make as few as possible changes that break backward compatibility..."
[^1]: Jan Wielemaker. SWI-Prolog: Directions. https://www.swi-prolog.org/Directions.html
Version history reference: https://www.swi-prolog.org/ChangeLog
When contributing to existing Prolog projects or open-source:
The aggressive adoption philosophy applies ONLY to codebases you own.
<core_philosophy> SWI-Prolog's design philosophy prioritizes knowledge-intensive interactive systems where logical correctness and development experience trump raw performance. As Jan Wielemaker (creator, 35+ years experience) explains: "My primary motivation has always been to build stuff that works rather than stuff that allows writing an academic paper."
The critical insight: Backtracking provides a time machine for exploring computation paths. Any use of the dynamic database (assert/retract) breaks this superpower. The database is explicitly documented as "a non-logical extension to Prolog" that "destroys all these nice goodies" of logical search.
Veteran developers avoid assert/retract except when information must genuinely survive backtracking, which is rare. Instead: thread state through arguments. </core_philosophy>
Use SWI-Prolog when the problem involves:
Don't choose Prolog for:
The 2010 "Prolog Story" by Kyle Cordes documents a 90% cost reduction on a complex scheduling system—not from coding faster, but from thinking more clearly. The declarative approach forced better problem understanding.
The fundamental shift from imperative programming: think in terms of two variables instead of one. You cannot write i = i + 1 in Prolog because no value equals itself plus one. Instead: I #= I0 + 1 describes the relation between two different variables I0 and I.
As Markus Triska emphasizes: "The same variable cannot reflect two different states, old and new, at the same time."
Procedural reading (wrong): "To find X such that Y holds, do the following steps..."
Declarative reading (correct): "X is related to Y when the following conditions hold..."
For insert(Key, Tree, NewTree), read it as: "insert/3 shows how a key is related to a tree with and a tree without that key." The predicate describes a relationship, not a procedure.
Everything is a relation, so programs work in multiple directions. append/3 with one definition provides four methods:
?- append([1,2], [3,4], ZS). % List construction
ZS = [1,2,3,4].
?- append([1,2], YS, [1,2,3,4]). % List subtraction
YS = [3,4].
?- append(XS, YS, [1,2,3,4]). % Generate all partitions
XS = [], YS = [1,2,3,4] ;
XS = [1], YS = [2,3,4] ;
XS = [1,2], YS = [3,4] ;
XS = [1,2,3], YS = [4] ;
XS = [1,2,3,4], YS = [].
?- append(XS, YS, ZS). % Most general query
Four methods for the price of one! Write predicates that work bidirectionally when possible.
% If this succeeds:
?- foo(X), X = x.
% Then this must succeed:
?- foo(x).
% And vice versa - if foo(x) succeeds, then foo(X), X = x must succeed.
Predicates that fail this test are broken by definition. Test steadfastness by:
Example of non-steadfast code (broken):
% BAD: Only works when Max is unbound
get_max(X, Y, Max) :- X >= Y, Max = X.
get_max(X, Y, Max) :- X < Y, Max = Y.
?- get_max(3, 5, M). % Works
M = 5.
?- get_max(3, 5, 5). % Should succeed but fails!
false.
Steadfast version:
% GOOD: Works regardless of Max instantiation
max_value(X, Y, X) :- X >= Y.
max_value(X, Y, Y) :- X < Y, Y > X.
</steadfastness>
The official SWI-Prolog CLP(FD) documentation[^2] states: "If you are used to the complicated operational considerations that low-level arithmetic primitives necessitate, then moving to CLP(FD) constraints may, due to their power and convenience, at first feel to you excessive and almost like cheating. It isn't."
[^2]: Markus Triska. CLP(FD): Constraint Logic Programming over Finite Domains. SWI-Prolog. https://www.swi-prolog.org/pldoc/man?section=clpfd
Constraints are integral to modern Prolog and designed to eliminate low-level primitives. When teaching Prolog, CLP(FD) constraints should be introduced before explaining low-level arithmetic because they're easier to explain and use.
The most important arithmetic constraint is #=/2, which subsumes both is/2 and =:=/2 over integers. Benefits:
% Not idiomatic (low-level, directional)
?- X is 5 + 3.
X = 8.
?- 8 is Y + 3. % Error! Can't work backwards
ERROR: is/2: Arguments are not sufficiently instantiated
% Idiomatic (declarative, multidirectional)
?- X #= 5 + 3.
X = 8.
?- 8 #= Y + 3. % Works backwards!
Y = 5.
?- Z #= A + B, Z #= 8, A #= 5. % Propagates constraints
Z = 8, A = 5, B = 3.
Standard approach for combinatorial problems:
solve(Vars) :-
Vars = [X, Y, Z],
Vars ins 1..10, % Domain declaration
X + Y #= Z, % Constraints
X #< Y,
label(Vars). % Search for solutions
Constraint propagation happens automatically. Use built-in constraints:
all_different/1 - All variables must take different valuessum/3 - Sum of list equals valuescalar_product/4 - Dot product constraintselement/3 - Indexing constraintscumulative/2 - Resource schedulingThis is the preferred approach for:
<dcg_patterns>
DCGs (Definite Clause Grammars) are not just for parsing—they're a general-purpose mechanism for threading state through computations. As Markus Triska states: "In every serious Prolog program, you will find many opportunities to use DCGs."
Veterans recognize DCGs as similar to monads in functional programming, providing automatic threading of two hidden arguments through all rules.
Use DCGs when:
The third case is critical for design—DCGs excel at implicit state threading when state flows through computation but only certain predicates need it.
Pattern recognition heuristic: When you see predicates passing through arguments like pred(..., N0, N1) where intermediate values N1, N2, N3 are only being threaded through without direct modification, consider DCGs. This is the accumulator pattern signaling a DCG opportunity.
The key idiom for explicit state access:
state(S), [S] --> [S]. % Read current state
state(S0, S), [S] --> [S0]. % Update state from S0 to S
Example: counting tree leaves without explicit threading:
num_leaves(Tree, N) :-
phrase(num_leaves_(Tree), [0], [N]).
num_leaves_(nil), [N] --> [N0], { N #= N0 + 1 }.
num_leaves_(node(_,Left,Right)) -->
num_leaves_(Left),
num_leaves_(Right).
Notice the second rule makes no reference to state—DCGs thread it implicitly. This is the power of the abstraction.
Tree to list conversion:
tree_nodes(nil) --> [].
tree_nodes(node(Name, Left, Right)) -->
tree_nodes(Left),
[Name],
tree_nodes(Right).
Pipeline pattern for transformations:
steps --> square, half, round.
square, [Y] --> [X], { Y is X * X }.
half, [Y] --> [X], { Y is X / 2 }.
round, [Y] --> [X], { Y is round(X) }.
convert(Float, Integer) :-
phrase(steps, [Float], [Integer]).
Lazy list processing with library(pio):
:- use_module(library(pio)).
process_file(Result) :-
phrase_from_file(my_dcg(Result), 'large_file.txt').
File contents loaded lazily; unused portions can be garbage collected.
From Paulo Moura (Logtalk creator): "DCGs provide a threading state abstraction: don't break it." Always use phrase/2 or phrase/3 to invoke DCGs. Never call DCG-compiled predicates directly—this breaks abstraction and ties code to implementation details.
</dcg_patterns>
<code_style>
The authoritative "Coding Guidelines for Prolog" (Covington et al., 2011, Theory and Practice of Logic Programming) states: "As far as we know, a coherent and reasonably complete set of coding guidelines for Prolog has never been published." This 39-page paper remains the definitive reference.
Reference: https://www.covingtoninnovations.com/mc/plcoding.pdf
Predicate names: Use underscore_style, never camelCase. Rationale: "Underscores resemble spaces...an important readability tool that is a thousand years old."
sorted_list/1, well_formed/1, in_tree/2)remove_duplicates/2, print_contents/1)Argument order: "Inputs before outputs" with the Space-Is-Time principle—if X is needed before Y temporally, place X before Y spatially. This aligns with first-argument indexing optimization.
Choose predicate names showing argument order: mother_child/2 not mother_of/2 (which is ambiguous).
Variable names: Use descriptive names like ResultsSoFar or Results_so_far. Single letters only with established conventions:
For threaded state: State0, State1, State2, ..., State (final) or State_in, State_tmp, State_tmp1, ..., State_out.
Indentation: 4 spaces (Covington) or 3 spaces (Lifeware/INRIA). Never tabs—"You cannot normally predict how tabs will be set." Maximum 80 columns.
Each subgoal on separate line except very short I/O sequences. First line has head and :- only:
predicate_name(Arg1, Arg2, Result) :-
goal1(Arg1, Intermediate),
goal2(Arg2, Intermediate, Result).
Vertical spacing:
Every public predicate must have PlDoc structured comments:
%! predicate_name(+Input:type, -Output:type) is det.
%
% Description using **Markdown** formatting.
% Multiple lines explaining behavior.
%
% @arg Input Description of input argument
% @arg Output Description of output argument
% @throws error_type When this condition occurs
%
% Example:
% ==
% ?- predicate_name(input, Output).
% Output = result.
% ==
predicate_name(Input, Output) :-
implementation.
Mode indicators (Covington system):
+ for nonvar input (must be instantiated)- for var output (will be instantiated)? for either direction@ for not further instantiated* for ground on entry! for mutable structureDeterminism indicators:
det - exactly once (deterministic)semidet - at most once (may fail, no choice points)multi - at least once (always succeeds, may have choice points)nondet - any number of times (may fail, may have choice points)
</code_style><cut_usage>
Green cut: Only prunes computation paths not contributing new solutions, doesn't change declarative meaning, safe to add or remove.
max(X, Y, X) :- X >= Y, !.
max(X, Y, Y) :- X < Y.
The cut is green because the test in the first clause guarantees the second won't succeed if the first does.
Red cut: Changes declarative meaning and program behavior.
% BAD: Red cut with major flaw
rc_minimum(X, Y, X) :- X =< Y, !.
rc_minimum(_, Y, Y).
?- rc_minimum(1, 2, 2). % Succeeds incorrectly!
true.
Guidelines:
make/0 is THE essential workflow tool. It checks timestamps, reloads only changed files, updates autoload indices, and preserves module context.
The edit-reload-test cycle:
?- edit(predicate). % Edit in configured editor
% [Make changes, save]
?- make. % Reload (auto-runs tests if configured)
?- test_predicate. % Manual testing
Configure tests to run automatically:
:- set_test_options([run(make)]).
Now every make. invocation runs the test suite.
Quote from documentation: "There is really no excuse not to write tests! Unit testing is less about testing than about proper and manageable coding—it's like having a lab notebook."
Tests document expected usage, validate implementation claims, and prevent regressions.
Basic test structure:
:- begin_tests(mymodule).
test(simple_case) :-
mymodule:predicate(input, expected).
test(with_verification, [true(X == 42)]) :-
mymodule:compute(X).
test(should_fail, [fail]) :-
mymodule:invalid_input(bad).
test(expecting_error, [error(type_error(_, _))]) :-
mymodule:predicate(wrong_type).
test(with_setup, [
setup(create_temp_file(File)),
cleanup(delete_file(File))
]) :-
mymodule:process_file(File).
test(all_solutions, [all(X == [1,2,3])]) :-
mymodule:generate(X).
test(nondet_expected, [nondet]) :-
mymodule:generate_multiple(_).
:- end_tests(mymodule).
Test organization:
.plt extension for test files matching .pl source filestest/ directoryswipl -g run_tests -t halt file.pl or ?- run_tests. from REPLLogic programming testing philosophy:
all(X == List))Tests document logical relationships more than execution order.
Run check. before every commit. This comprehensive check finds:
Individual checks:
list_undefined. - undefined predicateslist_void_declarations. - declared but not definedlist_trivial_fails. - always-failing predicatesGraphical cross-referencer: gxref. for visual dependency analysis.
Style checks (enable during development):
?- style_check(+singleton). % Warn on singleton variables
?- style_check(+discontiguous). % Warn on non-contiguous clauses
?- style_check(+no_effect). % Warn on ineffective code
Use gtrace, not trace: The graphical debugger is far superior to command-line.
?- gtrace. % Enable graphical tracing
?- gspy(pred/N). % Set spy point on predicate
Three-panel view shows source code, variables, and stack. Color-coded execution:
Supports hot-fixing (edit during debug), visualizes choice points, and provides source-level debugging.
Four-port model: Call → Exit (success) / Fail (failure) / Redo (backtrack)
Key commands: Space=step, s=skip, f=fail, r=retry, a=abort, b=break to REPL, e=edit source
Alternative: debug/3 library for instrumentation without tracing:
:- use_module(library(debug)).
?- debug(topic).
debug(algorithm, 'Processing: ~w', [Data]).
Zero overhead when disabled, fine-grained control by topic.
VSCode: Use "VSC-Prolog" or "New-VSC-Prolog" extensions. Features:
Emacs (recommended for serious development): Use Sweep (official, embeds SWI-Prolog as Emacs module).
Install from NonGNU ELPA:
(package-install 'sweeprolog)
Requires SWI-Prolog 8.5.18+ and Emacs 27+. Provides:
Alternative: lsp-mode + lsp_server pack for Language Server Protocol support. </tooling>
<project_structure>
Standard directory structure for larger projects:
myproject/
├── load.pl # Main loader with environment setup
├── run.pl # Application starter
├── save.pl # Creates saved states
├── debug.pl # Debugging configuration
├── prolog/ # Source code
│ ├── main.pl
│ ├── module1.pl
│ └── myproject/ # Private/helper modules
│ └── internal.pl
├── test/ # Unit tests
│ ├── test_module1.plt
│ └── test_module2.plt
├── examples/ # Usage examples
├── pack.pl # Pack metadata if distributing
├── README.md
└── LICENSE
Key organizational files:
load.pl - Sets up environment (Prolog flags, file search paths) and loads sourcesrun.pl - Starts application by loading load.pl in silent mode and calling startup predicatessave.pl - Creates saved states with qsave_program/2debug.pl - Sets up debugging environmentEvery .pl file should be a module. Module name must match filename: my_module.pl contains :- module(my_module, [...]).
Export list includes only public predicates. Private modules go in subdirectories named after the pack.
:- module(module_name, [
exported_predicate/2,
another_export/1,
op(700, xfx, custom_op)
]).
%! exported_predicate(+Input, -Output) is det.
% Documentation using PlDoc syntax
exported_predicate(Input, Output) :-
helper_predicate(Input, Output).
% Private helper - not exported
helper_predicate(X, Y) :- Y is X * 2.
Set up project-specific file search paths:
:- prolog_load_context(directory, Dir),
asserta(user:file_search_path(myapp, Dir)).
user:file_search_path(graph, myapp(graph)).
user:file_search_path(ui, myapp(ui)).
:- use_module(graph(algorithms)).
:- use_module(ui(main_window)).
</project_structure>
<essential_libraries>
Mandatory (in order of importance):
member/2, append/3, length/2, reverse/2, nth0/3, sort/2, sum_list/2maplist/2-5, foldl/4-6, include/3, exclude/3, partition/4 (higher-order operations)whites//0, string//1, number//1, integer//1, digits//1 (DCG utilities)must_be/2, is_of_type/2, error constructors for proper error handlingHighly recommended:
#=/2, ins/2, label/1, all_different/1)aggregate/3-4, aggregate_all/3-4 for aggregation operationsoption/2-3, select_option/3-4, merge_options/3 for option list handlingdebug/3, assertion/1 for instrumentation[X,Y]>>Body) for inline goalsSpecialized but important:
<common_mistakes>
<from_imperative>
Trying to modify state destructively:
% WRONG: Trying to treat variables like imperative memory
X = 5,
X = X + 1. % ERROR: Can't unify 5 with 6!
% RIGHT: Use different variables for different states
X0 = 5,
X1 is X0 + 1. % Or: X1 #= X0 + 1
Using assert/retract for intermediate results:
% WRONG: Using database for computation state
compute(Input, Result) :-
retractall(temp(_)),
assert(temp(Input)),
process,
temp(Result).
% RIGHT: Thread state through arguments
compute(Input, Result) :-
process(Input, Result).
Over-reliance on execution order:
% WRONG: Assuming left-to-right evaluation matters for correctness
pred(X, Y) :-
compute_y(Y), % Assumes Y needed first
validate_x(X).
% RIGHT: Write predicates that work regardless of goal order
pred(X, Y) :-
validate_x(X),
compute_y(Y).
% Both orders should produce same logical result
Fighting with the language: Having a war with Prolog usually indicates procedural thinking hasn't been abandoned. Embrace relational thinking. </from_imperative>
<from_functional>
Expecting pattern matching to work one-way:
% Prolog uses unification (bidirectional), not pattern matching (one-way)
% This works in multiple directions:
?- append([1,2], [3,4], Xs). % Forward
?- append([1,2], Ys, [1,2,3,4]). % Backward
?- append(As, Bs, [1,2,3,4]). % Generate all splits
Missing that variables can be uninstantiated:
In Haskell, all variables are bound. In Prolog, variables can remain unbound and represent constraints:
?- X #> 5, X #< 10.
X in 6..9. % X is constrained but not bound to specific value
Trying to use guards that don't exist:
Prolog doesn't have guards like Haskell. Use goals in body instead:
% Not valid Prolog syntax (Haskell-style guards)
% max(X, Y, X) | X >= Y.
% Correct Prolog
max(X, Y, X) :- X >= Y.
max(X, Y, Y) :- X < Y, Y > X.
</from_functional>
<from_oop>
Creating unnecessary getter/setter predicates:
% WRONG: Java-style getters/setters
get_name(person(Name, _), Name).
set_name(person(_, Age), Name, person(Name, Age)).
% RIGHT: Direct unification
?- Person = person(Name, Age).
% Access fields by unification, not getters
Blindly using is_ prefix for everything:
From Covington: "Do not blindly import styles from other languages. For instance, do not blindly follow the Java tradition by calling everything in sight is_xxx or get_yyy."
Use appropriate Prolog naming:
sorted/1, member/2, connected/2is_sorted/1, is_member/2, is_connected/2Importing Java naming conventions:
% WRONG: camelCase (Java style)
myPredicate(inputValue, outputResult).
% RIGHT: underscore_style (Prolog convention)
my_predicate(input_value, output_result).
</from_oop> </common_mistakes>
<anti_patterns>
Red flags that code is written by someone fighting the language:
Using assert/retract for intermediate results: Breaks backtracking's time-machine debugging and is usually slow. Pass state through arguments instead.
Cut at end of last clause: pred(X) :- goal1, goal2, !. on the final clause—what alternatives is it eliminating? Almost always wrong.
Procedural variable reuse: Trying to write X = 5, X = 6 expecting X to "change." Use different variables: X = 5, Y = 6.
Directional code that only works one way: Not writing steadfast predicates. get_max(X, Y, Max) that fails if Max is already bound is broken.
Over-use of cuts and guards: Trying to write imperative control flow. Prolog is not C with weird syntax.
Using is/2 instead of #=/2 for integer arithmetic: Low-level arithmetic is directional, constraints are relational and propagate.
Not using higher-order predicates: Writing manual recursion instead of maplist/foldl. Veterans recognize standard patterns.
Poor error handling: No input validation, unclear error messages, not following ISO error conventions. Use must_be/2 from library(error).
Fighting relational nature: Thinking "how do I modify this" instead of "what relation holds between old and new state." </anti_patterns>
<design_patterns>
Accumulator pattern for aggregating results through recursion:
sum_list(List, Sum) :-
sum_list_(List, 0, Sum).
sum_list_([], Acc, Acc).
sum_list_([H|T], Acc0, Sum) :-
Acc1 is Acc0 + H,
sum_list_(T, Acc1, Sum).
Difference lists for O(1) list concatenation:
% Difference list append: O(1) instead of O(n)
append_dl(A-B, B-C, A-C).
% Example usage
?- append_dl([1,2|X]-X, [3,4|Y]-Y, List-[]).
List = [1,2,3,4].
Higher-order patterns with library(apply):
% Map operation
?- maplist(plus(1), [1,2,3], Result).
Result = [2,3,4].
% Filter operation
?- include(>(5), [1,6,3,8,2], Result).
Result = [6,8].
% Fold operation
?- foldl(plus, [1,2,3,4], 0, Sum).
Sum = 10.
Constraint programming pattern (covered earlier):
<production_checklist>
Before considering code production-ready:
Mandatory:
run_tests.check. runs without errors or warningsHighly Recommended:
show_coverage/1)examples/ directoryFor Distributed Packs:
pack.pl with complete metadataPrimary references:
Key papers:
Community resources:
Richard O'Keefe's three themes (The Craft of Prolog):
Jan Wielemaker on philosophy: "My primary motivation has always been to build stuff that works rather than stuff that allows writing an academic paper."
Markus Triska on declarative thinking: "Better ask: What are the cases and conditions that make this predicate true? Think in terms of relations between entities you are describing."
Kyle Cordes on the Prolog advantage: "We cut the cost/time of this implementation by 90% or more, not by coding more quickly, but by thinking more clearly."
From Covington et al. on efficiency: "The most efficient program is the one that does the right computation, not the one with the most tricks. By choosing a better basic algorithm, you can sometimes speed up a computation by a factor of a million."
On quality: "There is really no excuse not to write tests! Automatic testing of software during development is probably the most important Quality Assurance measure."
data-ai
--- name: interactive-research description: Multi-source research orchestrated across a persistent agent team. Use for queries asking for research-style investigation of a topic — comparing, contrasting, surveying, investigating, evaluating, deeply researching, doing a literature review, or any similar research intent — or that ask what people are saying publicly about a topic (discourse, expert opinion, industry consensus, and related framings). Example phrasings (illustrative, not exhaustive):
development
Expert-level guidance for creating high-quality Claude Code skills. Use alongside Anthropic's skill-creator when creating new skills, improving existing skills, or needing guidance on skill content quality. Complements basic skill mechanics with research-driven content development, XML tag structuring, decision frameworks over mechanics, cross-references between skills, and systematic validation.
development
--- name: deep-research description: Deprecated name; the skill formerly here is now opinionated-research:interactive-research. Retained as a context-level notice because Claude Code now ships a built-in deep-research workflow that runs one-shot, non-interactive fan-out research (no conversation after the report). interactive-research instead runs a persistent agent team you can keep questioning once the report lands. Guidance for Claude: when the user makes a research request, ask whether they
development
Pedagogical framework for teaching programming through Socratic dialogue. Use when a learner wants to LEARN programming rather than have code written for them. Triggers include "teach me", "help me understand", "I'm learning", "tutor mode", or requests to not provide solutions. Emphasizes productive struggle, graduated hints, metacognitive scaffolding, and emotional support.