[Types-sig] A type checking system in Python

Clark C. Evans cce@clarkevans.com
Wed, 21 Mar 2001 05:09:13 -0500 (EST)


This was a very insightful post on the main list.  

---------- Forwarded message ----------
Date: Wed, 21 Mar 2001 09:15:50 GMT
From: Huaiyu Zhu <hzhu@mars.localdomain>

			Type checking system in Python

Classes implement a hierarchy of inheritance of attributes (variables and
methods).  They are useful for implementation modularization.

There is another type of modularization that is similar in nature, but is
independent of classes.  This is the hierarchy of polymorphic interfaces.

Python supports polymorphism in a very liberal way: if it walks like a duck
and quacks like a duck, then it must be a duck.  The archetypical examples
is that one can usually substitute a writable file with a user defined class
object that happens to have a write() methods with appropriate property.

In many programs it is necessary to ensure that a certain objects has
certain properties.  But how can we express the idea that a function expect
a file-like object, while that can mean an endless list of things, many have
not yet been defined?

One solution promoted by some other languages is to define some abstract
classes for type checking purpose, and require them to be inherited by any
class that behaves like them.  This is like what interfaces do in Java.
This solution is not satisfactory because

- It breaks polymorphism.  One has to harness the property at the time the
  classes are defined, instead of when they are used.  For example, if an
  application requires a property of "ReadableFile" which is satisfied by
  sys.stdin, it is difficult to retrofit the definition of sys.stdin to be
  an instance of such.

- It mixes two incompatible inheritances.  For example, suppose that class A
  has method A.m() but its subclass B overrides it with B.m().  The property
  A satisfies is not necessarily satisfied by B.

>From the above it is clear that there is a useful conceptual hierarchy that
is independent of the class hierarchy.  For lack of a better name let us
just call them Types.  The overridability means that classes do not imply
Types, and the polymorphism means that Types do not imply classes.  Their
main conceptual difference is that

- classes embody implementations
- Types embody requirements

Another way to describe the difference is

- classes are inherited by specialization
- Types are inherited by abstraction

For example, if we specify that A is a superclass of B, this does not change
anything about A, but it does mean that B will have all the attributes of A.
In contrast, if we specify that A is a superType of B, this does not change
anything about B, but it does mean that objects accepted as B must now be
accepted as A as well.  As a concrete example, consider a function expecting
a Number as an argument.  If we declare that 2.3 is a Float, and Float is a
special case of Number, then the function should accept 2.3 as argument.

Therefore, Types should have their own hierarchy, possibly with its own
syntax, that is independent of classes.  

Since Python is a very dynamically typed system, it is also desirable to
make the type checking as flexible as possible, and completely voluntary,
while being able to deliver the same level of guarantees that a strongly and
statically typed language would give.

Type can also be used to express properties like Immutable, which could only
be guaranteed by a compiler.  These properties will also be very useful for
compiler optimization.

All said, there is a role for classes to play in this affair - It is
possible to fake such a Type system using a class, with some caveats:

- It is dynamic, and very inefficient at run time
- It is dynamic, and can be circumvented in various ways.

Nevertheless, this dynamical type-checking does provide a fine illustration
of the concepts discussed above.  If and when they can be coded in compiler
it will be of much greater use.

==================================================================

#!/usr/bin/env python
# $Id: test.py,v 1.1.1.1 2000/08/29 00:38:54 hzhu Exp $
"""
A simulated type checking system
Types are inherited by abstraction

"""

class Type:
	def __init__(self, name):
		"Define a new abstract Type"
		self.name = name
		self.types = []
		self.classes = []
		self.subs = []
		self.attrs = []
		self.objs = []
		
	def include(self, obj):
		"Check to see if object is of my Type"
		
		# If it is explicitly included objects
		for o in self.objs:
			if obj == o: return 1

		# If it is of the right type or class
		if type(obj) is type(Type):
			for c in self.classes:
				if isinstance(obj, c): return 1
		else:
			for t in self.types:
				if type(obj) == t: return 1

		# If it is one of our sub-Type
		for s in self.subs:
			if s.include(obj): return 1
		else:
			return 0

	def satisfiedby(self, obj):
		"""Check to see if object has all the required attributes
		Is this necessary? """
		for a in self.attrs:
			if not hasattr(obj, a):
				print "%s has no attr %s" %(`obj`, a)
				raise AssertionError

	def add_type(self, t):
		if t not in self.types: self.types.append(t)

	def add_class(self, c):
		if c not in self.classes: self.classes.append(c)

	def add_sub(self, s):
		if s not in self.subs: self.subs.append(s)

	def add_attr(self, m):
		if m not in self.attrs: self.attrs.append(m)

	def add_obj(self, o):
		if o not in self.objs: self.objs.append(o)

	def add_typeof(self, o): self.add_type(type(o))
	def add_classof(self, o): self.add_class(o.__class__)
	def add_attrof(self, o):
		for attr in dir(o):		self.add_attr(attr)

	def __repr__(self): return "Type(%s)" % `self.name`


def require(x, t):
	try:
		assert t.include(x)
	except AssertionError:
		print "%s is not of %s" % (`x`, t)
		raise
	t.satisfiedby(x)

def require_either(x, list):
	for t in list:
		if t.include(x):
			t.satisfiedby(x)
			return
	else:
		print "%s is not of any Type in %s" % (`x`, list)
		raise AssertionError

def require_all(x, list):
	for t in list: require(x,t)


#------------------------------------------------------------------
String = Type("String")
String.add_typeof("")

Integer = Type("Integer")
Integer.add_typeof(1)
Integer.add_typeof(1L)

Number = Type("Number")
Number.add_sub(Integer)

Float = Type("Float")
Float.add_typeof(0.0)
Number.add_sub(Float)

File = Type("File")
File.add_attr("close")

#------------------------------------------------------------------
if __name__ == "__main__":

	def f(x):
		require(x, Number)
		print x*2

	f(1)
	f(2.3)

	def f(x):
		require_either(x, (Number, String))
		print x*2
		
	f("2")

	import sys
	File.add_obj(sys.stdout)
	require(sys.stdout, File)

	require_either("2", (Number, String))
	#require_all("2", (Number, String))




-- 
http://mail.python.org/mailman/listinfo/python-list