Quantcast
Viewing all articles
Browse latest Browse all 9596

Getting towards an infix notation for hy

Engineers need infix notation. It's a bold statement I know, but I am an engineer, teach engineers, and write a fair bit of mathematical programs. Your typical engineer is not a programmer, and just wants to write an equation they way we would write it on paper. It is hard to undo 20+ years of education on that point! So, here we consider how to adapt hy to use infix notation.

In a recentpost gilch suggested using strings with the builtin python eval function. There are some potential downsides to that approach including the overhead of byte-compiling each time it is eval'd, but the payoff is operator precedence, and doing it like you would do it in Python.

Here is one implementation.

(def py-eval (get __builtins__ "eval"))

And how to use it.

(import [infix [*]]) (print (py-eval "2+3*5")) (import [numpy :as np]) (print (py-eval "2 * np.exp(np.pi / 2)")) 17 9.62095476193

We can eliminate the need for quotes with the stringify code we previously developed.

(import [serialize [*]]) (import [infix [*]]) (print (py-eval (stringify `(2+3*5)))) (print (py-eval (stringify `(2 + 3 * 5)))) (import [numpy :as np]) (print (py-eval (stringify `(2 * np.exp(np.pi / 2))))) 17 17 9.62095476193

Let's just take that one more step with a new reader macro so we can avoid the quoting.

(defn pymath [code] (py-eval (stringify code))) (defreader p [code] (pymath code))

Now we can use it like this. We have to require the infix module to get the reader macro.

(import [serialize [*]]) (import [infix [*]]) (require infix) (print (pymath `(2 + 3))) (print #p(2 + 3 * 5)) (print #p((2 + 3) * 5))

That looks like it works, but this next example doesn't work. It seems to be a namespace issue, where np is not defined when the function is executed or the reader macro is expanded, which occurs in some separate namespace I think. The first example works, but the second and third don't. I don't know if that is fixable. If anyone knows how, drop me a note in the comments!

(import [serialize [*]]) (import [infix [*]]) (require infix) (import [numpy :as np]) (print (py-eval (stringify `(2 * np.exp(np.pi / 2))))) (try (print (pymath `(2 * np.exp(np.pi / 2)))) (except [e Exception] (print "pymath: " e))) ;; this excepts so bad the try doesn't even catch it! ;; (try ;; (print #p(2 * np.exp(np.pi / 2))) ;; (except [e2 Exception] ;; (print "reader macro: " e2))) 9.62095476193 pymath: name 'np' is not defined

This also does not work for a similar namespace issue.

(import [serialize [*]]) (import [infix [*]]) (require infix) (import [numpy :as np]) (setv x 5) (try (print (pymath `(3 * x))) (except [e Exception] (print e))) name 'x' is not defined

Well, no problem, we will try some other approaches.

1 Infix to prefix using code manipulation

This solution is inspired by https://sourceforge.net/p/readable/wiki/Solution/ , but probably isn't a full implementation. We will first develop a function to convert infix notation to prefix notation. This function is recursive to deal with nested expressions. So far it doesn't seem possible to recurse with macros (at least, I cannot figure out how to do it). We tangle this function to infix.hy so we can use it later.

It will have some limitations though:

No operator precedence. This is too difficult for me to code. We will use parentheses for precedence. Lisp syntax means 3+4 is not the same as 3 + 4. The first is interpreted as a name. So we will need spaces to separate everything. (try (print (3+4)) (except [e Exception] (print e))) (print (+ 3 4)) name '3+4' is not defined 7

So, here is our infix function. Roughly, the function takes a CODE argument. If the CODE is iterable, it is a list of symbols, and we handle a few cases:

If it is a string, we return it. if it has a length of one and is an expression we recurse on it, otherwise return the symbol. if it has a length of two, we assume a unary operator and recurse on each element. If there are three elements, we take the middle one as the operator, and switch it with the first element. Otherwise we switch the first and second elements, and recurse on the rest of the list. If it is not iterable we just return the element.

Two optional arguments provide some debug support to print what is happening.

(import [serialize [*]]) (defn nfx [code &optional [indent 0] [debug False]] "Transform the CODE expression to prefix notation. We assume that CODE is in infix notation." (when debug (print (* " " indent) "code: " code)) (cond [(coll? code) (cond [(= 1 (len code)) ;; element is an Expression (when debug (print "1: " code)) (if (isinstance (car code) hy.models.expression.HyExpression) (nfx (car code) (+ indent 1) debug) ;; single element code)] ;; {- 1} -> (- 1) [(= 2 (len code)) (when debug (print "2: " code)) `(~(nfx (get code 0) (+ indent 1) debug) ~(nfx (get code 1) (+ indent 1) debug))] ;; {1 + 2} -> (+ 1 2) [(= 3 (len code)) (when debug (print (* " " indent) "3: " code)) `(~(get code 1) ~(nfx (get code 0) (+ indent 1) debug) ~(nfx (get code 2) (+ indent 1) debug))] ;; longer expression, swap first two and take the rest. [true (when debug (print "expr: " code)) `(~(nfx (get code 1) (+ indent 1) debug) ~(nfx (get code 0) (+ indent 1) debug) (~@(nfx (cut code 2) (+ indent 1) debug)))])] ;; non-iterable just gets returned [true (when debug (print (* " " indent) "true: " code)) code]))

Now, for some tests. First, an example of debugging.

(import [infix [*]]) (print (nfx `(1 + (3 * 4)) :debug True)) code: (1L u'+' (3L u'*' 4L)) 3: (1L u'+' (3L u'*' 4L)) code: 1 true: 1 code: (3L u'*' 4L) 3: (3L u'*' 4L) code: 3 true: 3 code: 4 true: 4 (u'+' 1L (u'*' 3L 4L))

You can see we return a list of symbols, and the result is not evaluated. Now for some more thorough tests. I use a little helper function here to show the input and output.

(import [infix [*]]) (import [serialize [stringify]]) (defn show [code] (print (.format "{0} -> {1}\n" (stringify code) (stringify (nfx code))))) (show 1) (show `(1)) (show `(- 1)) (show `((1))) (show `(- (2 + 1))) (show `(2 ** 4)) (show `(3 < 5)) (show `(1 + 3 * 5 + 6 - 9)) (show `((1 + (1 + 2)) * 5 + 6 - 9)) (show `(1 + 1 * (5 - 4))) (show `(1 + 1 * (np.exp (17 - 10)))) (show `(x + long-name)) ; note name mangling occurs. (show `(1 + 1 + 1 + 1 + 1)) 1 -> 1 (1) -> (1) (- 1) -> (- 1) ((1)) -> (1) (- (2 + 1)) -> (- (+ 2 1)) (2 ** 4) -> (** 2 4) (3 < 5) -> (< 3 5) (1 + 3 * 5 + 6 - 9) -> (+ 1 (* 3 (+ 5 (- 6 9)))) ((1 + (1 + 2)) * 5 + 6 - 9) -> (* (+ 1 (+ 1 2)) (+ 5 (- 6 9))) (1 + 1 * (5 - 4)) -> (+ 1 (* 1 (- 5 4))) (1 + 1 * (np.exp (17 - 10))) -> (+ 1 (* 1 (np.exp (- 17 10)))) (x + long_name) -> (+ x long_name) (1 + 1 + 1 + 1 + 1) -> (+ 1 (+ 1 (+ 1 (+ 1 1))))

Those all look reasonable I think. The last case could be simplified, but it would take some logic to make sure all the operators are the same, and that handles if any of the operands are expressions. We save that for later.

Now, we illustrate that the output code can be evaluated. Since we expand to code, we don't seem to have the namespace issues.

(import [infix [*]]) (print (eval (nfx `(1 + 1 * (5 - 4))))) (import [numpy :as np]) (print (eval (nfx `(1 + 1 * (np.exp (17 - 10)))))) 2 1097.63315843

That syntax is not particularly nice, so next we build up a macro, and a new reader syntax. First, the macro.

(defmacro $ [&rest code] (import infix) `(eval (infix.nfx ~code)))

Now we can use the simpler syntax here. It seems we still have quote the math to prevent it from being evaluated (which causes an error).

(import infix) (require infix) (print ($ `(1 + 1 * (5 - 4)))) (import [numpy :as np]) (print ($ `(1 + 1 * (np.exp (17 - 10))))) 2 1097.63315843

For the penultimate act, we introduce a new syntax for this. In the sweet expression syntax we would use {} for this, but this isn't currently possible for hylang, and is also used for dictionaries. We define a reader macro for this.

(defreader $ [code] (import infix) (infix.nfx code)) (import [infix [*]]) (require infix) (import [numpy :as np]) (print #$(- 1)) (print #$(- (2 + 1))) (print #$(2 ** 4)) (print #$(3 < 5)) (print #$(1 + 3 * 5 + 6 - 9)) (print #$((1 + (1 + 2)) * 5 + 6 - 9)) (print #$(1 + 1 * (5 - 4))) (print #$(1 + 1 + 1 + 1 + 1)) (print #$(1 + 1 * (np.exp (17 - 10)))) (setv a 3 t 6) (print #$(a + t)) (setv long-a 5 long-b 6) (print #$(long-a + long-b)) -1 -3 16 True 7 8 2 5 1097.63315843 9 11

Wonderful! We get variables passed through, and the name-mangling doesn't seem to matter.

2 The final test

For the final act, we use infix notation in a real problem we posed before.

(import [numpy :as np]) (import [scipy.integrate [odeint]]) (import [scipy.special [jn]]) (import [matplotlib.pyplot :as plt]) (import [infix [*]]) (require infix) (defn fbessel [Y x] "System of 1st order ODEs for the Bessel equation." (setv nu 0.0 y (get Y 0) z (get Y 1)) ;; define the derivatives (setv dydx z ;; the Python way is: "1.0 / x**2 * (-x * z - (x**2 - nu**2) * y)" dzdx #$((1.0 / (x ** 2)) * ((- x) * z) - (((x ** 2) - (nu ** 2)) * y))) ;; Here is what it was with prefix notation ;; dzdx (* (/ 1.0 (** x 2)) (- (* (* -1 x) z) (* (- (** x 2) (** nu 2)) y)))) ;; return derivatives [dydx dzdx]) (setv x0 1e-15 y0 1.0 z0 0.0 Y0 [y0 z0]) (setv xspan (np.linspace 1e-15 10) sol (odeint fbessel Y0 xspan)) (plt.plot xspan (. sol [[Ellipsis 0]]) :label "Numerical solution") (plt.plot xspan (jn 0 xspan) "r--" :label "Analytical solution") (plt.legend :loc "best") (plt.savefig "bessel-infix.png")
Image may be NSFW.
Clik here to view.
Getting towards an infix notation for hy

That worked pretty well. The string approach doesn't seem to work with the namespace issues. The infix notation is pretty heavily parenthesized to get the operator precedence right. Probably it is worth trying to get that right, but that seems like a challenge. For now this feels like an improvement for engineering programs!

Copyright (C) 2016 by John Kitchin. See theLicense for information about copying.

org-mode source

Org-mode version = 8.2.10


Viewing all articles
Browse latest Browse all 9596

Trending Articles