{"id":16662746,"url":"https://github.com/gilch/drython","last_synced_at":"2025-04-09T19:00:56.480Z","repository":{"id":63160669,"uuid":"42851405","full_name":"gilch/drython","owner":"gilch","description":"metaprogramming for don't-repeat-yourself Python","archived":false,"fork":false,"pushed_at":"2019-05-16T02:10:52.000Z","size":191,"stargazers_count":23,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-23T20:51:18.478Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gilch.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-09-21T07:40:14.000Z","updated_at":"2025-02-02T21:51:03.000Z","dependencies_parsed_at":"2022-11-14T04:34:37.942Z","dependency_job_id":null,"html_url":"https://github.com/gilch/drython","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilch%2Fdrython","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilch%2Fdrython/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilch%2Fdrython/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilch%2Fdrython/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gilch","download_url":"https://codeload.github.com/gilch/drython/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248094591,"owners_count":21046768,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-12T10:38:47.611Z","updated_at":"2025-04-09T19:00:56.346Z","avatar_url":"https://github.com/gilch.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Drython #\n**Don't-Repeat-Yourself Python**\n\nDrython is a metaprogramming library for Python.\nMetaprogramming is writing programs that write programs--a\npowerful technique for abstracting away repetitive code.\n\nProgrammers make abstractions constantly. Functions are abstractions. Classes are abstractions.\nBut sometimes it's not enough.\nIf you find yourself writing boilerplate \"design patterns\" again and again,\nthen you're not using powerful enough abstractions.\nYou need metaprogramming.\n\n## Metaprogramming ##\n\nOften, the metaprogramming technique involves creating a miniature domain-specific language (DSL),\ntailor-fit to the problem at hand. One might think the addition of a DSL makes the program harder\nto understand, but if it makes the program a tenth as long as it would have been without it,\n(not unusual), then it's worth it.\n\nPython already includes some metaprogramming facilities.\nUsing decorators to modify functions is a limited example.\nUsing metaclasses to rewrite class declarations is a more powerful example.\n\nBut Python has more general metaprogramming capabilities.\nThe secret to metaprogramming is treating code as just another kind of data.\n(The DRY principle applies to any kind of data, *especially* the executable kind.)\n\nFor example, Python can write text files, including .py files, which it can then import as modules.\n\nAny programming language with access to the filesystem and a compiler could theoretically do this.\nPython can also create strings, including strings containing Python code, which it can execute with\nthe `exec()` function.\nSometimes this approach is appropriate, indeed,\nsome of the Python standard library (like `namedtuple`) uses this technique.\nBut manipulating text as Python code can be difficult and error-prone.\nCompiling text is also rather slow.\n\nAlternatives to text manipulation include manipulation of Python bytecodes\n(not for the faint of heart),\nand manipulation of abstract syntax trees using the `ast` module, which is arcane, but usable:\n```Python\nimport ast\nprint(ast.dump(ast.parse(r'''print(\"Hello, World!\")''')))\n# Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello, World!')], keywords=[], starargs=None, kwargs=None))])\n```\nReading AST is easier than writing it (malformed AST can segfault CPython),\nbut if it took that much for a simple `print('Hello, World!')`,\nyou can imagine it gets complex fast.\nUnfortunately, bytecode and abstract syntax trees are implementation details subject to change\nbetween Python versions and implementations.\n\nThere's an easier way. Drython provides *executable* data structures that are both simpler than AST,\nand are easier to work with than text.\nDrython specifically avoids using ast and bytecode manipulation,\nso it's portable across implementations, including CPython2.7/3.1+, PyPy, Jython, and IronPython.\n\n## The Statement Module ##\n\nCan you re-implement a simple if-statement in Python?\nI mean without writing a text compiler or interpreter, or modifying Python itself?\nSure, you don't have to, Python has a perfectly good if statement already, but can you?\n\nA DSL might need a three-way numeric if statement (-/+/0), or something like a switch/case.\nYes, you can use the boilerplate cascading-elif pattern instead\nfor any of your complex branching needs, but that's not an abstraction, is it?\nYou have to re-write the logic imitating the switch/case (or what-have-you)\n**every single time**.\nIf you can't make a simple `if` substitute,\nhow can you expect to make more advanced language components you might need for a DSL?\n\nYou might not think Python can do it, but it's actually trivial in Smalltalk.\n\"If\" isn't a statement in Smalltalk to begin with.\nIt's a method. On the booleans.\n\n```Smalltalk\nresult := a \u003e b\n    ifTrue:[ 'greater' ]\n    ifFalse:[ 'not greater' ]\n```\n\nThe `:=` is just an assignment (like Python's `=`).\nThe `a \u003e b` part evaluates to either true or false, just like Python.\nThe `[]` isn't a list; it's a **code block**.\nThe true boolean has an `ifTrue:ifFalse:` method that always executes the then-block,\nbut false has a different method *with the same name* that only executes the else-block. Polymorphic dispatch.\nIs that cool or what? Yes, `ifTrue:iffasle:` is one method, not two. An interesting quirk of Smalltalk is that\nthe arguments can go inside of the method name. There are also completely separate `ifTrue:` and\n`ifFalse:` methods that take one argument each.\n\nWe can achieve a very similar effect in Python.\nYou can't modify builtins, but a method is just a function that takes the instance as its first\nargument, which by convention we call `self`.\n\n\n```Python\nresult = (lambda self, *, ifTrue=None, ifFalse=None: ifTrue if self else ifFalse)(\na \u003e b,\n    ifTrue='greater',\n    ifFalse='not greater',\n)\n```\n\nBut there's a problem. This only works on values. What if we want effects?\n\n```Python\n(lambda self, *, ifTrue=None, ifFalse=None: ifTrue if self else ifFalse)(\na \u003e b,\n    ifTrue=print('greater'),\n    ifFalse=print('not greater'),\n)\n```\n\nClearly, this won't work! It prints *both* messages, since the print functions get evaluated before the if lambda\ncan do anything about it.\n\nSmalltalk uses the code blocks to prevent evaluation.\nToo bad Python doesn't have those blocks things, or re-implementing `if` would be easy. Or does it?\n\nActually, a code block is just an anonymous function.\nPython could do something similar with `def`.\n\n```Python\nmy_if = lambda self, *, ifTrue=None, ifFalse=None: (ifTrue if self else ifFalse)()\n\ndef anon_1():\n    print('greater')\n\ndef anon_2():\n    print('not greater')\n\nmy_if(a \u003e b,\n    ifTrue=anon_1,\n    ifFalse=anon_2,\n)\n```\n\nBut these functions are passed by name, so they're not really anonymous, are they?\nThe code is also not inside the control \"statement\" anymore, so it's kind of harder to read.\n\nYou could implement `ifTrue:` as a decorator instead,\n```Python\n\u003e\u003e\u003e iftrue = lambda b: lambda f: f() if b else None\n\u003e\u003e\u003e @iftrue(10 \u003e 5)\n... def result():\n...     print(\"was true\")\n...     return \"greater\"\nwas true\n\u003e\u003e\u003e result\n'greater'\n\n```\nNotice that a decorator doesn't *have* to return a wrapped function.\nIn this case it called its function to return a string instead of wrapping it.\nPython's decorators can only take one argument (the function `result` above),\nso how could we possibly also pass in a boolean (the expression `10 \u003e 5`)?\nThis is possible using a common Python trick:\nuse a factory function (`iftrue`) to create a *new* decorator with the arguments already built in--on the fly.\nThat's why there's a double lambda. It's the inner lambda that got the `result` function.\nDrython's `core` module has a `decorator` decorator to simplify this process:\n```Python\n\u003e\u003e\u003e from drython.core import decorator\n\u003e\u003e\u003e @decorator\n... def iftrue(block, boolean):\n...     if boolean:\n...         return block()\n...\n\u003e\u003e\u003e @iftrue(10 \u003e 5)\n... def result():\n...     print(\"was true\")\n...     return \"greater\"\n...\nwas true\n\u003e\u003e\u003e result\n'greater'\n\n```\nBut how do you pass in a second function for `ifTrue:ifFalse:`? Not possible?\nYou actually *can* do this with decorators, you just need to decorate *two* functions.\nBut decorators only accept one function, right? Don't forget that decorators also work on classes in Python.\nCombine the two with a class and decorate that.\nYou don't even need an instance if you get the functions directly from the class dict:\n```Python\n\u003e\u003e\u003e @decorator\n... def decr_if(blocks, boolean):\n...     blocks = vars(blocks)\n...     if boolean:\n...         return blocks['iftrue']()\n...     else:\n...         return blocks['iffalse']()\n\u003e\u003e\u003e @decr_if(10 \u003e 5)\n... class result:\n...     def iftrue():  # no self\n...         print(\"was true\")\n...         return \"greater\"\n...     def iffalse():\n...         print(\"wasn't true\")\n...         return \"not greater\"\nwas true\n\u003e\u003e\u003e result\n'greater'\n\n```\nDecorators are pretty useful.\n\nAs an aside, you don't even have to access the function directly from the class dict in Python 3.\nThe function doesn't have any args (no `self`), so it doesn't get converted to a method (unlike Python 2)\nso without reassigning `blocks` you can access raw functions via a dot as normal, like\n```Python\nreturn blocks.iftrue()\n```\nDrython's core module has an `attrs` class that lets you access a dictionary via dot syntax (like in Lua), so you could\nalso do this in Python 2 if you instead use the line\n```Python\nblocks = attrs(vars(blocks))\n```\n\nYou could easily implement the 3-way if like this.\nBut how would you implement a control strucure that takes an arbitrary number of blocks,\nlike switch/case, with decorators? Not so easy, right?\n\nWe need real, inline, anonymous functions. Python does have those though, with `lambda`.\n```Python\nmy_if(a \u003e b,\n    iftrue=lambda: print('was true'),\n    iffalse=lambda: print(\"wasn't true\"),\n)\n```\nMuch prettier. Too bad `lambda` only gets one line, or this might actually work.\n\nActually, with Drython, it does work.\n\nWhat if you had an expression that contained multiple expressions,\nand executed them one-by-one in order?\nWouldn't lambda be a lot more useful?\n\nYou do. It's a tuple. Think of the commas as semicolons and you get the idea.\n\nWhat if you just want to return the value of the last expression,\ninstead of a tuple of all of them?\nDeclare a tuple and immediately index it. `(...)[-1]`\nDrython's `do` function does exactly this, and also doesn't crash if its `args` tuple is empty.\n\n```Python\n\u003e\u003e\u003e from drython.statement import do, Print\n\u003e\u003e\u003e my_if = lambda self, iftrue=None, iffalse=None: (iftrue if self else iffalse)()\n\u003e\u003e\u003e result = my_if(10 \u003e 5,\n...              iftrue=lambda: do(\n...                  Print('was true'),\n...                  'greater',\n...              ),\n...              iffalse=lambda: do(\n...                  Print(\"wasn't true\"),\n...                  'lesser',\n...              ),\n...          )\nwas true\n\u003e\u003e\u003e result\n'greater'\n\n```\nIt's no worse than the decorator version in terms of length, but this version is an *expression*.\nThat means you can put the whole thing in a function call or a lambda body and it still works,\nunlike the decorator version, which is made of *statements*.\n\nA control structure could also take an arbitrary number of lambdas using a `*args` parameter to make more complex\nthings like a switch/case. This is much more difficult with decorators.\n\nUnfortunately, lambdas in Python can't contain *statements*,\nso even with `do` they can't work as general code bocks, right?\n\nWith Drython's `statement` module, they *can*.\n\nThe statement module contains expression substitutes for every\nPython reserved word that isn't already an expression or doesn't have an expression equivalent.\nThey work in lambdas.\nThey work in `eval`.\nThey're pretty handy in Drython's executable data structures,\nwhich therefore don't need to handle statement code.\nThis makes them a lot simpler than AST, and therefore easier to use.\n\nReady to write that three-way if?\n\nYou've just learned new metaprogramming abstractions.\nYou can extend Python's syntax without changing the grammar and write your DSL in that.\nNo need to write your own compiler or interpreter, because it's still just Python.\nReady for the next step?\n\n## The Stack Module ##\n\n`Def`, from Drython's `stack` module, is an alternative way to write anonymous functions.\nIt is an executable data structure in the tradition of stack languages like Forth,\nFactor, and Joy.\n\nA `Stack` represents a composition of special\nfunctions called *stack combinators* and their associated data.\n\nBecause stack combinators must accept a stack and return a stack, they are easy to combine into new\ncombinators, just by listing them one after another.\nCombinators execute immediately when pushed on a stack.\nTypically they pop some arguments off the stack and push the result on the return stack\n\nAny Python callable (Including the `statement` module's callables!)\nis interpreted as a stack combinator.\nBy default that takes one iterable off the stack as arguments, and pushes the result.\n```Python\n\u003e\u003e\u003e from drython.stack import Stack\n\u003e\u003e\u003e Stack([1,2,3],Print)\n1 2 3\nStack(None,)\n\n```\nOr, if the top element is a mapping, then a default combinator will take the top two elements,\nusing the mapping for the keyword arguments.\n```Python\n\u003e\u003e\u003e Stack([1,2,3],dict(sep='::'),Print)\n1::2::3\nStack(None,)\n\n```\nYou can, of course, call a function with no arguments if the iterable on top is empty.\nAn empty mapping is likewise harmless, but the iterable is required.\n\nA callable decorated with `@combinator` doesn't use this default conversion and\nmust explicitly accept and return a stack object. This gives it access to every item on the stack,\nbut it's rare to use more than the top four, and uncommon to even use four.\n\nDrython's `combinator` module has many of these nondefault combinators,\nincluding all possible stack permutation functions of depth four or less.\nThe code that generates them is an interesting example of Python's string metaprogramming. Check it out.\n\nIt's easy to see what stack programs are doing by using `Stack.trace`.\nThis is just like `Stack.push`, but it prints every step.\nHere's an example with the `dup` and `bi` combinators\n```Python\n\u003e\u003e\u003e from drython.stack import *; from drython.combinator import *; from operator import mul\n\u003e\u003e\u003e Stack(3).trace(dup,bi,mul)  # duplicate, then binary multiply\nStack(3,) \u003c\u003c dup\nStack(3, 3) \u003c\u003c bi\nStack((3, 3),) \u003c\u003c \u003cbuilt-in function mul\u003e\nStack(9,)\n\n```\nHere, `mul` is an ordinary Python function with the default interpretation,\nwhich is why you need `bi` to wrap the top two elements.\n\nThe `Def` constructor takes a stack program, that is, a sequence of combinators (and any associated data).\nThe resulting function (a callable instance of `Def`) uses a stack internally.\nThe internal stack initiallay has the args tuple and kwargs dict (in that order), from the function call.\nSo a call like `x(1,2,foo=3)` results in `Stack((1,2,),{'foo':3})` initially.\nThen the `Def` call pushes its combinator sequence onto this argument stack, and returns the top element that results.\n\nYou can get at the `(1, 2)` arguments using the `pop` combinator (which removes the top element) and the\nvery important `Ic` (I-combinator), which dumps an iterable's elements on the stack.\n```Python\n\u003e\u003e\u003e Stack((1,2),{'foo':3}).trace(pop,Ic)\nStack((1, 2), {'foo': 3}) \u003c\u003c pop\nStack((1, 2),) \u003c\u003c Ic\nStack(1, 2)\n\n```\nA nice thing about stack combinators is that you can copy-paste entire phrases and it mostly just works.\nOnly the *top* of the stack has to match up.\nWe can combine these two traced programs with `Def` to get a working `square` function.\n```Python\n\u003e\u003e\u003e square = Def(pop,Ic,dup,bi,mul)\n\u003e\u003e\u003e square(4)\n16\n\u003e\u003e\u003e square.trace(4)\nStack((4,), {}) \u003c\u003c pop\nStack((4,),) \u003c\u003c Ic\nStack(4,) \u003c\u003c dup\nStack(4, 4) \u003c\u003c bi\nStack((4, 4),) \u003c\u003c \u003cbuilt-in function mul\u003e\nStack(16,).peek()\n16\n\u003e\u003e\u003e square(7)\n49\n\u003e\u003e\u003e square  # unlike an ordinary Python function, the repr is readable.\nDef(pop, Ic, dup, bi, \u003cbuilt-in function mul\u003e)\n\u003e\u003e\u003e square[0]  # a Def is a type of tuple\npop\n\u003e\u003e\u003e square[2:-1]\n(dup, bi)\n\n```\n\nYou can create new combinators from phrases of existing combinators (instead of from scratch with `@combinator`)\nby using `@Phrase`\n```Python\n\u003e\u003e\u003e @Phrase(pop,Ic)\n... def pic(): \"\"\" for starting a simple Def, ignores kwargs and dumps args on the stack\"\"\"\n\u003e\u003e\u003e @Phrase(bi,mul)\n... def mul2(): \"\"\" multiples the top two elements and pushes the result. \"\"\"\n\n```\nNow you can use them in new stack programs\n```Python\n\u003e\u003e\u003e cube = Def(pic,dup,dup,mul2,mul2)\n\u003e\u003e\u003e cube(3)\n27\n\u003e\u003e\u003e cube\nDef(pic, dup, dup, mul2, mul2)\n\n```\nThe astute reader may wonder why we've gone back to decorators when we went to so much trouble to make everything an expression.\nIt seems like a step backwards.\nShouldn't we make a way to make anonymous functions from phrases from within a stack program?\n\nThe `@Phrase` decorator is just used for phrases with docstrings declared at the top level of a module.\nYou actually already have anonymous function capability:\nCompose a list of combinators on the stack. That's it. That's your anonymous function.\nInvoke it with `Ic`.\nIf you can manipulate lists of combinators programmatically, *then you can write stack programs programmatically.*\nThis is metaprogramming. Code that writes code.\n\nThese lists are a kind of quoted program. Some combinators take such programs as arguments.\nThis is similar to the way Smalltalk takes code blocks, so control structures (and therefore DSLs)\ncan be implemented as combinators in an analogous way.\nSee the `ifte` combinator in the `stack` module's companion `combinators` module for a familiar example.\n\n## s-expressions ##\n\nTired of writing `lambda ...: let(lambda:do(...,Return()))` when you just needed an anonymous\nfunction?\nSure, stack programs are a powerful alternative to lambda, but they can't introduce new variables like lambda can.\nThat sure sounds like a boilerplate code problem.\nYou need better abstractions again.\n\nWouldn't it be easier if you could write functions that get their arguments unevaluated?\nThen you wouldn't need to wrap everything in lambdas.\nLisp can do it with macros. Python can do it too, with Drython.\n\nAn s-expression represents a function *call*.\nYou create an s-expression instance with a function and its arguments.\n\n```Python\n\u003e\u003e\u003e from drython.s_expression import S\n\u003e\u003e\u003e S(Print, \"Hello,\", \"World!\")\nS(\u003cbuilt-in function print\u003e,\n  'Hello,',\n  'World!')\n\n```\n\nBut the call doesn't happen until you invoke its `s_eval()` method,\nat which point it calls `s_eval()` on all its s-evaluable arguments\n(typically nested s-expressions), and then applies the function to the results.\n\nWith this recursive evaluation and the statement replacements from the statement module,\nit is possible to write entire programs as nested s-expressions.\nThink of s-expressions as a simpler kind of abstract syntax trees.\n\n```Python\n\u003e\u003e\u003e S(Print, S(\"Hello,\".upper), S(\"World!\".lower)).s_eval({})\nHELLO, world!\n\n```\n\nNote the dictionary in the `s_eval` call.\ns-expressions have their own scope for delayed evaluation of `Symbol`s.\nFor a module-level s-expression, you might want to pass in the `globals()`,\nwhich will make them available as the equivalent symbol.\n```Python\n\u003e\u003e\u003e from drython.macro import setq\n\u003e\u003e\u003e spam = 7\n\u003e\u003e\u003e S(Print, S.spam).s_eval(globals())  # S.spam is the same as Symbol('spam')\n7\n\u003e\u003e\u003e S(setq, S.spam, 42).s_eval(globals())  # globals() is writable.\n\u003e\u003e\u003e spam\n42\n\n```\n\nYou can also use an s-expression as a kind of lambda. Calling one directly will call `s_eval` with\nthe kwargs dict.\n\n```Python\n\u003e\u003e\u003e S(Print, S.x, S.y, 3, sep=S.sep)(x=1, y=2, sep=':')\n1:2:3\n\n```\n\nIf the s-expression's function is a *macro*,\nthen it gets any s-evaluable arguments unevaluated,\nand returns (typically) an s-evaluable for evaluation.\nIn other words, macros can re-write code.\n\nSo you can define \"if\" like this:\n\n```Python\n@macro\ndef If(boolean, then, Else=None):\n    return S(s_eval,\n             S((Else, then).__getitem__,\n               S(bool,\n                 boolean)))\n```\n\nThe above macro rewrites the code into an s-expression that indexes a pair (2-tuple)\nusing the test part coerced into a boolean,\n(remember `True == 1` and `False == 0` in Python) and then `s_eval`s the selected s-expression.\n\nS-expression macros are a very powerful metaprogramming technique.\nEspecially powerful once you start using macros to write macros.\nIt's Lisp's \"secret sauce\".\nAnd they're great for creating DSLs.\n\nThe s-expression module has a companion `macros` module which\nincludes many useful basic macros to get you started.\n\n## The Expression Module ##\nThe expression module has a generator expression replacement, and an experimental `yield` replacement.\n\nUnlike statements, expressions already work in lambdas and eval,\nso why replace them too?\n\nBesides being easier to use with higher-order functions, the `stack`\nand `s-expression` modules work primarily with function calls, so these\nsubstitutes have uses in metaprogramming. In many cases you can use\nexpressions directly anyway, or convert a non-call expression to a\ncall with a lambda, but sometimes you need to manipulate the code of\nthe expression itself, in which case it must be made of calls to\nbegin with.\n\nDirect use acts like a constant in s-expressions (like a Lisp reader macro),\nsince it's evaluated before even macros can get to it.\n```Python\n\u003e\u003e\u003e from core import identity, entuple\n\u003e\u003e\u003e from drython.s_expression import S\n\u003e\u003e\u003e S(identity,[(x,y) for x in (1,2) for y in 'abc'])()\n[(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c')]\n\n```\nOn the other hand, the `lambda` version is adjustable with arguments at eval time.\n```Python\n\u003e\u003e\u003e S(lambda z:[(x,y) for x in (1,2) for y in z],'abc')()\n[(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c')]\n\n```\nThis is the function call version of the above using `expression.In`\n```Python\n\u003e\u003e\u003e from drython.expression import In\n\u003e\u003e\u003e from macro import L1\n\u003e\u003e\u003e S(list,\n...   S(In,(1,2),S(L1,S.x,\n...       S(In,'abc',S(L1,S.y,\n...           S(entuple,S(entuple,S.x,S.y)))))))()\n[(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c')]\n\n```\nWhy use the function call version when it's so much harder? Besides\nthe new `expression.whilst` feature, the main advantage here is that you can\nsimplify it with a macro.\n```Python\n\u003e\u003e\u003e from drython.s_expression import macro\n\u003e\u003e\u003e @macro\n... def genx(expr,*specs):\n...     if specs:\n...         return S(In,specs[1],S(L1,specs[0],S(genx,expr,*specs[2:])))\n...     else:\n...         return S(entuple,expr)\n\n```\nNow we've got generator s-expressions with arguments in familiar\nPython order.\n```Python\n\u003e\u003e\u003e S(list,\n...   S(genx, S(entuple, S.x, S.y), S.x, (1, 2), S.y, 'abc'))()\n[(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c')]\n\n```\nA more advanced macro could include Python's other features like `if`\nfilters and unpacking. But more importantly, since you can\nmetaprogram this, you can add new features in the macro that raw\nPython lacks, like whilst.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgilch%2Fdrython","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgilch%2Fdrython","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgilch%2Fdrython/lists"}