Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Give require the same features as import #1142

Merged
merged 5 commits into from
Nov 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/contrib/anaphoric.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ concise and easy to read.

To use these macros you need to require the hy.contrib.anaphoric module like so:

``(require hy.contrib.anaphoric)``
``(require [hy.contrib.anaphoric [*]])``

.. _ap-if:

Expand Down
4 changes: 2 additions & 2 deletions docs/contrib/flow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Example:

.. code-block:: hy

(require hy.contrib.flow)
(require [hy.contrib.flow [case]])

(defn temp-commenter [temp]
(case temp
Expand All @@ -48,7 +48,7 @@ Example:

.. code-block:: hy

(require hy.contrib.flow)
(require [hy.contrib.flow [switch]])

(defn temp-commenter [temp]
(switch temp
Expand Down
2 changes: 1 addition & 1 deletion docs/contrib/loop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Example:

.. code-block:: hy

(require hy.contrib.loop)
(require [hy.contrib.loop [loop]])

(defn factorial [n]
(loop [[i n] [acc 1]]
Expand Down
2 changes: 1 addition & 1 deletion docs/contrib/multi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ args and/or kwargs. Inspired by Clojure's take on ``defn``.

.. code-block:: clj

=> (require hy.contrib.multi)
=> (require [hy.contrib.multi [defmulti]])
=> (defmulti fun
... ([a] "a")
... ([a b] "a b")
Expand Down
78 changes: 73 additions & 5 deletions docs/language/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1210,16 +1210,84 @@ alternatively be written using the apostrophe (``'``) symbol.
require
-------

``require`` is used to import macros from a given module. It takes at least one
parameter specifying the module which macros should be imported. Multiple
modules can be imported with a single ``require``.
``require`` is used to import macros from one or more given modules. It allows
parameters in all the same formats as ``import``. The ``require`` form itself
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read the code correctly, following doesn't work:

(require [foo [bar :as baz]])

but similar does work for import though

(import [foo [bar :as baz]])

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Embarassingly, I had no idea that from foo import bar as baz is legal Python, let alone supported in Hy. I'll add that form for require.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

produces no code in the final program: its effect is purely at compile-time, for
the benefit of macro expansion. Specifically, ``require`` imports each named
module and then makes each requested macro available in the current module.

The following example will import macros from ``module-1`` and ``module-2``:
The following are all equivalent ways to call a macro named ``foo`` in the module ``mymodule``:

.. code-block:: clj

(require module-1 module-2)
(require mymodule)
(mymodule.foo 1)

(require [mymodule :as M])
(M.foo 1)

(require [mymodule [foo]])
(foo 1)

(require [mymodule [*]])
(foo 1)

(require [mymodule [foo :as bar]])
(bar 1)

Macros that call macros
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good addition. I got bitten by this multiple times, until I learned it hard way.

~~~~~~~~~~~~~~~~~~~~~~~

One aspect of ``require`` that may be surprising is what happens when one
macro's expansion calls another macro. Suppose ``mymodule.hy`` looks like this:

.. code-block:: clj

(defmacro repexpr [n expr]
; Evaluate the expression n times
; and collect the results in a list.
`(list (map (fn [_] ~expr) (range ~n))))

(defmacro foo [n]
`(repexpr ~n (input "Gimme some input: ")))

And then, in your main program, you write:

.. code-block:: clj

(require [mymodule [foo]])

(print (mymodule.foo 3))

Running this raises ``NameError: name 'repexpr' is not defined``, even though
writing ``(print (foo 3))`` in ``mymodule`` works fine. The trouble is that your
main program doesn't have the macro ``repexpr`` available, since it wasn't
imported (and imported under exactly that name, as opposed to a qualified name).
You could do ``(require [mymodule [*]])`` or ``(require [mymodule [foo
repexpr]])``, but a less error-prone approach is to change the definition of
``foo`` to require whatever sub-macros it needs:

.. code-block:: clj

(defmacro foo [n]
`(do
(require mymodule)
(mymodule.repexpr ~n (raw-input "Gimme some input: "))))

It's wise to use ``(require mymodule)`` here rather than ``(require [mymodule
[repexpr]])`` to avoid accidentally shadowing a function named ``repexpr`` in
the main program.

Qualified macro names
~~~~~~~~~~~~~~~~~~~~~

Note that in the current implementation, there's a trick in qualified macro
names, like ``mymodule.foo`` and ``M.foo`` in the above example. These names
aren't actually attributes of module objects; they're just identifiers with
periods in them. In fact, ``mymodule`` and ``M`` aren't defined by these
``require`` forms, even at compile-time. None of this will hurt you unless try
to do introspection of the current module's set of defined macros, which isn't
really supported anyway.

rest / cdr
----------
Expand Down
13 changes: 8 additions & 5 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -568,15 +568,18 @@ Macros are useful when one wishes to extend Hy or write their own
language on top of that. Many features of Hy are macros, like ``when``,
``cond`` and ``->``.

To use macros defined in a different module, it is not enough to
``import`` the module, because importing happens at run-time, while we
would need macros at compile-time. Instead of importing the module
with macros, ``require`` must be used:
What if you want to use a macro that's defined in a different
module? The special form ``import`` won't help, because it merely
translates to a Python ``import`` statement that's executed at
run-time, and macros are expanded at compile-time, that is,
during the translate from Hy to Python. Instead, use ``require``,
which imports the module and makes macros available at
compile-time. ``require`` uses the same syntax as ``import``.

.. code-block:: clj

=> (require tutorial.macros)
=> (rev (1 2 3 +))
=> (tutorial.macros.rev (1 2 3 +))
6

Hy <-> Python interop
Expand Down
4 changes: 2 additions & 2 deletions hy/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ def ideas_macro():

""")])

require("hy.cmdline", "__console__")
require("hy.cmdline", "__main__")
require("hy.cmdline", "__console__", all_macros=True)
require("hy.cmdline", "__main__", all_macros=True)

SIMPLE_TRACEBACKS = True

Expand Down
43 changes: 39 additions & 4 deletions hy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1737,10 +1737,45 @@ def compile_require(self, expression):
"unimport" it after we've completed `thing' so that we don't pollute
other envs.
"""
expression.pop(0)
for entry in expression:
__import__(entry) # Import it fo' them macros.
require(entry, self.module_name)
for entry in expression[1:]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems kind of odd that require and import handle a pretty much identical syntax in completely different ways. Would it somehow be possible to somehow merge the two?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They use the same syntax but do pretty different things because import actually emits code and require just changes the state of the compiler. I think trying to unify the implementations wouldn't make the compiler code much nicer.

if isinstance(entry, HySymbol):
# e.g., (require foo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REALLY minor nitpick, but this would be a little easier to read without the e.g., since I think it's obvious that it's an example.

__import__(entry)
require(entry, self.module_name, all_macros=True,
prefix=entry)
elif isinstance(entry, HyList) and len(entry) == 2:
# e.g., (require [foo [bar baz :as MyBaz bing]])
# or (require [foo [*]])
module, names = entry
if not isinstance(names, HyList):
raise HyTypeError(names,
"(require) name lists should be HyLists")
__import__(module)
if '*' in names:
if len(names) != 1:
raise HyTypeError(names, "* in a (require) name list "
"must be on its own")
require(module, self.module_name, all_macros=True)
else:
assignments = {}
while names:
if len(names) > 1 and names[1] == HyKeyword(":as"):
k, _, v = names[:3]
del names[:3]
assignments[k] = v
else:
symbol = names.pop(0)
assignments[symbol] = symbol
require(module, self.module_name, assignments=assignments)
elif (isinstance(entry, HyList) and len(entry) == 3
and entry[1] == HyKeyword(":as")):
# e.g., (require [foo :as bar])
module, _, prefix = entry
__import__(module)
require(module, self.module_name, all_macros=True,
prefix=prefix)
else:
raise HyTypeError(entry, "unrecognized (require) syntax")
return Result()

@builds("and")
Expand Down
3 changes: 2 additions & 1 deletion hy/contrib/curry.hy
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@


(defmacro defnc [name args &rest body]
`(def ~name (fnc [~@args] ~@body)))
`(do (require hy.contrib.curry)
(def ~name (hy.contrib.curry.fnc [~@args] ~@body))))
6 changes: 4 additions & 2 deletions hy/contrib/loop.hy
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
(defmacro defnr [name lambda-list &rest body]
(if (not (= (type name) HySymbol))
(macro-error name "defnr takes a name as first argument"))
`(setv ~name (fnr ~lambda-list ~@body)))
`(do (require hy.contrib.loop)
(setv ~name (hy.contrib.loop.fnr ~lambda-list ~@body))))


(defmacro/g! loop [bindings &rest body]
Expand All @@ -87,5 +88,6 @@
;; and erroring if not is a giant TODO.
(let [fnargs (map (fn [x] (first x)) bindings)
initargs (map second bindings)]
`(do (defnr ~g!recur-fn [~@fnargs] ~@body)
`(do (require hy.contrib.loop)
(hy.contrib.loop.defnr ~g!recur-fn [~@fnargs] ~@body)
(~g!recur-fn ~@initargs))))
12 changes: 8 additions & 4 deletions hy/contrib/meth.hy
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@
(defn ~name ~params
(do ~@code)))))

(defn rwm [name path method params code]
`(do (require hy.contrib.meth)
(hy.contrib.meth.route-with-methods ~name ~path ~method ~params ~@code)))

;; Some macro examples
(defmacro route [name path params &rest code]
"Get request"
`(route-with-methods ~name ~path ["GET"] ~params ~@code))
(rwm name path ["GET"] params code))

(defmacro post-route [name path params &rest code]
"Post request"
`(route-with-methods ~name ~path ["POST"] ~params ~@code))
(rwm name path ["POST"] params code))

(defmacro put-route [name path params &rest code]
"Put request"
`(route-with-methods ~name ~path ["PUT"] ~params ~@code))
(rwm name path ["PUT"] params code))

(defmacro delete-route [name path params &rest code]
"Delete request"
`(route-with-methods ~name ~path ["DELETE"] ~params ~@code))
(rwm name path ["DELETE"] params code))
38 changes: 26 additions & 12 deletions hy/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,36 @@ def _(fn):
return _


def require(source_module, target_module):
"""Load the macros from `source_module` in the namespace of
`target_module`.
def require(source_module, target_module,
all_macros=False, assignments={}, prefix=""):
"""Load macros from `source_module` in the namespace of
`target_module`. `assignments` maps old names to new names, but is
ignored if `all_macros` is true. If `prefix` is nonempty, it is
prepended to the name of each imported macro. (This means you get
macros named things like "mymacromodule.mymacro", which looks like
an attribute of a module, although it's actually just a symbol
with a period in its name.)

This function is called from the `require` special form in the compiler.

"""
macros = _hy_macros[source_module]
refs = _hy_macros[target_module]
for name, macro in macros.items():
refs[name] = macro

readers = _hy_reader[source_module]
reader_refs = _hy_reader[target_module]
for name, reader in readers.items():
reader_refs[name] = reader

seen_names = set()
if prefix:
prefix += "."

for d in _hy_macros, _hy_reader:
for name, macro in d[source_module].items():
seen_names.add(name)
if all_macros:
d[target_module][prefix + name] = macro
elif name in assignments:
d[target_module][prefix + assignments[name]] = macro

if not all_macros:
unseen = frozenset(assignments.keys()).difference(seen_names)
if unseen:
raise ImportError("cannot require names: " + repr(list(unseen)))


def load_macros(module_name):
Expand Down
14 changes: 14 additions & 0 deletions tests/compilers/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,20 @@ def test_ast_good_import_from():
can_compile("(import [x [y]])")


def test_ast_require():
"Make sure AST respects (require) syntax"
can_compile("(require tests.resources.tlib)")
can_compile("(require [tests.resources.tlib [qplah parald]])")
can_compile("(require [tests.resources.tlib [*]])")
can_compile("(require [tests.resources.tlib :as foobar])")
can_compile("(require [tests.resources.tlib [qplah :as quiz]])")
can_compile("(require [tests.resources.tlib [qplah :as quiz parald]])")
cant_compile("(require [tests.resources.tlib])")
cant_compile("(require [tests.resources.tlib [* qplah]])")
cant_compile("(require [tests.resources.tlib [qplah *]])")
cant_compile("(require [tests.resources.tlib [* *]])")


def test_ast_good_get():
"Make sure AST can compile valid get"
can_compile("(get x y)")
Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/contrib/alias.hy
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(require hy.contrib.alias)
(require [hy.contrib.alias [defn-alias]])

(defn test-defn-alias []
(defn-alias [tda-main tda-a1 tda-a2] [] :bazinga)
Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/contrib/anaphoric.hy
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
;; DEALINGS IN THE SOFTWARE.

(import [hy.errors [HyMacroExpansionError]])
(require hy.contrib.anaphoric)
(require [hy.contrib.anaphoric [*]])

;;;; some simple helpers

Expand Down
3 changes: 1 addition & 2 deletions tests/native_tests/contrib/botsbuildbots.hy
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
(import [hy.contrib.botsbuildbots [*]])
(require hy.contrib.botsbuildbots)
(require [hy.contrib.botsbuildbots [Botsbuildbots]])

(defn test-botsbuildbots []
(assert (> (len (first (Botsbuildbots))) 50)))
2 changes: 1 addition & 1 deletion tests/native_tests/contrib/curry.hy
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(require hy.contrib.curry)
(require [hy.contrib.curry [defnc]])


(defnc s [x y z] ((x z) (y z))) ; λxyz.xz(yz)
Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/contrib/loop.hy
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(require hy.contrib.loop)
(require [hy.contrib.loop [loop]])
(import sys)

(defn tco-sum [x y]
Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/contrib/meth.hy
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(require hy.contrib.meth)
(require [hy.contrib.meth [route post-route put-route delete-route]])

(defclass FakeMeth []
"Mocking decorator class"
Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/contrib/multi.hy
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
;; DEALINGS IN THE SOFTWARE.

(require hy.contrib.multi)
(require [hy.contrib.multi [defmulti]])


(defn test-basic-multi []
Expand Down
Loading