In Hy, one usually creates a class using the following approach (Note that I prefer to use lambda
rather than fn
. Hey you kids, get off my lawn!):
%%hylang
(defclass ClassName []
"Docstring for ClassName class."
[[--init--
(lambda [self]
None)]
[set-x
(lambda [self x]
"Set our x value"
(setv self.x x))]
[get-x
(lambda [self]
"Return our x value"
self.x)]])
Which would then be used in the following manner:
%%hylang
(def obj (ClassName))
(obj.set-x "Here's the 'x' value ...")
(obj.get-x)
"Here's the 'x' value ..."
The class definition isn't very Lispy, though. Take a look at how Common Lisp creates their version of methods on classes or how Clojure does it. It's got a very different feel (Clojure's apprach is different in many ways; I'm just talking about the syntax, not the underlying concepts or mechanics; this is, after all, an aesthetic exploration ...).
In the tutorial notebook, we created a Linear Model class to neatly wrap a bunch of functionality. Here it is:
%%hylang
(defclass PolynomialLinearModel []
"A pretty sweet utility Python-Lisp class for creating
a polynomial curve fitting model")
(def PolynomialLinearModel.--init--
(lambda [self x y degree]
(setv self.x x)
(setv self.y y)
(setv self.degree degree)
(setv self.results None)
(setv self.model None)
(setv [self.coeffs self.residuals self.rank self.singular-values self.rcond]
[None None None None None])
(self.polyfit)
None))
(def PolynomialLinearModel.get-y-mean
(lambda [self]
"Get the mean value of the observed data"
(/ (np.sum self.y) self.y.size)))
(def PolynomialLinearModel.get-ss-tot
(lambda [self]
"Get total sum of the squares"
(np.sum (** (- self.y (self.get-y-mean)) 2))))
(def PolynomialLinearModel.get-ss-reg
(lambda [self]
"Get the regression sum of squares"
(np.sum (** (- self.y-predicted (self.get-y-mean)) 2))))
(def PolynomialLinearModel.get-ss-res
(lambda [self]
"Get the sum of squares of residuals"
(np.sum (** (- self.y self.y-predicted) 2))))
(def PolynomialLinearModel.get-r-squared
(lambda [self]
"Get the R^2 value for the polynomial fit"
(- 1 (/ (self.get-ss-res) (self.get-ss-tot)))))
(def PolynomialLinearModel.polyfit
(lambda [self]
"Do all the business"
(setv [self.coeffs self.residuals self.rank self.singular-values self.rcond]
(apply np.polyfit [self.x self.y self.degree] {"full" true}))
(setv self.model (np.poly1d self.coeffs))
(setv self.y-predicted (self.model self.x))
(setv self.r-squared (self.get-r-squared))
(setv self.results {
"coeffs" (self.coeffs.tolist)
"residuals" (self.residuals.tolist)
"rank" self.rank
"singular-values" (self.singular-values.tolist)
"rcond" self.rcond
"r-squared" self.r-squared})))
(def PolynomialLinearModel.--str--
(lambda [self]
"Provide a string representation of the data"
(str self.results)))
(def PolynomialLinearModel.--repr--
(lambda [self]
"Provide a representation of the data"
(self.--str--)))
(def PolynomialLinearModel.predict
(lambda [self xs]
"Given a set of input values, produce outputs using the model"
(self.model xs)))
<function __main__._hy_anon_fn_10>
As you can see, this is quite different from the Hy standard (which is a reformulation of the Python approach). Here's the canonical example from the beginning of this notebook reformulated to use this approach:
%%hylang
(defclass OtherClassName []
"Docstring for ClassName class.")
(def OtherClassName.--init--
(lambda [self]
None))
(def OtherClassName.set-x
(lambda [self x]
"Set our x value"
(setv self.x x)))
(def OtherClassName.get-x
(lambda [self]
"Return our x value"
self.x))
<function __main__._hy_anon_fn_2>
Or, formulated with defun
instead of def
+ lambda
:
%%hylang
(defclass AnotherClassName []
"Docstring for ClassName class.")
(defun AnotherClassName.--init-- [self]
None)
(defun AnotherClassName.set-x [self x]
"Set our x value"
(setv self.x x))
(defun AnotherClassName.get-x [self]
"Return our x value"
self.x)
<function __main__._hy_anon_fn_3>
Let's confirm that this works as expected:
%%hylang
(def obj (AnotherClassName))
(obj.set-x "Here's the 'x' value ...")
(obj.get-x)
"Here's the 'x' value ..."
So, that's a little more lispy ... but here's what would be create:
None
in the constructorself
in the method argsThis would give us a form like the following:
(defclass ClassName []
"docstring")
(defmethod ClassName init []
"docstring"
(setv self.x None))
(defmethod ClassName set-x [x]
"docstring"
(setv self.x x))
(defmethod ClassName get-x []
"docstring"
self.x)
We've got some work to do before we get there, though. Here's a description of what we did above:
(defun class-name.method-name
params body)
And for init method:
(def class-name.init
params body None)
Let's create some helper functions for use in our macros:
%%hylang
(defun has-docstring? [code]
(if (and (> (len code) 4) (string? (get code 3)))
True
False))
(defun get-class [code]
(get code 0))
(defun get-method [code]
(let [[method-name (get code 1)]]
(if (= method-name "init")
"--init--"
method-name)))
(defun get-args [code]
(get code 2))
(defun get-docs [code]
(if (has-docstring? code)
(get code 3)
""))
(defun get-body [code]
(if (has-docstring? code)
(slice code 4)
(slice code 3)))
(defun constructor? [code]
(if (in (get-method code) ["init" "--init--" "__init__"])
True
False))
Testing these:
%%hylang
(def code1 ["ClassName-1" "method-1" ["arg1-1" "arg2-1"] "docstring" ["code-body-1"]])
(def code2 ["ClassName-2" "method-2" ["arg1-2" "arg2-2"] ["code-body-2"]])
(def code3 ["ClassName-3" "init" [] ["code-body-3"]])
(def code4 ["ClassName-4" "init" [] ["code1-4" "code2-4" "code3-4"]])
(def code5 ["ClassName-5" "init" [] ["code1-5"] ["code2-5"] ["code3-5"] ["code4-5"] ["code5-5"]])
(def code6 ["ClassName-6" "method-6" ["arg1-6" "arg2-6"] "returned string"])
[(get-class code1) (get-method code1) (get-args code1) (get-docs code1) (get-body code1)
(get-class code2) (get-method code2) (get-args code2) (get-docs code2) (get-body code2)
(get-class code3) (get-method code3) (get-args code3) (get-docs code3) (get-body code3)
(get-class code4) (get-method code4) (get-args code4) (get-docs code4) (get-body code4)
(get-class code5) (get-method code5) (get-args code5) (get-docs code5) (get-body code5)
(get-class code6) (get-method code6) (get-args code6) (get-docs code6) (get-body code6)]
['ClassName-1', 'method-1', ['arg1-1', 'arg2-1'], 'docstring', [['code-body-1']], 'ClassName-2', 'method-2', ['arg1-2', 'arg2-2'], '', [['code-body-2']], 'ClassName-3', '--init--', [], '', [['code-body-3']], 'ClassName-4', '--init--', [], '', [['code1-4', 'code2-4', 'code3-4']], 'ClassName-5', '--init--', [], '', [['code1-5'], ['code2-5'], ['code3-5'], ['code4-5'], ['code5-5']], 'ClassName-6', 'method-6', ['arg1-6', 'arg2-6'], '', ['returned string']]
Exploring possible macros:
%%hylang
(defmacro defmethod-1 [&rest code]
(let [[class-name (get-class code)]
[method (get-method code)]
[args (get-args code)]
[docstring (get-docs code)]
[body (get-body code)]]
`(setattr ~class-name ~method
(lambda [self] + ~args
~docstring
(do ~@body)
(if ~(constructor? method)
None)))))
(import [hy.models.list [HyList]]
[hy.models.symbol [HySymbol]]
[hy._compat [PY33 PY34]])
(defmacro defmethod-2 [&rest code]
(let [[class-name (get code 0)]
[method (get code 1)]
[args (get code 2)]
[docstring "a docstring"]
[body (slice code 3)]]
`(setattr ~class-name '~method "a")))
(defmacro defmethod-3 [&rest code]
(let [[class-name (get code 0)]
[method (get code 1)]
[args (get code 2)]
[docstring "a docstring"]
[body (slice code 3)]]
`(setattr ~class-name '~method
(lambda ~args
~docstring
(do ~@body)
~(if (in method ["init" "--init--" "__init__"])
None)))))
(defmacro defmethod [&rest code]
(let [[class-name (get code 0)]
[method (get code 1)]
[args (map quote (get code 2))]
[docstring "a docstring"]
;[body (slice code 3)]
[body [1 2 3]]
]
`(setattr ~class-name '~method
(lambda [self x]
"wassup"
[~@args]))))
Testing the macros:
%%hylang
(defclass A []
"The A Class")
(defmethod A init [x]
(setv self.x x))
(defmethod A get-x []
self.x)
(def a (A "apple"))
(a.get-x)