Package: pylambdex
lambdexlambdexallows you to write multi-line anonymous function expression (called alambdex) in an idiomatic manner. Below is a quick example of a recursive Fibonacci function:def_(lambdan:[if_[n<=0][raise_[ValueError(f'{n}should be positive')]],if_[n<=2][return_[1]],return_[callee_(n-1)+callee_(n-2)]])(10)# 55Compared with ordinary lambda, which only allows single expression as body, lambdex may contain multiple "statements" in analogue to imperative control flows, whilst does not violate the basic syntax of Python.Table of ContentMore about lambdexWHAT'S NEWInstallation & UsageLanguage FeaturesParametersVariable assignmentAugmented assignmentConditional statementLoopingWith statementTry statementYield statementAsync and AwaitMiscellaneousNested lambdexesRecursionRenaming functionsDetailed Compile-time and Runtime ErrorEDGE CASESRunning in an REPLDeclaration DisambiguityRuntime EfficiencyBytecode CachingBytecode Optimization at Function LevelBytecode Optimization at Module LevelCustomizationKeyword and Operator AliasingLanguage ExtensionCode FormattingStandalone lambdex formatterLambdex formatter as post-processorMocking existing formatter executableKnown Issues & FutureQ & ALicenseMore about lambdexAn anonymous function is a function definition that is not bound to an identifier, which is ubiquitous in most languages with first-class functions. The language feature could be handy for logics that appear to be short-term use, and therefore adopted widely in some functional programming paradigms.Python provideslambda <arg>: <expr>for such purpose. Lambdas are good for simple functionalities, but appear limited if logical complexity goes up. Consequently, higher-order functions (e.g., decorators) are often implemented as nested named functions, which is not concise enough.lambdexas an experimental complement to lambdas, aims to provide a syntax similar to Python for anonymous functions. The syntax itself is built upon valid Python expressions, and therefore requires no modification to the interpreter. This package transpiles lambdexes into Python bytecodes at runtime, and therefore ensures the efficiency.Installation & UsageYou can installlambdexfrom PyPI bypipinstallpylambdexor from Github bypipinstallgit+https://github.com/hsfzxjy/lambdexTo use lambdex, a simple import is required:fromlambdeximportdef_my_sum=def_(lambdaa,b:[return_[a+b]])That's it! You don't even need to import other keywords such asreturn_.Language FeaturesWe are going to explore a wide range of features supported bylambdexin the following sections.ParametersThe parameter declaration of lambdexes appears after thelambda. The syntax supports most variants of declaration just as ordinary functions.show code# ordinary parametersdef_(lambdaa,b:[...])# parameters with default valuesdef_(lambdaa,b=1:[...])# starred argumentsdef_(lambda*args,**kwargs:[...])# keyword-only argumentsdef_(lambda*,a,b:[...])# positional-only arguments (Python 3.8+)def_(lambdaa,b,/:[...])Variable assignmentLambdexes use<instead of=for assignments, since=in Python is valid only in statements.show codedef_(lambda:[foo<"bar",])show equivalent functiondefanonymous():foo="bar"<is chainable like ordinary=.show codedef_(lambda:[foo<baz<"bar",])show equivalent functiondefanonymous():foo=baz="bar"Note that<has a higher precedence thannot,and,orandif...else.... R-value with these operators should be enclosed by parentheses:show codedef_(lambda:[foo<(aorbandnotc),foo<(aifcondelseb),])Tuple destruction is also supported:show codedef_(lambda:[(a,b)<(b,a),(a,*rest,c)<[1,2,3],])In Python 3.8 or above, the walrus operator:=might also be used. But be careful that Python enforces parentheses around:=in many cases.show codedef_(lambda:[foo:="bar",# OKfoo:=baz:="bar",# syntax errorfoo:=(baz:="bar"),# OKif_[condition][foo:="bar",# syntax error(foo:="bar"),# OK]])Augmented assignmentThe augmented assignments are written as[op]_<, for example,+_<for+=. The snippet below illustrates all supported augmented assignments:show codedef_(lambda:[a+_<1,a-_<1,a*_<1,a/_<1,a//_<1,a@_<1,a%_<1,a<<_<1,a>>_<1,a**_<1,a&_<1,a|_<1,a^_<1,])show equivalent functiondefanonymous():a+=1a-=1a*=1a/=1a//=1a@=1a%=1a<<=1a>>=1a**=1a&=1a|=1a^=1Conditional statementLambdexes useif_,elif_andelse_for conditional control flows.show codedef_(lambda:[if_[condition_1][...,].elif_[condition_2][...,].else_[...,]])show equivalent functiondefanonymous():ifcondition_1:...elifcondition_2:...else:...LoopingLambdexes support the two kinds of looping by keywordsfor_andwhile_.show codedef_(lambda:[# for...in...else...for_[iinrange(10)][print_(i),].else_[print("the optional else clause"),],# while...else...while_[condition][...,].else_[print("the optional else clause"),]])show equivalent functiondefanonymous():# for...in...else...foriinrange(10):print(i)else:print("the optional else clause")# while...else...whilecondition:print("the optional else clause")break_andcontinue_are also supported.show codedef_(lambda:[for_[iinrange(10)][if_[i>=5][break_].else_[continue_]]])show equivalent functiondefanonymous():foriinrange(10):ifi>=5:breakelse:continueWith statementWith statements are supported by thewith_keyword. The optionalasis written using>.show codedef_(lambda:[# simple `with`with_[open("foo")][...]# `with` with `as`with_[open("foo")>fd][...]# multiple `with`with_[open("foo"),open("bar")>fd][...]])show equivalent functiondefanonymous():# simple `with`withopen("foo"):...# `with` with `as`withopen("foo")asfd:...# multiple `with`withopen("foo"),open("bar")asfd:...Try statementThe ordinary try statements are supported by keywordstry_,except_,else_andfinally_.show codedef_(lambda:[try_[...].except_[RuntimeError][...].except_[...].else_[...].finally_[...]])show equivalent functiondefanonymous():try:...exceptRuntimeError:...except:...else:...finally:...The optionalasinexceptclause is written as>.show codedef_(lambda:[try_[...].except_[RuntimeError>e][...]])show equivalent functiondefanonymous():try:...exceptRuntimeErrorase:...Yield statementTheyieldandyield...from...structures are supported by keywordsyield_andyield_from_. A lambdex contains one or moreyield_oryield_from_will automatically become a generator.show codedef_(lambda:[yield_[1,2],yield_from_[range(3,10)],])show equivalent functiondefanonymous():yield(1,2)yield fromrange(3,10)yield_itself is an expression, and thus can appear in anywhere an expression is allowed. Note that parentheses might be added.show codedef_(lambda:[a<(yield_),if_[a<(yield_)][...],with_[(yield_)>cm][...],for_[iin(yield_)][...]])show equivalent functiondefanonymous():a=(yield)ifa<(yield):...with(yield)ascm:...foriin(yield):...Async and Awaitlambdexsupports coroutines by keywordsasync_def_,async_for_,async_with_andawait_.show codefromlambdeximportasync_def_async_def_(lambda:[async_for_[ainb][...],async_with_[a>b][...],await_[a],])show equivalent functionasyncdefanonymous():asyncforainb:...asyncwithaasb:...awaitaMiscellaneousLambdexes support some other keywords in Python too.Thereturn_is analogue to keywordreturn.show codedef_(lambda:[return_[a,b]])show equivalent functiondefanonymous():returna,bThepass_is analogue to keywordpass.show codedef_(lambda:[pass_])show equivalent functiondefanonymous():passTheraise_is analogue to keywordraise.show codedef_(lambda:[try_[raise_[RuntimeError]].except_[ValueError>e][# the optional from clauseraise_[RuntimeError].from_[e]].except_[# the bare raiseraise_]])show equivalent functiondefanonymous():try:raiseRuntimeErrorexceptValueErrorase:raiseRuntimeErrorfromeexcept:raiseThedel_is analogue to keyworddel.show codedef_(lambda:[a<[1,2],del_[a[0],a],])show equivalent functiondefanonymous():a=[1,2]dela[0],aTheglobal_andnonlocal_are analogue to keywordsglobalandnonlocal.show codedef_(lambda:[global_[a],return_[def_(lambda:[nonlocal_[a],])]])show equivalent functiondefanonymous():globaladef_inner():nonlocalareturn_innerNested lambdexesLambdexes can be nested to construct more complicated logics. Lambdexes respect the nested scoping rules in Python, i.e., inner lambdex captures names defined in its parent scopes. For example, we can defineIIFElike in JavaScript to capture looping variables.show code# without IIFEarr=[]foriinrange(10):arr.append(def_(lambda:[print(i)]))forfuncinarr:func()# print "9" x10 times# with IIFEarr=[]foriinrange(10):func=def_(lambdai:[return_[def_(lambda:[print(i)])]])(i)arr.append(func)forfuncinarr:func()# print from "0" to "9"RecursionOne call always access the current lambdex itself viacallee_within a lambdex. The feature is quite handy since you don't need to assign a lambdex to a name for doing recursion.# Summing from 1 to 10(def_(lambdan:[if_[n==1][return_[n]],return_[callee_(n-1)+n]]))(10)Note thatcallee_within an inner lambdex repesents itself instead of the outer one:f=def_(lambda:[inner<def_(lambda:[return_[callee_]]),return_[inner,inner(),callee_]])f1,f2,f3=f()f1isf2# Truef3isf# TrueRenaming functionsA lambdex may have an optional name which uses the syntaxdef_.<name>(...). For example:def_.one_divided_by_zero(lambda:[1/0,])The name is used for improving readability of a traceback when an error occurs. For example, the function above yields an exception:Traceback (most recent call last):
  File "test.py", line 6, in <module>
    f()
  File "test.py", line 3, in one_divided_by_zero
    1 / 0,
ZeroDivisionError: division by zeroThe last frame of the traceback displays a nameone_divided_by_zeroinstead of someanonymous_xxxby default.But be careful thatthis feature does not imply any name bindings,that is, you can not use the name as a variable to reference a function:def_.one_divided_by_zero(lambda:[1/0,one_divided_by_zero,# NameError])one_divided_by_zero# NameErrordef_(lambda:[def_.inner_func(lambda:[inner_func# NameError]),inner_func,# NameError])Detailed Compile-time and Runtime Errorlambdexpreserves information of source code such as line number or token offsets. The information are used to provide detailed messages when error occurs.For example, the following code mis-types else_ as els_:fromlambdeximportdef_def_(lambda:[if_[cond][...].els_[...]])which will yield a SyntaxError at compile-time:Traceback (most recent call last):
  File "demo.py", line 3, in <module>
    def_(lambda: [
  --- Traceback omitted ---
  File "demo.py", line 6
    ].els_[
         ^
SyntaxError: expect 'else_' or 'elif_'Errors at runtime can also be located to corresponding lines. For example:fromlambdeximportdef_def_(lambda:[def_(lambda:[a<1/0,return_[a]])()])()will yield:Traceback (most recent call last):
  File "demo.py", line 3, in <module>
    def_(lambda: [
  File "demo.py", line 4, in anonymous_d598829c
    def_(lambda: [
  File "demo.py", line 5, in anonymous_dc2006c1
    a < 1 / 0,
ZeroDivisionError: division by zeroEDGE CASESWe are going to discuss several edge cases in this section.Running in an REPLIf you are using an interactive environment (REPL), like IDLE or IPython, you should import the keywords fromlambdex.repl:>>>fromlambdex.replimportdef_>>>my_sum=def_(lambdaa,b:[...return_[a+b]...])...>>>my_sum(1,2)3The statement should be executedat the beginningto ensure that corresponding patching stuff is enabled.Currentlylambdexhas been well tested on 3 REPL environments: the built-in Python REPL, IDLE and IPython (Jupyter). Other REPL may or may not be supported.Declaration DisambiguitySuppose you are running the following code:f1,f2=def_(lambdaa,b:[return_[a+b]]),def_(lambdaa,b:[return_[a*b]])The code yields an exceptionSyntaxError: ambiguious declaration 'def_'.What's going on here? The problem is thatthere are more than one lambdexes defined on the same line. Since CPython provides no effective way but a line number for locating a given lambda, the lambdex compiler fails to obtain the source code of the lambda in this case. A workaround is to prepend an identifier afterdef_of lambdex:f1,f2=def_.f1(lambdaa,b:[return_[a+b]]),def_.f2(lambdaa,b:[return_[a*b]])With this, the compiler can now tell them from each other.In the example above, it's not necessary to add identifier for both lambdexes. The following is also acceptable, as long as their declarations are different:f1,f2=def_.f1(lambdaa,b:[return_[a+b]]),def_(lambdaa,b:[return_[a*b]])Runtime EfficiencyThe transpilation procedure could be very time-consuming, and thus degrades the runtime efficiency. To solve the problem,lambdexitself provides several mechanisms on different levels for optimizing the bytecodes.Bytecode CachingBy default,a lambdex defined at a specific location will be compiled only once. The code object of compiled lambdex will be cached and reused in the future execution. Such mechanism applies to lambdexes defined either in a looping or as an inner function, i.e., the two lambdexes below would be compiled only once:s=0foriinrange(10000):def_(lambda:[# compiled at i = 0global_[s],s<s+i,])()deffoo(i):returndef_(lambda:[# compiled at the first time `foo()` executedglobal_[s],s<s+i,return_[s],])Bytecode Optimization at Function LevelBytecode caching reduces most of the redundant and heavy jobs, but still has some overhead -- the core oflambdexneeds to update some metadata (such as closure cellvars) every timedef_was executed. For example, one may find that the snippet below costs too much time to run (like >3s):fromlambdeximportdef_s=0defsum():n=1000000foriinrange(n):adder=def_(lambda:[global_[s],s<s+i])adder()asserts==n*(n-1)/2sum()To optimize, one can use the@asmoptdecorator:fromlambdeximportdef_,asmopts=0@asmoptdefsum():n=1000000foriinrange(n):adder=def_(lambda:[global_[s],s<s+i])adder()asserts==n*(n-1)/2sum()The running time will now reduce to ~0.3s, which is x10 faster and the same as using ordinary functions. The magical@asmopteliminatesdef_calling and directly stores compiled lambdex onsum. It is worth to note that@asmoptshould always be the innermost decorator.Bytecode Optimization at Module LevelThe previous mechanism only applies to lambdexes within some functions, and still has some overhead at module initialization phase. Can we do better? Absolutely yes! One can use the# lambdex: modoptdirective to optimize the whole module, and persist the optimized bytecode into corresponding .pyc files.# modopt_demo.py# the directive could be placed everywhere# lambdex: modoptfromlambdeximportdef_s=0n=1000000foriinrange(n):adder=def_(lambda:[global_[s],s<s+i])adder()asserts==n*(n-1)/2sum()$timepython3-mmodopt_demo#  > 3s: 1st time, unoptimized$timepython3-mmodopt_demo# ~0.3s: 2nd time and later, optimized$timepython3-mmodopt_demo# ~0.3sOptimized bytecodes will be invalidated when the source file is edited, but be available in the following executions. Thus you can see that the script costs rather long time at first, but becomes efficient afterwards.It's worth to note that such mechanism is unavailable when you run the file as a script viapython3 modopt_demo.py, which is a limitation of CPython. In other cases, such as usingpython3 -m modopt_demoor importing as a module in other files, the mechanism works well.CustomizationUsers are able to customize some aspects oflambdex, in order to fit their preference.Keyword and Operator AliasingIf you don't like the default keywords or operators,lambdexallows you to use alternative ones. See thedocfor detailed configuration.Language Extensionlambdexallows you to customize some of the syntax. For how to enable specific extension, please forward to thedoc.Currently the following ones are supported:await_attributeWith this enabled, you can use Rust-style await expressions.show codeasync_def_(lambda:[a.await_.b.await_.c,])show equivalent functionasyncdefanonymous():(await(awaita).b).cimplicit_returnWith this enabled, the last statement of a function body will be regarded as the return value.show codedef_(lambda:[1+1])show equivalent functiondefanonymous():return1+1But be careful that this doesn't apply to assignments at the last:show codedef_(lambda:[a<1])show equivalent functiondefanonymous():a=1Code FormattingThe proposed lambdex syntax violates the convention of most code formatters. In order to keep the code tidy, this library provides a light-weight formatterlxfmtfor lambdex syntax, which can either work standalonely or cooperate with existing formatters.Here's an example of whatlxfmtdoes:show codefromlambdeximportdef_deff():returndef_.myfunc(# comment1lambdaa,b:[# comment2if_[condition][f2<def_(lambda:[a+b]),return_[f2],],try_[# comment3body,].except_[Exception>e][except_handler]# comment4,],# comment5)show codefromlambdeximportdef_deff():returndef_.myfunc(lambdaa,b:[# comment1# comment2if_[condition][f2<def_(lambda:[a+b]),return_[f2],],try_[# comment3body,].except_[Exception>e][except_handler],# comment4# comment5])Standalone lambdex formatterThe usage of standalonelxfmtis shown below:usage: lxfmt [-h] [-d | -i | -q] [-p] [files [files ...]]

Default formatter for lambdex

positional arguments:
  files           reads from stdin when no files are specified.

optional arguments:
  -h, --help      show this help message and exit
  -d, --diff      print the diff for the fixed source
  -i, --in-place  make changes to files in place
  -q, --quiet     output nothing and set return value
  -p, --parallel  run in parallel when formatting multiple files.For example, uselxfmt -i file.pyto format in-place, orlxfmt -d file.pyto show the difference before and after formatting.Lambdex formatter as post-processorlxfmtcan work as a post-processor of existing formatter, such asyapf. One can specify a formatter backend by prepending-- -b BACKENDto the command. The overall usage is shown below:usage: lxfmt [ARGS OF BACKEND] -- [-h] [-b BACKEND] [-e EXECUTABLE]

Lambdex formatter as a post-processor for specific backend

optional arguments:
  -h, --help            show this help message and exit
  -b BACKEND, --backend BACKEND
                        name of formatter backend (default: dummy)
  -e EXECUTABLE, --executable EXECUTABLE
                        executable of backendNote that[ARGS OF BACKEND]are the arguments fed to the specified backend.For example, to uselxfmtafteryapf, with yapf style configuration at~/.config/yapf/style, one may use:lxfmtfile.py--style~/.config/yapf/style---byapfCurrentlylxfmtsupports only yapf. Adapters for other formatters will be added in the future.Mocking existing formatter executableThe-- -b BACKENDappears to be verbose, and sometimes you may want to alias the command of "formatter backend + post-processor" to save the typing work. The library provides another toollxfmt-mockto do the job.usage: lxfmt-mock [-h] [-r] BACKEND

Mock or reset specified formater backend

positional arguments:
  BACKEND      The backend to be mocked/reset

optional arguments:
  -h, --help   show this help message and exit
  -r, --reset  If specified, the selected command will be resetFor example, runninglxfmt-mock yapf, the tool will search for and list out available yapf executables to be mocked:$lxfmt-mockyapf[?]Whichonedoyouwanttomock?:❯/home/me/.local/bin/yapf/usr/data/anaconda3/bin/yapfBy choosing a executable, e.g./home/me/.local/bin/yapf,/home/me/.local/bin/yapf <args>will become a shorthand forlxfmt <args> -- -b yapf -e /home/me/.local/bin/yapf. The original executable will be stored at/home/me/.local/bin/original_yapf.To reset a mocked executable, simply runlxfmt-mock yapf -rand choose from the list.Mocking a formatter backend could be very useful when you want to enable lambdex code formatting in your IDE/editor. By mocking the executable your IDE/editor uses, you can enjoy the feature on the fly without modifying any settings.Known Issues & FutureCurrently lambdex doesn't support:type annotationimportstatementsType annotation [1] andimportstatements [2] will not be supported.Lambdexes also violate linters, which is inevitable.Besides, the upcoming versions will:add style options forlxfmt, in order to provide a better developing experience.Q & AWhy using brackets "[]" to enclose statement heads and bodies instead of parentheses "()"?Brackets are easier to type than parentheses on most of the keyboards.Why using "<" and ">" for assignment and as?The design is from three considerations.1)Comparators such as "<", "<=", ">" or ">="have lower precedencethan most of the other operators, thus allowing R-values without parentheses for most of the time;2)in AST representation, chained comparators have a flat structure, which is easier to parse;3)"<" and ">" visually illustrate the direction of data flows.The preference of "<" ">" over "<=" "=>" is that the previous ones consume only one character and are easier to type.Why use configuration file based keyword and operator aliasing instead of a programmatic approach?The design is from two concerns.A programmatic approaches may cause inconsistency at runtime, which is difficult for troubleshooting. For example, if one declares the aliasing inmod/__init__.pyand uses the new keywords inmod/A.py, the aliasing works fine ifmod/A.pyimported asmod.A, but fails if run as a standalone script.The compiler and formatter should behave consistently when processing the same file. If a programmatic approach used, the formatter must apply semantic analysis to figure out the aliasing rules, which is far more complicated.Lambdex appears to be less readable than functions and will mess up my code. Why should I use it?The project is not to criticize the present design of Python, but an experimental attempt to provide alternative for the ones who need a better anonymous function expression. The need may be from a second language they are familiar with, or the paradigms they want to use.lambdexdecides not to perform any modification on the interpreter, but build the new syntax upon existing Python syntax. The choice determines that keywords should be aliased and artifacts like "[]" would appear everywhere, which reduces the readability. To mitigate this,lambdexis paying effort to make the lambdex syntax resemble the Python syntax.It is true that there's a trade-off between readability and functionality. The decision should depend on your own requirements.What's the magic behind lambdex?def_ordef_.<ident>are actually callables, which take a lambda object as input, transpile it into an ordinary function and then return. The definition is inlambdex.keywordsmodule.The transpilation process can be roughly separated into three stages.In the first stage, we try to find the source code of given lambda object and parse it into AST. Source code searching is performed bylambdex.utils.ast::ast_from_source, which is modified frominspect.getsourcelinesto work more robust on lambdas. The obtained source is parsed into AST, which then pattern-matched to locate the Lambda node. The entry of this stage islambdex.ast_parser::lambda_to_ast.In the second stage, we traverse the AST of lambda object, replacing some node patterns with correponding Python statements, recursively building up the body of new function. Inner lambdexes are detached from where they lay, and become nested functions within the constructed body. This part is atlambdex.compiler.dispatcherandlambdex.compiler.rules.In the last stage, we compile the new AST into code object, restore metadata (globals, closures, etc.) from the original lambda object, and wrap it by a Function object. Modification might be applied to AST to correct the compilation result, e.g., wrap AST in a dummy function to make specific names nonlocal instead of global. This is done inlambdex.compiler.core::compile_lambdex. Bytecode caching also happens in this stage.For better understanding, you may look into the source code and check the detailed implementation.LicenseCopyright (c) 2021 Jingyi Xie (hsfzxjy). Licensed under theGNU General Public License version 3.