potassco / clingo

🤔 A grounder and solver for logic programs.
https://potassco.org/clingo
MIT License
601 stars 79 forks source link

Rewrite: extern(Device, function) ==> function(Device) #517

Open ejgroene opened 1 month ago

ejgroene commented 1 month ago

Question.

I have a logic program for PLCs in which a predicate like:

def_extern(Device, request)

means there is an external literal:

extern(Device, request).

With these I create rules like:

extern(Device, switch)  :-  extern(Device, request).

This rule is then tested with additional logic, which is the whole point of having this ASP program. If all tests succeed, this rule, exactly as it is, then is translated to code for the PLC. I use grounding and reification for that.

However, I would like to write this rule and all its tests in a more direct way, like:

switch(Device)  :-  request(Device).

This would make my test code more readable and independent of the way it is derived from the definitions. I know I can project things using #show, but beside for the tests, there is no solving and hence no model.

Is there a way to rewrite the following?

def_extern(Device, function)  ==>  function(Device)
tortinator commented 1 month ago

Hi @ejgroene , I am happy about your interest in ASP and our systems! Are you aware of our community mailing lists potassco-users@lists.sourceforge.net and potassco-announce@lists.sourceforge.net Your question would be an interesting contribution to the first of the two lists. Best -torsten

rkaminsk commented 1 month ago

I don't understand the question/problem. From the snippets above, I do not see why you cannot use function(Device) instead extern(Device, function). I suspect there might be a reason but that's only a guess.

ejgroene commented 1 month ago

Hi @ejgroene , I am happy about your interest in ASP and our systems! Are you aware of our community mailing lists potassco-users@lists.sourceforge.net and potassco-announce@lists.sourceforge.net Your question would be an interesting contribution to the first of the two lists. Best -torsten

Thank you for bringing these under my attention. I subscribed to both.

ejgroene commented 1 month ago

I don't understand the question/problem. From the snippets above, I do not see why you cannot use function(Device) instead extern(Device, function). I suspect there might be a reason but that's only a guess.

Thank you for your response. Yes, there is a reason, but I left it out to make the question simpler.

In fact, I am creating a tool to be used by others and they can define how in- and outputs map to I/O ports, like:

def_extern(Device, function, <port mapping>).

If they have to make these mappings themselves every time, the tool would be very clumsy to use.

Inside the tool, I translate it to (keeping the port-mapping elsewhere):

extern(Device, function)

That works, but I'd rather write the rules using (without having to write translation rules by hand):

function(Device)

And if that works, if would be very nice for the users to be able to also write the def_ it more directly:

function(Device, <port mapping>).

This would hopefully to cleaner, more readable code with less mistakes.

Can the Python API do this?

rkaminsk commented 1 month ago

You can rewrite a program using clingo's ast. But you could also come up with any other way to specify the input, yaml for example, and translate it into the format you require.

ejgroene commented 1 month ago

After a lot of thinking and experimenting, I came up with a relatively simple solution, with one ingredient that might be interesting for enhancement to Clingo.

I created a simple @function to rewrite (f, a) into f(a), and use that to create an alias and a substitution rule:

alias(@function(F, T, D), bool(T, B))   :-   def_input(D, F, B),   input(T, D, F).
substitute(A,  input(T, D, F))  :-  alias(A, bool(T, B)),   def_input(D, F, B).

(substitutions are recursive replacements, aliases are simple replacements)

The alias and the substitution allow me to automate 80% of the machinery. Most importantly, the rules for alias and substitution can be generic, defined in my api.lp so API users are not bothered with it.

One thing though still must be written each and every time the API is used. Since the newly created literals (by @function) cannot be injected in the current namespace, the compiler complains that they do not exist when used. So code that uses the API is littered with statements like (real code):

rijweg_vrij(T, R)           :-  input(T, R, rijweg_vrij).
rijdenopzicht(T, S)         :-  input(T, S, rijdenopzicht).
virtueel_d(T, S)            :-  input(T, S, virtueel_d).
output(T, S, groen)         :-  groen(T, S).
output(T, S, geel)          :-  geel(T, S).
output(T, S, geelknipper)   :-  geelknipper(T, S).
output(T, S, groenknipper)  :-  groenknipper(T, S).
output(T, S, cijfer(C))     :-  cijfer(T, S, C).

These are just to introduce the functions rijweg_vrij/2, rijdenopzicht/2, virtueel_d/2, groen/2and so on into the current namespace.

As I was doing this, I discovered that Clingo works with arbitrary function names:

@function("f(a/b)", 42)

just gives a new function f(a/b)(42) with the name being a string f(a/b) that cannot be written in ASP directly, but works nonetheless.

As a functional programmer, this makes me think: wouldn't it be nice if ASP would allow functions to be first class citizens and have them assigned to variables right in the code? One could then write:

F(A)  :-  predicate(F, A).

Given F=name and A=42, this would introduce name(42) into the current namespace. That is, so it seems, also the only thing to do, as functions in ASP are already mostly first class in the backend machinery, and only language support is not complete.

Wouldn't that be a nice enhancement to metaprogramming, next to --reify and python, lua or C++?

rkaminsk commented 1 month ago

After a lot of thinking and experimenting, I came up with a relatively simple solution, with one ingredient that might be interesting for enhancement to Clingo.

I created a simple @function to rewrite (f, a) into f(a), and use that to create an alias and a substitution rule:

alias(@function(F, T, D), bool(T, B))   :-   def_input(D, F, B),   input(T, D, F).
substitute(A,  input(T, D, F))  :-  alias(A, bool(T, B)),   def_input(D, F, B).

(substitutions are recursive replacements, aliases are simple replacements)

The alias and the substitution allow me to automate 80% of the machinery. Most importantly, the rules for alias and substitution can be generic, defined in my api.lp so API users are not bothered with it.

One thing though still must be written each and every time the API is used. Since the newly created literals (by @function) cannot be injected in the current namespace, the compiler complains that they do not exist when used. So code that uses the API is littered with statements like (real code):

rijweg_vrij(T, R)           :-  input(T, R, rijweg_vrij).
rijdenopzicht(T, S)         :-  input(T, S, rijdenopzicht).
virtueel_d(T, S)            :-  input(T, S, virtueel_d).
output(T, S, groen)         :-  groen(T, S).
output(T, S, geel)          :-  geel(T, S).
output(T, S, geelknipper)   :-  geelknipper(T, S).
output(T, S, groenknipper)  :-  groenknipper(T, S).
output(T, S, cijfer(C))     :-  cijfer(T, S, C).

These are just to introduce the functions rijweg_vrij/2, rijdenopzicht/2, virtueel_d/2, groen/2and so on into the current namespace.

As I was doing this, I discovered that Clingo works with arbitrary function names:

@function("f(a/b)", 42)

just gives a new function f(a/b)(42) with the name being a string f(a/b) that cannot be written in ASP directly, but works nonetheless.

You are entering dangerous territory here. Function names should follow identifier syntax. This is currently not enforced and there are already some features that would break if you do this. For example, reading aspif format. I might add checks in the future to catch this.

As a functional programmer, this makes me think: wouldn't it be nice if ASP would allow functions to be first class citizens and have them assigned to variables right in the code? One could then write:

F(A)  :-  predicate(F, A).

Given F=name and A=42, this would introduce name(42) into the current namespace. That is, so it seems, also the only thing to do, as functions in ASP are already mostly first class in the backend machinery, and only language support is not complete.

Wouldn't that be a nice enhancement to metaprogramming, next to --reify and python, lua or C++?

Such kind of statements would not increase the expressiveness of the system. You could simply replace all occurrences of F(A) by something like dynamic(F,A). Now the system could be extended to do that internally but the rewriting would have to be done for all atoms in the program. We would end up with one big domain for all atoms, which would be bad for grounding performance. In my opinion, it is better to leave this to the user.

If you know the possible values of F, you can generate these rules via the API.

ejgroene commented 1 month ago

You are entering dangerous territory here. Function names should follow identifier syntax. This is currently not enforced and there are already some features that would break if you do this. For example, reading aspif format. I might add checks in the future to catch this.

I will not enter that territory, I understand that things could break; it is only what triggered me thinking about first class functions.

Such kind of statements would not increase the expressiveness of the system. You could simply replace all occurrences of F(A) by something like dynamic(F,A).

Exactly! For me, being able to offer a library that allows user to write cleaner and better readable code using F(A) instead of dynamic(F, A) is quite important. The practical expressiveness would be better, reducing cognitive load. Indirectly referring to each and every F makes the program hard to read.

Now the system could be extended to do that internally but the rewriting would have to be done for all atoms in the program. We would end up with one big domain for all atoms, which would be bad for grounding performance. In my opinion, it is better to leave this to the user.

If you know the possible values of F, you can generate these rules via the API.

I don't really understand what you are saying here. Is grounding not about knowing all possible values for a variable? That means that possible 'dynamic' functions are known as well, isn't it?

Anyway, thanks for your remarks; they made me think and come up with a new solution.

ejgroene commented 3 weeks ago

A short update. I have implemented a reify predicate that allows for adding new predicates in two steps:

First a program defines reify/1 predicates:

b(f(42)).
reify(A) :- b(A).

These reify predicates are then used in a Python program to add the argument to the AST as a new external fact, after which the the application can use it like:

c :- f(42).

This allows me to create an ASP library that supports its users to write cleaner code. Of course it only makes sense for externals.