diff --git a/NEWS.rst b/NEWS.rst index f8d746ef..f4817a53 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,5 +1,19 @@ .. default-role:: code +Unreleased +====================================================== + +Removals +------------------------------ +* `defn/a+` has been removed. Use `(defn+ :async …)` instead. +* `fn/a+` has been removed. Use `(fn+ :async …)` instead. + +Bug Fixes +------------------------------ +* A lot of incompatibilities of `meth`, `ameth`, `defn+`, and `fn+` + with the core `defn` and `fn` have been fixed. You can now use `:async`, + docstrings are recognized properly, etc. + 0.8.0 (released 2025-01-08) ====================================================== diff --git a/docs/index.rst b/docs/index.rst index c7044195..de6013ba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -75,10 +75,8 @@ API ~~~~~~~~~~ .. hy:automacro:: defn+ -.. hy:automacro:: defn/a+ .. hy:automacro:: dict=: .. hy:automacro:: fn+ -.. hy:automacro:: fn/a+ .. hy:automacro:: let+ .. hy:automacro:: setv+ diff --git a/hyrule/destructure.hy b/hyrule/destructure.hy index 6ae01586..0a971af4 100644 --- a/hyrule/destructure.hy +++ b/hyrule/destructure.hy @@ -1,61 +1,61 @@ ;;; Hy destructuring bind -" -This module is heavily inspired by destructuring from Clojure and provides very -similar semantics. It provides several macros that allow for destructuring within -their arguments. - -Destructuring allows one to easily peek inside a data structure and assign names to values within. For example, :: - - (setv+ {[{name :name [weapon1 weapon2] :weapons} :as all-players] :players - map-name :map - :keys [tasks-remaining tasks-completed]} - data) +#[=[ +This module is heavily inspired by destructuring from Clojure and provides +similar semantics. It provides several macros that allow for destructuring +within their arguments, not unlike iterable unpacking, as in ``(setv [a #* b c] +(range 10))``, but also functioning on dictionaries and allowing several special +options. + +Destructuring allows you to easily peek inside a data structure and assign names to values within. For example, suppose you have a data structure like this:: + + (setv data {:players [{:name "Joe" :weapons [:sword :dagger]} + {:name "Max" :weapons [:axe :crossbow]}] + :map "Dungeon" + :tasks-remaining 4}) -would be equivalent to :: +You could manually write out a lot of assignments like this:: (setv map-name (.get data ':map) tasks-remaining (.get data ':tasks-remaining) tasks-completed (.get data ':tasks-completed) all-players (.get data ':players) name (.get (get all-players 0) ':name) - weapon1 (get (.get (get all-players 0) ':weapons) 0) - weapon2 (get (.get (get all-players 0) ':weapons) 1)) + weapon1 (get all-players 0 :weapons 0) + weapon2 (get all-players 0 :weapons 1)) -where ``data`` might be defined by :: +Or, for the same result, you could use a destructuring macro:: - (setv data {:players [{:name Joe :weapons [:sword :dagger]} - {:name Max :weapons [:axe :crossbow]}] - :map \"Dungeon\" - :tasks-remaining 4}) - -This is similar to unpacking iterables in Python, such as ``a, *b, c = range(10)``, however it also works on dictionaries, and has several special options. + (setv+ {[{name :name [weapon1 weapon2] :weapons} :as all-players] :players + map-name :map + :keys [tasks-remaining tasks-completed]} + data) .. warning:: - Variables which are not found in the expression are silently set to ``None`` if no default value is specified. This is particularly important with ``defn+`` and ``fn+``. :: + Variables which are not found in the data are silently set to ``None`` if no default value is specified. This is particularly important with ``defn+`` and ``fn+``. :: (defn+ some-function [arg1 - {subarg2-1 \"key\" + {subarg2-1 "key" :or {subarg2-1 20} :as arg2} [subarg3-1 :& subargs3-2+ :as arg3]] - {\"arg1\" arg1 \"arg2\" arg2 \"arg3\" arg3 - \"subarg2-1\" subarg2-1 \"subarg3-1\" subarg3-1 \"subargs3-2+\" subargs3-2+}) + {"arg1" arg1 "arg2" arg2 "arg3" arg3 + "subarg2-1" subarg2-1 "subarg3-1" subarg3-1 "subargs3-2+" subargs3-2+}) - (some-function 1 {\"key\" 2} [3 4 5]) - ; => {\"arg1\" 1 \"arg2\" {\"key\" 2} \"arg3\" [3 4 5] - ; \"subarg2-1\" 2 \"subarg3-1\" 3 \"subargs3-2+\" [4 5]} + (some-function 1 {"key" 2} [3 4 5]) + ; => {"arg1" 1 "arg2" {"key" 2} "arg3" [3 4 5] + ; "subarg2-1" 2 "subarg3-1" 3 "subargs3-2+" [4 5]} (some-function 1 2 []) - ; => {\"arg1\" 1 \"arg2\" None \"arg3\" [] - ; \"subarg2-1\" 20 \"subarg3-1\" None \"subargs3-2+\" []} + ; => {"arg1" 1 "arg2" None "arg3" [] + ; "subarg2-1" 20 "subarg3-1" None "subargs3-2+" []} (some-function) - ; => {\"arg1\" None \"arg2\" None \"arg3\" None - ; \"subarg2-1\" 20 \"subarg3-1\" None \"subargs3-2+\" None} + ; => {"arg1" None "arg2" None "arg3" None + ; "subarg2-1" 20 "subarg3-1" None "subargs3-2+" None} - Note that variables with a default value from an ``:or`` special option will fallback to their default value instead of being silently set to ``None``. + Notice how a variable with a default value from an ``:or`` special option will fall back to that value instead of ``None``. Pattern types ~~~~~~~~~~~~~ @@ -65,34 +65,41 @@ Several kinds of patterns are understood. Dictionary patterns ------------------- -Dictionary patterns are specified using dictionaries, where the keys corresponds to the symbols which are to be bound, and the values correspond to which key needs to be looked up in the expression for the given symbol. :: +Dictionary patterns are specified using a :class:`hy.models.Dict`, where the keys corresponds to the symbols to be bound, and the values correspond to keys to be looked up in the data. :: - (setv+ {a :a b \"b\" c #(1 0)} {:a 1 \"b\" 2 #(1 0) 3}) + (setv+ {a :a b "b" c #(1 0)} + {:a 1 "b" 2 #(1 0) 3}) [a b c] ; => [1 2 3] -The keys can also be one of the following 4 special options: ``:or``, ``:as``, ``:keys``, ``:strs``. +A key in a dictionary pattern can also be one of the following special options: - ``:or`` takes a dictionary of default values. -- ``:as`` takes a variable name which is bound to the entire expression. -- ``:keys`` takes a list of variable names which are looked up as keywords in the expression. -- ``:strs`` is the same as ``:keys`` but uses strings instead. +- ``:as`` takes a symbol, which is bound to the entire input dictionary. +- ``:keys`` takes a list of symbols, which are looked up as keywords in the data. +- ``:strs`` works like ``:keys``, but looks up strings instead of keywords. + +For example:: + + (setv+ {:keys [a b] :strs [c d] :or {b 2 d 4} :as full} + {:a 1 :b 2 "c" 3}) + [a b c d full] ; => [1 2 3 4 {:a 1 :b 2 "c" 3}] -The ordering of the special options and the variable names doesn't matter, however each special option can be used at most once. :: +The ordering of the special options and the ordinary variable names doesn't matter, but each special option can be used at most once. - (setv+ {:keys [a b] :strs [c d] :or {b 2 d 4} :as full} {:a 1 :b 2 \"c\" 3}) - [a b c d full] ; => [1 2 3 4 {:a 1 :b 2 \"c\" 3}] +If a lookup fails, and the symbol to be bound to doesn't have an associated default, ``None`` is bound instead:: -Variables which are not found in the expression are set to ``None`` if no default value is specified. + (setv+ {out "a"} {}) + (is out None) ; => True List patterns ------------- -List patterns are specified using lists. The nth symbol in the pattern is bound to the nth value in the expression, or ``None`` if the expression has fewer than n values. +List patterns are specified with a :class:`hy.models.List`. The ``n``\th symbol in the pattern is bound to the ``n``\th value in the data, or ``None`` if the data has fewer than ``n`` values. -There are 2 special options: ``:&`` and ``:as``. +List patterns support these special options: -- ``:&`` takes a pattern which is bound to the rest of the expression. This pattern can be anything, including a dictionary, which allows for keyword arguments. -- ``:as`` takes a variable name which is bound to the entire expression. +- ``:&`` takes a pattern, which is bound to the rest of the data. This pattern can be anything, including a dictionary pattern, which allows for keyword arguments. +- ``:as`` takes a symbol, which is bound to the whole input iterable. If the special options are present, they must be last, with ``:&`` preceding ``:as`` if both are present. :: @@ -102,13 +109,19 @@ If the special options are present, they must be last, with ``:&`` preceding ``: (setv+ [a b :& {:keys [c d] :or {c 3}}] [1 2 :d 4 :e 5] [a b c d] ; => [1 2 3 4] -Note that this pattern calls ``list`` on the expression before binding the variables, and hence cannot be used with infinite iterators. +Note that list patterns call ``list`` on the data before binding the variables, so they can't be used with infinite iterators. Iterator patterns ----------------- -Iterator patterns are specified using round brackets. They are the same as list patterns, but can be safely used with infinite generators. The iterator pattern does not allow for recursive destructuring within the ``:as`` special option. -" +Iterator patterns are specified with a :class:`hy.models.Expression`. They work the same as list patterns except that they only consume as much of the input as is required for matching, so they can be safely used with infinite iterators. ``:rest`` and ``:as`` create iterables instead of lists, and no recursive destructuring within ``:as`` is allowed. :: + + (import itertools [count islice]) + (setv+ (a b :& rest :as full) (count)) + [a b] ; => [0 1] + (list (islice rest 5)) ; => [2 3 4 5 6] + (list (islice full 5)) ; => [0 1 2 3 4] +]=] (require hyrule.argmove [->>] @@ -118,20 +131,17 @@ Iterator patterns are specified using round brackets. They are the same as list itertools [starmap chain count] functools [reduce] hy.pyops * - hyrule.iterables [rest] - hyrule.collections [by2s]) + hyrule.collections [by2s] + hyrule.macrotools [parse-defn-like]) (defmacro setv+ [#* pairs] - "Assignment with destructuring for both mappings and iterables. - - Destructuring equivalent of ``setv``. Binds symbols found in a pattern - using the corresponding expression. +#[=[ - Examples: - :: +Take pairs of destructuring patterns and input data structures, assign to variables in the current scope as specified by the patterns, and return ``None``. :: - (setv+ pattern_1 expression_1 ... pattern_n expression_n) - " + (setv+ {a "apple" b "banana"} {"apple" 1 "banana" 2} + [c d] [3 4]) + [a b c d] ; => [1 2 3 4]]=] (setv gsyms []) `(do (setv ~@(gfor [binds expr] (by2s pairs) @@ -140,10 +150,14 @@ Iterator patterns are specified using round brackets. They are the same as list (del ~@gsyms))) (defmacro dict=: [#* pairs] - "Destructure into dict + #[[ + +Take pairs of destructuring patterns and input data structures, and return a dictionary of bindings, where the keys are :class:`hy.models.Symbol` objects. The syntax is the same as that of :hy:func:`setv+`. :: + + (dict=: {a "apple" b "banana"} + {"apple" 1 "banana" 2}) + ; => {'a 1 'b 2}]] - Same as ``setv+``, except returns a dictionary with symbols to be defined, - instead of actually declaring them." (setv gsyms [] result (hy.gensym 'dict=:)) `(do @@ -330,48 +344,24 @@ Iterator patterns are specified using round brackets. They are the same as list s [(hy.models.Keyword k) v] s))))) -(defmacro! defn+ [fn-name args #* doc+body] - "Define function `fn-name` with destructuring within `args`. - - Note that `#*` etc have no special meaning and are - intepretted as any other argument. - " - (setv [doc body] (if (isinstance (get doc+body 0) str) - [(get doc+body 0) (rest doc+body)] - [None doc+body])) - `(defn ~fn-name [#* ~g!args #** ~g!kwargs] - ~doc - ~(_expanded-setv args g!args g!kwargs) - ~@body)) - -(defmacro! fn+ [args #* body] - "Return anonymous function with destructuring within `args` +(defmacro defn+ [#* args] + "As :hy:func:`defn`, but the lambda list is destructured as a list pattern. The usual special parameter names in lambda lists, such as `#*`, aren't special here. No type annotations are allowed are in the lambda list, but a return-value annotation for the whole function is allowed." + (destructuring-fn 'defn args)) - Note that `*`, `/`, etc have no special meaning and are - intepretted as any other argument. - " - `(fn [#* ~g!args #** ~g!kwargs] - ~(_expanded-setv args g!args g!kwargs) - ~@body)) +(defmacro fn+ [#* args] + "A version of :hy:func:`fn` that destructures like :hy:func:`defn+`." + (destructuring-fn 'fn args)) -(defmacro! defn/a+ [fn-name args #* doc+body] - "Async variant of ``defn+``." - (setv [doc body] (if (isinstance (get doc+body 0) str) - [(get doc+body 0) (rest doc+body)] - [None doc+body])) - `(defn/a ~fn-name [#* ~g!args #** ~g!kwargs] +(defn destructuring-fn [like args] + (setv [headers params doc body] (parse-defn-like like args)) + (setv args (hy.gensym) kwargs (hy.gensym)) + `(~@headers [#* ~args #** ~kwargs] ~doc - ~(_expanded-setv args g!args g!kwargs) - ~@body)) - -(defmacro! fn/a+ [args #* body] - "Async variant of ``fn+``." - `(fn/a [#* ~g!args #** ~g!kwargs] - ~(_expanded-setv args g!args g!kwargs) + ~(_expanded-setv params args kwargs) ~@body)) (defmacro let+ [args #* body] - "let macro with full destructuring with `args`" + "A version of :hy:func:`let` that allows destructuring patterns in place of plain symbols for binding." (when (% (len args) 2) (raise (ValueError "let bindings must be paired"))) `(let ~(lfor [bs expr] (by2s args) diff --git a/hyrule/macrotools.hy b/hyrule/macrotools.hy index 75785454..8c79c58e 100644 --- a/hyrule/macrotools.hy +++ b/hyrule/macrotools.hy @@ -358,3 +358,33 @@ (if ident `(. ~imp ~@(map hy.models.Symbol ident)) imp)) + + +(defn parse-defn-like [like args] + ; Currently, this is just for internal use. + + (cond + (= like 'fn) + (setv [headers args] (if (= (get args 0) ':async) + [['fn (get args 0)] (rest args)] + [['fn] args])) + (= like 'defn) (do + (setv headers ['defn] args (list args)) + ; Pop leading elements from `args` until we hit the function name. + (while True + (.append headers (.pop args 0)) + (when (or + ; The function name is a symbol or an annotation expression. + (isinstance (get headers -1) hy.models.Symbol) + (and + (isinstance (get headers -1) hy.models.Expression) + (get headers -1) + (= (get headers -1 0) 'annotate))) + (break))))) + + (setv [params #* args] args) + (setv [doc body] (if (and (> (len args) 1) (isinstance (get args 0) str)) + [(get args 0) (cut args 1 None)] + [None args])) + + #(headers params doc body)) diff --git a/hyrule/oop.hy b/hyrule/oop.hy index 62db2df3..2d226e83 100644 --- a/hyrule/oop.hy +++ b/hyrule/oop.hy @@ -1,5 +1,5 @@ (import - hyrule.macrotools [map-model]) + hyrule.macrotools [map-model parse-defn-like]) (defmacro meth [#* args] @@ -35,17 +35,15 @@ The symbol ``@,`` is replaced with just plain ``self``. By contrast, the symbol ``@`` is left untouched, since it may refer to the Hy core macro :hy:func:`@ <hy.pyops.@>`.]] - (if (and args (isinstance (get args 0) hy.models.List)) - (setv [decorators name params #* body] args) - (setv decorators [] [name params #* body] args)) - `(defn ~decorators ~name ~@(_meth params body))) + (_meth 'defn args)) -(defmacro ameth [params #* body] +(defmacro ameth [#* args] "Define an anonymous method. ``ameth`` is to :hy:func:`meth` as :hy:func:`fn` is to :hy:func:`defn`: it has the same syntax except that no method name (or decorators) are allowed." - `(fn ~@(_meth params body))) + (_meth 'fn args)) -(defn _meth [params body] +(defn _meth [like args] + (setv [headers params doc body] (parse-defn-like like args)) (setv to-set []) (setv params (map-model params (fn [x] (when (and (isinstance x hy.models.Symbol) (.startswith x "@")) @@ -58,9 +56,9 @@ (= x '@) '@ (= x '@,) 'self True `(. self ~(hy.models.Symbol (cut x 1 None)))))))) - `[ - [self ~@params] + `(~@headers [self ~@params] + ~doc ~@(gfor sym to-set `(setv (. self ~sym) ~sym)) - ~@body]) + ~@body)) diff --git a/tests/test_destructure.hy b/tests/test_destructure.hy index dead2d75..9e669b12 100644 --- a/tests/test_destructure.hy +++ b/tests/test_destructure.hy @@ -329,6 +329,7 @@ (assert (= zot "text"))) (defn test-defn+ [] + (defn+ foo [[a b] {:keys [c d]} :& {:strs [e f]}] "bar foo" [a b c d e f]) @@ -336,16 +337,52 @@ (assert (= "bar foo" foo.__doc__)) (defn+ bar [&optional &rest &kwonly] (+ &optional &rest &kwonly)) - (assert (= (bar 1 2 3) 6))) + (assert (= (bar 1 2 3) 6)) + + (defn+ foo []) + (assert (is (foo) None))) + +(do-mac (when (>= hy.I.sys.version-info #(3 12)) +'(defn test-defn+-fancy [] + + (defn decorator1 [f] + (setv f.a1 1) + f) + (defn decorator2 [f] + (setv f.a2 2) + f) + + (defn+ + :async + [decorator1 decorator2] + :tp [T] + #^ (get list T) fancy-fun + [[a b]] + [(- a b)]) + + (assert (= (hy.I.asyncio.run (fancy-fun [5 2])) [3])) + (assert (= fancy-fun.a1 1)) + (assert (= fancy-fun.a2 2)) + (assert (= + (str (:return (hy.I.inspect.get-annotations fancy-fun))) + "list[T]"))))) (defn test-fn+ [] + (setv f (fn+ [[a b] {:keys [c d]} :& {:strs [e f]}] [a b c d e f])) (assert (= [1 2 3 4 5 6] (f [1 2] {:c 3 :d 4} "e" 5 "f" 6))) (setv g (fn+ [&optional &rest &kwonly] (+ &optional &rest &kwonly))) - (assert (= 6 (g 1 2 3)))) + (assert (= 6 (g 1 2 3))) + + (setv f (fn+ [])) + (assert (is (f) None)) + + (setv f (fn+ [] "hello world" 1)) + (assert (= f.__doc__ "hello world")) + (assert (= (f) 1))) (defn test-let+ [] (let+ [a 1] diff --git a/tests/test_oop.hy b/tests/test_oop.hy index 1f378dc1..6d491933 100644 --- a/tests/test_oop.hy +++ b/tests/test_oop.hy @@ -65,10 +65,12 @@ (defclass Pony [] (meth __init__ [a1 @i1 [@i2 "i2-default"] [a2 "a2-default"] #* @ia #** @ikw] + "docstring" (nonlocal got) (setv got [a1 @i1 i2 a2 @ia @ikw]) (setv @i1 "override"))) + (assert (= Pony.__init__.__doc__ "docstring")) (setv x (Pony 1 2)) (assert (= got [1 2 "i2-default" "a2-default" #() {}])) (assert (= x.i1 "override")) @@ -98,6 +100,27 @@ (assert (= x.attr 2))) +(do-mac (when (>= hy.I.sys.version-info #(3 12)) +'(defn test-meth-fancy [] + + (defclass Pony [] + (meth + :async + [example-decorator] + :tp [T] + #^ (get list T) fancy-meth + [a @b] + [(- a @b)])) + + (setv p (Pony)) + (assert (= (hy.I.asyncio.run (.fancy-meth p 5 2)) [3])) + (assert (= p.b 2)) + (assert (= Pony.fancy-meth.da "hello")) + (assert (= + (str (:return (hy.I.inspect.get-annotations Pony.fancy-meth))) + "list[T]"))))) + + (defn test-ameth [] (defclass Pony []