Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Built-in predicate request: function_property/2 #1181

Open
pmoura opened this issue Jun 8, 2023 · 17 comments
Open

Built-in predicate request: function_property/2 #1181

pmoura opened this issue Jun 8, 2023 · 17 comments

Comments

@pmoura
Copy link
Contributor

pmoura commented Jun 8, 2023

Latest versions of LVM and Trealla Prolog implement a function_property/2 built-in predicate, which provides similar functionality to the de facto standard predicate_property/2 built-in predicate, allowing checking or enumerating the properties of a given arithmetic function. The function_property/2 predicate allows clean checking if an arithmetic function is defined, simplifying e.g. portability linter checks for arithmetic expressions. The first argument is a function template (e.g. abs(_)) and the second argument is the property. Four properties are specified:

  • built_in (function is a defined built-in arithmetic function)
  • foreign (function is a defined arithmetic function implemented using the FLI)
  • static (function is a static arithmetic function)
  • dynamic (function is a dynamic arithmetic function)

The dynamic is meant for Prolog systems that allow runtime definition of new arithmetic functions (e.g. LVM). Built-in and foreign functions usually also have the static property.

The predicate spec is:

Template:
function_property(Function, Property)
Modes and number of proofs:
function_property(+callable, ?function_property) - zero_or_more

Exceptions:

  • Function is a variable:
    instantiation_error
  • Function is neither a variable nor a callable term:
    type_error(callable, Function)
  • Property is neither a variable nor an atom:
    type_error(atom, Property)
  • Property is an atom but not a valid function property:
    domain_error(function_property, Property)

Examples:

Check that popcount/1 is a built-in arithmetic function:

?- function_property(popcount(_), built_in).
true.

Would it be possible to add this predicate to SWI-Prolog?

@pmoura
Copy link
Contributor Author

pmoura commented Jun 9, 2023

Update

An additional property, that enables e.g. more sophisticated linter checks, is template/2. It allows querying function arguments types and return types. For example:

?- function_property(abs(_), Property).
   Property = built_in ;
   Property = static ;
   Property = template(abs(integer), integer) ;
   Property = template(abs(float), float) ;
   false.

For some Prolog systems, the necessary internal tables to implement this property seems to already be there. For other, it would be more work. And in the case of SWI-Prolog?

@JanWielemaker
Copy link
Member

Thanks. The template property seems useful. As is, SWI-Prolog provides current_arithmetic_function/1, to query existing functions. Reasoning about their types seems useful. But, it is more complicated, in particular when ration numbers are involved. The type for e.g., ** is hard to predict. If there is a correct rational, this is returned, but if the result is not rational, it becomes a float, so we have rat**rat -> rat|float.

@pmoura
Copy link
Contributor Author

pmoura commented Jun 12, 2023

Wondering about the possible trouble of having:

?- function_property(_ ** _, Property).
   Property = built_in ;
   Property = static ;
   Property = template(rat ** rat, rat) ;
   Property = template(rat ** rat, float) ;
   ...

Does this problem exists for types other than rat?

@pmoura
Copy link
Contributor Author

pmoura commented Jun 16, 2023

A more concise (but also ambiguous) alternative would be to simply use in this case:

template(number ** number, number)

@JanWielemaker
Copy link
Member

Possibly. Roughly SWI-Prolog only knows rationals and floats. Any function (except for casting) with at least one float argument will produce a float result. If all arguments are rational the result is rational if well defined. Well, we do not consider special cases such as sin(0) -> 0.

I'm now on holiday. I think it makes sense to add this as a library, or possibly add the type info to the built-in functions. Note that for several Prolog systems, evaluation simply calls predicates with an extra argument and values are not even limited to numners.

@pmoura
Copy link
Contributor Author

pmoura commented Jun 23, 2023

At didoudiaz/gprolog#40 Daniel makes a good point that using instead evaluable_property/2 fits better with the ISO Prolog Core standard. Both me and Andrew agree with the change for LVM and Trealla Prolog. The standard describes evaluable functors, not arithmetic functions. It also works better for arithmetic constants (e.g. epsilon and pi) and type errors (where the type is specified as evaluable.

@JanWielemaker
Copy link
Member

What about functions that do not change the type, e.g., +(X) simply returns X. should that have template(+(T),T), or a template for each supported type? Another case is max/2. It returns the max of its arguments without modifying the type, so it returns either the 1st or 2nd arg unmodified. We could represent that as

 template(max(T,_), T)
 template(max(_,T), T)

@JanWielemaker
Copy link
Member

See a proposal in https://github.com/SWI-Prolog/swipl-devel/blob/master/library/prolog_evaluable.pl

I think the first argument should not be required to be instantiated, so we can enumerate evaluable functions, consistent with predicate_property/2 and most (all?) other built-in *_property/2 predicates. We might also consider to add a property visible, to enumerate all functions regardless of how they are defined? SWI-Prolog got that for predicate_property/2 to deal with the many ways predicates may be defined in SWI-Prolog (including lazy loading).

@pmoura
Copy link
Contributor Author

pmoura commented Jul 3, 2023

What about functions that do not change the type, e.g., +(X) simply returns X. should that have template(+(T),T), or a template for each supported type? Another case is max/2. It returns the max of its arguments without modifying the type, so it returns either the 1st or 2nd arg unmodified. We could represent that as

 template(max(T,_), T)
 template(max(_,T), T)

Similar concerns by other Prolog system implementers (notably, for ECLiPSe). Currently, the template/2 property specification should be considered WIP.

@pmoura
Copy link
Contributor Author

pmoura commented Jul 3, 2023

See a proposal in https://github.com/SWI-Prolog/swipl-devel/blob/master/library/prolog_evaluable.pl

The static, dynamic, and foreign properties should still be accepted (i.e. recognized as valid), if when they don't apply to a particular Prolog system.

I think the first argument should not be required to be instantiated, so we can enumerate evaluable functions, consistent with predicate_property/2 and most (all?) other built-in *_property/2 predicates. We might also consider to add a property visible, to enumerate all functions regardless of how they are defined? SWI-Prolog got that for predicate_property/2 to deal with the many ways predicates may be defined in SWI-Prolog (including lazy loading).

Please see the discussion on didoudiaz/gprolog#40. There, the idea is to allow enumeration using a second current_evaluable/1 predicate.

Genuine question: did you (or anyone else) ever found a sensible use case for being able to enumerate predicates using predicate_property/2? Or, for that matter, to enumerate all built-in predicates using current_predicate/1? Personally, even with all the work I done on developer tools, I'm yet to find a single case that would require enumerating built-ins. I'm interested in others experience here.

@pmoura
Copy link
Contributor Author

pmoura commented Jul 3, 2023

A visible property could simplify some code, even if only testing if an evaluable functor is available. Daniel is also suggestion an iso property for evaluable functors specified in ISO Prolog standard documents.

@JanWielemaker
Copy link
Member

The static, dynamic, and foreign properties should still be accepted (i.e. recognized as valid), if when they don't apply to a particular Prolog system.

As with the other *_property predicates, there is no domain error on unknown properties, simply failure. That allows implementations to add other properties.

current_evaluable/1

That is SWI's current_arithmetic_function/1. I'd be happy to deprecate that in favor of evaluable_property(X, visible).

Genuine question: did you (or anyone else) ever found a sensible use case for being able to enumerate predicates using predicate_property/2?

Yes. Only in IDEs though, i.e., completing predicate/function names. I also used it to create the rough Prolog template predicate 😄 Its more consistent and I think the implementation should not be a big issue for most systems.

iso property

I'm in favor of that.

@pmoura
Copy link
Contributor Author

pmoura commented Jul 3, 2023

The static, dynamic, and foreign properties should still be accepted (i.e. recognized as valid), if when they don't apply to a particular Prolog system.

As with the other *_property predicates, there is no domain error on unknown properties, simply failure. That allows implementations to add other properties.

The current tests actually throw a domain error on unknown properties. Your argument here is a reasonable one. But so is detecting typos/errors in property names; if not done by the compiler, that would need to be done by a linter.

current_evaluable/1

That is SWI's current_arithmetic_function/1. I'd be happy to deprecate that in favor of evaluable_property(X, visible).

The current_evaluable/1 predicate actually takes an evaluable functor indicator, not a template as it's based on current_predicate/1.

Genuine question: did you (or anyone else) ever found a sensible use case for being able to enumerate predicates using predicate_property/2?

Yes. Only in IDEs though, i.e., completing predicate/function names. I also used it to create the rough Prolog template predicate 😄 Its more consistent and I think the implementation should not be a big issue for most systems.

In the case of IDEs as in your code completion example, I always use an explicit table for built-ins. A main reason to do that in general (instead of enumerating them) is to only list documented built-ins instead of all of them.

iso property

I'm in favor of that.

Good. But its exact meaning needs to be well specified. Notably, if it simply means that the evaluable functor is specified by the ISO standard (light requirement) or that the implementation actually follows the standard specification (stricter requirement).

@JanWielemaker
Copy link
Member

The current tests actually throw a domain error on unknown properties. Your argument here is a reasonable one. But so is detecting typos/errors in property names; if not done by the compiler, that would need to be done by a linter.

IMO, both extensibility and consistency with the predicate_property/2 asks for silent failure. Note that the current spec already has properties that only have meaning in some systems. A linter is better in dealing with typos.

I always use an explicit table for built-ins. A main reason to do that in general (instead of enumerating them) is to only list documented built-ins instead of all of them.

? All functions are supposed to be documented, no? For predicates one typically enumerates over properties such as exported. Independent static tables need to be maintained 😢

Notably, if it simply means that the evaluable functor is specified by the ISO standard (light requirement) or that the implementation actually follows the standard specification (stricter requirement).

Good question. Also a bit grey area and, at least for SWI-Prolog, depending on flags such as prefer_rationals (turning 3/4 into a rational rather than 0.75), float_overflow, float_undefined, etc. Adding types such as complex numbers will stop sqrt(-1) to be an error. Assuming functions with an ISO name/arity at least in spirit perform the requested operation (e.g., possibly returning a more precise type or accepting additional types), i'd vote for iso to simply mean it is part of the ISO standard and implements the same mathematical function. That would be consistent with SWI's predicate_property(?P, iso).

@pmoura
Copy link
Contributor Author

pmoura commented Jul 3, 2023

The current tests actually throw a domain error on unknown properties. Your argument here is a reasonable one. But so is detecting typos/errors in property names; if not done by the compiler, that would need to be done by a linter.

IMO, both extensibility and consistency with the predicate_property/2 asks for silent failure. Note that the current spec already has properties that only have meaning in some systems. A linter is better in dealing with typos.

Note that the ISO Prolog standard specifies domain errors for the predicate_property/2 predicate for invalid properties. For example:

$ gprolog
GNU Prolog 1.6.0 (64 bits)
Compiled Jul  2 2023, 17:25:19 with /opt/local/bin/gcc-mp-12
Copyright (C) 1999-2023 Daniel Diaz

| ?- predicate_property(once(_), foobar).
uncaught exception: error(domain_error(predicate_property,foobar),predicate_property/2)

A good argument here is that a failure would mask invalid properties. If a property only was meaning in system A, there's no sound reason in general to assume its value should be either true or false in system B.

I always use an explicit table for built-ins. A main reason to do that in general (instead of enumerating them) is to only list documented built-ins instead of all of them.

? All functions are supposed to be documented, no? For predicates one typically enumerates over properties such as exported. Independent static tables need to be maintained 😢

My experience is that most systems provide more built-ins that those that are documented. Either because they are really internal predicates and not meant to be called by the user or because they are not ready or part of a stable API. On the plus side, the set of built-ins are is infrequently updated.

Notably, if it simply means that the evaluable functor is specified by the ISO standard (light requirement) or that the implementation actually follows the standard specification (stricter requirement).

Good question. Also a bit grey area and, at least for SWI-Prolog, depending on flags such as prefer_rationals (turning 3/4 into a rational rather than 0.75), float_overflow, float_undefined, etc. Adding types such as complex numbers will stop sqrt(-1) to be an error. Assuming functions with an ISO name/arity at least in spirit perform the requested operation (e.g., possibly returning a more precise type or accepting additional types), i'd vote for iso to simply mean it is part of the ISO standard and implements the same mathematical function. That would be consistent with SWI's predicate_property(?P, iso).

Issues like these indeed make me a bit reluctant to support this iso property.

@JanWielemaker
Copy link
Member

A good argument here is that a failure would mask invalid properties. If a property only was meaning in system A, there's no sound reason in general to assume its value should be either true or false in system B.

True. It is pretty impractical though. Same holds for e.g. current_prolog_flag(foobar, X). The code gets rather unreadable and slow if we have to put catch/3 around all these things. Best would probably be to have inspection predicates that can tell us which properties exist. That way we could also write portable linters 😄

Issues like these indeed make me a bit reluctant to support this iso property.

Demanding this property might indeed not be a good idea. I just did add it to the library as I think it is useful for IDEs and linters.

I'm also unsure about the template. As is, we have e.g.

1 ?- evaluable_property(1+2, template(T,R)).
T = float+_,
R = float ;
T = _+float,
R = float ;
T = rational+rational,
R = rational.

Some alternatives are template(number+number, number) or replacing the variables in the above by number. We could also use given (constant) arguments to give a tighter template. Currently the arguments are ignored.

Another issue is supporting partial evaluation. For that, we need to know whether a function is pure, e.g., its result only depends on its arguments. SWI-Prolog has a few exceptions: cputime, random(Below) and random_float. Possibly that was not a good idea. cputime got inherited from C-Prolog though 😄

@pmoura
Copy link
Contributor Author

pmoura commented Jul 3, 2023

A good argument here is that a failure would mask invalid properties. If a property only was meaning in system A, there's no sound reason in general to assume its value should be either true or false in system B.

True. It is pretty impractical though. Same holds for e.g. current_prolog_flag(foobar, X). The code gets rather unreadable and slow if we have to put catch/3 around all these things. Best would probably be to have inspection predicates that can tell us which properties exist. That way we could also write portable linters 😄

I don't think there's a real problem here. When writing portable code, we should only use standard properties. In this case, a catch is not required and the domain errors will alert to any mistake. If we're using non-standard properties, i.e. writing non-portable code, again a catch is not required and if you make a typo, a domain error again will alert you.

Issues like these indeed make me a bit reluctant to support this iso property.

Demanding this property might indeed not be a good idea. I just did add it to the library as I think it is useful for IDEs and linters.

I'm also unsure about the template. As is, we have e.g.

1 ?- evaluable_property(1+2, template(T,R)).
T = float+_,
R = float ;
T = _+float,
R = float ;
T = rational+rational,
R = rational.

Some alternatives are template(number+number, number)

This is what LVM and Trealla Prolog currently implement.

or replacing the variables in the above by number. We could also use given (constant) arguments to give a tighter template. Currently the arguments are ignored.

Another issue is supporting partial evaluation. For that, we need to know whether a function is pure, e.g., its result only depends on its arguments. SWI-Prolog has a few exceptions: cputime, random(Below) and random_float. Possibly that was not a good idea. cputime got inherited from C-Prolog though 😄

One step at the time. It will take some time to find a consensual and useful spec for the template/2 property. Better linter checks and test set conditions are/were the initial motivation and for that the simply four proposed atomic properties are enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants