Skip to content

Commit

Permalink
Merge pull request #113 from Kodiologist/superstructure
Browse files Browse the repository at this point in the history
Fix a lot of bugs in `meth`, `defn+`, etc.
  • Loading branch information
Kodiologist authored Jan 29, 2025
2 parents 9989306 + 1fe146a commit 2731af7
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 114 deletions.
14 changes: 14 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -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)
======================================================

Expand Down
2 changes: 0 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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+
Expand Down
188 changes: 89 additions & 99 deletions hyrule/destructure.hy
Original file line number Diff line number Diff line change
@@ -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
~~~~~~~~~~~~~
Expand All @@ -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. ::

Expand All @@ -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 [->>]
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions hyrule/macrotools.hy
Original file line number Diff line number Diff line change
Expand Up @@ -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))
20 changes: 9 additions & 11 deletions hyrule/oop.hy
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(import
hyrule.macrotools [map-model])
hyrule.macrotools [map-model parse-defn-like])


(defmacro meth [#* args]
Expand Down Expand Up @@ -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 "@"))
Expand All @@ -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))
Loading

0 comments on commit 2731af7

Please sign in to comment.