FAQ

(Frequently Anticipated Questions (and complaints))

Anticipated? Didn’t you mean “asked”?

Well, this project is still pretty new.

Why should I use Hissp?

Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.

—Greenspun’s Tenth Rule

If the only programming languages you’ve tried are those designed to feel familiar to C programmers, you might think they’re all the same. I assure you, they are not.

At some level you already know this. Why not use assembly language for everything? Or why not the binary machine code?

Because you want the highest-level language you can get your hands on. Lisp has few rivals in this regard, but many dialects, Hissp among them.

You want access to the Python ecosystem. Python has had slow and steady growth for decades, and now it’s a top industry language. Its ecosystem is very mature and widely used. And Hissp can use it and participate in it as easily as Python can, because compiled Hissp is Python.

But, Python’s is not the only mature ecosystem. If you’re attached to a different one, perhaps Hissp is not the Lisp for you.

Is Hissp a replacement for Python?

Hissp is a modular metaprogramming supplement to Python.

How much Python you replace is up to you:

  • Use small amounts of readerless-mode Hissp directly in your Python code.

  • Compile individual Lissp modules to Python and import them in your Python projects.

  • Write your entire project in Lissp, including the main module, and launch it with lissp.

This also works in reverse.

  • Use small amounts of Python code directly in your Lissp code via the inject macro .#.

  • Import Python modules and objects with qualified identifiers.

  • Write the main module in Python (or compile it) and launch it with python.

While the Hissp language can do anything Python can, Hissp is compiled to Python, so it requires Python to work.

You must be able to read Python code to understand the output of the Lissp REPL, which shows the Python compilation, the normal Python reprs for objects, and the same kind of tracebacks and error messages Python would.

Can Hissp really do anything Python can when it only compiles to a subset of it?

Yes.

Short proof: Hissp has strings and can call exec().

But you usually won’t need it because you can import anything written in Python by using Module Literals and Qualified Identifiers.

Strings at the Hissp level represent raw Python code, although the compiler does do some minimal preprocessing for the qualifiers, it pretty much injects the string contents verbatim. This is usually used for identifiers, but could be anything. Hissp macros and Lissp reader macros can return any type of object, including strings.

However, the main use of Hissp is metaprogramming with syntactically-aware macros. If you wanted to do string metaprogramming you could have just used exec(), so you’re giving up a lot of Hissp’s power. Expressions are relatively safe if you’re careful, but note that statements would only work at the top level.

What’s 1 + 1?

Two.

I mean how do you write it in Hissp without operators? Please don’t say eval().

We have all the operators because we have all the standard library functions.

(operator..add 1 1)

That’s really verbose though.

Hmm, is this better? Top-level imports are a good use of inject.

#> .#"import operator as op"
>>> import operator as op

#> (op.add 1 1)
>>> op.add(
...   (1),
...   (1))
2

The result is a bit less desirable in templates. But it’s not technically wrong.

#> `op.add
>>> '__main__..op.add'
'__main__..op.add'

And you can still qualify it yourself instead of letting the reader do it for you:

#> `operator..add
>>> 'operator..add'
'operator..add'

Yeah, that’s better, but in Python, it’s just +.

You can, of course, abbreviate these.

#> (define + operator..add)
>>> # define
... __import__('operator').setitem(
...   __import__('builtins').globals(),
...   'xPLUS_',
...   __import__('operator').add)

#> (+ 1 1)
>>> xPLUS_(
...   (1),
...   (1))
2

Yes, + is a valid symbol. It gets munged to xPLUS_. The result is all of the operators you might want, using the same prefix notation used by all the calls.

You can define these however you want, like upgrading them to use a reduce so they’re multiary like other Lisps:

#> (define +
#..  (lambda (: :* args)
#..    (functools..reduce operator..add args)))
>>> # define
... __import__('operator').setitem(
...   __import__('builtins').globals(),
...   'xPLUS_',
...   (lambda *args:
...     __import__('functools').reduce(
...       __import__('operator').add,
...       args)))

#> (+ 1 2 3)
>>> xPLUS_(
...   (1),
...   (2),
...   (3))
6

You mean I have to do this one by one for each operator every time?

Write it once, then you just import it. That’s called a “library”. And no, you don’t copy/paste the implementation. That would violate the DRY principle. Implement it once and map the names.

Why isn’t that in the Hissp library already?

It is in the library already! It’s called operator.

Hissp is a modular system. Hissp’s output is guaranteed to have no dependencies you don’t introduce yourself. That means Hissp’s standard library is Python’s. All I can add to that without breaking that rule is some basic macros that have no dependencies in their expansions, which is arguably not the right way to write macros. So I really don’t want that collection to get bloated. But I needed a minimal set to test and demonstrate Hissp. A larger application with better alternatives should probably not be using the basic macros at all.

If you don’t like Python’s version, then add a dependency to something else. If some open-source Hissp libraries pop up, I’d be happy to recommend the good ones in Hissp’s documentation, but they will remain separate packages.

I want infix notation!

Hissp is a Lisp. It’s all calls! Get used to it.

Fully parenthesized prefix notation is explicit and consistent. It’s very readable if properly indented. Don’t confuse “easy” with “familiar”. Also, you don’t have to be restricted to one or two arguments.

Fine. You can write macros for any syntax you please.

Also consider using Hebigo, which keeps all Python expressions, instead of Lissp.

Also recall that both reader and compiler macros can return arbitrary Python snippets and the compiler will emit them verbatim. You should generally avoid doing this, because then you’re metaprogramming with strings instead of AST. You’re giving up a lot of Hissp’s power. But optimizing complex formulas is maybe one of the few times it’s OK to do that.

Recall the inject .# reader macro executes a form and embeds its result into the Hissp.

#> (define quadratic
#..  (lambda (a b c)
#..    .#"(-b + (b**2 - 4*a*c)**0.5)/(2*a)"))
>>> # define
... __import__('operator').setitem(
...   __import__('builtins').globals(),
...   'quadratic',
...   (lambda a,b,c:(-b + (b**2 - 4*a*c)**0.5)/(2*a)))

But for a top-level define like this, you could have just used exec().

How do I make bytes objects in Lissp?

#> (bytes '(1 2 3))
>>> bytes(
...   (1, 2, 3))
b'\x01\x02\x03'

Or, if you prefer hexadecimal,

#> (bytes.fromhex "010203")
>>> bytes.fromhex(
...   ('010203'))
b'\x01\x02\x03'

But that’s just numbers. I want ASCII text.

You do know about the str.encode method, don’t you?

There’s really no bytes literal in Lissp?

Technically? No.

However, they do work in Python injections:

#> [b'bytes',b'in',b'collection',b'atoms']
>>> [b'bytes', b'in', b'collection', b'atoms']
[b'bytes', b'in', b'collection', b'atoms']

#> .#"b'injected bytes literal'"
>>> b'injected bytes literal'
b'injected bytes literal'

And, if you have the basic macros loaded, you can use the b# reader macro.

#> b#"bytes from reader macro"
>>> b'bytes from reader macro'
b'bytes from reader macro'

Bytes literals can be implemented fairly easily in terms of a raw string and reader macro. That’s close enough, right? You can make all sorts of “literals” the same way.

Why aren’t any escape sequences working in Lissp strings?

Lissp’s strings are raw by default. Lissp doesn’t force you into any particular set of escapes. Some kinds of metaprogramming are easier if you don’t have to fight Python. You’re free to implement your own.

I like Python’s, thanks. That sounds like too much work!

Python’s are still available in injections:

#> .#"'\u263a'"
>>> '\u263a'
'☺'

Or use the hash-string read syntax for short:

#> #"\u263a"
>>> ('☺')
'☺'

Wait, hash strings take escapes? Why are raw strings the default? In Clojure it’s the other way around.

Then we’d have to write byte strings like b##"spam". Python has various other prefixes for string types. Raw, bytes, format, unicode, and various combinations of these. Reader macros let us handle these in a unified way in Lissp and create more as needed, such as regex patterns, among many other types that can be initialized with a single string, and that makes raw strings the most sensible default. With a supporting reader macro all of these are practically literals. It’s easy to process escapes in reader macros. It isn’t easy to unprocess them. Not to mention Python code injections, which can contain their own strings with escapes.

Clojure’s hash strings are already regexes, not raws, and their reader macros aren’t so easy to use, so it doesn’t come up as much.

Look at your strings in Python and you’ll find that most of the time you don’t need the escapes.

Why can’t I make a backslash character string?

You can.

#> (len #"\\")
>>> len(
...   ('\\'))
1

The Lissp tokenizer assumes backslashes are paired in strings, so you can’t do it with a raw string:

#> (len "\\")
>>> len(
...   ('\\\\'))
2

#> "\"
#..\\"
>>> ('\\"\n\\\\')
'\\"\n\\\\'

Python makes the same assumption, even for raw strings. So raw strings in Python have the same limitation.

How do I start the REPL again?

If you installed the distribution using pip, you can use the provided lissp console script.

$ lissp

You can also launch the Hissp package directly using an appropriate Python interpreter from the command line

$ python3 -m hissp

There’s no macroexpand. How do I look at expansions?

Invoke the macro indirectly somehow so the compiler sees it as a normal function, and pass all arguments quoted.

((getattr hissp.basic.._macro_ "define") 'foo '"bar")

One could, of course, write a function or macro to automate this.

You can also use the method call syntax for this purpose, which is never interpreted as a macro invocation. This syntax isn’t restricted solely to methods on objects. Due to certain regularities in Python syntax, it also works on callable attributes in any kind of namespace.

(.define hissp.basic.._macro_ : :* '(foo "bar"))

You’ll find that you often don’t bother macroexpanding because you can instead look at the compiled Python output, which is also presented in the REPL. It’s indented, so it’s not that hard to read, once you get used to Hissp. The compiler also helpfully includes a comment in the compiled output whenever it expands a macro.

There’s no for? What about loops?

Sometimes recursion is good enough. Try it. list(), map() and filter() plus lambda can do anything list comprehensions can. Ditch the list() for lazy generators. Replace list() with set() for set comprehensions. Dict comprehensions are a little trickier. Use dict() on an iterable of pairs. zip() is an easy way to make them, or just have the map’s lambda return pairs. Remember, you can make data tuples with template quotes.

This is so much harder than comprehensions!

Not really. But you can always write a macro if you want different syntax. You can pretty easily implement comprehensions this way.

That’s comprehensions, but what about for statements? You don’t really think I should build a list just to throw it away?

Side effects are not good functional style. Avoid them for as long as possible. It’s not that Hissp can only do the functional style, but there’s no reason to introduce extra difficulties like side effects and mutation if you don’t have to.

Still, you do need side effects eventually if you want your program to do anything.

Use any() for side-effects to avoid building a list. Usually, you’d combine with map(), just like the comprehensions. Make sure the lambda returns Nones (or something false), because a true value acts like break in any(). Obviously, you can use this to your advantage if you want a break, which seems to happen pretty often when writing imperative loops.

If you like, there’s a hissp.basic.._macro_.any-for that basically does this.

See also itertools, iter.

There’s no builtin if!? I want a programming language, not a calculator! Branching is fundamental!

No, it’s really not. Stop thinking in Fortran. Turing completeness can be present in surprisingly minimal systems. (E.g. here’s a working C compiler that only outputs mov instructions.)

You already learned how to for loop above. Isn’t looping zero or one times like skipping a branch or not? Note that False and True are special cases of 0 and 1 in Python. range(False) would loop zero times, but range(True) loops one time.

See also hissp.basic._macro_.when.

What about if/else ternary expressions?

(lambda b, *then_else: then_else[not b]())(
    1 < 2,
    lambda: print('yes'),
    lambda: print('no'),
)

Look up a thunk and run it. There’s a hissp.basic.._macro_.if-else that basically expands to this. I know it’s a special form in other Lisps (or cond is), but Hissp doesn’t need it. Smalltalk pretty much does it this way. Once you have if you can make a cond. Lisps implementations differ on which is the special form and which is the macro.

You have to define three lambdas just for an if?! isn’t this really slow? It really ought to be a special form.

It’s not that slow. Like most things, performance is really only an issue in a bottleneck. If you find one, there’s no runtime overhead for using .# to inject some Python.

Also recall that macros are allowed to return strings of Python code. All the usual caveats for text-substitution macros apply. Use parentheses.

(defmacro !if (test then otherwise)
  "Compiles to if/else expression."
  (.format "(({}) if ({}) else ({}))"
           : :* (map hissp.compiler..readerless
                     `(,then ,test ,otherwise))))

Take it from Knuth: Premature optimization is the root of all evil (or at least most of it) in programming.

Don’t use text macros unless you really need them. Even if you think you need one, you probably don’t.

Syntactic macros are powerful not just because they can delay evaluation, but because they can read and re-write code. Using a text macro like the above can hide information that a syntactic rewriting macro needs to work properly.

Is Hissp a Scheme, Common Lisp, or Clojure implementation?

No, but if you’re comfortable with any Lisp, Lissp will feel familiar.

Of these, ClojureScript may be the most similar, in that it transpiles to another high-level language. But unlike JavaScript, Python already comes with batteries included. Hissp doesn’t include a standard library. Because Python already provides so much, in many ways Hissp can be even more minimal than Scheme.

Hissp draws inspiration from previous Lisps, including Scheme, Common Lisp, ClojureScript, Emacs Lisp, Arc, and Hy.

Does Hissp have tail-call optimization?

No, because CPython doesn’t. If a Python implementation has it, Hissp will too, when run on that implementation.

Isn’t that required for Lisp?

No, you’re thinking Scheme. The Common Lisp standard does not require TCO (though many popular implementations have it). Clojure and ClojureScript don’t have it either.

You can increase the recursion limit with sys.setrecursionlimit. Better not increase it too much if you don’t like segfaults, but you can trampoline instead. See Drython’s loop() function. Or use it. Or Hebigo’s equivalent macro. Clojure does it about the same way.

Heresy! It’s not Lisp without list processing!

Clojure uses vectors in its forms and I still call it a Lisp. Python is quite capable of processing tuples. Readerless mode also looks pretty lispy. Creating a linked list type or cons cell would have complicated things too much.

Do you have those immutable persistent data structures like Clojure?

No, but tuples are immutable in Python. (Although their elements need not be.)

If you want those, check out Pyrsistent and Immutables.

How do I make a tuple?

Use tuple().

But I have to already have an iterable, which is why I wanted a tuple in the first place!

lambda *a: a

You can also make an empty list with [] or (list), and then .append to it. (Try the cascade macro.) Finally, the template syntax `() makes tuples. Unquote , calls/symbols if needed.

There are no statements?! How can you get anything done?

There are expression statements only (each top-level form). That’s plenty.

But there’s no assignment statement!

That’s not a question.

For any complaint of the form “Hissp doesn’t have feature X”, the answer is usually “Write a macro to implement X.”

Use the hissp.basic.._macro_.define and hissp.basic.._macro_.let macros for globals and locals, respectively. Look at their expansions and you’ll see they don’t use assignment statements either.

See also types.SimpleNamespace/setattr and dict/operator.setitem.

Also, Python 3.8 added assignment expressions. Those are expressions. A macro could expand to a string containing the walrus :=, but as with text-substitution macros generally, this approach is not recommended.

How do I reassign a local?

You don’t. let is single-assignment. This is also true for let in Scheme and Clojure.

But Scheme has set! and Clojure has atoms.

And Python has dict and types.SimpleNamespace.

(The walrus := also works in an injection, but this use is discouraged in Hissp.)

But I need it for recursion. Where’s the letrec/letfn?

Use methods in a class. You can get the other methods from the self argument.

How do I make a class?

Use type(). (Or whatever metaclass.)

Very funny. That just tells me what type something is.

No, seriously, you have to give it all three arguments. Look it up.

Well now I need a dict!

Use dict(). Obviously. It’s especially easy if the keys are identifiers, because you can use kwargs instead of making pairs.

That seems too verbose. In Python it’s easier.

You mostly don’t need classes though. Classes conflate data structures with the functions that act on them, and tend to encourage fragmented, mutable state which doesn’t scale well. Experienced Python developers learn to rely on them less. They’re most useful for their magic methods to overload operators and such. But Hissp mostly doesn’t need that since it has no operators to speak of.

If you just need single dispatch, Python’s already got you covered, no classes necessary.

As always, you can write a function or macro to reduce boilerplate. There’s actually a hissp.basic.._macro_.deftype macro for making a top-level type.

I’ve got some weird metaclass magic from a library. type() isn’t working!

Try types.new_class instead.

How do I raise exceptions?

(operator..truediv 1 0) seems to work. Exceptions tend to raise themselves if you’re not careful.

But I need a raise statement for a specific exception message.

Exceptions are not good functional style. Haskell uses the Maybe monad instead, so you don’t need them. If you must, you can still use a raise in exec(). (Or use Drython’s Raise(), or Hebigo’s equivalent macro.)

If you want a Maybe in Python, returns has them. But should you use them? Maybe not.

Use exec? Isn’t that slow?

If the exceptions are only for exceptional cases, then does it matter? Premature optimization is the root of all evil.

What about catching them?

Try not raising them in the first place? Or contextlib.suppress.

But there’s no with statement either!

Use contextlib.ContextDecorator as a mixin and any context manager works as a decorator. Or use Drython’s With().

How do I use a decorator?

You apply it to the function (or class): call it with the function as its argument. Decorators are just higher-order functions.

Any context manager? But you don’t get the return value of __enter__()! And what if it’s not re-entrant?

suppress works with these restrictions, but point taken. You can certainly call .__enter__() yourself, but you have to call .__exit__() too. Even if there was an exception.

But I need to handle the exception if and only if it was raised, for multiple exception types, or I need to get the exception object.

Context managers can do all of that!

from contextlib import ContextDecorator

class Except(ContextDecorator):
    def __init__(self, catch, handler):
        self.catch = catch
        self.handler = handler
    def __enter__(self):
        pass
    def __exit__(self, exc_type, exception, traceback):
        if isinstance(exception, self.catch):
            self.handler(exception)
            return True

@Except((TypeError, ValueError), lambda e: print(e))
@Except(ZeroDivisionError, lambda e: print('oops'))
def bad_idea(x):
    return 1/x

bad_idea(0)  # oops
bad_idea('spam')  # unsupported operand type(s) for /: 'int' and 'str'
bad_idea(1)  # 1.0

You can translate all of that to Hissp.

How?

Like this

(hissp.basic.._macro_.prelude)

(deftype Except (contextlib..ContextDecorator)
  __init__ (lambda (self catch handler)
             (attach self catch handler)
             None)
  __enter__ (lambda (self))
  __exit__ (lambda (self exc_type exception traceback)
             (when (isinstance exception self.catch)
               (.handler self exception)
               True)))

(define bad_idea
  (-> (lambda (x)
        (operator..truediv 1 x))
      ((Except ZeroDivisionError
               (lambda (e)
                 (print "oops"))))
      ((Except `(,TypeError ,ValueError)
               (lambda (e)
                 (print e))))))

Remarkable how much that threading macro makes it resembles a try/except, isn’t it? Language “features” are just a thin skin built on syntax trees, which Lisp is, fundamentally. And a macro could factor out all the repeated bits, including the lambdas. Make any syntax you want!

#> (bad_idea 0)
>>> bad_idea(
...   (0))
oops

#> (bad_idea "spam")
>>> bad_idea(
...   ('spam'))
unsupported operand type(s) for /: 'int' and 'str'

#> (bad_idea 1)
>>> bad_idea(
...   (1))
1.0

Wow. That is so much harder than a try statement.

The definition of the context manager is, sure. but it’s not that hard. You only have to do that part once, and I already did it for you. Using the decorator once you have it is really not that bad.

Or, to make things easy, use exec() to compile a try with callbacks.

Isn’t this slow?! You can’t get away with calling this an “exceptional case” this time. The happy path would still require compiling an exec() string!

Not if you define it as a function in advance. Then it only happens once on module import. Something like,

(exec "
def try_statement(block, target, handler):
    try:
        block()
    except target as ex:
        handler(ex)")

Once on import is honestly not bad. Even the standard library does it, like for named tuples. But at this point, unless you really want a single-file script with no dependencies, you’re better off defining the helper function in Python and importing it. You could handle the finally/else blocks similarly. See Drython’s Try() for how to do it. Or just use Drython. Hebigo also implements one. If Hebigo is installed, you can import and use Hebigo’s macros, even in Lissp, because they also take and return Hissp.

Isn’t Hissp slower than Python? Isn’t Python slow enough already?

“Slow” usually only matters if it’s in a bottleneck. Hissp will often be slower than Python because it compiles to a functional subset of Python that relies on defining and calling functions more. Because Python is a multiparadigm language, it is not fully optimized for the functional style, though some implementations may do better than CPython here.

Premature optimization is the root of all evil. As always, don’t fix it until it matters, then profile to find the bottleneck and fix only that part. You can always re-write that part in Python (or C).

Yield?

We’ve got itertools. Compose iterators functional-style. You don’t need yield.

But I need it for co-routines. Or async/await stuff. How do I accept a send?

What color is your function? Async was probably a mistake.

Still, we want Python compatibility, don’t we?

Make a collections.abc.Generator subclass with a send() method.

Or use Drython’s Yield().

Generator-based coroutines have been deprecated. Don’t implement them with generators anymore. Note there are collections.abc.Awaitable and collections.abc.Coroutine abstract base classes too.

How do I add a docstring to a module/class/function?

Assign a string to the __doc__ attribute of the class or function object. That key in the dict argument to type() also works. For a module, __doc__ works (make a __doc__ global) but you should just use a string at the top, same as Python.

The REPL is nice and all, but how do I run a .lissp module?

You can launch a .lissp file as the main module directly.

If you have the entry point script installed that’s:

$ lissp foo.lissp

To be able to import a .lissp module, you must compile it to Python first.

At the REPL (or main module if it’s written in Lissp) use:

(hissp.reader..transpile __package__ 'spam 'eggs 'etc)

Where spam, eggs, etc. are the module names you want compiled. (If the package argument is None or "", it will use the current working directory.)

Or equivalently, in Python:

from hissp.reader import transpile

transpile(__package__, "sausage", "bacon")

Consider putting the above in each package’s __init__.py to auto-compile each Hissp module in the package on package import during development. You can disable it again on release, if desired, but this gives you fine-grained control over what gets compiled when. Note that you usually would want to recompile the whole project rather than only the changed files like Python does, because macros run at compile time. Changing a macro in one file normally doesn’t affect the code that uses it in other files until they are recompiled.

See transpile.

How do I import things?

Just use a qualified identifier. You don’t need imports.

But it’s in a deeply nested package with a long name. It’s tedious!

So assign a global to it:

(define Generator collections.abc..Generator)

But be aware of the effects that has on qualification in templates.

But I need the module object itself! The package __init__.py doesn’t import it or it’s not in a package.

A module literal will do it for you.

#> collections.abc.
>>> __import__('collections.abc',fromlist='?')
<module 'collections.abc' from '...abc.py'>

You can likewise assign a global, like any other value:

(define np numpy.)
(define pd pandas.)

But I want a relative import or a star import.

Module Literals and Qualified Identifiers have to use absolute imports to be reliable in macroexpansions.

But you can still import things the same way Python does.

How do I import a macro?

The same way you import anything else: with a qualified identifier. In Lissp, you can use a reader macro to abbreviate qualifiers. hissp.basic.._macro_.alias can define these for you.

Any callable in the current module’s _macro_ namespace will work unqualified. Normally you create these with hissp.basic.._macro_.defmacro, but the compiler doesn’t care how they get there.

Importing the _macro_ namespace from another module will work, but then uses of hissp.basic.._macro_.defmacro will mutate another module’s _macro_ namespace, which is probably not what you want, so make a copy, or or make a new one and insert individual macros into it.

The basic macros have no dependencies on the Hissp package in their expansions, which allows you to use their compiled output on another Python that doesn’t have Hissp installed. However, if you import a _macro_ at runtime, you’re creating a runtime dependency on whatever module you import it from.

The hissp.basic.._macro_.prelude macro will clone the basic macro namespace only if available. It avoids creating a runtime dependency this way.

hissp.basic.._macro_.prelude is a convenience for short scripts, especially those used as the main module. Larger projects should probably be more explicit in their imports, and may need a more complete macro library anyway.

How do I write a macro?

Read the Macro Tutorial.

tl;dr

Make a function that accepts the syntax you want as parameters and returns its transformation as Hissp code (the template reader syntax makes this easy). Put it in the _macro_ namespace. There’s a nice hissp.basic.._macro_.defmacro to do this for you. It will even create the namespace if it doesn’t exist yet.

Some tips:

  • Hissp macros are very similar to Clojure or Common Lisp macros.

    • Tutorals on writing macros in these languages are mostly applicable to Hissp.

  • Output qualified symbols so it works in other modules.

    • The template reader syntax does this for you automatically.

    • You have to do this yourself in readerless mode.

    • You can interpolate an unqualified symbol into a template by unquoting it, same as any other value.

  • Use gensyms ($#spam) to avoid accidental capture of identifiers.

How do I define a reader macro?

Make a function that accepts the syntax you want as its parameter and returns its transformation as Hissp code.

You can use it directly as a qualified reader macro. Or add it to the _macro_ namespace with a name ending in # to use it unqualified.

Remember hissp.basic.._macro_.defmacro can do this for you. Don’t forget to escape the # in the macro name.

Why the weird prompts at the REPL?

The REPL is designed so that you can copy/paste it into doctests or Jupyter notebook cells running an IPython kernel and it should just work. IPython will ignore the Lissp because its #>/#.. prompts makes it look like a Python comment, and it’s already set up to ignore the initial >>>/.... But doctest expects these, because that’s what the Python interpreter looks like.

Keeping the Python prompts >>>/... also helps you to distinguish the compiled Python from the result of evaluating it. The Lissp REPL could have been implemented to not display the Python at all, but transparency into the process is super helpful when developing and debugging, even if you ignore that part most of the time. It’s easier to scroll up than to have to ask for it.

How do I add a shebang line?

Same as for any executable text file, use a line starting with #! followed by a command to run Lissp. (E.g. /usr/bin/env lissp) The transpiler will ignore it if it’s the first line. If you set the executable bit, like chmod foo.lissp +x, then you can run the file directly.

I mean how do I add a shebang line to the compiled file?

A text editor works. It’s just a Python file.

I don’t want to have to do that manually every time I recompile!

You can use the .# reader macro to inject arbitrary text in the compiled output. Use e.g. .#"#/usr/bin/env python" as the first compiled line.

Is Hissp a Lisp-1 or a Lisp-2?

Hissp doesn’t fit into your boxes. Hissp variables are Python variables. They’re not implemented as cells in symbols.

Hissp can’t have a function and variable with the same name at the same time in the local scope, so if I had to pick one, I’d have to say it’s technically a Lisp-1.

By this logic, Ruby is a Lisp-2 and Python is a Lisp-1 (although neither is a Lisp), so Lisp-1 is the most natural fit for a Lisp based on Python.

Functional programming is more natural in a Lisp-1. Lisp-2 tended to work better for macros in practice, because it mostly prevents accidental name collisions between variables and function names in macroexpansions. But whatever advantages that had for macros were obsoleted by Clojure’s syntax quote, which qualifies symbols automatically and prevents such collisions even in a Lisp-1. This solution is the best of both worlds. Lissp’s template quote qualifies symbols automatically, like Clojure’s syntax quote.

However, you can have a macro and a variable (possibly a function) with the same name at the same time in the local scope in Hissp. The macro will be used for direct invocations in the “invocation position” (if it’s the first element of the tuple), and the variable will be used in the “variable position” (anywhere else). If you want to use the macro itself as a variable (and unlike functions, this is rare) then you can qualify it with _macro_.. This behavior is very much like a Lisp-2.

This allows you do do things like write a macro that inlines a small function, while still being able to pass a function object to higher-order functions (like map) using the same unqualified name. This behavior is similar to Hy, which uses this ability for its operators, but is completely unlike Clojure.

And it does have a cost: Unlike Clojure’s syntax quote, there are cases when the way Lissp’s template quote should qualify a symbol is ambiguous. The same symbol might refer to a builtin, a macro, and a global, each of which would have to be qualified differently. The template-quote qualification rules were designed to mostly just work, but you may run into edge cases in Lissp that couldn’t exist in Clojure.

If you wanted semantics more like a Lisp-2, Lissp can do it pretty easily. You could write a defun macro that creates a function and put it in a global types.SimpleNamespace named xHASH_xQUOTE_.

Note that you can define macros that behave like functions: maybe such a macro foo would rewrite an invocation like (foo bar baz) to (#'.foo bar baz).

If you want to import a Python function (instead of just using it qualified) then put it in the #' namespace instead of in the globals (you still need the associated macro). You can even (define __builtins__ (dict : __import__ __import__)) to hide all the builtins but __import__ (which is required for qualified identifiers to work).

None of this is going to raise an error if you manage to get a function variable in the invocation position that isn’t already shadowed by a macro. To fix that, you’d have to use a _macro_ namespace object instancing a class that overrides object.__getattr__ to check the #' namespace for a name and return the rewrite macro in that case, or raise an error otherwise.

What the heck is a Lisp-2?!

These are not great names, but they kind of stuck. Read the original paper and then go back and read the previous answer, and you might have a chance of understanding it. Learning both Scheme and Common Lisp so you can contrast them should do it, but that might take longer.

What version of Python is required?

The compiler itself currently requires Python 3.8+. However, the compiled output targets such a small subset of Python that Hissp would probably work on 3.0 if you’re careful not to use unsupported features in lambda, invocations, injections, or any parts of the standard library that didn’t exist yet. The output of Lissp’s template syntax may require Python 3.5+ to work.

Qualified macros might still be able to use the 3.8+ features, because they run at compile time, as long as unsupported features don’t appear in the compiled output.

Even more limited versions of Python (2.7?) might work with minor compiler modifications. The Hissp compiler is easy enough to understand that you could realistically try.

Is Hissp stable?

Too much of the deep stuff has changed too recently for me to answer “yes”. Hissp’s version number still starts with zero, and it will stay that way until it either sees significant adoption (in which case I might have to burn through a few major version numbers), or I’ve proven Hissp can do what I set out for it, including, at least, Hebigo, a microKanren, the goals in the README, nearly-full test coverage (not unrealistic), and a code-walking yield macro.

This project is relatively new. Hissp is certainly usable in its current form, and has been for some time now, although maybe some things could be nicer.

Expect some breaking changes each release. If you want to be an early adopter, either pin the release version, or keep up with the changes on the master branch as they come.

The core Hissp language itself seems pretty settled, but the implementation may change as the bugs are ironed out. It was stable enough to prototype Hebigo.

There’s probably no need to ever change the basic language, except perhaps to keep up with Python, since the macro system makes it so flexible. But if I discover that deeper changes are required to meet Hissp’s goals as stated in the README, I will do it.

Hissp is still unproven in any major project, so who knows? The only way it will get proven is if some early adopter like you tries it out and lets me know how it goes.