This article is about declarative programming in python and intended for consumption by humans. If you're wondering what declarative means, then go back and re-read the first sentence until it sinks in. In a world where we can do everything declaratively, the rest of the article would've wrote itself, after finishing the first sentence.

As you might have guessed by now, or known already, declarative programming has something to do with declaring intentions and getting back the expected results. In contrast, imperative programming is about stating how a task should be done, and the results, whether the expected or not, are just a side-effect.

With declarative programming or languages, you don't care about how something is done. You declare what the result should be, and the details of how that happens, would, or rather should, stay for the most part out of your sight.

Let's explore with an example. If you've ever used Django even for a little bit, you must've came across its database models models module. In Django, models are declared in the following manner:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

This is an example of declarative programming that you might have done without noticing. You have a Person, who in turn has two character fields. A first_name and a last_name. Now, for a good many reasons you wanted to make sure that each of these fields would not surpass a certain max_length of characters. Instead of manually doing the validation, you simply tell Django what max_length should be. In this case 30.

Wikipedia describes declarative programming as "a style of building structure and elements of computer programs, that expresses the logic of a computation without describing its control flow."

Query languages are as a matter of course declarative. They abstract away the details of how an action is taken, and focus solely on what it should entail. That Django models are declarative should come as no surprise.

But where do we draw the lines between imperative and declarative? When is a program/language declarative and when is it not? don't all computer languages require an implementation, that exactly tells the computer how to do things? doesn't that make everything imperative? The short answers to those questions are: hard, hard, yes, no. In no specific order.

Let's put this theoretical-walk on the side for a bit and consider the practicalities instead. We'll do that by walking through the implementation of what inspired this article in the first place. Bluew is a BLE library for python that I work on. It offers the user amongst others what can be considered a declarative API. One which can be used to declare other APIs.

Bluetooth Low Energy, or BLE devices share some properties with the common database, and in fact even carry one with them. Each BLE device has a set of attributes that can be discovered, read and written by other devices. The ATT, for attribute, protocol governs the rules of these interactions. And GATT, for Generic Attribute Profile, - a framework on top of ATT - makes services, and characteristics out of these attributes. This makes it easier to reason about what each attribute means.

By using Bluew, a hardware developer can quickly and on the go create an API for their device, without bothering with any of the implementation details. Here's a real example:

from bluew.rapid import RapidAPI, Read, Write
from typing import Tuple


class MyoArmband(RapidAPI):
    battery_level = Read(BATTERY_SERVICE_UUID)
    vibrate = Write(CONTROL_SERVICE, accept_vals=VALID_VIB_VALUES)
    set_mode = Write(CONTROL_SERVICE, accept_vals=VALID_MODES)
    set_colors = Write(COLOR_SERVICE, accept_types=Tuple[int, int, int])
    emg_subscribe = Notify(CONTROL_SERVICE, EMG_CHRCS)

That was all it took to create a simplified API for the Myo Armband, which doesn't have official support for Linux.

>>> from myo import MyoArmband, STRONG_VIB
>>>
>>> myo = MyoArmband('xx:xx:xx:xx:xx')
>>> myo.battery_level
57
>>> myo.vibrate(STRONG_VIB)
>>>

Looks cool, but how does this thing actually work?

This uses a not very well known python feature. Descriptors. The python documentation says that, "In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an object, it is said to be a descriptor."

Let's break down that paragraph into smaller pieces.

Object attribute. An attribute in an object which can be accessed and modified with object.attribute and object.attribute = x. That means that the functionality of the descriptor can only be used, if the descriptor is an attribute of another object or class.

Binding behavior. This means that our descriptor object has overridden one of the magic methods that define how an object is accessed or modified. By using a custom __get__(), we can override what the expression object.attribute returns; By overriding __set__(), we control how an object is assigned to, object.attribute = x; and by overriding __delete__(), we can change the behavior of del object.attribute.

Here's the classical OOP example of a Point object:

class Point:
    """A 3D Point object"""
    
    def __init__(self, x: int, y: int, z: int) -> None:
        self.x: int = x
        self.y: int = y
        self.z: int = z

and here's how we can make that Point a descriptor:

class Point:
    """A 3D Point object, that is also a descriptor"""
    
    def __init__(self, x: int, y: int, z: int) -> None:
        self.x: int = x
        self.y: int = y
        self.z: int = z
    
    def __get__(self, obj, type=None) -> Any:
        pass

Out Point descriptor doesn't do much, but let's see how we can use it anyway:

>>> class Polygon:
...     """Just a Polygon"""
...     position = Point(0, 0, 0)
>>>
>>> polygon = Polygon()
>>> print(polygon.position)
None
>>>

If you're wondering why polygon.position is returning None, then go back to our second version of Point and look at the __get__() method. We've overridden it and just put pass there, which is the equivalent of return None. When we invoke polygon.position python sees that our attribute is a descriptor, and gives our custom function control of the action instead.

Descriptors come in two flavors. Those which define both __get__() and __set__() are called data descriptors, and those which don't are called non-data descriptors.

Now that we know how we can override attribute access and modification, let's see how bluew uses those features to offer a declarative API.

The first object we have is a RapidAPI class which does noting other than keeping a Connection object as an attribute. The reason for going this way instead of simply using Connection is because we wanna hide the low-level methods, that our API should not expose.

class RapidAPI:
    """
    All declarative bluew APIs should inherent from this class.
    """

    def __init__(self, mac: str) -> None:
        self.con = Connection(mac)

Next in line is the Read descriptor.

class Read(object):
    """
    This descriptor should be used to declare a readable attribute.
    """
    def __init__(self, uuid: str) -> None:
        self.uuid = uuid

    def __get__(self, instance: RapidAPI, owner):
        return instance.con.read_attribute(self.uuid)

This is simple as well, all it does is take the uuid of the attribute we want our user to access, and override __get__() to call the read_attribute method from Connection. If this is too fast, or doesn't make sense go back to the MyoArmband class from the top, and you should be able to see the point clearer.

class Write:
    """
    This descriptor should be used to declare a function that takes input
    and writes it to an attribute. for example:
        class FooAPI(RapidAPI):
            do_some_bar = Write('UUID_HERE', accept=['valid1', 'valid2'])
        foo_api = FooAPI('xx:xx:xx:xx:xx')
        foo_api.do_some_bar('valid1')  # works!
        foo_api.do_some_bar('notvalid')  # Raises ValueError
    """
    def __init__(self, uuid: str, accept: Iterable = None) -> None:
        self.uuid = uuid
        self.accept = accept

    def __get__(self, instance: RapidAPI, owner) -> Callable:
        return lambda val: self._write(instance, val)

    def _write(self, instance: RapidAPI, val) -> None:
        if self.accept and val not in self.accept:
            raise ValueError('Valued passed is not valid input.')
        instance.con.write_attribute(self.uuid, val)

This one looks slightly more complicated, but that's just because it's more code than what we're used to by now. Write like Read takes a uuid for the attribute, and on top of that an optional iterable accept. And just like Read it also overrides the __get__() method, but instead of returning a value it returns a function, which validates its input based on the values in accept.

If you've read the code thoroughly you might be wondering why we're passing instance to _write and why we don't use self instead, and the reason for that is that self is the descriptor instance, and instance is the instance of the calling object. If we'd write data to self, we might, and would at some point, end up with unexpected problems, since the same descriptor is shared between all instances of the class that has it. So instead of adding that to our debugging bill, we just propagate the instance argument to the function we wanna return, by using a lambda.

I'm craving that declarative text-editor more and more as this article gets longer and longer, but before I set you free running with descriptors, let's quickly go through the descriptor protocol again, but in a more organized manner.

def __get__(self, instance, owner) -> val: receives self, which is the instance of the descriptor itself; owner, which is the class where the descriptor was declared; and instance, which is the instance of the owner class, that accessed the descriptor.

def __set__(self, instance, value) -> None: receives self, which is the instance of the descriptor itself; instance, which is the instance of the owner class, that accessed the descriptor; and value, which is the value that was passed in obj.desc = value.

def __del__(self, instance) -> None: receives self, which is the instance of the descriptor itself; instance, which is the instance of the owner class, that accessed the descriptor.

This is as far as we'll go with this article. If you're interested in writing declarative code, and see the value in using descriptors for that, I encourage you to read the official python documentation. Feel free to contact me directly with questions, comments, and fore and foremost complaints.

UPDATE: Check Declarative Programming with Python II for a comparison between our Myo class written declaratively vs imperatively.