public
Last active

draft summary of some changes I dream of seeing in Python

  • Download Gist
python_proposal.md
Markdown

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 def Statement and Functions Definitions

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.

Blocks

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.")
        )
    )
)

"Macros"

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.")

More Examples

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.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.