Skip to content

Lissp tricks and idioms

gilch edited this page Jan 3, 2024 · 83 revisions

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/.

libraries

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.

pseudo special forms

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 only works in the invocation position because these are operators, not true functions. You can think of them like special forms (although the compiler really isn't treating them specially). Use the operator. equivalents (not_, pos, neg, and invert) in higher-order functions.

You can also make a data tuple by using (||, ||) brackets. This almost looks like read syntax, but the equivalent readerless using empty str atoms works too.

(|| 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. 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 probably 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 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.

statements work at the top level

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, although they're even less useful at the top level.

Arbitrary Python code can be injected with || fragments or .# applied to a string. At the top level, this includes statements. It's possible to inject arbitrary text this way, including magic comments recognized by other tools, such as a shebang line.

changing the current module in the REPL

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.

reloading modules

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.

the prelude is optional

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.

alias

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 ns

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.

lambda idioms

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.

python command options

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.

starting the REPL with the prelude

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.

Clone this wiki locally