Skip to content

Commit

Permalink
from-string function creation
Browse files Browse the repository at this point in the history
  • Loading branch information
rm-hull authored and Richard Hull committed Mar 27, 2016
1 parent 441d90b commit c683dff
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 57 deletions.
99 changes: 75 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ There is a version hosted at [Clojars](https://clojars.org/rm-hull/infix).
For leiningen include a dependency:

```clojure
[rm-hull/infix "0.1.2"]
[rm-hull/infix "0.2.0"]
```

For maven-based projects, add the following to your `pom.xml`:
Expand All @@ -38,60 +38,111 @@ For maven-based projects, add the following to your `pom.xml`:
<dependency>
<groupId>rm-hull</groupId>
<artifactId>infix</artifactId>
<version>0.1.2</version>
<version>0.2.0</version>
</dependency>
```

## Basic Usage

```clojure
(refer 'infix.macros :only '[infix])
=> nil
(refer 'infix.macros :only '[infix from-string])
; => nil

(infix 3 + 5 * 8)
=> 43
; => 43

(infix (3 + 5) * 8)
=> 64
; => 64
```

Some `Math` functions have been aliased (see [below](#aliased-operators--functions) for full list), so single argument functions can be
used as follows:

```clojure
(infix √(5 * 5))
=> 5.0
; => 5.0

(infix121)
=> 11.0
; => 11.0

(infix 2 ** 6)
=> 64.0
; => 64.0

(def t 0.324)
=> #'user/t
; => #'user/t

(infix sin(2 * t) + 3 * cos(4 * t))
=> 1.4176457261295824
; => 1.4176457261295824

(macroexpand-1 '(infix sin(2 * t) + 3 * cos(4 * t))
=> (+ (Math/sin (* 2 t)) (* 3 (Math/cos (* 4 t))))
; => (+ (Math/sin (* 2 t)) (* 3 (Math/cos (* 4 t))))
```

### Evaluating infix expressions from a string

A function can created at runtime from an expression held in a string as
follows. When building from a string, a number of binding arguments should be
supplied, corresponding to any variable that may be used in the string
expression, for example:

```clojure
(def hypot (from-string "sqrt(x**2 + y**2)" x y))
; => #'user/hypot

(hypot 3 4)
; => 5
```

In effect, this is equivalent to creating the following function:

```clojure
(defn hypot [x y]
(infix sqrt(x ** 2 + y ** 2)))
```

However, it does so with recourse to `eval` and `read-string` - instead it is
built using a custom infix parser-combinator with a restricted base environment
of math functions, as outlined in the next section.

Functions may be passed where they are not in the base environment, e.g.:

```clojure
(defn rad
"Calculate the radians for the supplied degrees"
[deg]
(infix deg * π / 180))
; => user/rad

(def rhs-triangle-height
(from-string "tan(rad(angle)) * base" base angle rad)
; => user/rhs-triangle-height

(rhs-triangle-height 10 45 rad)
; => 9.9999999999998
```

> **TODO:** allow base env to be extended and passed in, possibly
> along the lines of `with-redefs`.

### Aliased Operators & Functions

| Alias | Operator | | Alias | Operator | | Alias | Operator |
|--------|-----------------|---|--------|-----------------|---|--------|-----------------|
| && | and | | abs | Math/abs | | sin | Math/sin |
| \|\| | or | | signum | Math/signum | | cos | Math/cos |
| == | = | | ** | Math/pow | | tan | Math/tan |
| != | not= | | exp | Math/exp | | asin | Math/asin |
| % | mod | | log | Math/log | | acos | Math/acos |
| << | bit-shift-left | | e | Math/E | | atan | Math/atan |
| >> | bit-shift-right | | π | Math/PI | | sinh | Math/sinh |
| ! | not | | sqrt | Math/sqrt | | cosh | Math/cosh |
| & | bit-and | | √ | Math/sqrt | | tanh | Math/tanh |
| \| | bit-or | | . | * | | | |
| Alias | Operator | | Alias | Operator | | Alias | Operator |
|--------|------------------------|---|--------|-----------------|---|--------|-----------------|
| && | and | | abs | Math/abs | | sin | Math/sin |
| \|\| | or | | signum | Math/signum | | cos | Math/cos |
| == | = | | ** | Math/pow | | tan | Math/tan |
| != | not= | | exp | Math/exp | | asin | Math/asin |
| % | mod | | log | Math/log | | acos | Math/acos |
| << | bit-shift-left | | e | Math/E | | atan | Math/atan |
| >> | bit-shift-right | | π | Math/PI | | sinh | Math/sinh |
| ! | not | | sqrt | Math/sqrt | | cosh | Math/cosh |
| & | bit-and | | √ | Math/sqrt | | tanh | Math/tanh |
| \| | bit-or | | root | ba | | sec | Secant |
| | | | φ | Golden ratio | | csc | Cosecant |
| gcd | Greatest common divsor | | fact | Factorial | | cot | Cotangent |
| lcm | Least common multiple | | ∑ | Sum | | asec | Arcsecant |
| | | | ∏ | Product | | acsc | Arccosecant |
| | | | | | | acot | Arccotangent |

## References

Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject rm-hull/infix "0.1.2"`
(defproject rm-hull/infix "0.2.0"`
:description "A small Clojure/ClojureScript library for expressing LISP expressions as infix rather than prefix notation"
:url "https://github.com/rm-hull/infix"
:license {
Expand Down
18 changes: 1 addition & 17 deletions src/infix/grammar.clj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

(def digit (from-re #"[0-9]"))

; TODO: allow unicode/utf8 characters
(def letter (from-re #"[a-zA-Z]"))

(def alpha-num (any-of letter digit (match "_") (match "-")))
Expand Down Expand Up @@ -175,20 +176,3 @@
(return
(fn [env]
((op env) (t1 env) (t2 env)))))))

(def base-env
(merge
; wrapped java.lang.Math constants & functions
(->>
(ns-publics 'infix.math)
(map (fn [[k v]] (vector (keyword k) v)))
(into {}))

; Basic ops
{
:+ +
:- -
:* *
:/ /
:% mod
}))
48 changes: 46 additions & 2 deletions src/infix/macros.clj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
;; SOFTWARE.


(ns infix.macros)
(ns infix.macros
(:require
[infix.parser :refer [parse-all]]
[infix.grammar :refer [expression]]
[infix.math :as m]))

(def operator-alias
{'&& 'and
Expand All @@ -47,12 +51,27 @@
'sinh 'Math/sinh
'cosh 'Math/cosh
'tanh 'Math/tanh
'sec 'm/sec
'csc 'm/csc
'cot 'm/cot
'asec 'm/asec
'acsc 'm/acsc
'acot 'm/acot
'exp 'Math/exp
'log 'Math/log
'e 'Math/E
'π 'Math/PI
'φ 'm/φ
'sqrt 'Math/sqrt
'√ 'Math/sqrt })
'√ 'Math/sqrt
'root 'm/root
'gcd 'm/gcd
'lcm 'm/lcm
'fact 'm/fact
'sum 'm/sum
'∑ 'm/sum
'product 'm/product
'∏ 'm/product })

(def operator-precedence
; From https://en.wikipedia.org/wiki/Order_of_operations#Programming_languages
Expand Down Expand Up @@ -107,3 +126,28 @@
infix expressions into standard LISP prefix expressions."
[& expr]
(-> expr resolve-aliases rewrite))

(def base-env
(merge
; wrapped java.lang.Math constants & functions
(->>
(ns-publics 'infix.math)
(map (fn [[k v]] (vector (keyword k) v)))
(into {}))

; Basic ops
{
:+ +
:- -
:* *
:/ /
:% mod
}))

(defmacro from-string [expr & bindings]
`(if-let [f# (parse-all expression ~expr)]
(with-meta
(fn [~@bindings]
(f# ~(into base-env (map #(vector (keyword %) %) bindings))))
{:doc ~expr})
(throw (java.text.ParseException. (str "Failed to parse expression: '" ~expr "'") 0))))
8 changes: 3 additions & 5 deletions src/infix/math.clj
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.

(ns infix.math
(:require
[infix.macros :refer [infix]]))
(ns infix.math)

(defmacro ^:private defunary [func-name & [alias]]
(let [arg (gensym "x__")]
Expand Down Expand Up @@ -58,7 +56,7 @@
(defbinary pow)
(defbinary pow **)

(def φ (infix (1 + 5) / 2))
(def φ (/ (inc ( 5)) 2))
(def e Math/E)
(def π Math/PI)
(def pi Math/PI)
Expand All @@ -77,7 +75,7 @@
(defn lcm [a b]
(/ (* a b) (gcd a b)))

(defn factorial [n]
(defn fact [n]
(if (zero? n)
1
(apply * (range 1 (inc n)))))
Expand Down
11 changes: 5 additions & 6 deletions test/infix/grammar_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
(:require
[clojure.test :refer :all]
[infix.grammar :refer :all]
[infix.parser :refer [parse-all]]))
[infix.parser :refer [parse-all]]
[infix.macros :refer [base-env]]))

(defn float=
([x y] (float= x y 0.00001))
Expand Down Expand Up @@ -120,10 +121,8 @@
(is (float= (Math/atan (/ 1 0.21)) ((parse-all expression "acot(0.21)") base-env)))
(is (float= (+ 1 2 5.7 4) ((parse-all expression "sum(1, 2, 5.7, 4)") base-env)))
(is (float= (* 1 2 5.7 4) ((parse-all expression "product(1, 2, 5.7, 4)") base-env)))
(is (= 1 ((parse-all expression "factorial 0") base-env)))
(is (= 120 ((parse-all expression "factorial 5") base-env)))
(is (= 1 ((parse-all expression "fact 0") base-env)))
(is (= 120 ((parse-all expression "fact 5") base-env)))
(is (= 4 ((parse-all expression "gcd(8, 12)") base-env)))
(is (= 24 ((parse-all expression "lcm(8, 12)") base-env)))

)
(is (= 24 ((parse-all expression "lcm(8, 12)") base-env))))

14 changes: 12 additions & 2 deletions test/infix/macros_tests.clj
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@


(ns infix.macros-tests
(:use [clojure.test]
[infix.macros]))
(:require
[clojure.test :refer :all]
[infix.macros :refer [infix from-string]]))

(def ε 0.0000001)

Expand Down Expand Up @@ -52,3 +53,12 @@
y 3 ]
(is (= 12 (infix x . y)))
(is (= 64.0 (infix x ** y)))))

(deftest check-from-string
(is (= 7 ((from-string "x + 3" x) 4)))
(is (thrown-with-msg? java.text.ParseException #"Failed to parse expression: 'x \+ '"
((from-string "x + ") 3)))
(is (thrown-with-msg? clojure.lang.ArityException #"Wrong number of args \(2\) passed to: .*"
((from-string "x + 3") 2 3)))
(is (thrown-with-msg? IllegalStateException #"x is not bound in environment"
((from-string "x + 3")))))

0 comments on commit c683dff

Please sign in to comment.