Yes, these are the sorts of things that should be possible, but it’s still a work in progress. Even more speculative, I’d like to use the same arithmetic operators used for numbers to do things like multiplication/addition on matrices or complex numbers, i.e., user defined arithmetic types.
Perhaps. But I’m starting to think the goal expansion in module arithmetic
is misguided. Would anything bad happened it it just treated any unrecognized function as legitimate and any arguments were permitted, i.e., it never generated any errors? I think:
-
No existing code would break, we’re only relaxing “compile time” restrictions. Anything illegitimate would be caught at evaluation time, just like it does now if there’s a logic variable in the expression that gets bound to something illegitimate.
-
You could probably dispense with the
arithmetic_function
directive. Peter’sstring_length
example would just work, as would any other visible predicate that “returns” its result in the final argument. This sounds like how user defined arithmetic functions work in other Prolog implementations. -
User functions could return values other than numbers as long as they were input arguments to (other user) functions that handled those values. Of course they wouldn’t fare very well as input values to the builtin arithmetic functions.
-
Evaluation errors actually reference the user code that was wrong as opposed to
do_expand_function
which most users have never heard of.
Any disadvantages?
Yes. That forces the user-defined functions to evaluate their arguments. As the argument is passed as a term, this looses arithmetic compilation. Worse, user-defined functions inside the argument will not be evaluated unless you use arithmetic_expression_value/2 for purely interpreted evaluation of the arguments.
Purely interpreted arithmetic is done by several Prolog systems. The advantage is indeed that you can easily plug in your own functions and even use is/2 as a general replacement of f(A1, A2, … An) to “A is f(A1, A2, … An-1)”. I have my doubt this is a good idea. It makes just about any optimization of arithmetic impossible. There is already a huge flaw in the ISO standard that allows variables in arithmetic expressions to be arbitrary terms containing functions. This breaks a lot of options for optimization. I’m afraid I’m guilty as SWI-Prolog could do that from the start. There should have been an eval/1 function that allows evaluating arbitrary functions such that we can properly optimize most arithmetic expressions. Note that the opimization has quite drastic effects on code doing a lot of arithmetic. Surely few Prolog applications involve a lot of arithmetic, but some do.
I’d be more in favor of @peter.ludemann’s proposal for ?Expression and deal with A[N] using functional notation rewrite. Surely that has its own pitfals, in particular when combined with high order predicates In the end, Prolog is an untyped relational language …
I don’t think it has to be the case any more than it is already. Looking at the code it appears to me that if evaluable/1
is not changed all numeric expressions will be “compiled” just as they are now.
Let me propose a very minimalist extension which I don’t think is any more flawed than the current reality. Permit any atomic
to appear as a literal in a “arithmetic” expression. Such atomics are just expanded to themselves and evaluate to themselves (identity function in functional programming terms). So 1 additional clause in do_expand_function/3
and one change from number
to atomic
in eval/3
. (No changes to evaluable/1
.) Everything else can be done with the arithmetic_function
directive.
With this simple extension to arithmetic
I can:
?- N is string_length("abc")+1.
N = 4.
?- matrix(A,2*2), A[1,1] is 1, A[1,2] is 42, X is A[A[1,1],3-1]+2.
A = '$Mx'('$Arr'('$Arr'(1, 42), '$Arr'(_81386230, _81386236))),
X = 44.
And I don’t think I’ve done anything that affects any optimization of the existing numeric arithmetic.
It may not be the general case, but I’m interested in mixed mode arithmetic supporting user defined arithmetic types, not general functional evaluation. So IMO extending the existing arithmetic system is much better for this purpose than inventing a new notation. It’s not just about arrays and their notation - that’s mostly syntax which is covered nicely by the []
block operator.
In this scheme I also want to support variadic terms as user types, but that’s a separate discussion.
Which seems to be a another reason not to introduce a general purpose function capability. Rather, I’m trying to make best use of a feature that, for better or worse, already exists.
I don’t see why is/2 should be limited to numbers. Why not allow, for example, S2 is S1[:-1]
(equivalent to sub_string(S1, _, _, 1, S2)
)?
I would hope for a better notation than ‘?
’.
IBM Prolog also used ‘<-
’ for ‘:-
’ and ‘&
’ for ‘,
’, so its notations aren’t a great precedent.
Perhaps not a compelling demonstration but declaring
:- arithmetic_function(user:string_code/2).
the following works:
?- X is string_code(1+2,"abcde")-[a]+1.
X = 3.
?- arithmetic:math_goal_expansion(X is string_code(1+2,"abcde")-[a]+1,G).
G = (_13748 is 1+2, string_code(_13748, "abcde", _13706), X is _13706-[a]+1).
To pull off the same trick with array indices, evaluable/1
should fail given a compound term in a list, e.g., [1+2]
, a use case which isn’t supported by the native arithmetic, so that shouldn’t be an issue.
Peter’s example requires a bit more work:
?- S2 is "abcde"[:-1].
S2 = "abcd" ;
S2 = "bcd" ;
S2 = "cd" ;
S2 = "d" ;
S2 = "".
?- S2 is "abcde"[:- 4-3].
S2 = "abcd" ;
S2 = "bcd" ;
S2 = "cd" ;
S2 = "d" ;
S2 = "".
?- arithmetic:math_goal_expansion(S2 is "abcde"[:- 4-3],G).
G = (_6092 is 4-3, (_6092:-_6052), '[|]'(_6052, [], _6006), [](_6006, "abcde", S2)).
More optimization is possible, but not for the native arithmetic.
My mistake: I should have defined S2 is S1[:-1]
as equivalent to sub_string(S1, 0, _, 1, S2)
… presumably that also works, with only a single result?
Fixed?
?- S2 is "abcde"[:-1].
S2 = "abcd".
I’m curious about the actual notation. Prolog parses it as :-(1)
but I suspect it’s modelled on Python sequence slicing, e.g., x[0:-1]
. (My Python’s pretty rusty.)
Yes, I was using Python sequence slicing notation (although I don’t think it’s original with Python; ISTR it being essentially the same in Icon or Snobol). A negative value is taken relative to the length of the sequence. The range is inclusive on the first arg and exclusive on the 2nd arg (and there’s a 3rd arg for an increment).
The defaults for s[start:end:incr] are start=0, end=len(s), incr=1.
So, s[:-1] is the same as s[0:length(s)-1,1], which gives all but the last item (because the end argument is exclusive).
Details here: Built-in Types — Python 3.9.1 documentation
if you want to play with it: Online Python Compiler (Interpreter)
I mentioned this (forgetting that “:-
” is an operator in Prolog, so it should have been “s[: -1]
”) as an example of extending is/2 and function evaluation to non-numeric things, such as strings. Also, I think that S1[: -1]
is more readable than ? sub_string(S1, 0, _, 1)
.
As is, an arithmetic expression containing user-defined functions is compiled into a sequence of native arithmetic evaluation and calls to the user defined function. Thus,
A is 1+f(1+B)
becomes
T1 is 1+B,
f(T1, T2),
A is 1+T2
Note that T = 1+f(1+B), A is T
does not work (raises an exception that f/1 is not a function). This is a rather fragile construction that was invented after dropping full support for user-defined functions because it blocked serious optimization of arithmetic. Some of your stuff with strings and lists happen to work thanks to the old compatibility for A is "b"
which requires some weird hacks that deal with lists and strings. Nothing to be proud of.
I do see some merits in defining f/2 to take something other than a number as first argument that would change the above translation into the code below which should make you happy (I think).
f(1+B, T1),
A is 1+T1

Permit any
atomic
to appear as a literal in a “arithmetic” expression. Such atomics are just expanded to themselves and evaluate to themselves (identity function in functional programming terms).
Note that some atoms are (constant) functions, e.g., A is pi
. Doing this merely seems to allow user-defined arithmetic functions on atoms (with some exceptions) and strings (again with exception of one-character strings leading to inconsistent results).

I don’t see why is/2 should be limited to numbers.
Mainly because the compile time expansion leads to inconsistent results as we have also seen for several other extensions based on code transformation. As long as any variable in an expression may be a term holding functions that need to be evaluated, full and reliable compilation is not possible and the only consistent solution for is/2 is to interpret it. At least, I do not see anything better.
I’d be more in favor of either a general function like ?/1 (or some variation) or another predicate than is/2 for evaluating expressions holding compound terms. Both also come with their own problems
Some other random things that cross my mind:
- Stopping is/2 to be numeric reduces the options for future type analysis.
- What about
f1(X) < f2(Y)
? Does this need to be numeric? Or do we also allow overloading </2?

Note that
T = 1+f(1+B), A is T
does not work (raises an exception that f/1 is not a function).
I’m aware that the current support is “static”, i.e., expansion is done at load time, and no “pre-processing” is done on T=Exp
goals. Even more unfortunate is that T=eval(string_length("abcde")), A is T
and T=string_length("abcde"), A is eval(T)
don’t work either. So until the built-in eval
function becomes user function aware, users will have to use arithmetic_expression_value/2
as a workaround. I don’t see this as a significant use case however; perhaps meta-interpreters(?).

Some of your stuff with strings and lists happen to work thanks to the old compatibility for
A is "b"
which requires some weird hacks that deal with lists and strings. Nothing to be proud of.
I’d be interested in knowing what those hacks are. As long as is
means unify A
with the result of evaluating the right hand side expression, I don’t understand the problem. The only subtle wrinkle might be that the value of "b"
is 98
, and the value of "ab"
is "ab"
; that’s because built-in arithmetic takes precedence (first principle: do no harm). I guess I’d have to represent a single character string literal by "b"[0]
, or some such.

Note that some atoms are (constant) functions, e.g.,
A is pi
.
As long as pure arithmetic expressions (as defined by evaluable/1
) are processed before atomics by do_expand_function
(do no harm), nothing would change. It would mean any atomic arguments that evaluate to a number as defined by the builtin arithmetic, won’t be seen by the user function. Since I’m interested in mixed mode arithmetic, I see that as a feature, not a bug.

Mainly because the compile time expansion leads to inconsistent results as we have also seen for several other extensions based on code transformation. As long as any variable in an expression may be a term holding functions that need to be evaluated, full and reliable compilation is not possible and the only consistent solution for is/2 is to interpret it.
I don’t think I’m suggesting anything that would make this worse than it currently is. On the other hand, it enables you to do a lot more (see examples).

I’d be more in favor of either a general function like ?/1 (or some variation) or another predicate than is/2 for evaluating expressions holding compound terms.
Maybe I’m missing something but I don’t see what another notation buys you; particularly as I’m interested in expressions mixing numeric arithmetic with other user defined types. Why have X = ? M[1,2]*X1+M[2,1]*X2
when X is M[1,2]*X1+M[2,1]*X2
fits perfectly well into the existing paradigm. At the very least the new “thing” would have to do everything the existing extensible arithmetic does.

Stopping is/2 to be numeric reduces the options for future type analysis.
Depends on what that means in detail. Isn’t arithmetic just the tip of the iceberg? Isn’t this a different debate for another time?

What about
f1(X) < f2(Y)
? Does this need to be numeric? Or do we also allow overloading ?
I think these can these can be left as numeric, i.e., f1(X)
and f2(X)
evaluate to numbers. The user can overload any of them if he uses goal expansion and a unique syntax. In fact the arrays example does exactly that, e.g., M[1,2] is M[2,1]
. I suppose if a user type had a defined linear order, overloading of comparison operators makes sense, but I don’t think that affects this discussion.
All I’m proposing is minor extensions to user defined arithmetic. These should not affect anything that’s currently working. They do not address any perceived deficiencies in the current design; they just give it expanded capabilities. I think the best approach is to extend the current arithmetic module since I’ve yet to see any disadvantages. But if that isn’t acceptable, I’ll think about cloning the module and releasing an extended version as a package. Unfortunately that would raise the possibility of dueling goal expansions.
P.S. Perfect is the enemy of good enough.

Yes, I was using Python sequence slicing notation (although I don’t think it’s original with Python; ISTR it being essentially the same in Icon or Snobol).
Ah, that makes more sense. My Snobol is even rustier - that’s university days and I won’t mention when that was. I’ve tested a subset with just [S:E]
, both required, just using existing definition of :
operator. Missing operands require additional prefix operator def for :
so left it for now. Here’s a quick hack for indexing and slicing using block notation:
% block notation
:- arithmetic_function(user:[]/2).
% slicing operation
:- arithmetic_function(user:':'/2).
':'(B,E,B:E). % identity
[](Ix, St, X) :-
string(St), !, % requirement for string_eval
string_eval(Ix,St,X).
string_eval([B:E],St,X) :- integer(B), integer(E), !,
string_length(St,L),
(B<0 -> SB is L+B ; SB=B),
(E<0 -> SL is L+E-SB ; SL is E-SB),
sub_string(St, SB, SL, _, X).
string_eval([At],St,X) :- integer(At), !,
string_length(St,L),
(At<0 -> SB is L+At ; SB=At),
sub_string(St, SB, 1, _, X).
Looks like a conditional expression would also be useful. With a string_length
function I could write something like:
SB is (At<0 ? string_length(St)+At ; At)
(depending on operator precedence).

For if-then-else evaluable function you need non-strict evaluation.
Good point, I hadn’t thought of that. Of course we do have the option of not generating such arithmetic exceptions:
?- Y=0, X is 1/Y.
Y = 0,
X = 1.0Inf.

I think the best approach is to extend the current arithmetic module since I’ve yet to see any disadvantages. But if that isn’t acceptable, I’ll think about cloning the module and releasing an extended version as a package. Unfortunately that would raise the possibility of dueling goal expansions.
I’m not against changing library(arithmetic). In my view it is mostly a backward compatibility hack anyway, so as long as it remains reasonably compatible I’m happy. My preference would (still) be to allow for a
:- arithmetic_function(name(type1, type2, ...)).
next to :- arithmetic_function(name/N)
for controlling the expansion and allow for future static analysis. A small difference in type handing wrt lists and strings doesn’t bother me either. I don’t think creating a complete functional notation infrastructure as a hack on top of is/2 is where I would like to go. I’m happy to make the core system support it though. Nobody has to use it
If you can live with that, please make a PR.

P.S. Perfect is the enemy of good enough.
I agree. The early development in SWI-Prolog followed this idea very closely. Over the years I have become a little more conservative

I’m not against changing library(arithmetic). In my view it is mostly a backward compatibility hack anyway, so as long as it remains reasonably compatible I’m happy.
OK, I’ll make that my working assumption.

My preference would (still) be to allow for a
:- arithmetic_function(name(type1, type2, ...)).
next to
:- arithmetic_function(name/N)
for controlling the expansion and allow for future static analysis.
I don’t have any quarrel with this, but I don’t want to conflate this with the immediate objective.

A small difference in type handing wrt lists and strings doesn’t bother me either.
As of the moment, I think atomics (includes strings and the empty list) may be all that’s required. List “evaluation” can be done with a user defined function.

Nobody has to use it
Indeed. Any nobody that’s using the current feature capability should see any difference.

If you can live with that, please make a PR.
I will do that as soon as I’m happy with a solution to variadic data terms. I’ll assume you’ll want any tests updated as well. (Statement, not a question )

You also do a non-strict if-then-lese for performance and side effects,
here is an example with side effect:
More good points, but I view these as corner cases. My main objective is arithmetic functions which are “pure” in the functional programming sense. If that’s not the case, e.g., random
, conditionals won’t necessarily work as expected. And If you stick with the intended meaning of random
, rather than implementation, even this is a non-issue.
Likewise for performance. Don’t depend on short circuited evaluation for optimization reasons. In fact, conditional expressions will never be as efficient as the Prolog “if-then-else”.
Main point is to transparent about how conditional expressions are evaluated.

They are the same.
All true, but I was referring to a SWIP user defined arithmetic function. In this context you would have to call an additional predicate and either a) pre-evaluate any expressions or b) meta evaluate the expression, foregoing compiled arithmetic.
But, as you point out, you could treat it as a special case expansion and it shouldn’t be less efficient than “hand-coding”.
I need to pin down the semantics of the builtin arithmetic function .(+Int,[])
. Experimenting:
?- X is [a].
X = 97.
?- X is [ab].
ERROR: Type error: `character' expected, found `ab' (an atom)
ERROR: In:
ERROR: [10] _1618310 is [ab]
ERROR: [9] <user>
?- X is [22].
X = 22.
?- X is [-22].
ERROR: Type error: `character' expected, found `-22' (an integer)
ERROR: In:
ERROR: [10] _1620700 is [-22]
ERROR: [9] <user>
?- X is [1.0].
ERROR: Type error: `character' expected, found `1.0' (a float)
ERROR: In:
ERROR: [10] _1621912 is [1.0]
ERROR: [9] <user>
?- X is [1r2].
ERROR: Type error: `character' expected, found `1r2' (a rational)
ERROR: In:
ERROR: [10] _1623130 is [1r2]
ERROR: [9] <user>
?- current_prolog_flag(double_quotes,Q).
Q = string.
?- X is ["a"].
ERROR: Type error: `character' expected, found `"a"' (a string)
ERROR: In:
ERROR: [10] _1624342 is ["a"]
ERROR: [9] <user>
From this I infer the rule for checking the legality of [Code]
in expressions is:
eval_code(Code) :- var(Code).
eval_code(Code) :- integer(Code), Code>=0.
eval_code(Code) :- atom(Code), atom_length(Code,1).
I’m not proposing this be changed, but it’s not what’s documented. In particular, there’s no mention of the somewhat odd IMO integer case, and the non-implementation of the single character string case. (I would prefer it to continue to not work on any string.)
Any thoughts?
The only element of the list must be something that can be interpreted as a character code. So, all these are the ASCII lowercase “a”, preferably written as 0'a
.
?- X is `a`.
X = 97.
?- X is "a".
X = 97.
?- X is [a].
X = 97.
?- X is [0'a].
X = 97.
?- X is [97].
X = 97.
I remember someone saying that [a]
is a workaround from the times when people hadn’t come up with 0'a
. Maybe I remember wrong :-/
From the examples above, those three are the same:
`a`
[0'a]
[97]
The funny ones are:
"a"
(it works because it used to, back when "a"
was a list of character codes?)
[a]
(it works because it used to, back when 0'a
did not exist?)
Please someone correct me if I am wrong.