-
-
Notifications
You must be signed in to change notification settings - Fork 11
Lissp tricks and idioms
This page collects tricks and idioms learned from experience that may not be obvious from reading the other docs. Parts may eventually be moved into docs/
.
Hissp is designed to be standalone, requiring only Python itself and its standard library to run its compiled output. If you don't need this feature, it can be really helpful to install a few functional libraries like toolz
and pyrsistent
for a more Clojure-like experience. Hebigo also has a more powerful macro suite, which is compatible with Lissp. It implements equivalents of most Python statements and a Clojure-style destructuring let
.
While it may look like an identifier, not
is a reserved word and unary Boolean operator in Python, not a builtin. In the invocation position, Hissp will attempt to compile it like a function call, as if it were an identifier. The resulting Python happens to be valid and also works correctly.
(not foo)
not(
foo)
The other Python unary operators (|+|
, |-|
, and |~|
) work for the same reasons. Unlike not
, they don't look like identifiers, so you need the ||
. (In readerless, these are just str
atoms.)
This works in the invocation position only, because these are operators, not first-class function objects. You can think of them like special forms instead (although the compiler really isn't treating them specially). Use the operator.
equivalents (not_
, pos
, neg
, and invert
, respectively) in higher-order functions instead.
You can also make a tuple display (full interpolation) by using (||
, ||)
brackets. This almost looks like read syntax, but the equivalent readerless using empty str
atoms works too.
(|| (sub 1 1) "a" 'b ||)
(
sub(
(1),
(1)),
('a'),
'b',
)
# => (0, 'a', 'b')
Again, not a true special form, because Hissp is not treating this specially. It's compiling like a normal function call, but the function's name happens to be empty. In Python, that looks like a tuple. The trailing ||
is compiling like a normal positional argument, which again, happens to be empty. You only need that to add the trailing comma, which Hissp wouldn't bother including otherwise (and the unary operator trick wouldn't work if it did). Python doesn't need it either, unless the tuple happens to be made with a single argument. If you forget it, it's not a syntax error, but the result may not a tuple either ((|| 1)
is an int, but (|| 1 ||)
is a tuple).
This is a bit more of a compiler hack than the unary operators, which are better behaved (and, at least in the case of not
, idiomatic). It's usually better to use the template reader macros or en#tuple
to make data tuples in Lissp. However, this form is more concise and also works in readerless mode, so it does have uses.
You can do unpacking the same way as you would in en#tuple
(or any other function call), but then the trailing ||
needs to be paired (with :?
), which kind of destroys the illusion that it's part of a special bracket. (|| : :* "abc" :? ||)
is ('a', 'b', 'c')
, but without that last pair, it's a syntax error. A :* ""
pair would also work but might add a little run time overhead. If you know you're going to unpack a single argument (not always the case when metaprogramming), then you're better off using the builtin, like (tuple "abc")
. If you know you're going to have at least two arguments, then you don't need the trailing ||
at all.
Similar to the unary not
operator, (del foo)
happens to work correctly when compiled like a function call, but only at the top level. (Unlike a not
expression, del
is a statement.) This can be handy in the REPL but is less useful in modules where you're probably better off using let
when you need a variable to have limited scope. Similarly, the one-argument forms of raise
and assert
can be compiled this way, but they're even less useful at the top level.
Arbitrary Python code (includes statements at the top level) can be injected with ||
fragments or .#
applied to a string, and for embedded languages of nontrivial length, you might find it easier to inject a <<#
comment string than a ""
token. It's possible to inject arbitrary text this way, including magic comments recognized by other tools, such as a shebang line.
It's kind of hard to use a shell without cd
. Clojurians are used to using (in-ns)
to change namespaces in the REPL, but most Pythonistas don't know how to do that (although you can).
In the Lissp REPL, you can use (hissp..interact (vars foo.))
to start a subrepl inside module foo
without losing the current state of your application. Globals saved in the subrepl will become attributes of that module, and functions defined will use that module's globals. Note that starting a subrepl does not clobber the module's _macro_
. If the module doesn't have an appropriate _macro_
, then unqualified macro names will not be available. You'll have to use the fully-qualified names instead.
Use EOF (Ctrl+D, or Ctrl+Z ⏎ on Windows) to terminate the subrepl and get back to __main__
(or wherever you were before), again without losing the state of your application. You can also start a REPL from the command line in a non-main module via
lissp -ic "(hissp..interact (vars foo.))"
(The -i
will let you drop back into __main__
after quitting the subrepl.)
The prompt in Clojure's REPL shows the current ns. LisspREPL doesn't, but a long prompt is kind of a pain, especially for multiline entries. Check the __name__
global if you forget which module you're in, similar to a pwd
command in shell.
Reload a module using (importlib..reload foo.)
. If it's a Lissp module you've been editing, you'll probably want to recompile it first, so (hissp..transpile __package__ 'foo)
. If you're playing with individual forms in your editor, you can usually just paste new versions into the subrepl without recompiling the whole file.
Reloading runs the module code again, but it doesn't clear the module dict first (and doesn't replace it either). defonce
is made to take advantage of this. Resources you don't want reloaded can be preserved until you explicitly delete them with e.g., (del foo)
. You can also keep utility code in a _#
comment so it won't load with the module and then paste into the REPL as needed. The _#
can be on its own line, and it can discard a following tuple and everything inside it.
The Python idiom of if __name__ == '__main__': ...
has an analogue in Lissp: (when (eq __name__ '__main__) ...)
. Hissp normally executes each top-level form in turn when compiling a program. This is necessary when a macro is used in the same module it is defined in, because macros run at compile time. Usually top-level forms are only definitions, and this is harmless, however, at least one module (typically __main__
) must also have top-level instructions in order for the application to do anything. The guard can prevent these effects during compilation, when starting a subrepl in the module, or when importing its definitions in other modules, while still allowing the effects to happen when run as main.
If you do want to clear the current module, you can do that with (.clear (globals))
. However, you can't reload it until you restore __name__
, because reload
needs that, so (hissp.._macro_.define __name__ 'foo)
first.
If you want to clear some other module foo.
, you can do that more easily with (delitem sys..modules 'foo)
. The next usage of foo.
will import it again.
Writing
(hissp.._macro_.prelude)
at the top of your Lissp modules will bring the environment up to a basic standard of usability very quickly, and this doesn't require a Hissp installation to run the compiled output. It's great for single-file scripts and implied for lissp -c
commands. However, it does pollute the module's global namespace with star imports, which is not ideal. A module's interface is not as discoverable (via dir()
, for instance) if there is more noise to sort through. This can be mitigated somewhat by including a __all__
, which can be inspected to discover the intended public interface. Some tooling (like IDLE) also respects this (for suggested completions), but it mostly only affects other star imports, which are not best practice in larger projects anyway.
Even without the prelude, it's still possible to use the other bundled macros via fully-qualified names. Likewise, the prelude imports from the standard library can be replaced with names qualified with functools.
, itertools.
, or operator.
. This doesn't include the inlined "en-" group definitions engarde
, enter
, and Ensure
, however.
The qualifiers can be abbreviated using alias
, e.g., with
(hissp.._macro_.alias - hissp.._macro_)
at the top of your Lissp module, you can use any of the bundled macros with a -#
. Reader macros are also available by passing extras to the alias, so examples might look like -#!b"bytes"
, or (-#define foo 2)
. A compiled module using an alias in this way does not require Hissp to be installed either.
prelude
operates via an exec
call and takes an optional ns
argument, which defaults to the globals, and is passed to exec
, but could be some other dict. This makes it possible to pick out something without otherwise affecting the current namespace, e.g.,
(-#let (ns (dict)
ks '(engarde enter Ensue))
(-#prelude ns)
(.update (globals) (zip ks (map ns.get ks))))
Alternatively, a project with multiple modules need only expand it in one and the others can import things from there.
The bundled O#
X#
XY#
XYZ#
and XYZW#
are shorthand for making lambdas with 0-4 positional arguments, which are very common.
These are often combined with a ||
injection to make a function from a Python expression.
en#X#
makes a variadic lambda.
The lambda
special form is often written with a symbol in place of a parameter tuple. This is like using the individual characters as positional arguments. Beware of munging; a single character can expand to a much longer munged name. A single colon (the empty control word) can also be used in place of an empty tuple, which is equivalent to (:)
. O#
is commonly used instead when available, unless it would be used on a progn
form.
The python
command has many more options than lissp
. You can still use these by running python -m hissp
instead of lissp
.
For example, python -O -m hissp -c "(assure 0 bool)"
wouldn't raise an AssertionError
, but lissp -c "(assure 0 bool)"
would.
The prelude is implied for lissp -c
, so lissp -ic ""
will run the prelude, run an empty command, and then drop into a REPL. You can alias this in your shell if you like. The default REPL does start with a _macro_
namespace that includes all the bundled macros, so if you forget, you can always invoke (prelude)
later, without typing out the fully-qualified name. Note that this replaces your _macro_
and everything inside, however.