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 None
s (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.
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.
Where’s cons
? How do you add links to your lists?¶
We don’t have one. Hissp code is not actually made of linked lists. It uses Python tuples, which are backed by arrays.
While conceptually elegant, pervasive consing is a persistent source performance problems in Lisp. Large linked lists tend to get scattered all over the heap and cause frequent cache misses. Experienced Lispers learn to avoid it.
You can splice ,@
into a template to approximate cons
pretty well.
Allocating new arrays may be slightly less efficient than adding a link,
but this is mostly only done in macros which run at compile time when it doesn’t matter so much.
If you need to accumulate values into a collection at runtime,
learn to use more efficient alternatives,
like Python’s list
.
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.
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.
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).
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.
exec()
animport
or afrom
import
statement.The inject macro
.#
works on statements if it’s at the top level.
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.
I got here from a link in Hy’s docs. What are the main differences between Hissp and Hy?¶
They’re both Lisps that can import Python, but Hissp has a very different approach and philosophy.
I was also a major contributor to the open-source Hy project. Hy is obviously a much older project than Hissp with more contributors and more time to develop. While my experience with Hy informs my design of Hissp, Hissp is not a fork of Hy’s source code, but a completely new project with a fundamentally different architecture.
The biggest difference is that Hy compiles to Python abstract syntax trees (or “AST”, an intermediate stage in the compilation of Python code to Python bytecode). In contrast, Lissp works more like ClojureScript: the Lissp language uses Hissp as its AST stage instead, and compiles that to Python code, which Python then compiles normally. Hy compiles to a moving target—Python’s AST API is not stable. This helps to make Hissp’s compiler simpler than Hy’s.
Hy has an intermediate layer of Hy model objects it uses to represent code,
although it tries to convert them automatically,
this can make it even more confusing.
E.g. (type '42)
is <class 'hy.models.HyInteger'>
in Hy,
but <class 'int'>
in Lissp.
Except for tuples, which represent invocations,
and strings, which represent raw Python (and module literals/qualifiers),
Hissp’s representation for Python objects are simply the objects.
Hissp will at least attempt to compile in anything,
even if it has no literal representation in Python code,
by falling back to pickle
, if necessary.
Hy will just crash if you insert anything it can’t model.
This means that Hissp has a much higher degree of homoiconicity that Hy.
Hissp code is made of ordinary Python tuples that serve the same role as linked lists in other Lisps, or Hy’s model objects. Using these directly in Python (“readerless mode”) is much more natural than writing code using Hy’s model objects, although using the Lissp (or Hebigo) language reader makes writing these tuples even easier than doing it directly in Python.
Hissp is designed to be more modular than Hy. It supports two different readers (Lissp and Hebigo) with the potential for more. These compile different languages that represent the same underlying Hissp trees. The separate Hebigo language is indentation-based, like Python, while the included Lissp reader uses the traditional S-expressions.
Hy code requires the hy
package as a dependency.
You need Hy’s import hooks just to load Hy code.
But Hissp only requires hissp
to compile the code to Python.
Once that’s done,
the output has no dependencies other than Python itself.
(Unless you import some other package, of course.)
This may make Hissp more suitable for integration into other projects
where Hy would not be a good fit due to its overhead.
Hy’s compiler has a special form for every Python statement and operator
and has to do a lot of work to create the illusion that its statement
special forms behave like expressions.
This complicates the compiler a great deal,
and doesn’t even work right in some cases,
but allows Hy to retain a very Python-like feel.
The unparsed
AST also looks like pretty readable Python.
Not quite what a human would write,
but a good starting point if you wanted to translate a Hy project back to Python.
But after writing Drython,
I realized that the expression subset of Python is sufficient for a compilation target.
There is no need to do the extra work to make statements act like
expressions if you only compile to expressions to begin with.
It turns out that Hissp only required two special forms: quote
and lambda
.
(And you could almost implement lambda via a text macro.)
This makes Hissp’s compiler much simpler than Hy’s,
and makes full code-walking macros much easier to implement,
because once all the macros are expanded,
there aren’t that many special cases to deal with.
But, the lack of statements makes it feel a bit more like Scheme and a bit less like Python.
And, of course, the expression-only output,
while a direct and sensible translation once you understand Hissp,
is completely unpythonic.
Another major difference is Hissp’s module literals and qualified symbols, which allows macros to easily import their requirements from other modules. Macro dependencies are much harder to work with in Hy. Lissp’s template quote automatically qualifies symbols like Clojure does. Hy can’t do that.
Hy passes Python keyword arguments using HyKeywords,
while Lissp passes them using symbols,
so print(1, 2, 3, sep=",", end="!\n")
in Python would be
(print 1 2 3 :sep "," :end "!\n")
in Hy and
(print 1 2 3 : sep "," end #"!\n")
in Lissp.
Hissp’s lambda
special form parallels this call syntax for defining parameters,
with :
as the separator; :?
as a placeholder;
:*
and :**
to parallel unpacking;
plus :/
for positional-only, based on Python’s syntax.
Hy’s syntax on the other hand,
has to use special symbols &optional
, &rest
, &kwonly
, and &kwargs
,
based on Common Lisp’s approach to distinguish its parameter types,
in addition to the #*
and #**
it uses for unpacking.
It can’t do positional-only parameters at all yet.
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.