mirror of
https://gitlab.com/Cian-H/my_blog.git
synced 2025-12-22 22:31:58 +00:00
Published new post
This commit is contained in:
@@ -0,0 +1,726 @@
|
||||
+++
|
||||
date = '2025-02-18T01:23:46Z'
|
||||
draft = false
|
||||
title = 'Prolog: if Stockholm syndrome was a programming language'
|
||||
+++
|
||||
|
||||
I have an embarrassing admission to make that will probably raise many eyebrows but please, hear me
|
||||
out:
|
||||
|
||||
> I love Prolog
|
||||
|
||||
And before anyone says it: yes, I know I shouldn't. I know it's an obsolete, slow, unoptimisable
|
||||
borderline undebuggable language. I know it is based on a programming paradigm that is long since
|
||||
dead and that most programmers (if they're even aware of it) spit on the grave of and bid an enraged
|
||||
"good riddance" to. Some people might describe it as a kind of Stockholm syndrome I experience as
|
||||
somebody who spends a lot of time working on symbolic AI concepts but I promise you: you just don't
|
||||
know Prolog like I do! Prolog really does love me, I swear! It's just that we're going through a
|
||||
rough patch right now, that's all...
|
||||
|
||||
Off-colored jokes aside, any time that I dust off the old SWIPL interpreter and write some Prolog it
|
||||
can almost feel like I'm in an abusive relationship with the language. I want to love it, I truly
|
||||
do. It is so elegant, and beautiful, and does things that feel like genuine magic. But it is also
|
||||
just a nightmare to program in! For reasons that I **think** most people will understand in
|
||||
principal but not understand the sheer scale of the pain suffered by anyone cursed to walk the path
|
||||
of Prolog for any length of time.
|
||||
|
||||
As a quick comparison, the most common complaint from many people about rust is that the compiler
|
||||
places too many semantic restrictions on the programmer and that these rules can be frustrating.
|
||||
While I understand this perspective I do think they should have some empathy for the `rustc`
|
||||
compiler: how would you feel if you were in the compiler's shoes? How would you feel if you spent
|
||||
hours, or even days (insert "rust compiler slow" joke here) meticulously crafting logical rules only
|
||||
for some jackass to come along and poke holes in them? Wouldn't you feel like screaming
|
||||
|
||||
```console
|
||||
the trait bound `WhatTheHellIsThisMonstrosity<Box<Arc<Mutex<[i64; 4]>>>>: From<(i64, i64, u64, &String)>` cannot be satisfied
|
||||
```
|
||||
|
||||
too? Wouldn't you also be a little unsatisfied with that idiot? Not sure? Well, if you're reading
|
||||
this and you've never written a line of prolog in your life (I imagine most programmers will fall
|
||||
into this category) then try it! You too can spend hours writing a beautifully crafted set of rules
|
||||
then cry in dismay as a mathematically perfect engine of logical consistency shits all over your
|
||||
rules, and insults your logic skills while it is at it! By the time you reach your 5th hackerrank
|
||||
problem you might just find yourself empathising with `rustc` a little bit...
|
||||
|
||||
## The pain of trying to write prolog
|
||||
|
||||
Let me try to walk you through the process of solving a simple problem in prolog, so you those who
|
||||
are lucky enough to have avoided this language can understand what I mean. I am by no means an
|
||||
expert prolog programmer, but I do have at least several dozen hours of experience writing it for
|
||||
various experiments and projects. My prolog might be a little rusty, as its been a few moinths but I
|
||||
am not a _complete_ novice either. So, lets try to solve a classic coin permutations problem in
|
||||
prolog. Project euler problem 31 should do nicely!
|
||||
|
||||
> In the United Kingdom the currency is made up of pound (£) and pence (p). There are eight coins in
|
||||
> general circulation:
|
||||
>
|
||||
> 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), and £2 (200p).
|
||||
>
|
||||
> It is possible to make £2 in the following way:
|
||||
>
|
||||
> 1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p
|
||||
>
|
||||
> How many different ways can £2 be made using any number of coins?
|
||||
|
||||
Seems simple enough. In prolog, it is often best to start by defining predicates for our goals.
|
||||
Basically: prolog's paradigm naturally incentivizes an almost TDD type workflow. Starting simple, we
|
||||
want a predicate with a number, a list, and a result for the check if the sum equals the number.
|
||||
Sounds simple enough.
|
||||
|
||||
```prolog
|
||||
sum_equals(N, L, X) :-
|
||||
sum_list(L, M),
|
||||
X is M =:= N.
|
||||
```
|
||||
|
||||
This predacate sets `M` as the sum of `L`, and then asserts that `X` is the result of an equality
|
||||
comparison between `M` and `N`. First goal written, makes perfect sense, if we query
|
||||
`?- sum_equals(10, [7, 3], X).` we should get back `X = true.`. Let's run that to test it and
|
||||
|
||||
```console
|
||||
ERROR: /home/cianh/Programming/prolog_tests/main.pro:3:7: Syntax error: Operator priority clash
|
||||
```
|
||||
|
||||
Ok then. Thanks SWI prolog, very well explained... well, it says line 3 character 7, which is where
|
||||
the `is` keyword is. Maybe it can't tell which operator takes precedence between `is` and `=:=` so I
|
||||
need to add clearer brackets? I would have thought that the is keyword should clearly take
|
||||
precedence here but lets try that and see if it fixes the bug.
|
||||
|
||||
```prolog
|
||||
sum_equals(N, L, X) :-
|
||||
sum_list(L, M),
|
||||
X is (M =:= N).
|
||||
```
|
||||
|
||||
```prolog
|
||||
?- sum_equals(10, [7, 3], X).
|
||||
ERROR: Arithmetic: `(=:=)/2' is not a function
|
||||
ERROR: In:
|
||||
ERROR: [13] _1934 is (10=:=10)
|
||||
ERROR: [11] toplevel_call(user:user: ...) at
|
||||
/nix/store/zp5w45y9qpmp6aybwzgsi26n1aamk33p-swi-prolog-9.2.7/lib/swipl/boot/toplevel.pl:1317
|
||||
ERROR:
|
||||
ERROR: Note: some frames are missing due to last-call optimization.
|
||||
ERROR: Re-run your program in debug mode (:- debug.) to get more detail.
|
||||
^ Exception: (4) setup_call_cleanup('$toplevel':notrace(call_repl_loop_hook(begin, 0)),
|
||||
'$toplevel':'$query_loop'(0), '$toplevel':notrace(call_repl_loop_hook(end, 0))) ? creep
|
||||
```
|
||||
|
||||
... Did that interpreter just call me a creep? What the hell?!?! See? I wasn't exaggerating, this
|
||||
language will **LITERALLY** find the logical inconsistency in your rules and then insult you for it!
|
||||
Might need a few more iterations. I guess the issue might be that I'm attempting to _assign_ the
|
||||
comparison result to `X` when I should be _unifying_ them instead? This might be a confusing
|
||||
distinction coming from a more imperative or functional perspective, but the distinction is
|
||||
important in logic. Assigning is keeping a value for later, whereas unifying is an actual logical
|
||||
operation. It wouldn't be very prolog-ey to assign and pass a result out, its more idiomatic to
|
||||
constrain the result through unification.
|
||||
|
||||
```prolog
|
||||
sum_equals(N, L, X) :-
|
||||
sum_list(L, M),
|
||||
X = (M =:= N).
|
||||
```
|
||||
|
||||
Yes, thats what the `=` operator means in prolog. Weird right?
|
||||
|
||||
```prolog
|
||||
?- sum_equals(10, [7, 3], X).
|
||||
X = (10=:=10).
|
||||
```
|
||||
|
||||
Huh? I did not expect that. At least there were no errors, but why on earth is it unifying to an
|
||||
expression instead of a value? Oh no, im going to have to program a separate path for each outcome
|
||||
here, amn't I?
|
||||
|
||||
```prolog
|
||||
sum_equals(N, L, true) :-
|
||||
sum_list(L, M),
|
||||
M =:= N,
|
||||
!. % This just tells prolog to return early if it finds a `true`
|
||||
|
||||
sum_equals(N, L, false) :-
|
||||
sum_list(L, M),
|
||||
M =\= N.
|
||||
```
|
||||
|
||||
```prolog
|
||||
?- sum_equals(10, [7, 3], X).
|
||||
X = true.
|
||||
|
||||
?- sum_equals(10, [8, 3], X).
|
||||
X = false.
|
||||
```
|
||||
|
||||
SUCCESS!!! Now that we have a sum_equals predicate, we're going to need one that returns all
|
||||
possible combinations of a list. For this, we will need to create a predicate that takes a list and
|
||||
expands each value out so that we have enough of each to make up the whole £2 with only that type of
|
||||
coin. We'll need a helper predicate to achieve this (`repeat_element`), and then the main predicate
|
||||
(`pad_coins`).
|
||||
|
||||
```prolog
|
||||
pad_coins([], _, []).
|
||||
pad_coins([H|T], N, Result) :-
|
||||
repeat_element(H, N // H, RepeatedH),
|
||||
pad_coins(T, N, RepeatedT),
|
||||
append(RepeatedH, RepeatedT, Result),
|
||||
!.
|
||||
|
||||
repeat_element(_, 0, []).
|
||||
repeat_element(E, N, [E|T]) :-
|
||||
N > 0,
|
||||
N1 is N - 1,
|
||||
repeat_element(E, N1, T).
|
||||
```
|
||||
|
||||
```prolog
|
||||
?- pad_coins([2, 5, 10], 10, X).
|
||||
X = [2, 2, 2, 2, 2, 5, 5, 10].
|
||||
```
|
||||
|
||||
This time, I managed to get it right first time. Great! Finally, we need to be able to get every
|
||||
possible permutation of this list. This isn't too hard to do in prolog actually, but SWI prolog
|
||||
provides a handy `permutation` function, so lets just use that. Oh! And we need to remember to apply
|
||||
an aggressive pruning condition to try and help optimise performance a bit, so lets prune out every
|
||||
list with a sum greater than `N`. When writing prolog it's important to bare in mind that the engine
|
||||
will exhaustively search each branch via backtracking, so conditions like this can **dramatically**
|
||||
increase performance.
|
||||
|
||||
However, at this point, the improvement in performance probably won't be enough. This program will
|
||||
end up running out of RAM, and triggering an OOM error. Yes, I know the implementation was lazy, and
|
||||
brute-force. And before anyone says it: I do know this should be solved with more of a dynamic
|
||||
programming solution. The reason I've spent this time going down this path is to demonstrate
|
||||
something:
|
||||
|
||||
1. You can't be lazy when writing prolog, it won't let you get away with that
|
||||
2. Prolog is difficult and esoteric to debug
|
||||
3. I am _extremely_ tired, it's 1am, and i don't feel like being precious about programming best
|
||||
practice when I really want to go to bed and i'm 99% sure nobody will ever read this
|
||||
4. Prolog is mean to me
|
||||
|
||||
## Why do you still love Prolog then?
|
||||
|
||||
This is the natural question to ask when a language turns out to be as frustrating as Prolog can be.
|
||||
However, I think most programmers already know the answer even if they don't want to admit it:
|
||||
|
||||
> It makes you feel smart
|
||||
|
||||
Genuinely, there is an indescribable feeling of satisfaction and "I did that!" when you actually get
|
||||
a Prolog program to work properly and it isn't an incomprehensible mess. Is that a good thing for a
|
||||
programming language? Hell no! If I am surprised and feel like a genius if I can write a basic
|
||||
program in your language: it's a shite language. Every now and then though, when the conditions are
|
||||
just right, and the domain fits, and the stars align Prolog produces the most elegant solution you
|
||||
have ever seen and suddenly, it was all worth it. Or, at least, that's what you tell yourself.
|
||||
|
||||
Moreso than just the feeling of accomplishment it gives you, I think I love the **idea** of Prolog a
|
||||
lot more than the **reality**. When somebody says:
|
||||
|
||||
> I have this cool language to show you. You don't actually define what the program does, you just
|
||||
> define what you want from it! And then, a mathematically perfect and exhaustive engine explores
|
||||
> all possible solutions for your problem and gives them all to you!
|
||||
|
||||
It sounds like genius. It sounds like the perfect way to program. It sounds too good to be true. So,
|
||||
let's start by looking at Prolog at it's best, so you can understand why I want so desperately to
|
||||
love it despite its flaws.
|
||||
|
||||
## Where Prolog shines: finite sets of simple, logical rules
|
||||
|
||||
Lets take a simple problem that most programmers have tried to solve, that turns out to be tricker
|
||||
than you might think in an imperative language. Lets solve a sudoku!
|
||||
|
||||
Luckily, for this one, we have a handy little page on
|
||||
[Rosetta code](https://rosettacode.org/wiki/Sudoku) that we can pull some prebuilt solutions from to
|
||||
get an idea of how these solutions look in various languages. We should probably start with the
|
||||
universal lingua franca of programming: the venerable C programming language. How do we solve a
|
||||
sudoku in C? Cutting it down to just the main logic of the solution, we get:
|
||||
|
||||
```C
|
||||
#include <stdio.h>
|
||||
|
||||
void show(int *x)
|
||||
{
|
||||
int i, j;
|
||||
for (i = 0; i < 9; i++) {
|
||||
if (!(i % 3)) putchar('\n');
|
||||
for (j = 0; j < 9; j++)
|
||||
printf(j % 3 ? "%2d" : "%3d", *x++);
|
||||
putchar('\n');
|
||||
}
|
||||
}
|
||||
|
||||
int trycell(int *x, int pos)
|
||||
{
|
||||
int row = pos / 9;
|
||||
int col = pos % 9;
|
||||
int i, j, used = 0;
|
||||
|
||||
if (pos == 81) return 1;
|
||||
if (x[pos]) return trycell(x, pos + 1);
|
||||
|
||||
for (i = 0; i < 9; i++)
|
||||
used |= 1 << (x[i * 9 + col] - 1);
|
||||
|
||||
for (j = 0; j < 9; j++)
|
||||
used |= 1 << (x[row * 9 + j] - 1);
|
||||
|
||||
row = row / 3 * 3;
|
||||
col = col / 3 * 3;
|
||||
for (i = row; i < row + 3; i++)
|
||||
for (j = col; j < col + 3; j++)
|
||||
used |= 1 << (x[i * 9 + j] - 1);
|
||||
|
||||
for (x[pos] = 1; x[pos] <= 9; x[pos]++, used >>= 1)
|
||||
if (!(used & 1) && trycell(x, pos + 1)) return 1;
|
||||
|
||||
x[pos] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void solve(const char *s)
|
||||
{
|
||||
int i, x[81];
|
||||
for (i = 0; i < 81; i++)
|
||||
x[i] = s[i] >= '1' && s[i] <= '9' ? s[i] - '0' : 0;
|
||||
|
||||
if (trycell(x, 0))
|
||||
show(x);
|
||||
else
|
||||
puts("no solution");
|
||||
}
|
||||
```
|
||||
|
||||
Oooookay then. Being honest: I've never been great at `C`, so maybe it's just me but I didn't expect
|
||||
that solution to require bit shifting. Maybe it's just my origins as a lowly python scrub that is
|
||||
the problem? Maybe the solution would make more sense to me in a high level language like python? It
|
||||
would probably be shorter too, right?
|
||||
|
||||
```python
|
||||
def printGrid(grid):
|
||||
for i in range(0, 9):
|
||||
if i > 0 and i % 3 == 0:
|
||||
print("------+-------+------", end="")
|
||||
print()
|
||||
for j in range(0, 9):
|
||||
if j > 0 and j % 3 == 0: print("| ", end="")
|
||||
n = grid[i][j]
|
||||
c = "." if n == 0 else str(n)
|
||||
print(c, end=" ")
|
||||
print()
|
||||
|
||||
def valid(row, col, n):
|
||||
res = True
|
||||
for i in range(0, 9):
|
||||
for j in range(0, 9):
|
||||
if ( i == row or j == col or
|
||||
i // 3 == row // 3 and j // 3 == col // 3 ):
|
||||
if grid[i][j] == n: res = False
|
||||
return res
|
||||
|
||||
def solve(grid):
|
||||
for row in range(0, 9):
|
||||
for col in range(0, 9):
|
||||
if grid[row][col] == 0:
|
||||
for n in (range(1, 10)):
|
||||
if valid(row, col, n):
|
||||
grid[row][col] = n
|
||||
solve()
|
||||
grid[row][col] = 0
|
||||
return
|
||||
printGrid(grid)
|
||||
input("\nPress enter to check for more solutions\n")
|
||||
```
|
||||
|
||||
Oh wow... do i see a triply nested for loop making recursive call, conditioned on a doubly nested
|
||||
for loop? Moving swiftly along before I go on a 5,000 word rant about that terrible python code:
|
||||
functional bros, what you got? Surely functional languages solve this? You always go on about how
|
||||
lovely and clean functional solutions are compared to imperative ones! Let's see the solution, as
|
||||
implemented in the posterchild of functional languages: haskell.
|
||||
|
||||
```Haskell
|
||||
module Sudoku
|
||||
(Sudoku,
|
||||
readSudoku,
|
||||
runSudoku,
|
||||
evalSudoku,
|
||||
execSudoku,
|
||||
showSudoku,
|
||||
valAt, rowAt, colAt, boxAt,
|
||||
place)
|
||||
where
|
||||
import Data.Array.Diff
|
||||
import MonadNondet
|
||||
import Control.Monad.State
|
||||
|
||||
newtype Sudoku a = Sudoku (StateT (DiffUArray (Int,Int) Int) Nondet a)
|
||||
deriving (Functor, Monad, MonadPlus)
|
||||
|
||||
initialSudokuArray = listArray ((1,1),(9,9)) [0,0..]
|
||||
|
||||
runSudoku (Sudoku k) = runNondet (runStateT k initialSudokuArray)
|
||||
|
||||
evalSudoku = fst . runSudoku
|
||||
execSudoku = snd . runSudoku
|
||||
|
||||
showSudoku = Sudoku $ do
|
||||
a <- get
|
||||
return $ unlines [unwords [show (a ! (i,j)) | j <- [1..9]] | i <- [1..9]]
|
||||
|
||||
readSudoku :: String -> Sudoku ()
|
||||
readSudoku xs = sequence_ $ do
|
||||
(i,ys) <- zip [1..9] (lines xs)
|
||||
(j,n) <- zip [1..9] (words ys)
|
||||
return $ place (i,j) (read n)
|
||||
|
||||
valAt' (i,j) = do
|
||||
a <- get
|
||||
return (a ! (i,j))
|
||||
|
||||
rowAt' (i,j) = mapM valAt' [(i, k) | k <- [1..9]]
|
||||
|
||||
colAt' (i,j) = mapM valAt' [(k, j) | k <- [1..9]]
|
||||
|
||||
boxAt' (i,j) = mapM valAt' [(i' + u, j' + v) | u <- [1..3], v <- [1..3]]
|
||||
where i' = ((i-1) `div` 3) * 3
|
||||
j' = ((j-1) `div` 3) * 3
|
||||
|
||||
valAt = Sudoku . valAt'
|
||||
rowAt = Sudoku . rowAt'
|
||||
colAt = Sudoku . colAt'
|
||||
boxAt = Sudoku . boxAt'
|
||||
|
||||
place :: (Int,Int) -> Int -> Sudoku ()
|
||||
place (i,j) n = Sudoku $ do
|
||||
v <- valAt' (i,j)
|
||||
when (v == 0 && n /= 0) $ do
|
||||
rs <- rowAt' (i,j)
|
||||
cs <- colAt' (i,j)
|
||||
bs <- boxAt' (i,j)
|
||||
guard $ (n `notElem`) $ rs ++ cs ++ bs
|
||||
a <- get
|
||||
put (a // [((i,j),n)])
|
||||
```
|
||||
|
||||
Ok (and i say this as a serial enjoyer of the monads): what the hell is that??? What am I looking at
|
||||
here? As the meme goes: where's the whitepaper explaining `MonadNondet` here??? I'm no expert at
|
||||
haskell but still, I just dont get it.
|
||||
|
||||
So, last but not least, lets have a look at the prolog implementation.
|
||||
|
||||
```Prolog
|
||||
:- use_module(library(clpfd)).
|
||||
|
||||
sudoku(Rows) :-
|
||||
length(Rows, 9), maplist(length_(9), Rows),
|
||||
append(Rows, Vs), Vs ins 1..9,
|
||||
maplist(all_distinct, Rows),
|
||||
transpose(Rows, Columns), maplist(all_distinct, Columns),
|
||||
Rows = [A,B,C,D,E,F,G,H,I],
|
||||
blocks(A, B, C), blocks(D, E, F), blocks(G, H, I).
|
||||
|
||||
length_(L, Ls) :- length(Ls, L).
|
||||
|
||||
blocks([], [], []).
|
||||
blocks([A,B,C|Bs1], [D,E,F|Bs2], [G,H,I|Bs3]) :-
|
||||
all_distinct([A,B,C,D,E,F,G,H,I]),
|
||||
blocks(Bs1, Bs2, Bs3).
|
||||
```
|
||||
|
||||
Now **that** is an elegant solution! A translation for any non-prolog (i.e: sane) programmers that
|
||||
might be reading, this code:
|
||||
|
||||
- Defines that a row must have 9 unique numbers between 1 and 9
|
||||
- Applies the same rule to the columns
|
||||
- Defines that a block is a 3x3 grid of unique numbers between 1 and 9
|
||||
- Declares that the blocks to the right of those that have been checked should also be checked
|
||||
|
||||
The prolog engine then mathematically searches every possible solution and returns the ones that
|
||||
satisfy these simple logical rules. It does it in so few lines, yet they're all readable. All
|
||||
sensible. All beautiful.
|
||||
|
||||
Another area where prolog absolutely shines is in programming GUIs. It might seem strange at first,
|
||||
looking at this strange logic programming language and wondering how you would even use it to
|
||||
program a UI. But remember, prolog is a **declarative** logic programming language, and nowadays
|
||||
declarative UI libraries are all the rage. There is no ends to the amount of praise heaped on
|
||||
svelte, flutter, and swiftUI from developers. So of course, the OG of declarative programming might
|
||||
have some surprises for modern programmers in how it handles UI. To see what I mean, let's look at
|
||||
Rosetta code's "GUI Component Interaction" problem solved in a modern language considered great at
|
||||
GUIs (C# specifically), a modern beloved web framework (svelte), and prolog.
|
||||
|
||||
### C#
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
class RosettaInteractionForm : Form
|
||||
{
|
||||
class NumberModel: INotifyPropertyChanged
|
||||
{
|
||||
|
||||
Random rnd = new Random();
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged = delegate {};
|
||||
|
||||
int _value;
|
||||
public int Value
|
||||
{
|
||||
get { return _value; }
|
||||
set
|
||||
{
|
||||
_value = value;
|
||||
PropertyChanged(this, new PropertyChangedEventArgs("Value"));
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetToRandom(){
|
||||
Value = rnd.Next(5000);
|
||||
}
|
||||
}
|
||||
|
||||
NumberModel model = new NumberModel{ Value = 0};
|
||||
|
||||
RosettaInteractionForm()
|
||||
{
|
||||
var tbNumber = new MaskedTextBox
|
||||
{
|
||||
Mask="0000",
|
||||
ResetOnSpace = false,
|
||||
Dock = DockStyle.Top
|
||||
};
|
||||
tbNumber.DataBindings.Add("Text", model, "Value");
|
||||
|
||||
var btIncrement = new Button{Text = "Increment", Dock = DockStyle.Bottom};
|
||||
btIncrement.Click += delegate
|
||||
{
|
||||
model.Value++;
|
||||
};
|
||||
var btDecrement = new Button{Text = "Decrement", Dock = DockStyle.Bottom};
|
||||
btDecrement.Click += delegate
|
||||
{
|
||||
model.Value--;
|
||||
};
|
||||
var btRandom = new Button{ Text="Reset to Random", Dock = DockStyle.Bottom };
|
||||
btRandom.Click += delegate
|
||||
{
|
||||
if (MessageBox.Show("Are you sure?", "Are you sure?", MessageBoxButtons.YesNo) == DialogResult.Yes)
|
||||
model.ResetToRandom();
|
||||
};
|
||||
Controls.Add(tbNumber);
|
||||
Controls.Add(btIncrement);
|
||||
Controls.Add(btDecrement);
|
||||
Controls.Add(btRandom);
|
||||
}
|
||||
static void Main()
|
||||
{
|
||||
Application.Run(new RosettaInteractionForm());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Svelte
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let value = 0;
|
||||
|
||||
function increment() {
|
||||
value++;
|
||||
}
|
||||
|
||||
function randomize() {
|
||||
if (confirm("Are you sure you want to generate a random number?")) {
|
||||
value = Math.floor(Math.random() * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
if (isNaN(value)) {
|
||||
alert("Please enter a valid number.");
|
||||
value = 0;
|
||||
} else {
|
||||
value = parseInt(value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
margin: 5px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<label for="value">Value:</label>
|
||||
<input type="number" id="value" bind:value on:blur={handleInput} />
|
||||
|
||||
<button on:click={increment}>Increment</button>
|
||||
<button on:click={randomize}>Random</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Prolog
|
||||
|
||||
```prolog
|
||||
dialog('GUI_Interaction', [
|
||||
object := GUI_Interaction,
|
||||
parts := [
|
||||
GUI_Interaction := dialog('Rosetta Code'),
|
||||
Input_field := text_item(input_field),
|
||||
Increment := button(increment),
|
||||
Random := button(random)
|
||||
],
|
||||
modifications := [Input_field := [label := 'Value :', length := 28]],
|
||||
layout := [
|
||||
area(Input_field, area(54, 24, 251, 24)),
|
||||
area(Increment, area(54, 90, 80, 24)),
|
||||
area(Random, area(230, 90, 80, 24))
|
||||
],
|
||||
behaviour := [
|
||||
Increment := [message := message(@prolog, increment, Input_field )],
|
||||
Random := [message := message(@prolog, my_random, Input_field)],
|
||||
Input_field := [
|
||||
message := message(
|
||||
@prolog,
|
||||
input,
|
||||
GUI_Interaction,
|
||||
Increment,
|
||||
@receiver,
|
||||
@arg1
|
||||
)
|
||||
]
|
||||
]
|
||||
]).
|
||||
|
||||
gui_component :- make_dialog(S, 'GUI_Interaction'), send(S, open).
|
||||
|
||||
increment(Input) :-
|
||||
get(Input, selection, V),
|
||||
atom_number(V, Val),
|
||||
Val1 is Val + 1,
|
||||
send(Input, selection, Val1).
|
||||
|
||||
my_random(Input) :-
|
||||
new(D, dialog('GUI Interaction')),
|
||||
send(D, append(label(lbl,'Confirm your choice !'))),
|
||||
send(D, append(button(ok, message(D, return, ok)))),
|
||||
send(D, append(button(cancel, message(D, return, ko)))),
|
||||
send(D, default_button(ok)),
|
||||
get(D, confirm, Rval),
|
||||
free(D),
|
||||
(Rval = ok -> X is random(10000), send(Input, selection, X)).
|
||||
|
||||
input(Gui, Btn, Input, Selection) :-
|
||||
catch(
|
||||
(term_to_atom(T, Selection), number(T), send(Gui, focus, Btn)),
|
||||
_,
|
||||
(send(@display, inform, 'Please type a number !'), send(Input,clear))
|
||||
).
|
||||
```
|
||||
|
||||
Putting aside the prolog-isms and the muiltilanguage syntax of svelte, the similarities in approach
|
||||
the approach taken by the 2 languages here aren't too different. In fact, if we put aside the
|
||||
definitions for the functions being grouped at the bottom of the prolog file and look at what is
|
||||
above the `gui_component` line theyre strangely similar! They both have a block describing layout
|
||||
(`<div>`/`layout`), a block describing the style (`<style>`/`modifications`), and a block describing
|
||||
the behaviour (`<script>`/`behaviour`). Sure, there are some differences, but for languages in
|
||||
completely different language families with 45 years between their initial release the similarity is
|
||||
kinda dumbfounding. Contrast it with the C# code, which takes a much more object oriented approach.
|
||||
C# constructs a bunch of object that all have properties and behaviours bound to them. Both
|
||||
approaches work, but I do rather prefer the approach that results in a separation based on function
|
||||
(i.e: the declarative approach) rather than locality. As time progresses, more and more modern UI
|
||||
frameworks rediscover this declarative approach pioneered by prolog and that really is a testament
|
||||
to just how nice this style of writing GUIs is.
|
||||
|
||||
You may now be asking "what's the problem?". The language looks really great, right? It does cool
|
||||
stuff, it does it in an enjoyable way to work with, prolog really does love me right??? Well, if we
|
||||
continue the strained analogy of an abusive relationship with Prolog: this is Prolog buying me
|
||||
flowers, wining and dining me, and laughing at my crappy jokes. This is Prolog in love-bombing mode.
|
||||
But sadly, it only does that so it can get its hooks into me...
|
||||
|
||||
## Where Prolog fails: literally anything else!
|
||||
|
||||
Ok, so if Prolog can be so clean and beautiful then why has it gone extinct outside of certain
|
||||
circles like symbolic AI research? Well, lets take a look at how prolog performs at some other
|
||||
simple programming tasks. How about some simple binary search? It's a simple, classic algorithm that
|
||||
every programmer has implemented a thousand times before. If prolog can solve a sudoku as elegantly
|
||||
as above, surely a binary search in prolog is a clean, one-line case of "if greater go right if less
|
||||
than go left", right? Ok, I've danced around it long enough, you know by my tone that I'm being
|
||||
facetious here. It's actually horrifying...
|
||||
|
||||
```prolog
|
||||
bin_search(Elt,List,Result):-
|
||||
length(List,N), bin_search_inner(Elt,List,1,N,Result).
|
||||
|
||||
bin_search_inner(Elt,List,J,J,J):-
|
||||
nth(J,List,Elt).
|
||||
bin_search_inner(Elt,List,Begin,End,Mid):-
|
||||
Begin < End,
|
||||
Mid is (Begin+End) div 2,
|
||||
nth(Mid,List,Elt).
|
||||
bin_search_inner(Elt,List,Begin,End,Result):-
|
||||
Begin < End,
|
||||
Mid is (Begin+End) div 2,
|
||||
nth(Mid,List,MidElt),
|
||||
MidElt < Elt,
|
||||
NewBegin is Mid+1,
|
||||
bin_search_inner(Elt,List,NewBegin,End,Result).
|
||||
bin_search_inner(Elt,List,Begin,End,Result):-
|
||||
Begin < End,
|
||||
Mid is (Begin+End) div 2,
|
||||
nth(Mid,List,MidElt),
|
||||
MidElt > Elt,
|
||||
NewEnd is Mid-1,
|
||||
bin_search_inner(Elt,List,Begin,NewEnd,Result).
|
||||
```
|
||||
|
||||
Ok, so if you speak prolog it makes perfect sense I suppose. We split our list in two, compare the
|
||||
midpoint to the ends, select a sublist, rinse and repeat. But look at the amount of repetition on
|
||||
each branch, the inflation of arguments being passed up and down the callstack here. It could be
|
||||
worse I suppose; at least it tail recurses and the top level interface at `bin search` is nice.
|
||||
Comparing to a python function just makes the prolog implementation look positively painful.
|
||||
|
||||
```python
|
||||
def binary_search(l, value):
|
||||
low, high = 0, len(l)-1
|
||||
while low <= high:
|
||||
mid = (low + high) // 2
|
||||
if l[mid] > value:
|
||||
high = mid-1
|
||||
elif l[mid] < value:
|
||||
low = mid+1
|
||||
else:
|
||||
return mid
|
||||
return -1
|
||||
```
|
||||
|
||||
And it's not just binary searches where this problem appears. Browse the
|
||||
[Rosetta code prolog page](https://rosettacode.org/wiki/Category:Prolog) and buckle up for a wild
|
||||
ride where you will go hoarse from saying "what the hell is that?!?!" over and over.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Prolog sucks, and I love it. That is the end of my rant about an obsolete, dead programming language
|
||||
that almost nobody has ever written in their life. Why have I written it then? Cos I do symbolic ML
|
||||
and apparently I hate myself. I desperately need to go to sleep now. Good night.
|
||||
Reference in New Issue
Block a user