Skip to content

Instantly share code, notes, and snippets.

@v--
Last active August 26, 2022 10:17
Show Gist options
  • Save v--/2795b6ded9daea156f44851114103676 to your computer and use it in GitHub Desktop.
Save v--/2795b6ded9daea156f44851114103676 to your computer and use it in GitHub Desktop.
Achieve full runtime nondeterminism by randomly selecting which function to run
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"id": "60b1a609-e6a2-46d2-97b4-20e94db5a43f",
"metadata": {},
"source": [
"# Party mode: randomly selecting which function to run\n",
"\n",
"Have you ever wondered what would happen if, whatever function you call, another function may get called instead? Without even disturbing the type system? Wonder no more.\n",
"\n",
"The party mode decorator does precisely this. It stores a list of functions groupped by signature and decides at runtime which of them to call. See the examples below. It gets bonkers towards the end."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1c4e9c13-662e-4ab2-8242-238e54f19565",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import random\n",
"import inspect\n",
"import functools\n",
"from typing import Callable\n",
"\n",
"\n",
"_party_dict: dict[inspect.Signature, list[Callable]] = {}\n",
"\n",
"\n",
"def party_mode(fun: Callable):\n",
" sig = inspect.signature(fun)\n",
"\n",
" if sig not in _party_dict:\n",
" _party_dict[sig] = []\n",
"\n",
" choices = _party_dict[sig]\n",
" choices.append(fun)\n",
"\n",
" @functools.wraps(fun)\n",
" def wrapper(*args, **kwargs):\n",
" return random.choice(choices)(*args, **kwargs)\n",
"\n",
" return wrapper"
]
},
{
"cell_type": "markdown",
"id": "0a138a6f-3e4d-4874-8c80-b6ee4b3986e5",
"metadata": {},
"source": [
"The simplest example is just not knowing which of the two strings we would get "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "64279aa1-9057-46ef-b036-a2ee448603da",
"metadata": {},
"outputs": [],
"source": [
"@party_mode\n",
"def a():\n",
" return 'a'\n",
"\n",
"\n",
"@party_mode\n",
"def b():\n",
" return 'b'\n",
"\n",
"\n",
"a() # It can return either 'a' or 'b' depending on the gods of nondeterminism"
]
},
{
"cell_type": "markdown",
"id": "03c07754-5f61-420d-958c-834e558f603b",
"metadata": {},
"source": [
"A slightly more exciting example is not knowing whether two vectors would get added or subtracted"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d356828b-c3b5-464a-841f-168a540aec3c",
"metadata": {},
"outputs": [],
"source": [
"from __future__ import annotations\n",
"from dataclasses import dataclass\n",
"\n",
"\n",
"@dataclass\n",
"class Vec2D:\n",
" x: float\n",
" y: float\n",
"\n",
" # We add return types in order for the types to be a part of the signature\n",
" @party_mode\n",
" def __add__(self, other: Vec2D) -> Vec2D:\n",
" return Vec2D(self.x + other.x, self.y + other.y)\n",
"\n",
" @party_mode\n",
" def __sub__(self, other: Vec2D) -> Vec2D:\n",
" return Vec2D(self.x - other.x, self.y - other.y)\n",
"\n",
"\n",
"a = Vec2D(1, 2)\n",
"b = Vec2D(3, 4)\n",
"a + b # Would b be added to a or subtracted from it?"
]
},
{
"cell_type": "markdown",
"id": "26d02b09-882a-48ae-a112-61486dea8d1a",
"metadata": {},
"source": [
"It's fun enough as it is but what if we apply the decorator to classes? Well, classes are callable in Python and they have the signature of their initializer minus the 'self' parameter. So party mode would magically work, albeit in a rather weird way."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9af639e6-86cf-400c-a87a-05ee7eabde3d",
"metadata": {},
"outputs": [],
"source": [
"@party_mode\n",
"@dataclass\n",
"class Worker:\n",
" name: str\n",
" age: int\n",
"\n",
"\n",
"@party_mode\n",
"@dataclass\n",
"class Planet:\n",
" name: str\n",
" age: int\n",
"\n",
"\n",
"Worker('John', 43) # It may be a Worker instance but it may also be a Planet instance"
]
},
{
"cell_type": "markdown",
"id": "1dfc6a22-7091-43cc-8d8f-e75b4199c60b",
"metadata": {},
"source": [
"That's not what I would expect from party mode on a class, however. It breaks some safety guarantees - Worker and Planet may have completely different fields defined and we would not know in advance whether a method is defined or not. We do not want that type of unsafety in our programs. We can even add a condition on top of the party_mode decorator to return classes as they are.\n",
"\n",
"Instead, we can write the following decorator, which would create new classes by simply replace all methods in a class recursively (hence for nested classes also)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "057c8154-c805-464b-84a8-01cb2bea2cfe",
"metadata": {},
"outputs": [],
"source": [
"from typing import TypeVar\n",
"\n",
"\n",
"# These methods (and some more) should not be randomized in order to preserve correctness\n",
"magic_method_blacklist = ['__init__', '__weakref__', '__hash__', '__eq__', '__repr__', '__dataclass_fields__', '__dataclass_params__']\n",
"\n",
"\n",
"T = TypeVar('T')\n",
"\n",
"\n",
"def recursive_party_mode(cls: type[T]) -> type[T]:\n",
" fields: dict[str, Callable] = {}\n",
"\n",
" for name, field in vars(cls).items():\n",
" if isinstance(field, Callable) and name not in magic_method_blacklist:\n",
" if isinstance(field, type):\n",
" fields[name] = recursive_party_mode(field)\n",
" else:\n",
" fields[name] = party_mode(field)\n",
" else:\n",
" fields[name] = field\n",
"\n",
" return type(\n",
" cls.__name__,\n",
" (cls, ), # Make this a subclass of cls itself\n",
" fields\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "b374b3e6-90ab-4251-a1b5-e9f2f80c9f4b",
"metadata": {},
"source": [
"Let's see how to use it:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "66c006dc-7a33-4f7d-9a51-33a5a179f3d1",
"metadata": {},
"outputs": [],
"source": [
"@recursive_party_mode\n",
"@dataclass\n",
"class Vec2D:\n",
" x: float\n",
" y: float\n",
"\n",
" @dataclass\n",
" class Bound:\n",
" start: Vec2D\n",
" end: Vec2D\n",
"\n",
" def reverse(self) -> Vec2D.Bound:\n",
" return Vec2D.Bound(self.end, self.start)\n",
"\n",
" def reflect_around_origin(self) -> Vec2D.Bound:\n",
" return Vec2D.Bound(-self.start, -self.end)\n",
"\n",
" # We add return types in order for the types to be a part of the signature\n",
" def __add__(self, other: Vec2D) -> Vec2D:\n",
" return Vec2D(self.x + other.x, self.y + other.y)\n",
"\n",
" def __sub__(self, other: Vec2D) -> Vec2D:\n",
" return Vec2D(self.x - other.x, self.y - other.y)\n",
"\n",
" def __neg__(self) -> Vec2D:\n",
" return Vec2D(-self.x, -self.y)\n",
"\n",
"\n",
"a = Vec2D(1, 2)\n",
"b = Vec2D(3, 4)\n",
"a + b # What would it do? Nobody knows in advance.\n",
"Vec2D.Bound(a, b).reverse() # Would this reverse the bound vector or would it reflect it around the origin?"
]
},
{
"cell_type": "markdown",
"id": "8f06efd6-fb95-47fa-9560-331a34099c82",
"metadata": {},
"source": [
"Up until now, all we had were toy examples. We haven't considered what would happen if we swapped `random.seed` with `exit`. These functions can both be called without arguments and do not have return values. They still have different signatures but the idea is clear.\n",
"\n",
"How about monkey-patching every single class and method from all currently loaded modules? That way even the party_mode decorator call fall victim of itself. Who knows."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "31d31c48-29bb-4151-9852-f566c05388e3",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"\n",
"def enable_full_party_mode():\n",
" for module in sys.modules.values():\n",
" for name, obj in vars(module).items():\n",
" if isinstance(obj, type):\n",
" setattr(module, name, recursive_party_mode(obj))\n",
" elif isinstance(obj, Callable):\n",
" setattr(module, name, party_mode(obj))"
]
},
{
"cell_type": "markdown",
"id": "0727d694-779e-4752-9ffb-8ac1796a516d",
"metadata": {},
"source": [
"Would you run it?"
]
}
],
"metadata": {
"jupytext": {
"formats": "ipynb,py:percent"
},
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment