Skip to content
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

Closed
gilch opened this issue Jan 1, 2022 · 17 comments
Closed

Rethink prelude #139

gilch opened this issue Jan 1, 2022 · 17 comments

Comments

@gilch
Copy link
Owner

gilch commented Jan 1, 2022

The current prelude exec's the following Python code

from functools import partial,reduce
from itertools import *;from operator import *
def entuple(*xs):return xs
def enlist(*xs):return[*xs]
def enset(*xs):return{*xs}
def enfrost(*xs):return __import__('builtins').frozenset(xs)
def endict(*kvs):return{k:i.__next__()for i in[kvs.__iter__()]for k in i}
def enstr(*xs):return''.join(''.__class__(x)for x in xs)
def engarde(xs,f,*a,**kw):
 try:return f(*a,**kw)
 except xs as e:return e
_macro_=__import__('types').SimpleNamespace()
try:exec('from hissp.basic._macro_ import *',vars(_macro_))
except ModuleNotFoundError:pass

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 apply en# 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 than enfrost, en#dict is not very helpful, en#str doesn't work, and neither does en#"".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, but engarde is a completely different animal, and still seems important. It pretty much can't be implemented as a direct macro, short of inlining an exec. The prelude seems less bad here. There is the contextlib version in the FAQ,

(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)
               (self.handler exception)
               True)))

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

But inlining this much also seems bad. The result seems a bit more usable than engarde though, which pretty much requires an enlosing let and cond or something. engarde seems incomplete compared to the Hebigo, Drython, and toolz versions.

@gilch
Copy link
Owner Author

gilch commented Jan 2, 2022

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 (entuple 1 2 3) becomes (@ 1 2 3), (enlist 1 2 3) becomes (list (@ 1 2 3)), and (endict 1 2 3 4) becomes (dict (@ (@ 1 2) (@ 3 4))), which is not as nice, but not terrible either. (enstr "foo" 2 "bar") becomes (.join "" (map str (@ "foo" 2 "bar"))), which is still pretty bad, but .format is still there.

A slightly less simple system could have one-character names with mnemonics @ (array) for enlist, % (pair) for endict, # (hash) for enset, and continue using ` template quotes for tuples, or (tuple (@ ... when that gets confusing.

Maybe add inflections for "frozen" versions, like @f for entuple and #f for enfrost.

I feel like enstr and engarde are already short enough for their use case.

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 enfrost maybe made more sense than QzHASH_f, although maybe they don't need it as much as Lissp does.

We maybe don't have to give up on en# and can still have that in the basic macros as an alternative system, but this doesn't help in readerless or Hebigo much either.

@gilch
Copy link
Owner Author

gilch commented Jan 2, 2022

Given the en- group collection functions as @f @ # #f %, I don't think we need collection literals anymore #130, but unlike the literals, they would still require the prelude to work, which is not as nice.

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

Having the prelude import using these special names for you is worth considering, but I think the spelled-out names from operator are fine here.

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 v, for example, is shorter than enlist and could replace the use of lists in most situations. The choice is not as clear when it's v vs @, and worse, having @ as enlist might make you too reluctant to import v as @, which might be a good idea, since v is a common local name used in dict item unpacking.

But maybe this doesn't come up. If you can have dependencies like pyrsistent, then you can have Hebigo too and you really don't need the prelude at all. You can also write your own utility module and import it. The prelude is only for more limited use cases (like embedding as Brython in a web page, or 1-file scripts) when you can't add dependencies, and doing it Python's way makes sense.

@gilch
Copy link
Owner Author

gilch commented Jan 2, 2022

engarde is quite minimal,

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.

engarde works fine if you're just suppressing an exception, but if you need to handle one, you still need to do that somehow, which means you probably wrap it in a let and cond like,

(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 repr or something as your exception handler.

(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 engardes, or use a cond in the handler. Short of Hebigo's try macro, this is pretty good.

@gilch
Copy link
Owner Author

gilch commented Jan 2, 2022

Current plan:

  • add en# macro. (No kwargs yet.)
  • rename en- group to @f @ # #f %
  • add the handler to engarde
  • update docs for all of this.

I'm gonna let that stew a while before implementing it.

@gilch
Copy link
Owner Author

gilch commented Jan 2, 2022

BTW, nested engardes get flattened, which looks pretty good:

(engarde FooException
  handlefoo
  engarde BarException
  handlebar
  dangerous ...)

It's just like a try-except, except upside-down. And no finally, but that's not hard to implement given the try-except and functions. Python mostly uses context managers for finally cases anyway.

@gilch
Copy link
Owner Author

gilch commented Jan 2, 2022

I thought engarde was a clever name (and enlist and endict, especially), but with the rest of the en- group getting shorter names, that just leaves enstr, so there's really no reason to keep the "en-" prefix. We'll have the en# macro instead.

But then we'd want new names for engarde and enstr as well. The natural choice is except, but that's a reserved word. Java(Script) uses throw and catch, and therefore Clojure/Script as well, so catch is an option. Toolz uses excepts, but it's a bit different. except_ is also an option. We already use names like that for certain functions in the operator module, but I think I like catch better.

enstr was based on Clojure's str, but that's already the name of a builtin in Python. Arc has string, which is the name of a module in Python's standard library, but that's an option. Given .format, mod, .join, and string.Template, it has questionable importance, and maybe doesn't deserve to be in the prelude. Removing it from the prelude altogether is an option. A short name like $ is a possibility, but I think I'd sooner implement f-strings with f#, and I kind of liked an enstr function better.

@gilch gilch mentioned this issue Jan 3, 2022
@gilch
Copy link
Owner Author

gilch commented Jan 15, 2022

I think that having only @ as entuple isn't good enough without the collection atoms. But to replace those, we need tuple, set, list, and dict.

I don't feel great about the @f though. If it's going to be two characters anyway, we might as well use \,.

But then #f doesn't fit in that well. It was a questionable addition to the prelude to begin with though. It lacks a literal notation in Python. I still think it's weird that we can't nest sets. Set theory is all about that. We also can't use them as dict keys. We need a frozenset, that's why I included one, but a prelude without it would be no worse than Python.

But do we need entuple at all given quote, templates, and en#? Even if you unquote everything, it still looks like commas! compare:

(\, 1 2 3)
`(,1 ,2 ,3)

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 en#tuple.

enstr is still questionable. The prelude is supposed to be the absolute necessities when you can't add a dependency on a proper functional library. We still have str.format(). That's good enough, and when it isn't, it's trivial to define it yourself.

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 # matches Clojure's set. The @ and % are from the Perl sigils.

I think engarde is not a bad name, but catching or excepting could also work:

(engarde FooException
  handlefoo
  engarde BarException
  handlebar
  dangerous ...)

(catching FooException
  handlefoo
  catching BarException
  handlebar
  dangerous ...)

(excepting FooException
  handlefoo
  excepting BarException
  handlebar
  dangerous ...)

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

@gilch
Copy link
Owner Author

gilch commented Jan 15, 2022

More concise version with lambdas. defs are better for error messages because they keep the function name, but maybe with the short names, it doesn't matter? Also, Hissp uses lambdas a lot generally.

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

@gilch
Copy link
Owner Author

gilch commented Jan 15, 2022

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.

@gilch
Copy link
Owner Author

gilch commented Jan 16, 2022

I'm still wondering if we can't get rid of the prelude altogether. It's kind of a hack.

The @ and # could easily be macros:

(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 en#list and en#set. Although en# probably ought to be a function too, come to think of it.

% is easiest with an injection. This breaks the rule about not doing injections in the basic macros. prelude was already an exception though.

(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 kvs will be a tuple. The strict is a newer feature. We're using positional-only parameters in engarde already though. Without it, the zip will silently drop the last key. We'd be better off with an error. An alternative would be to use itertools..zip_longest, but then the last value would be None when it's odd. This is less bad than dropping, but I'd still rather have an error. We could just add a check that it's an even number, but this feels unnatural. Mimicking the comprehension would require an inner lambda for the let, and the map, which also needs an entuple (or enlist), which would expand to another lambda. Here's just the body, without expansions.

(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 endict. The builtin dict works fine for identifier keys, which is the common case. If you need other key types, @ helps.

(dict (@ (@ 1 2)
         (@ 3 4)))

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 engarde replacement.

@gilch
Copy link
Owner Author

gilch commented Jan 16, 2022

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. defmacro is already bending the rules a bit by defining a global, the _macro_ namespace, but conditionally. The exception handler could do the same thing: exec some kind of global Python definition (conditioned on it not being present), and then use it in the expansion. We'd probably use a gensym, since unlike _macro_, nothing else should use it. That way it would only have to appear in the expansion the first time (per module).

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 defmacro magic is worth it. Subtle bugs are possible. #132. The prelude actually seems less bad. I kind of don't want to go there.

We could avoid the exec to create the try statement by using the context manager version, but that's probably not worth it. execing once on import is not bad. execing in-line, in an inner loop, would be very bad though. However, there's no way for the macro to tell that it's in a loop. If the first try macro were to be in a loop, it could only expand to the exec usage or not, and both are a problem. This kind of didn't come up with defmacro, because (like the rest of the def- group) it should normally only be used at the top level because it always defines a global.

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 execs the same way. We could seriously create a module object, exec some code in its namespace, and give it a gensym name in the cache, conditional upon it not already being there. We still have to do that the first time, but afterwards it's just a lookup. We do have to expand to the full code (that not only execs the Python, but caches it) every time, but it only runs that code once. Afterwards, it just checks the cache and always skips that branch. The expansions are pretty bloated, which can get annoying when debugging, but the run-time impact is about as minimal as I know how to make it. This is probably feasible.

@gilch
Copy link
Owner Author

gilch commented Feb 10, 2022

Unpacking inside lists and sets pretty much works.

#> (@ 1 2 : :* "AB"  :? 42  :* "XY")
>>> QzAT_(
...   (1),
...   (2),
...   *('AB'),
...   (42),
...   *('XY'))
[1, 2, 'A', 'B', 42, 'X', 'Y']

But, you need the :? in some cases. It would be nicer if you could say (@ 1 2 :* "AB" 42 :* "XY"). A macro can certainly do that. A function almost could, but it can't distinguish :* from ":*". This only works in the function position, but we still have en#list. (And we could still have a shadowed function with the same name.)

Unpacking inside dicts, on the other hand, doesn't work so well. You can use :** inside (dict), but only if keys are identifiers. The more general case gets ugly. I think

#> (en#dict : :* (.items (% 1 2  3 4))  :* (.items (% 5 6))  :? (@ 7 8))
>>> (lambda *_xs_QzNo32_:
...   dict(
...     _xs_QzNo32_))(
...   *QzPCENT_(
...      (1),
...      (2),
...      (3),
...      (4)).items(),
...   *QzPCENT_(
...      (5),
...      (6)).items(),
...   QzAT_(
...     (7),
...     (8)))
{1: 2, 3: 4, 5: 6, 7: 8}

is about the best we can do.

Ideally, we could say (% :** (% 1 2 3 4) :** (% 5 6) 7 8), or even (% :* (@ 1 2 3 4) :** (% 5 6) 7 8). Again, possible with a macro. A function could almost do it, but again can't distinguish quoted control words from active ones.

Maybe @, #, and % don't need to be in the prelude at all. That really just leaves excepting and some quick imports. The alias macro and module literals mean we could do without the imports. excepting though. There's a lot of overhead required to avoid the exec. Maybe a virtual module could work.

@gilch
Copy link
Owner Author

gilch commented Feb 11, 2022

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. excepting with no prelude. The first invocation does a JIT exec to create the real function (and call it). After that, it just uses the function. Less elegant than the prelude, and a bit more overhead as well, but it works.

@gilch
Copy link
Owner Author

gilch commented Feb 13, 2022

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 _macro_ module.

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 _macro_ is easy, but very wrong as soon as more than one module does it, and it creates a dependency on Hissp in the compiled output. Bad, bad. Alias is the current best solution. The incantation is now something like (hissp.._macro_.alias foo hissp.._macro_).

Referring individual macros is also straightforward. It's like individual imports, but you use the _macro_'s vars instead of the module globals. The incantation is something like (.update (vars _macro_) : define hissp.._macro_.define), for whatever pairs you like, but again this creates a dependency on Hissp, unlike aliasing. This, of course, assumes _macro_ already exists, but making it is not hard either: (.update (globals) : _macro_ (types..SimpleNamespace)).

This will get long-winded if you want to refer all of the bundled macros. (.update (vars _macro_) (vars hissp.._macro_)) works, but also adds private names that probably shouldn't be there. The best workaround I've come up with is (exec "from hissp.macros._macro_ import *" (vars _macro_)). Note that this can't use hissp._macro_, the import system can't find it there, so this solution isn't a general one. Maybe non-modules don't need the filtering though. And the dependency on Hissp is still there. The prelude just catches the import error and gives up.

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 _macro_ like defmacro: only when not present. It should refer everything listed in the __all__ attribute of the targeted _macro_, or filter out private names if it doesn't have one, and give up silently on an import error (yikes!). The incantation would be something like (hissp.._macro_.refer-all hissp.). I kind of want an explicit silence, so maybe (hissp.._macro_.refer-all hissp. :silent) or (hissp.._macro_.refer-all-silent hissp.) or (silent (hissp.._macro_.refer-all hissp.).

@gilch
Copy link
Owner Author

gilch commented Feb 13, 2022

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

(excepting FooException
  handlefoo
  (lambda : (excepting BarException
                       handlebar
                       dangerous ...)

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

(try dangerous
  (BarException e
    handle bar)
  (FooException e
    handle foo)
  (:finally
    clean up))

But then what does it expand to? Hebigo has one but it uses helper functions defined in Python.

@gilch
Copy link
Owner Author

gilch commented Feb 20, 2022

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

The prelude lets you say e.g. getitem or eq instead of operator..getitem or operator..eq, but with a bundled alias, it could be shortened to op#getitem and op#eq, or even *#getitem and *#eq, using * as a generic operator mnemonic. We'd need a separate one for the itertools though. it# maybe sounds too generic. itr# or itt are better, but an overhead of four characters, when it could theoretically be as few as two. Being super short seems a bit less important for itertools than for operators though. Maybe i#? I'd kind of like to reserve single-letter reader macros for the user, so that leaves symbol characters, and many of those are taken, or two-character mnemonics. Maybe xs#. Maybe @#.

That still leaves the partial and reduce imports. We're not star importing from functools, because the rest of that module is not as critical, so an alias doesn't make as much sense. partial could maybe be made a reader macro in its own right. Instead of saying (partial foo a b c) you'd say (partial#foo a b c), which would expand to something like

((lambda (f : :* a :** kw)
   (functools..partial f : :* a :** kw))
 a b c)

But we're almost re-implementing partial at this point. Maybe partial#(foo a b c) makes more sense though. Or we could do without these two. It is just a convenience.

@gilch
Copy link
Owner Author

gilch commented Apr 10, 2022

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 engarde, but added enter (for with statements) and Ensue for generators (yield).

I am quite pleased that I was able to get Ensue working. I had to resort to threads in Drython because I couldn't figure this approach out. It's been a gap in Hissp for a long time. I thought this would end up requiring a code-walking macro to get to this level of functionality. I understand to how to do this now. It's a relatively small class. It so happens that the most compact way to implement said class in Python code was in terms of a generator function, but I can see now that this isn't strictly necessary. Hissp could have done this all along (with a deftype), but it was far from obvious how until now.

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 :( engarde and enter are also extremely minimal. Given my experiment with engarde, I don't think these can be made into standalone macros. Macros could certainly make usage of these definitions easier, (and this is how any sensible macro library would approach most things) but I don't want to go there in the bundled macros.

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 try or with statement, and many junior Python devs don't even know yield exits, or maybe have seen it, but have no idea how it works. engarde and ensue are so small that they seem harmless enough in the prelude even if you never use them. I'm not so sure about Ensue. Maybe it can be split off as a separate opt-in.

Using it would feel no worse than an import. A defEnsue could simply inline the definition code or an initEnsue could inline code to dynamically generate a modue, and skip evaluation of it once the module exists, importing it either way. The latter only makes sense if there are multiple modules in your project, but once you have those, why not defEnsue in one of these yourself and import it normally everywhere else?

Maybe I'm overthinking this. Ensue is not that much overhead compared to @ and friends defining inline lambdas everywhere. It's got all of four methods, and the rest are inherited.

@gilch gilch closed this as completed Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant