potassco / clorm

🗃️ A Python ORM-like interface for the Clingo Answer Set Programming (ASP) reasoner
https://clorm.readthedocs.io
MIT License
52 stars 5 forks source link

Proposal: new pydantic/dataclasses like API for Predicate #130

Closed evan0greenup closed 6 months ago

evan0greenup commented 7 months ago

Currently, to define a predicate account/3:

class Account(clorm.Predicate):
    id = clorm.StringField
    person = clorm.ConstantField
    balance = clorm.IntegerField

For modern Python IDE or Lint checker, Account.id or <instance of Account>.id has type type. All the operation related to this member will cause static analysis error.

However, for pydantic or dataclasses module. They define a structural object via "type-hint". (<details https://docs.pydantic.dev/latest/concepts/models/> and https://docs.python.org/3/library/dataclasses.html).

This is very concise and efficient. All type checker and autocomplete system work properly.

It would be a good idea to follow the architecture and design pattern as pydantic. For example:

class Account(clorm.Predicate):
    id: str
    person: clorm.ConstantField
    balance: int

For id and balance field, the base class clorm.Predicate do conversion automatically. when accessing the member, it do conversion back also in automatic manner.

This would be better to everyone.

@daveraja , @florianfischer91 , what are your opinions?

daveraja commented 7 months ago

Hi @evan0greenup.

Clorm does actually allow for this syntax. So the following should work as you expect:

from clorm import Predicate, ConstantStr

class Account(Predicate):
    id: str
    person: ConstantStr
    balance: int

ConstantStr is a subclass of str and corresponds to clingo/logic-programming constants while str corresponds to strings. So, the python code Account("x", "y", 3) should map to the logic programming fact account("x", y, 3).

Also if you want the predicate to have a name that is not the default one you can use the following more convenient syntax:

class Account(Predicate, name="my_account"):
    id: str
    person: ConstantStr
    balance: int

So, the above example will be my_account("x", y, 3). Finally, you can also reference an existing predicate sub-class to form more complex definitions:

class A(Predicate):
    foo: int
    bar: str

class B(Predicate):
    blah: int
    blob: A

which will match facts like b(1, a(3,"x")).

There are some things that I want to clean up before updating the documentation. The main limitation that I need to fix is that currently this type annotation syntax doesn't work when you have from __future__ import annotations at the start of the file. I had a brief look to try to understand how pydantic implements it but unfortunately I couldn't find what I wanted; pydantic is a larger more complex code base so I need to spend more time to try to understand how it is implemented.

But apart from that the type annotation syntax works well and that is what I've been using for sometime.

evan0greenup commented 7 months ago

@daveraja

I had a brief look to try to understand how pydantic implements it but unfortunately I couldn't find what I wanted; pydantic is a larger more complex code base so I need to spend more time to try to understand how it is implemented.

Why don't you use decorator to define predicate, similar to dataclasses.dataclass in Python standard library. For example:

@clorm.as_predicate
class A:
    foo: int
    bar: str

If you want some extra argument for the predicate, like change default name

@clorm.as_predicate(name="my_a")
class A:
    foo: int
    bar: str

clorm.as_predicate is equal to clorm.as_predicate() like how dataclasses.dataclass do.

And you can provide extra function like clorm.is_predicate to replace what isinstance(obj, clorm.Predicate) do previosuly.

Please have a look on https://raw.githubusercontent.com/python/cpython/main/Lib/dataclasses.py and get some heuristics.

daveraja commented 7 months ago

Why don't you use decorator to define predicate, similar to dataclasses.dataclass in Python standard library. For example:

Yes, this certainly could be added as an optional syntax and would be more dataclass-like. But to be honest I don't see a particular advantage of one over the other, so it would be low on my list of priorities.

Please have a look on https://raw.githubusercontent.com/python/cpython/main/Lib/dataclasses.py and get some heuristics.

Can you please provide details as I'm not sure what you mean by "heuristics" in this context?

evan0greenup commented 7 months ago

Can you please provide details as I'm not sure what you mean by "heuristics" in this context?

You previously said that pydantic is large project and difficult to understand. I think dataclasses is relatively small codebase which is much easier to understand.

daveraja commented 7 months ago

Can you please provide details as I'm not sure what you mean by "heuristics" in this context?

You previously said that pydantic is large project and difficult to understand. I think dataclasses is relatively small codebase which is much easier to understand.

Ok. Thanks.

daveraja commented 6 months ago

With the release 1.5.0 the Pydantic style of type annotations for specifying Predicate sub-classes has now become the preferred mechanism and the documentation has been updated.

Closing this issue.

evan0greenup commented 6 months ago

@daveraja ,

Does the error when from __future__ import annotations be resolved?

daveraja commented 6 months ago

@evan0greenup Yes. This is fixed with v1.5.0. There is a remaining bug that it doesn't work for inner predicate classes. But this is already fixed in the master branch and I will release it as v1.5.1 early next week. If you find any other cases where it doesn't work please open an issue.