├── .gitignore ├── .vscode └── settings.json ├── README.md ├── expr.py ├── expr_test.py ├── http.py ├── http_server.py ├── http_test.py ├── is_prime.py └── tcp_server.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-your-self 2 | 3 | Python, with no statements! 4 | 5 | Inside `expr.py` you'll find some utility functions and classes that let you write programs without any statements (barring a single import at the top of the file...). 6 | 7 | I've written a (sort-of working) [TCP server](https://github.com/christianscott/express-your-self/blob/master/tcp_server.py) and an [HTTP server](https://github.com/christianscott/express-your-self/blob/master/http_server.py). 8 | 9 | Here's an example of the code you might write. This starts a mutli-threaded TCP server that simply echos whatever you send it. Try it out using `netcat`. (warning: it's a little broken) 10 | 11 | ```python 12 | from expr import * 13 | 14 | # `do` lets use sequence "statements" 15 | do([ 16 | # "walrus operator" gives us variables (python 3.8+) 17 | socket := require('socket'), 18 | threading := require('threading'), 19 | 20 | spawn := lambda target, args: do([ 21 | handler_thread := threading.Thread(target=target, args=args), 22 | setattr(handler_thread, 'daemon', True), 23 | handler_thread.start() 24 | ]), 25 | 26 | ends_with_newline := lambda bytes_: \ 27 | len(bytes_) > 0 and bytes_[len(bytes_) - 1] == ord('\n'), 28 | 29 | handle_client := lambda current_connection, client_addr: do([ 30 | print(f"client connected at {client_addr}"), 31 | # loop_while calls the provided lambda over and over, until the final expression is falsy 32 | loop_while(lambda: do([ 33 | recvd_bytes := current_connection.recv(1024), 34 | current_connection.send(recvd_bytes), 35 | 36 | not ends_with_newline(recvd_bytes), 37 | ])), 38 | ]), 39 | 40 | 41 | listen := lambda host, port: do([ 42 | connection := socket.socket(socket.AF_INET, socket.SOCK_STREAM), 43 | connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1), 44 | connection.bind((host, port)), 45 | 46 | # Listen for clients (max 10 clients in waiting) 47 | connection.listen(10), 48 | 49 | print(f"server listening on {host}:{port}"), 50 | 51 | loop(lambda: do([ 52 | client_connection := connection.accept(), 53 | spawn(target=handle_client, args=client_connection), 54 | ])) 55 | ]), 56 | 57 | 58 | listen("localhost", 3000) 59 | ]) 60 | 61 | ``` 62 | 63 | The code in `http.py` and `http_server.py` is much more interesting. Cool things not included in this snippet: 64 | 65 | - `t` is a way to get "data classes". You give it a name and a list of properties, as follows: `Pair := t('Pair', ['one', 'two'])` 66 | - `Box` is to get around the fact that we don't have mutable bindings. Instead of the binding being mutable, just stick it in a container! 67 | - `klass`, for when a data class isn't enough. Used to define `Box`: 68 | ```python 69 | Box = klass('Box', { 70 | '__init__': lambda self, value: setattr(self, 'value', value), 71 | 'get': lambda self: self.value, 72 | 'set': lambda self, setter: setattr(self, 'value', setter(self.value)), 73 | }) 74 | ``` 75 | 76 | ## improvements 77 | 78 | - [ ] Exceptions! There's no way to catch exceptions at the moment. It would be easy to write a helper function using `try/catch` statements, but that's cheating. 79 | -------------------------------------------------------------------------------- /expr.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import enum 3 | import sys 4 | import importlib 5 | 6 | 7 | throw = lambda message, constructor = Exception: (_ for _ in ()).throw(constructor(message)) 8 | 9 | if_then = lambda predicate, action: predicate and action() 10 | 11 | if_then( 12 | not sys.version_info[0] >= 3 and sys.version_info[1] >= 8, 13 | lambda: throw("must be using at least python 3.8. expr requires assignment expressions (e.g. x := 1), which were introduced in 3.8") 14 | ) 15 | 16 | func = lambda bound_variables, body: body(**bound_variables) 17 | 18 | let = lambda **kwargs: kwargs 19 | 20 | last = lambda iterable: func( 21 | let( 22 | array=[*iterable] 23 | ), 24 | lambda array: \ 25 | array[len(array) - 1], 26 | ) 27 | 28 | do = lambda statements: len(statements) > 0 and last(statements) 29 | 30 | klass = lambda name, attrs: type(name, (), attrs) 31 | 32 | Box = klass('Box', { 33 | '__init__': lambda self, value: setattr(self, 'value', value), 34 | 'get': lambda self: self.value, 35 | 'set': lambda self, setter: setattr(self, 'value', setter(self.value)), 36 | }) 37 | 38 | consume = lambda iterable: collections.deque(iterable, maxlen=0) 39 | 40 | loop = lambda fn: consume(fn() for _ in iter(int, 1)) 41 | 42 | Consumable = klass('Consumable', { 43 | "__init__": lambda self, fn: setattr(self, 'fn', fn), 44 | "__next__": lambda self: self.fn(), 45 | "__call__": lambda self: self.fn(), 46 | }) 47 | 48 | loop_while = lambda fn: do([ 49 | consumable := Consumable(fn), 50 | consume(iter(consumable, False)), 51 | ]) 52 | 53 | for_each = lambda iterable, callback: consume( 54 | callback(entry) for entry in iterable 55 | ) 56 | 57 | require = importlib.import_module 58 | 59 | t = collections.namedtuple 60 | 61 | export = lambda module_name, value: do([ 62 | module := sys.modules[module_name], 63 | setattr(module, value.__name__, value) 64 | ]) 65 | -------------------------------------------------------------------------------- /expr_test.py: -------------------------------------------------------------------------------- 1 | from expr import * 2 | 3 | # do 4 | assert do([]) == False, "do with empty array returns False" 5 | assert do([1]) == 1, "do with single value returns the value" 6 | assert do([1, 2, 3]) == 3, "do with multiple values returns the last value" 7 | assert do([ 8 | x := 1, 9 | y := 2, 10 | x + y 11 | ]) == 3, "do with assignment expressions OK" 12 | 13 | # loop_while 14 | loops = Box(0) 15 | loop_while(lambda: do([ 16 | loops.set(lambda n: n + 1), 17 | loops.get() < 5 18 | ])) 19 | assert loops.get() == 5, "loop_while calls the callback until the value is False" 20 | 21 | # for_each 22 | total = Box(0) 23 | for_each(range(5), lambda next: total.set(lambda total_: total_ + next)) 24 | assert total.get() == sum(range(5)), "for_each iterates over all values in a sequence" 25 | 26 | print("all tests passed") 27 | -------------------------------------------------------------------------------- /http.py: -------------------------------------------------------------------------------- 1 | from expr import * 2 | 3 | do([ 4 | re := require('re'), 5 | 6 | bisect := lambda iterable, predicate: do([ 7 | front := [], 8 | back := [], 9 | has_flipped := Box(False), 10 | 11 | for_each(iterable, lambda item: do([ 12 | if_then( 13 | not has_flipped.get(), 14 | lambda: has_flipped.set(lambda _: predicate(item)), 15 | ), 16 | (back if has_flipped.get() else front).append(item), 17 | ])), 18 | 19 | (front, back), 20 | ]), 21 | 22 | Header := t('Header', ['name', 'value']), 23 | 24 | parse_header := lambda header: do([ 25 | split_header := re.split(': ', header), 26 | Header(split_header[0], split_header[1]) 27 | ]), 28 | 29 | Request := t('Request', [ 30 | 'method', 31 | 'path', 32 | 'headers', 33 | 'body', 34 | ]), 35 | 36 | export(__name__, Request), 37 | 38 | parse := lambda message: do([ 39 | lines := re.split('\r?\n', message), 40 | 41 | split_message := bisect(lines, lambda line: line == ""), 42 | 43 | message_header := split_message[0], 44 | message_body := "\n".join(split_message[1]), 45 | 46 | request_line := message_header[0].split(' '), 47 | method := request_line[0], 48 | path := request_line[1], 49 | 50 | headers := [] if len(message_header) == 1 else \ 51 | [parse_header(header) for header in message_header[1:] if len(header)], 52 | 53 | Request(method, path, headers, message_body), 54 | ]), 55 | 56 | export(__name__, parse) 57 | ]) 58 | -------------------------------------------------------------------------------- /http_server.py: -------------------------------------------------------------------------------- 1 | from expr import * 2 | 3 | do([ 4 | socket := require('socket'), 5 | threading := require('threading'), 6 | http := require('http'), 7 | 8 | spawn := lambda target, args: do([ 9 | handler_thread := threading.Thread(target=target, args=args), 10 | setattr(handler_thread, 'daemon', True), 11 | handler_thread.start() 12 | ]), 13 | 14 | CHUNK_SIZE := 1024, 15 | 16 | handle_client := lambda current_connection, client_addr: do([ 17 | bytes_ := bytearray(), 18 | loop_while(lambda: do([ 19 | chunk := current_connection.recv(CHUNK_SIZE), 20 | bytes_.extend(chunk), 21 | len(chunk) == CHUNK_SIZE, 22 | ])), 23 | message := bytes_.decode('utf8'), 24 | print(http.parse(message)), 25 | current_connection.send(b"HTTP/1.1 200 OK\r\n"), 26 | ]), 27 | 28 | listen := lambda host, port: do([ 29 | connection := socket.socket(socket.AF_INET, socket.SOCK_STREAM), 30 | connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1), 31 | connection.bind((host, port)), 32 | 33 | # Listen for clients (max 10 clients in waiting) 34 | connection.listen(10), 35 | 36 | print(f"server listening on {host}:{port}"), 37 | 38 | loop(lambda: do([ 39 | client_connection := connection.accept(), 40 | spawn(target=handle_client, args=client_connection), 41 | ])) 42 | ]), 43 | 44 | listen("localhost", 3000), 45 | ]) 46 | -------------------------------------------------------------------------------- /http_test.py: -------------------------------------------------------------------------------- 1 | import http 2 | 3 | message = 'POST /foo HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: 4\r\n\r\n{"hello": "world"}\r\n' 4 | request = http.parse(message) 5 | 6 | assert request.method == "POST", "parses request method correctly" 7 | assert request.path == "/foo", "parses request path correctly" 8 | 9 | assert len(request.headers) == 2, "parses request headers correctly" 10 | assert request.headers[0].name == "Content-Type", "parses request headers correctly" 11 | assert ( 12 | request.headers[0].value == "application/json" 13 | ), "parses request headers correctly" 14 | assert request.headers[1].name == "Content-Length", "parses request headers correctly" 15 | assert request.headers[1].value == "4", "parses request headers correctly" 16 | 17 | # TODO(christianscott): is this correct? 18 | assert request.body == '\n{"hello": "world"}\n', "parses request body correctly" 19 | 20 | print("all tests passed") 21 | -------------------------------------------------------------------------------- /is_prime.py: -------------------------------------------------------------------------------- 1 | from expr import * 2 | 3 | do([ 4 | math := require('math'), 5 | 6 | is_prime := lambda n: do([ 7 | False if n <= 1 else \ 8 | True if n <= 3 else \ 9 | False if n % 2 == 0 else \ 10 | do([ 11 | possible_divisors := range(3, math.floor(n / 2) + 1), 12 | has_divisors := any(n % m == 0 for m in possible_divisors), 13 | not has_divisors 14 | ]) 15 | ]), 16 | 17 | consume( 18 | print(f"{i}: {is_prime(i)}") for i in range(30) 19 | ) 20 | ]) 21 | -------------------------------------------------------------------------------- /tcp_server.py: -------------------------------------------------------------------------------- 1 | from expr import * 2 | 3 | do([ 4 | socket := require('socket'), 5 | threading := require('threading'), 6 | 7 | spawn := lambda target, args: do([ 8 | handler_thread := threading.Thread(target=target, args=args), 9 | setattr(handler_thread, 'daemon', True), 10 | handler_thread.start() 11 | ]), 12 | 13 | ends_with_newline := lambda bytes_: \ 14 | len(bytes_) > 0 and bytes_[len(bytes_) - 1] == ord('\n'), 15 | 16 | handle_client := lambda current_connection, client_addr: do([ 17 | print(f"client connected at {client_addr}"), 18 | loop_while(lambda: do([ 19 | recvd_bytes := current_connection.recv(1024), 20 | current_connection.send(recvd_bytes), 21 | 22 | not ends_with_newline(recvd_bytes), 23 | ])), 24 | ]), 25 | 26 | 27 | listen := lambda host, port: do([ 28 | connection := socket.socket(socket.AF_INET, socket.SOCK_STREAM), 29 | connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1), 30 | connection.bind((host, port)), 31 | 32 | # Listen for clients (max 10 clients in waiting) 33 | connection.listen(10), 34 | 35 | print(f"server listening on {host}:{port}"), 36 | 37 | loop(lambda: do([ 38 | client_connection := connection.accept(), 39 | spawn(target=handle_client, args=client_connection), 40 | ])) 41 | ]), 42 | 43 | 44 | listen("localhost", 3000) 45 | ]) 46 | --------------------------------------------------------------------------------