Picat is my favorite new toolbox language
logic programming + constraint solving + imperative programming (?!) = joy.
I'm teaching a TLA+ workshop in two weeks! Register here, use the code C0MPUT3RTHINGS
for 15% off.
I always like to find new "toolbox languages". These are languages that can quickly be used to quickly solve a set of problems with just the base language and without a whole lot of typing. That's what drove me to learn Raku, so I could quickly do things like "generate 5 random 10-character strings":
for ^5 {say ('a'..'z').roll(10).join}
One toolbox language I've had my eye on is Picat,1 a "logic-based multi-paradigm programming language aimed for general-purpose applications". I'm not sure where I first learned about it, maybe around the same time I encountered MiniZinc? The userbase is extremely small, and as with many small communities, the documentation assumes you have the same background as the inventors. But it's also an incredibly interesting language and I'd like to try explaining why.
"Logic-Based"
(Heads-up, I've only dabbled in Prolog a little and I'm sure I've got a hundred subscribers who are much better at logic programming than I'll ever be. Nonetheless I'm going to do my best to explain it.)
Logic programming is, very broadly, where you program by writing a set of equations and ask the program to find values for each variable for that make all the equations true. For example, if I want to concatenate two strings in Picat, I'd write
Picat> [1,2] ++ [3, 4] = Z.
It will then try to find a matching value for Z:
Z = [1,2,3,4]
yes
The yes
means that it was able to find a satisfying value for Z. The cool part is that this is bidirectional: it can also find inputs that give the output!
Picat> [1,2] ++ Y = [1,2,3,4].
Y = [3,4]
yes
When if there are multiple satisfying solutions (unifications), Picat can list them all. EX writing member(X, [1, Y])
gives both X=1
and Y=X
as possible solutions.
"Multiparadigm"
Picat is, at its heart, a logic programming language. It borrows a lot of core ideas and most (most) of its syntax from Prolog. But it also adds two paradigms on top of that.
The first is constraint solving. Like logic programming, constraint solving is about finding values that match a set of equations. Unlike logic programming, we're exclusively talking numbers. Usually we're also trying to find the solution that mini/maximizes some other metric.
% Maximize area for given perimeter
Vars = {Len, Width},
Vars :: 0..10,
Len*2+Width*2 #<= 22,
solve([$max(Len*Width)], Vars).
Combining constraint solving and logic programming isn't that usual; SWI-Prolog has the CLP(FD) module. Picat's far more unusual paradigm is imperative programming:
{1, 2} ++ {3, 4} = Z,
foreach (I in 1..Z.len)
Z[I] := Z[I] * 2
end,
Z = {2,4,6,8}
I never thought I'd see the day where a new language advertised "we're more imperative" as a selling point, but it's actually really nice. I've had a few cases where I could mostly express a problem logically but need to use a bit of imperative assignment to get it all working. Maybe I wouldn't need it if I was an experience prologer. But for n00bs like me it's a great escape hatch.
My favorite feature
The planner
module took me from "this could be useful" to "damn this is cool". You define an initial state, a final state, and a set of actions, and it tries to find a sequence of actions that get you from here to there. It's like a model checker in reverse!
Here's an example problem: we have a list of numbers that we add one at a time to reach a target number. If we have input [2,2,3,4]
and had to get 9
, we could do 2,3,4
to go 2->5->9
. Now let's also add in "forbidden numbers" you cannot pass through. If 5
was forbidden, you'd instead have to do 4,2,3
and go 4->6->9
.
In Picat:
import planner.
action(S, NextS, Action, ActionCost) =>
S = {Target, Current, L, Forbidden},
member(X, L),
Next = Current + X,
not member(Next, Forbidden),
NextS = {Target, Next, delete(L, X), Forbidden},
Action = {add, X, Next},
ActionCost = 1,
true.
final({Target, Current, _, _}) =>
Target = Current.
Our state is a 4-tuple consisting of the target value, the current value, the numbers we can add (including duplicates), and the forbidden numbers. We only have one action, which picks a number, checks that adding it to Current
doesn't give us a forbidden number, and then sets the values in the next state (NextS
). The ActionCost needs to be assigned but doesn't matter here.
main =>
S0 = {19, 0, [6, 6, 9, 5, 9, 2], []},
S1 = {S0[1], S0[2], S0[3], [17]},
plan(S0, Plan0),
plan(S1, Plan1),
writeln(Plan0),
writeln(Plan1),
nl.
We could also call plan(S0, Steps, Plan0, Cost)
in which case it would only find plans up to Steps
long and with a cost under Cost
.
% output
[{add,6,6},{add,6,12},{add,5,17},{add,2,19}] % Plan0
[{add,6,6},{add,6,12},{add,2,14},{add,5,19}] % Plan1
How cool is that?!
This gets especially useful when you combine it with the other language features. One thing I've done is put a constraint problem inside of final
and then add an action that loosens the constraints. So it first tries to solve the basic problem, and if it can't it gradually loosens the constraints until it's possible.
My use cases
Right now Picat is too unpolished to be "general-purpose" language. The documentation is especially sparse and highly technical, I had to do a good amount of experimentation to figure out how everything. I still have no idea how the index
keyword works or how to use the actor features. I don't know if it's suited to become a general-purpose language, the kind of thing you'd write a webserver in.
But it's a fantastic toolbox language. I was originally motivated by a vacation scheduling problem: we had a set of activities we wanted to do, each with a set of restrictions. IE certain activities couldn't be done on the same day, all the things that required a car had to happen in a 24-hour span, stargazing needed a clear sky and being outside the city, stuff like that. Picat elegantly handled all the requirements and spit back schedules.2 Two other ways I've used it:
- Testing if a logical puzzle has a unique solution or not. I've done this in both MiniZinc and Alloy, but Picat handles this better than either. I want to try using the planning module to also test puzzle variations.
- I wrote a petri net simulator in <70 lines of code. Check it out here!
I hope this convinced you to check Picat out! Two resources to get started:
- A User's Guide to Picat: First chapter is a real barebones tutorial, rest of the book is reference material. Lots about implementation details, not much on good practice.
- Constraint Solving and Planning with Picat: A lot more conventional introduction, really heavily emphasizes constraint solving over other paradigms. Also written ~circa Picat 1.3, it's up to 3.5 now.
There's also a WebIDE, but it doesn't show errors.
If you're reading this on the web, you can subscribe here. Updates are once a week. My main website is here.
My new book, Logic for Programmers, is now in early access! Get it here.