-
-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rethink prelude #139
Comments
If we wanted easy alternatives to Python's collection literals, they should have very short names. A very simple system would be (define @
(lambda (: :* xs)
xs)) Now A slightly less simple system could have one-character names with mnemonics Maybe add inflections for "frozen" versions, like I feel like We're giving up short names for a couple of operators and also giving up those short names for Pyrsistent's use, but that's only if you actually use the prelude. These names are only short in Lissp. These are not as nice in Hebigo or readerless mode, where the en- group names like We maybe don't have to give up on |
Given the en- group collection functions as Idiomatic Lissp right now spells out the operator names, or injects a Python string for the really complex formulas. Importing operators as their special character names would not be strange, but you probably wouldn't do this in a file using the basic prelude anyway, since that star imports from Having the prelude import using these special names for you is worth considering, but I think the spelled-out names from Another reason not to do the short en- group names (and the main reason I didn't do it that way in the first place) is that I want to encourage better practices by not making the bad practices too easy. Pyrsistent's But maybe this doesn't come up. If you can have dependencies like |
def engarde(xs,f,*a,**kw):
try:return f(*a,**kw)
except xs as e:return e It could maybe be even shorter as def engarde(xs,f):
try:return f()
except xs as e:return e but then it's harder to use because you likely have to define a function to immediately call instead of just calling the one you already have handy which might raise an exception. Doesn't seem worth it.
(let (res (engarde (entuple FooException BarException)
dangerous ...))
(cond (isinstance res FooException) (handlefoo res)
(isinstance res BarException) (handlebar (heh))
:else res)) This is not very nice, but I'm not sure what to do about it. Maybe a better macro would help here. Does Clojure have a good one for cases like this? Not sure. A fairly simple tweak would be def engarde(xs,h,f,*a,**kw):
try:return f(*a,**kw)
except xs as e:return h(e) Now you have to have a handler function, which takes the exception as an argument. It's a little more work if you just wanted a suppress, but you could use (engarde FooException
repr
dangerous ...) or if you needed handling, you could define an anonymous function. (engarde FooException
(lambda e ...)
dangerous ...) If you already have a handler function handy, just pass that. If you want to handle multiple exception types, you can just nest |
Current plan:
I'm gonna let that stew a while before implementing it. |
BTW, nested (engarde FooException
handlefoo
engarde BarException
handlebar
dangerous ...) It's just like a try-except, except upside-down. And no |
I thought But then we'd want new names for
|
I think that having only I don't feel great about the But then But do we need
It doesn't look that bad. Python already separates everything with a comma and a space. We'd just do a space and a comma. The overhead for commas would be less noticeable with longer expressions. It only really gets confusing when it's nested in another template. At that point, you can still use
Python's (1, 2, 3) # tuple
[1, 2, 3] # list
{1, 2, 3} # set
{1: 2, 3: 4} # dict Clojure's '(1 2 3) ; (linked-)list, but recursively quoted.
`(~1 ~2 ~3) ; same, but without internal quoting via syntax-quote
(list 1 2 3) ; same, but more idiomatic for data.
[1 2 3] ; vector
#{1 2 3} ; set
{1 2, 3 4} ; map Lissp (proposed, with prelude) '(1 2 3) ; tuple, but recursively quoted.
`(,1 ,2 ,3) ; same, but without internal quoting via template quote
(en#tuple 1 2 3) ; same, but maybe easier in nested templates
(@ 1 2 3) ; (array) list
(# 1 2 3) ; (hash) set
(% 1 2 3 4) ; dict (of key to value pairs) Note that the I think
Current proposal: from functools import partial,reduce
from itertools import *;from operator import *
def QzAT_(*xs):return[*xs]
def QzHASH_(*xs):return{*xs}
def QzPCENT_(*kvs):return{k:i.__next__()for i in[kvs.__iter__()]for k in i}
def excepting(xs,h,f,/,*a,**kw):
try:return f(*a,**kw)
except xs as e:return h(e)
_macro_=__import__('types').SimpleNamespace()
try:exec('from hissp.basic._macro_ import *',vars(_macro_))
except ModuleNotFoundError:pass |
More concise version with lambdas. from functools import partial,reduce
from itertools import *;from operator import *
QzAT_=lambda a:[*a];QzHASH_=lambda a:{*a}
QzPCENT_=lambda*p:{k:i.__next__()for i in[p.__iter__()]for k in i}
def excepting(xs,h,f,/,*a,**kw):
try:return f(*a,**kw)
except xs as e:return h(e)
_macro_=__import__('types').SimpleNamespace()
try:exec('from hissp.basic._macro_ import *',vars(_macro_))
except ModuleNotFoundError:pass Not that much shorter though. I might be golfing at this point. I think I'll keep the |
I want to release the en- group version before experimenting too much with this. I think the docs are in a pretty good state right now. If I make changes like this again, it could take a while to get them settled. The release following that will probably use this prelude and remove the collection atoms. |
I'm still wondering if we can't get rid of the prelude altogether. It's kind of a hack. The (defmacro @ (: :* xs)
`((lambda (: :* $#xs)
(list $#xs))
,xs))
(defmacro # (: :* xs)
`((lambda (: :* $#xs)
(set $#xs))
,xs)) They really ought to be functions, but macros would mostly work. Without a prelude, the no-dependencies rule means they'd have to be macros. If you need to pass them to higher-order functions, you can still use
(defmacro % (: :* kvs)
`((lambda (,'kvs)
.#"{k:i.__next__()for i in[kvs.__iter__()]for k in i}")
,kvs)) This could certainly be done without it, but it gets complicated. For readability, here's an expansion of the function definition (lambda (: :* kvs)
(dict (zip (.__getitem__ kvs (slice 0 None 2))
(.__getitem__ kvs (slice 1 None 2))
: strict True))) Slicing always works because (let (ikvs kvs)
(dict (map (lambda i (en#tuple i (.__next__ ikvs)))
ikvs))) This would be fine for a function definition, but inlining this much seems excessive. Honestly, the inject might still be worth it here, but I'd sooner use a strict zip than inline all of this. We could also just omit the
It's definitely not as nice, but it isn't terrible either, as long as you're not trying to nest them, which would usually mean identifier keys. Usually. Hmm. That leaves the |
The preferred way of defining macros is to implement as much of it as possible as functions, and then just expand to calls to those, rather than inlining everything, but the no-dependencies rule prohibits this pattern. But where does it end? All of the en- group collections could work that way. So could the template quotes. So could an entire functional/lispy library. It's already not clear to me that this We could avoid the That sounds like a knock-down argument against using this kind of conditional definition outside of the def- group. But, macros also expand to fully-qualified names (and this is fundamental to the template quote system), which compile to imports every time! I'm not too worried about these appearing in loops. Why? because modules in Python are imported only once, and then cached. Subsequent imports amount to a dict lookup. Not quite as fast as a local, true, but pretty close. Python does dict lookups all the time for globals and instance variables. It's fast. Maybe we could avoid the excess |
Unpacking inside lists and sets pretty much works.
But, you need the Unpacking inside dicts, on the other hand, doesn't work so well. You can use
is about the best we can do. Ideally, we could say Maybe |
No real need for a virtual module. A gensym global should suffice. (defmacro excepting (: :* args)
(let (gs `$#excepting
py "def {}(xs,h,f,/,*a,**kw):
try:return f(*a,**kw)
except xs as e:return h(e)")
`((.get (globals)
',gs
(lambda (: :* $#a :** $#kw)
(exec ',(.format py gs)
(globals))
(,gs : :* $#a :** $#kw)))
,@args))) Tested. |
I think I got the collection macros working. The function versions were the 80% solution. They also looked a lot cleaner on compilation, but such is the cost of zero-dependency macros. That ship has sailed. The Prelude still has star imports and clones the The star imports are merely a convenience. Hissp has module literals. You can assign shorter names for the modules or use the alias macro to abbreviate. It's also easy enough to inject or exec these imports yourself. Importing Referring individual macros is also straightforward. It's like individual imports, but you use the This will get long-winded if you want to refer all of the bundled macros. Maybe this much-diminished prelude is still worth keeping, but it feels like refer-all needs a general solution. Seems like we still need one more macro. It should create a |
Excepting doesn't work as well as engarde. One catch, sure, but you can't nest them the same way. (engarde FooException
handlefoo
engarde BarException
handlebar
dangerous ...) becomes
This form really wants to be a function. Ideally, a macro wouldn't need to pass in a function at all though. It should be able to wrap it in a thunk by itself. Ideal syntax might be like
But then what does it expand to? Hebigo has one but it uses helper functions defined in Python. |
We can replace the star imports in the prelude with short aliases, and then they'll be available in the repl or when you clone the bundled The prelude lets you say e.g. That still leaves the ((lambda (f : :* a :** kw)
(functools..partial f : :* a :** kw))
a b c) But we're almost re-implementing |
The process from #154 has resulted in a lot of changes to the bundled macros, including the prelude. Here's the current version. from functools import partial,reduce
from itertools import *;from operator import *
def engarde(xs,h,f,/,*a,**kw):
try:return f(*a,**kw)
except xs as e:return h(e)
def enter(c,f,/,*a):
with c as C:return f(*a,C)
class Ensue(__import__('collections.abc').abc.Generator):
send=lambda s,v:s.S(v);throw=lambda s,*x:s.T(*x);From=0;Except=()
def __init__(s,p):s.p,g=p,s._(s);s.S,s.T=g.send,g.throw
def _(s,k,v=None):
while isinstance(s:=k,__class__):
try:s.value=v;k,y=s.p(s),s.Yield;v=(yield from y)if s.From else(yield y)
except s.Except as e:v=e
return k
_macro_=__import__('types').SimpleNamespace()
try:exec('from hissp.macros._macro_ import *',vars(_macro_))
except ModuleNotFoundError:pass It's like a mini Drython. Pure Hissp is usable without this, but we need this stuff for Python interop. I think everything in here needs to be here. I've removed all of the definitions but I am quite pleased that I was able to get The prelude is now longer than when I opened this issue, mostly thanks to Ensue. I really cannot make this class any shorter than it is. I've tried. The hand minification makes it looks smaller than it is. Exceptions really make Python generators complicated :( Do all small Python scripts need all of this? No. The operators probably, but you don't need the prelude for that. It's entirely possible for a small Python script to not need a Using it would feel no worse than an import. A Maybe I'm overthinking this. |
The current prelude exec's the following Python code
Most of the en- group could be replaced with a single reader macro:
(defmacro en\# (f) `(lambda (: :* $#xs) (,f $#xs)))
Now you can use
en#tuple en#list en#set en#frozenset
, and applyen#
to anything else with a single iterable argument, i.e. anything you can use direct genexpr in without the extra()
, which is quite a lot. It does add a bit of overhead, but this seems useful. It could also maybe be enhanced to accept kwargs without ambiguity.I kind of wondered if the prelude could be eliminated with that. It seems like kind of a hack. But,
en#frozenset
is much longer thanenfrost
,en#dict
is not very helpful,en#str
doesn't work, and neither doesen#"".join
nor(en#.join "" ...
, but(.join "" (en#list ...
would.frozenset
was kind of a questionable addition to begin with, and.format
still works for strings, so whatever, butengarde
is a completely different animal, and still seems important. It pretty much can't be implemented as a direct macro, short of inlining anexec
. The prelude seems less bad here. There is the contextlib version in the FAQ,But inlining this much also seems bad. The result seems a bit more usable than
engarde
though, which pretty much requires an enlosinglet
andcond
or something.engarde
seems incomplete compared to the Hebigo, Drython, and toolz versions.The text was updated successfully, but these errors were encountered: