Work in progress.
All names, identifiers and interfaces outlined in this document may be seen as placeholders, the functionality they provide is the important part.
Python is a wonderful language, but its metaprogramming facilities are limited and can lead to unnecessary verbosity. This document proposes a set of backward compatible changes which would allow developers more flexibility.
This proposal comes from the position that it's more important to allow experienced users the most flexibility possible than to prevent inexperienced users from creating a mess. These changes may make it easier for inexperienced users ignoring best practices to write unclear code, but should have the opposite effect for more skilled developers.
These changes may not be seen as in-line with the direction the language is taking or the Zen of Python, and as such there is no expectation that they will be implemented in CPython. [I'm writing this for feedback and so I can dream that a genius will see this, agree with the ideas and write a Python fork that supports it. Maybe I'll do it one day, when/if I know how.] This is unfortunate, because it would be wonderful to have all of Python's strengths coupled with a more expressive syntax.
The current method of function declaration,
def identifier(arguments):
body
would be broken down into two separate expressions, a def
statement
and a function definition. The syntax for def
would become
def identifier value
and would function roughly analogously to
identifier = value
value.__name__ = "identifier"
This would allow it to be used to create "named" objects other than functions, though it is beyond the scope of this proposal if/when that would be considered good style. The main product of this change would be the function syntax.
(arguments):
body
This would no longer be restricted to being used in conjunction with a def statement. An addition, implicit returns would be supported and it would not be necessary to start the indented block on the next line.
The following would all be valid definitions, though the first would presumably remain the recommended style.
def add(a, b, c):
result = a + b + c
return(result)
def add(a, b, c): result = a + b + c
result
add = (a, b, c):
a + b + c
object.add = (self, other): self + other
This would obsolete the current lambda
expressions, though they
would have to remain supported (beyond a no-op) for backwards
compatibility reasons, as this does not support unparenthesized
argument lists.
Functions would be anonymous until "named", generally by a def
statement.
Block expressions would also be added to the language. In their simplest form/usage they function as no-argument functions, and that is their syntax.
: body
A trivial example of this usage:
value = : 5 + 5
print(value()) # prints "10"
Blocks would support some more advanced features that current functions do not. (Though it is desirable to also add these features to functions, this may be impractical for performance reasons. You be the judge.)
Starting in py3k, through metaclasses you were allowed to specify the
dictionary used in evaluating the class body (which was then used as
the argument dictionary). Through the namespace
argument we may do
the same for our block, changing the way values are stored or
pre-populating it.
a = 5
value = : 5 + a
print(value()) # prints # "10"
print(value(namespace={ "a": 0 })) # prints "5"
A line_callback
argument will also be supported. For each line of
the block (multi-line expressions are considered a single line for the
purpose of this explanation) it will be called with the value of the
expression on that line.
To demonstrate:
lines = list()
block = :
"This is an example"
a = 5
b = 10
a + b
block(line_callback=lines.append)
print(lines)
Would print:
[ "This is an example", None, None, 15 ]
(Remember that assignment statements have no value.)
To demonstrate what's possible, here's an example using an imaginary ui library whose classes support initialization from blocks.
import imaginary_us_library as ui
window = ui.Window(:
caption = "Hello World"
dimensions = (400, 300)
ui.Label(:
caption = "This is an example"
position = (50, 50)
)
ui.Button(:
caption = "Click me! Click me!"
ui.on_click(:
print("The button was clicked.")
)
)
)
I don't know that "macro" is the right name to be using here, but since names are hard I'll use it as a placeholder.
While the last example looks pretty good, those parentheses aren't doing us any favours, and used as above they look rather unpythonic.
Currently, whitespace-separated expressions are illegal. Attempting
foo bar
won't get you anything more than a SyntaxError
. These changes would
cause
expression_a expression_b
to behave as
expression_a.__macro__(expression_b)
provided that expression_a
contains no unenclosed parentheses, as
that would cause ambiguity with function definitions when used in
conjunction with blocks.
A standard decorator function macro()
would be added to transform a
single-argument function into a macro (__call__
->
__macro__
). These can be nicely used in conjunction with blocks.
@macro
def five_times(callable):
for n in range(5):
callable()
five_times:
print("Hello world")
Coming back to our UI example, it's new free of those wretched parens:
import imaginary_us_library as ui
window = ui.Window:
caption = "Hello World"
dimensions = (400, 300)
ui.Label:
caption = "This is an example"
position = (50, 50)
ui.Button:
caption = "Click me! Click me!"
ui.on_click:
print("The button was clicked.")
Python doesn't include a switch/case
statement, but if a developer
really wanted to, they'd be able to implement their own.
n = 3
switch n:
case 1:
print("n is one")
case 2:
print("n is two")
default:
print("n is neither one or two")
by injecting "case" into the namespace of a block in "switch", we would only need to add one identifier to the global namespace. Some of the implementation code might look like this.
@macro
def switch(value):
@macro
def _switch(block):
cases = dict()
def line_callback(value):
if isinstance(value, SwitchCase):
cases[SwitchCase.value] = SwithCase.block
elif isinstance(value, DefaultCase):
default = DefaultCase.block
block(line_callback=line_callback,
namespace={ "case": SwitchCase,
"default": DefaultCase })
try:
result_block = cases[value]
except KeyError:
result_block = default
return(result_block())
return(_switch)
Examples are hard. The following would be a horrible idea, but shows off the macros fairly well. I think this would work if macros were a low-enough prescedence operation.
c_for :x = 0, :x < 10, :x += 1:
print(x)
for_x_in_0_to_9 = c_for :x = 0, :x < 10, :x += 1
for_x_in_0_to_9:
print(x)
implementation would be something like
@macro
def c_for(start_test_tick):
start, test, tick = start_test_tick
@macro
def _c_for(block):
d = dict()
start(namespace=d)
while test(namespace=d):
block(namespace=d)
tick(namespace=d)
return(_c_for)
Here's something rather contrived: what if we decided to implement our own for loops for no particular reason?
for_ "x" in_ range(10):
print(x)
possible implementation:
in_ = object()
class for_(object):
@classmethod
def __macro__(cls, variable):
self = cls()
self.variable = variable
return(macro(self._in_getter))
def _in_getter(self, what_should_be_in):
if what_should_be_in is not in_:
raise(SyntaxError("Invalid for loop syntax."))
return(macro(self._iter_getter))
def _iter_getter(self, iterable):
self.iterable = iterable
return(macro(self._block_getter))
def _block_getter(self, block):
iterator = self.iterable.__iter__()
try:
while True:
value = iterator.next()
block(namespace={ self.variable: value })
except StopIteration:
pass
That's not even too horrendous to look at.
That's all I've got for now. Feedback (email, reddit, HN) is greatly appreciated.