Case for types
Growing up (as a programmer) i was thought, as a whole lot of us were i guess, to type my every variable (also, to type things like BEGIN and END) and so i did, because i had to. When i later found languages like Perl and JavaScript i thought to myself, well, they're right, i too do not make those kinds of mistakes explicit typing could have somehow and probably quite magically saved me from. And i liked getting rid of some of the noise types can produce. Oh my, just imagine the hubris up there on top of mount stupid. Because the thing about making that class of mistakes is, i did make them, still do, everybody does. It's liars, geniuses and mountaineers who claim otherwise, but that is entirely besides the point.
As very clever ones have said, we do not write code for ourselves and we do not write code for the computer, we write it for (other) humans. Nobody is questioning such basic things like expressive identifiers or formatting or high-level languages. The reason nobody does that, is because they make code more readable for us. Explicit Types can do that too. And they have one, i think interesting advantage over many other approaches for increasing readability: there is usually no debate necessary, or even possible about what type a certain thing should have since it naturally and implicitly already is of that type. At the very least compilers tend to have some strong feelings about that, and they do get the deciding vote if there would every be any disagreement.
Humans are very good at some things and very bad at some other things. To infere the type of a variable in any program longer than few tens of lines of code, i am afraid is not one of their strengths. It is not one of mine, that much i know. Also, i wouldn't want that job, and i am not the one for guessing games. I like to use my brain to solve some of the actually interesting problems. I automate the rest, i write little programs to do those boring, repetitive, error-prone jobs, and love it.
If most of the above is true, then explicit types make programs more readable and that is a very good thing for any program to be. This is a thought i carried around for some time now and it is quite clear that i'm neither the first nor the only one. When i recently started to read Expert Twisted (good read so far) it surfaced again. So i typed one of the first code examples into my little computer and ran the program and it worked as i expected. Then i typed that program and ran it again, still worked exactly the same. So, no big surprise there, my computer did not care one bit about the type hints. (Note: This works because Python is not explicitly typed but allows for type hints which can be statically checked.)
Then i looked at the two versions, and what a surprise! The typed version reads a lot easier. Below you'll see the two versions. The less familiar you are with the concepts in it, or even with Python the clearer i hope to make my point.
Both versions were also black'ed - automated formatting FTW!
No types
import errno
import select
import socket
class Reactor(object):
def __init__(self):
self._readers = {}
self._writers = {}
def addWriter(self, writeable, handler):
self._writers[writeable] = handler
def addReader(self, readable, handler):
self._readers[readable] = handler
def removeWriter(self, writeable):
self._writers.pop(writeable, None)
def removeReader(self, readable):
self._readers.pop(readable, None)
def run(self):
while self._readers or self._writers:
r, w, _ = select.select(self._readers.keys(), self._writers.keys(), [])
for readable in r:
self._readers[readable](self, readable)
for writeable in w:
if writeable in self._writers:
self._writers[writeable](self, writeable)
class BuffersWrites(object):
def __init__(self, dataToWrite, onCompletion):
self._buffer = dataToWrite
self._onCompletion = onCompletion
def bufferingWrite(self, reactor, sock):
if self._buffer:
try:
written = sock.send(self._buffer)
except socket.error as e:
if e.errno != errno.EAGAIN:
raise
return
else:
print("wrote", written, "bytes")
self._buffer = self._buffer[written:]
if not self._buffer:
reactor.removeWriter(writeable=sock)
self._onCompletion(reactor, sock)
def accept(reactor, listener):
sever, _ = listener.accept()
reactor.addReader(sever, read)
def read(reactor, sock):
data = sock.recv(1024)
if data:
print("server received", len(data), "bytes")
else:
sock.close()
print("server closed")
reactor.removeReader(readable=sock)
def write(reactor, sock):
writer = BuffersWrites(b"".join(DATA), onCompletion=write)
reactor.addWriter(sock, writer.bufferingWrite)
print("client buffering", len(DATA), "bytes to write")
DATA.extend(DATA)
DATA = [b"*", b"*"]
if __name__ == "__main__":
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.bind(("127.0.0.1", 0))
listener.listen(1)
client = socket.create_connection(listener.getsockname())
loop = Reactor()
loop.addWriter(writeable=client, handler=write)
loop.addReader(readable=listener, handler=accept)
loop.run()
With type hints (aka typings)
import errno
import select
import socket
from typing import Dict, List, Callable
MAX_BYTES = 1024
DATA = [b"*", b"*"]
class Reactor(object):
def __init__(self):
self._readers: Dict = {}
self._writers: Dict = {}
def addWriter(self, writeable: socket.socket, handler: Callable):
self._writers[writeable] = handler
def addReader(self, readable: socket.socket, handler: Callable):
self._readers[readable] = handler
def removeWriter(self, writeable: socket.socket):
self._writers.pop(writeable, None)
def removeReader(self, readable: socket.socket):
self._readers.pop(readable, None)
def run(self):
while self._readers or self._writers:
r: List[socket.socket]
w: List[socket.socket]
_: List[socket.socket]
r, w, _ = select.select(self._readers.keys(), self._writers.keys(), [])
readable: socket.socket
for readable in r:
self._readers[readable](self, readable)
writeable: socket.socket
for writeable in w:
if writeable in self._writers:
self._writers[writeable](self, writeable)
class BuffersWrites(object):
def __init__(self, dataToWrite: bytes, onCompletion: Callable) -> None:
self._buffer: bytes = dataToWrite
self._onCompletion: Callable = onCompletion
def bufferingWrite(self, reactor: Reactor, sock: socket.socket):
if self._buffer:
try:
written: int = sock.send(self._buffer)
except socket.error as e:
if e.errno != errno.EAGAIN:
raise
return
else:
print("wrote", written, "bytes")
self._buffer: bytes = self._buffer[written:]
if not self._buffer:
reactor.removeWriter(writeable=sock)
self._onCompletion(reactor, sock)
def accept(reactor: Reactor, listener: socket.socket):
sever: socket.socket
_: socket.socket
sever, _ = listener.accept()
reactor.addReader(sever, read)
def read(reactor: Reactor, sock: socket.socket):
data: bytes = sock.recv(MAX_BYTES)
if data:
print("server received", len(data), "bytes")
else:
sock.close()
print("server closed")
reactor.removeReader(readable=sock)
def write(reactor: Reactor, sock: socket.socket):
writer = BuffersWrites(b"".join(DATA), onCompletion=write)
reactor.addWriter(sock, writer.bufferingWrite)
print("client buffering", len(DATA), "bytes to write")
DATA.extend(DATA)
if __name__ == "__main__":
listener: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.bind(("127.0.0.1", 0))
listener.listen(1)
client: socket.socket = socket.create_connection(listener.getsockname())
loop: Reactor = Reactor()
loop.addWriter(writeable=client, handler=write)
loop.addReader(readable=listener, handler=accept)
loop.run()
In a nutshell, this program lets two components communicate via a socket. More and more data is sent every time until it eventually becomes too much. This is obviously not suited for production, it is just to help me make my point, that (a) types can make code more expressive and readable and therefore (b) we type for us, not for the computer. That incredible piece of metal and wiring and awe most likely doesn't care at all about that, it is exactly sure what it thinks that everything is at any point in time anyways.
Obviously this means we type when it actually adds value (once per line is fine, BTW). Just as we do not necessarily need to type anything if doesn't help. I wouldn't opt for types to become dogma.