One Predicate to Parse and Generate Protocol Messages with Prolog Unification
Define one message shape in Prolog and use unification to both parse incoming messages and serialize outgoing packets without duplicating logic.
31 Jul 2025, 17:03 UTC

The Parser-Serializer Drift Problem
Many network services maintain two separate routines for the same wire format: one parser that turns incoming bytes into data structures, and one serializer that turns those structures back into bytes. The problem is that they inevitably drift. A new field added to the parser is forgotten in the serializer, or a validation rule changes in one place but not the other. The useful takeaway is that Prolog's unification lets you describe the message shape once, and the same clause can validate, extract, or construct it.
Unification Works in Both Directions
Unification is Prolog's built-in mechanism for making two terms identical. A term can be a constant, a variable, or a compound term such as msg(telemetry, 101, 19.3). When Prolog unifies a pattern containing variables with a concrete term, it binds the variables to the matching subterms. Because the process is symmetric, the same pattern can be used to check an incoming message or to build an outgoing one.
Worked Example: Telemetry Message Frame
Suppose your service exchanges telemetry frames shaped as msg(telemetry, Id, Temp). Instead of writing separate pack and unpack functions, define one clause that states the expected shape.
% Run this in the SWI-Prolog top-level (swipl). Standard user permissions are sufficient.
% Placeholders: Id and Temp are variables; Msg is a compound term.
validate_msg(msg(telemetry, Id, Temp), Id, Temp).Execution and verification: Load the file, then run each query below. These describe expected behavior of a standard Prolog engine; they are not claimed test output.
?- validate_msg(msg(telemetry, 101, 19.3), 101, 19.3).
Expected check: succeeds.?- validate_msg(msg(telemetry, Id, Temp), 101, 19.3).
Expected check:Id = 101, Temp = 19.3. This extracts fields from an incoming frame.?- validate_msg(Msg, 101, 19.3).
Expected check:Msg = msg(telemetry, 101, 19.3). This constructs an outgoing frame from field values.?- validate_msg(msg(alarm, 101, 19.3), 101, 19.3).
Expected check: fails, because the message type does not match.
Risk: If the term shape is wrong, unification fails silently; it does not raise a descriptive exception. If you search through many frames with member/2, the cost is linear in the list length.
Trade-offs and Limits
Standard unification performs literal structural matching. It cannot express constraints such as "any temperature above 20 degrees." For domain constraints, use a Constraint Logic Programming library such as clpfd for integers.
Another limit is the occurs-check. By default, SWI-Prolog and many other implementations allow X = f(X), creating a cyclic term rather than rejecting it. This is usually harmless for message frames, but it can cause non-termination in symbolic manipulation. Use unify_with_occurs_check/2 when you need the stricter behavior.
Practical Verification Before You Ship
Test your predicate with three scenarios: a fully ground matching term should succeed; a ground term with a wrong field should fail; and a query with variables should return bindings through backtracking. If all three behave as expected, the clause is correctly describing the message shape.
Closing
Pick one message type, write a single clause that names its fields, and reuse that clause for both parsing and generation. Add unify_with_occurs_check/2 only if you are building symbolic terms where cycles matter. The result is less duplicated code and one place to update when the protocol changes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.