Skip to content

Commit

Permalink
Merge pull request #2493 from Kodiologist/quasiquote
Browse files Browse the repository at this point in the history
Improve quasiquote testing and docs
  • Loading branch information
Kodiologist authored Aug 16, 2023
2 parents 9d8566b + 151f0a1 commit f9df10b
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 161 deletions.
113 changes: 43 additions & 70 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -792,42 +792,60 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
beware that significant leading whitespace in embedded string literals will
be removed.

.. hy:macro:: (quasiquote [form])
.. hy:macro:: (quasiquote [model])
.. hy:macro:: (unquote [model])
.. hy:macro:: (unquote-splice [model])
``quasiquote`` allows you to quote a form, but also selectively evaluate
expressions. Expressions inside a ``quasiquote`` can be selectively evaluated
using ``unquote`` (``~``). The evaluated form can also be spliced using
``unquote-splice`` (``~@``). Quasiquote can be also written using the backquote
(`````) symbol.
``quasiquote`` is like :hy:func:`quote` except that it treats the model as a template, in which certain special :ref:`expressions <expressions>` indicate that some code should evaluated and its value substituted there. The idea is similar to C's ``sprintf`` or Python's various string-formatting constructs. For example::

:strong:`Examples`
(setv x 2)
(quasiquote (+ 1 (unquote x))) ; => '(+ 1 2)

::
``unquote`` indicates code to be evaluated, so ``x`` becomes ``2`` and the ``2`` gets inserted in the parent model. ``quasiquote`` can be :ref:`abbreviated <more-sugar>` as a backtick (\`), with no parentheses, and likewise ``unquote`` can be abbreviated as a tilde (``~``), so one can instead write simply ::

;; let `qux' be a variable with value (bar baz)
`(foo ~qux)
; equivalent to '(foo (bar baz))
`(foo ~@qux)
; equivalent to '(foo bar baz)
`(+ 1 ~x)

(In the bulk of Lisp tradition, unquotation is written ``,``. Hy goes with Clojure's choice of ``~``, which has the advantage of being more visible in most programming fonts.)

.. hy:macro:: (quote [form])
Quasiquotation is convenient for writing macros::

``quote`` returns the form passed to it without evaluating it. ``quote`` can
alternatively be written using the apostrophe (``'``) symbol.
(defmacro set-foo [value]
`(setv foo ~value))
(set-foo (+ 1 2 3))
(print foo) ; => 6

:strong:`Examples`
Another kind of unquotation operator, ``unquote-splice``, abbreviated ``~@``, is analogous to ``unpack-iterable`` in that it splices an iterable object into the sequence of the parent :ref:`sequential model <hysequence>`. Compare the effects of ``unquote`` to ``unquote-splice``::

::
(setv X [1 2 3])
(hy.repr `[a b ~X c d ~@X e f])
; => '[a b [1 2 3] c d 1 2 3 e f]

If ``unquote-splice`` is given any sort of false value (such as ``None``), it's treated as an empty list. To be precise, ``~@x`` splices in the result of ``(or x [])``.

Note that while a symbol name can begin with ``@`` in Hy, ``~@`` takes precedence in the parser, so if you want to unquote the symbol ``@foo`` with ``~``, you must use whitespace to separate ``~`` and ``@``, as in ``~ @foo``.

=> (setv x '(print "Hello World"))
=> x ; variable x is set to unevaluated expression
hy.models.Expression([
hy.models.Symbol('print'),
hy.models.String('Hello World')])
=> (hy.eval x)
Hello World
.. hy:macro:: (quote [model])
Return the given :ref:`model <models>` without evaluating it. Or to be more pedantic, ``quote`` complies to code that produces and returns the model it was originally called on. Thus ``quote`` serves as syntactic sugar for model constructors::

(quote a)
; Equivalent to: (hy.models.Symbol "a")
(quote (+ 1 1))
; Equivalent to: (hy.models.Expression [
; (hy.models.Symbol "+")
; (hy.models.Integer 1)
; (hy.models.Integer 1)])

``quote`` itself is conveniently :ref:`abbreviated <more-sugar>` as the single-quote character ``'``, which needs no parentheses, allowing one to instead write::

'a
'(+ 1 1)

See also:

- :hy:func:`quasiquote` to substitute values into a quoted form
- :hy:func:`hy.eval` to evaluate models as code
- :hy:func:`hy.repr` to stringify models into Hy source text that uses ``'``

.. hy:macro:: (require [#* args])
Expand Down Expand Up @@ -1078,51 +1096,6 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``.
=> (f #* [1] #* [2] #** {"c" 3} #** {"d" 4})
[1 2 3 4]

.. hy:macro:: (unquote [symbol])
Within a quasiquoted form, ``unquote`` forces evaluation of a symbol. ``unquote``
is aliased to the tilde (``~``) symbol.

::

=> (setv nickname "Cuddles")
=> (quasiquote (= nickname (unquote nickname)))
'(= nickname "Cuddles")
=> `(= nickname ~nickname)
'(= nickname "Cuddles")


.. hy:macro:: (unquote-splice [symbol])
``unquote-splice`` forces the evaluation of a symbol within a quasiquoted form,
much like ``unquote``. ``unquote-splice`` can be used when the symbol
being unquoted contains an iterable value, as it "splices" that iterable into
the quasiquoted form. ``unquote-splice`` can also be used when the value
evaluates to a false value such as ``None``, ``False``, or ``0``, in which
case the value is treated as an empty list and thus does not splice anything
into the form. ``unquote-splice`` is aliased to the ``~@`` syntax.

::

=> (setv nums [1 2 3 4])
=> (quasiquote (+ (unquote-splice nums)))
'(+ 1 2 3 4)
=> `(+ ~@nums)
'(+ 1 2 3 4)
=> `[1 2 ~@(when (< (get nums 0) 0) nums)]
'[1 2]

Here, the last example evaluates to ``('+' 1 2)``, since the condition
``(< (nth nums 0) 0)`` is ``False``, which makes this ``if`` expression
evaluate to ``None``, because the ``if`` expression here does not have an
else clause. ``unquote-splice`` then evaluates this as an empty value,
leaving no effects on the list it is enclosed in, therefore resulting in
``('+' 1 2)``.

A symbol name can begin with ``@`` in Hy, but ``~@`` takes precedence in the
parser. So, if you want to unquote the symbol ``@foo`` with ``~``, you must
use whitespace to separate ``~`` and ``@``, as in ``~ @foo``.

.. hy:macro:: (while [condition #* body])
``while`` compiles to a :py:keyword:`while` statement, which executes some
Expand Down
36 changes: 16 additions & 20 deletions hy/core/result_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,9 @@ def compile_inline_python(compiler, expr, root, code):

@pattern_macro(["quote", "quasiquote"], [FORM])
def compile_quote(compiler, expr, root, arg):
level = Inf if root == "quote" else 0 # Only quasiquotes can unquote
stmts, _ = render_quoted_form(compiler, arg, level)
ret = compiler.compile(stmts)
return ret

return compiler.compile(render_quoted_form(compiler, arg,
level = Inf if root == "quote" else 0)[0])
# Only quasiquotes can unquote

def render_quoted_form(compiler, form, level):
"""
Expand All @@ -198,21 +196,19 @@ def render_quoted_form(compiler, form, level):

op = None
if isinstance(form, Expression) and form and isinstance(form[0], Symbol):
op = unmangle(mangle(form[0]))
if level == 0 and op in ("unquote", "unquote-splice"):
if len(form) != 2:
raise HyTypeError(
"`%s' needs 1 argument, got %s" % op,
len(form) - 1,
compiler.filename,
form,
compiler.source,
)
return form[1], op == "unquote-splice"
elif op == "quasiquote":
level += 1
elif op in ("unquote", "unquote-splice"):
level -= 1
op = mangle(form[0]).replace('_', '-')
if op in ("unquote", "unquote-splice", "quasiquote"):
if level == 0 and op != "quasiquote":
if len(form) != 2:
raise HyTypeError(
"`%s' needs 1 argument, got %s" % op,
len(form) - 1,
compiler.filename,
form,
compiler.source,
)
return form[1], op == "unquote-splice"
level += 1 if op == "quasiquote" else -1

name = form.__class__.__name__
body = [form]
Expand Down
2 changes: 1 addition & 1 deletion hy/reader/mangling.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def mangle(s):
to :ref:`Hy's mangling rules <mangling>`. ::
(hy.mangle 'foo-bar) ; => "foo_bar"
(hy.mangle "🦑") ; => "hyx_squid"
(hy.mangle "🦑") ; => "hyx_XsquidX"
If the stringified argument is already both legal as a Python identifier
and normalized according to Unicode normalization form KC (NFKC), it will
Expand Down
11 changes: 8 additions & 3 deletions tests/native_tests/functions.hy
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,17 @@


(defn test-yield-in-try []
(setv hit-finally False)
(defn gen []
(setv x 1)
(try (yield x)
(finally (print x))))
(try
(yield x)
(finally
(nonlocal hit-finally)
(setv hit-finally True))))
(setv output (list (gen)))
(assert (= [1] output)))
(assert (= [1] output))
(assert hit-finally))


(defn test-midtree-yield []
Expand Down
1 change: 0 additions & 1 deletion tests/native_tests/model_patterns.hy
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
(defn f [loopers]
(setv head (if loopers (get loopers 0) None))
(setv tail (cut loopers 1 None))
(print head)
(cond
(is head None)
`(do ~@body)
Expand Down
Loading

0 comments on commit f9df10b

Please sign in to comment.