-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Expression builder pattern for evaluation control #25620
Comments
I think something like this is exactly what is needed for end users to specify how to create unevaluated expressions. We should add something like this now and make use of it for things like # uses ExprBuilder internally:
expr = parse_latex(text, evaluate=True/False) Ultimately what is needed though is that an ExprBuilder could return an expression type with none of its methods using automatic evaluation e.g. this creates an expression without evaluation
but all of the methods like |
Usage of nested lists is way easier to use and maintain, i.e. Have a look at the Mathematica parser. |
I think that the S-expressions are more useful for serializing the symbolic expressions. For parsers, or evaluation controls, I'm afraid that S-expression can just be making another intermediate language that needs to be evaluated later, and this may not may not solve the problem. sympy/sympy/parsing/mathematica.py Lines 974 to 1054 in e998c1a
and the difference between this or lark transformer, or mathematica parser, is very subtle like dictioary vs function vs object vs ... |
People are going to want to be able to create expressions with operators. No one wants to type out |
For that true unevaluated expressions are needed i.e. expressions whose methods do not evaluate (so it should not be Basic).
Actually in many contexts like e.g. the LaTeX parser you do generally want to handle all of these programmatically and so this is fine. Many users would not like it but anyone doing programmatic things would probably be fine with it and it would still be useful within SymPy itself. |
There have long been suggestions to make SymPy classes unevaluated by default. Unfortunately this requires a lot of core rewriting and is unlikely to happen. Matthew Rocklin made some attempts at creating a parallel system of expressions costructors and destructors years ago, have a look at the unify module. I believe the easiest path to unevaluated expressions is to just use lists. |
I don't think that we can just make the classes unevaluated by default because they are widely used and it would break compatibility. What is needed is a class that represents inert non-evaluating expressions that is separate from the existing classes. Evaluated expressions (which many users do want even if they are not always desirable) can be implemented over the top of unevaluating expressions. |
There is also another gotcha or limitation of using arithmetic under with evaluate(False):
expr = a + b + c + d
sstr(expr, order='none')
# '((a + b) + c) + d' And it is not possible to generate |
That's another good point. I think what we would really want for evaluate(False) is a macro system, that rewrites the code under the block. That isn't actually possible in Python, though, so we just fake it. It would probably be possible to do this with something like https://github.com/lihaoyi/macropy, but that's an extension to the language, so I don't think we really want to go down that path. Actually, even with a completely unevaluated core, it's impossible to distinguish |
With unevaluated expressions the Python expression There can be an explicit flatten function for flattening and other explicit methods that do obvious things like |
Well, |
Problems of
evaluate=False
Passing
evaluate=evaluate
causes mistakeI have some examples taken from the ongoing development of LaTeX parser:
where all other methods have
evaluate
implemented,but forgot to implement at
Eq
And also, the other mistake not easy to notice is that:
where unary negation
-tokens[2]
is used insidesympy.Add(tokens[0], -tokens[2], evaluate=False)
,which causes that specific part to ignore the option and evaluate regardless.
So the major problem is
evaluate
keyword argument is optional, so someone can forget to implement that. It is logical error and cannot be detected by any means of lint, type checking, or runtime error.evaluate
keyword cannot be used with arithmetic operators,+, -, *
, so we can't use arithmetic operators of SymPy in this context. If someone is using that, it is always an logical error.Passing
evaluate=evaluate
is verboseFor example
becomes
And this has 4 different sympy functions, so
becomes much more worse like:
because there are 4 different sympy functions used,
And there isn't any way to eliminate the verbosity, in any means, either by assigning to temporarily variables
or either by assigning to temporary lambda
Suggestion: Expression builder
Introduction
sympy/sandbox/abstract.py
Although they use the type annotations, it is not scary and can always be optional
You can switch to
Any
thanExpr
if SymPy has some problems of inferring some types.sympy/sandbox/evaluatefalse.py
sympy/sandbox/evaluatetrue.py
What we done so far, is that we split the sympy functions into
evaluate=False
andevaluate=True
.For example,
dot_product
can be implemented as:The thing you notice is that module itself, goes to the function signature
and it allows that you can just switch the modules like
evaluatefalse
to give different effects of evaluation.Similarly, the pythagorian identity function can also be implemented like this
The only thing you notice is that everything is changed to
B.add, B.mul, ...
which may slightly be verbose, however, it should be familar for some people who may already used
sympy.sin, sympy.cos
.However, the point I have is that, if we use
evaluate=False
, it should always be applied simultaneously, all true or all false.So we can always substitute that keyword argument, with some form of object oriented programming, to encapsulate that keyword argument.
Customization
We can also give the very general solution for user customizability,
For example, if some user want to evaluate only on
sin, cos, pow
but not on other functions,users can customize the behavior very generally, and SymPy have little cost to support that generality.
sympy/sandbox/evaluatesome.py
Users may be able to do much more general thing, about changing the algebra of SymPy itself, than a simple control of evaluation, like:
sympy/sandbox/evaluatetropical.py
Advantages
Safety:
evaluate=
cohesively in one place, like in a module or in a class, which makes it less prone to mistakes.pyright
ormypy
with the example, you may notice that it quickly raises warnings about type incompatibility, ifevaluate...
does not have some required function, or functions does not match the number of arguments, return types, ...Generality:
Performance:
Verbosity:
however, if we properly support
evaluate=False
in SymPy, like in examplereturn Add(Pow(sin(x, evaluate=evaluate), 2, evaluate=evaluate), Pow(cos(x, evaluate=evaluate), 2, evaluate=evaluate))
there is no way to eliminate the verbosity in any means,
So keeping the variants in one place, like modules, is optimal in that respect.
Backward Compatibility:
evaluate=False
option.Notes
The usage of module systems,
Protocol
may look esoteric for some readers.I can easily implement my suggestion with traditional inheritance, ABC, ... however, I didn't like to do that
because I think that protocols and module systems are minimal with respect to other object-oriented solutions, and keeps many interesting properties (being singleton, need no construction, …) that starts to break when you use traditional object oriented programming.
Although we can implement in more dynamic way, like inspecting sympy module by
__dir__
or__dict__
, this can have some drawback (like losing the static type systm) and we can encounter other issues that happens dynamically, like false positives, naming conflicts, cyclic imports.So I'd just suggest to define the builder functions for every list of sympy functions. Even if it looks a bit stupid, it should be very foolhardy.
We can try to substitute bad function namings of SymPy, like
class sin, class Heaviside
, and make them consistent withsnake_case
if someone implements this.This is my technical suggestion to replace
evaluate(False)
.I may receive any questions, if this fits the implementation like making
latex_parser
more cleaner,or could apply in dirty corners of
evaluate
in SymPy.The text was updated successfully, but these errors were encountered: