so the other day for whatever reason i really wanted to invoke python functions as if they were c++ template functions… like so:

template <int x>
int add(int y) {
    return x + y;
}


printf("%d\n", add<5>(3));

if we ignore semantics, add<5>(3) seems to be a valid python expression – roughly translating to add.__lt__(5).__gt__(3)

i quickly whipped up a prototype:

class TemplateAdd:
    def __init__(self):
        self.x = None

    def __lt__(self, x):
        self.x = x
        return self

    def __gt__(self, y):
        return self.x + y


add = TemplateAdd()
print(add<5>(3))

but somehow it prints True… thanks, chained comparisons. the expression add<5>(3) evaluates to add.__lt__(5) and 5 > 3, and with no way to overload the and operator, the dream is dead unless i come to terms with the deranged syntax of (add<5)>3 (doesnt <5)>3 kind of look like a fish?)

i then settled for the sane option, which i probably shouldve gone with the first time around: add[5](3). easy enough, when the language is on your side:

class TemplateAdd:
    def __getitem__(self, x):
        return lambda y: x + y


add = TemplateAdd()
print(add[5](3))

but of course im not writing a new class every time i want a template function, luckily python decorators provide a syntax very similar to c++ templates. the desired syntax in a perfect world looks something like:

@template[x]
def add(y):
    return x + y

referencing the undefined name x is an original sin, especially the one passed to the decorator, where there is no space for shenanigans – alas, how many times have i wished to be able to customize globals().__getitem__

let us take a step back:

@template("x")
def add(y, *, x):
    return x + y

this syntax has the approval of my lsp, and it even supports optionally passing in the template parameter x directly – but i see that as a shortcoming, since thats not how c++ templates work

class TemplateFunction:
    def __init__(self, func, argname):
        self.func = func
        self.argname = argname

    def __call__(self, *a, **kw):
        return self.func(*a, **kw)

    def __getitem__(self, argvalue):
        return lambda *a, **kw: self.func(*a, **kw, **{self.argname: argvalue})


def template(argname):
    return lambda func: TemplateFunction(func, argname)


@template("x")
def add(y, *, x):
    return x + y


print(add[5](3))
print(add(3, x=5))

this works quite well, except when the template function is also a member function:

class C:
    @template("x")
    def add(self, y, *, x):
        return x + y


print(C().add[5](3))  # C.add() missing 1 required positional argument: 'y'

it took me many prompts for chatgpt to finally understand what i wanted (or did i give up and found the solution on stackoverflow? i forgor) – apparently theres something called a descriptor, something something, __get__ solves all of our problems:

class TemplateFunction:
    ...

    def __get__(self, instance, _):
        return TemplateFunction(
            lambda *a, **kw: self.func(instance, *a, **kw), self.argname
        )

this way when instance.template_func is called, instance is passed to the __get__ first where we can capture it before it is lost forever

written so far is my recollection of how i arrived at ParametrizedFunc, which is a more polished version of the code above. while writing this post ive also discovered a despicable way to reference undefined names:

import types


class TemplateFunction:
    def __init__(self, func, argname):
        self.func = func
        self.argname = argname

    def __getitem__(self, argvalue):
        return types.FunctionType(self.func.__code__, {self.argname: argvalue})


def template(argname):
    return lambda func: TemplateFunction(func, argname)


@template("x")
def add(y):
    return x + y


print(add[5](3))

somehow i feel sick…