Utility Classes :: Typeclass
The Typeclass class is all about future-proofing.
Say you have a function that, as a contrived example, adds two objects:
def add(a, b):
return a + b
Now, if you wanted to restrict this function to only operating on numbers. One possibility would be to use the IsOneOf() utility class like so:
Number = IsOneOf(int, float, long, complex)
@accepts(Number, Number)
@returns(Number)
def add(a, b):
return a + b
That's all fine and dandy, and it works.
The downside to this approach is that you've excluded yourself from using the Decimal class from the decimal module, which offers much better precision.
"No problem," you say, "I'll just add Decimal to the IsOneOf instance":
Number = IsOneOf(int, float, long,
complex, decimal.Decimal)
@accepts(Number, Number)
@returns(Number)
def add(a, b):
return a + b
Ok, now say you want your function to operate on objects from the Numeric package? You see what I'm getting at.
The solution is to use typeclasses.
You feed the Typeclass() constructor a list of types and/or classes, and Typeclass figures out the interface common to all these types.
The Typeclass instance will then approve any object that implements this set of methods.
So, let's try that earlier example again, this time using typeclasses:
Number = Typeclass(int, float, long, complex)
@accepts(Number, Number)
@returns(Number)
def add(a, b):
return a + b
Now, without changing the Number typeclass, you can use decimal.Decimals or objects from Numeric freely.
To get you started, we've provided a whole set of typeclasses built around the built-in types. Get to know them; they're your friend.
Details
The
Typeclass()constructor requires at least one argument. If only theselfparameter is passed, aTypeErrorwill be raised.All arguments to the
Typeclass()constructor must be types, classes or instances ofTypeclass.-
Typeclassobjects will only compare equal (i.e.,==) if all the types and classes in the left-hand side are found in the right-hand side, and vice versa.All
Typeclassinstances that compare equal with==will produce the same value when run through the built-inhash()function. -
The
Typeclassclass has been given some intelligence about boolean equality, meaning that it will optimise the creation of certain instances. For instance:Typeclass(Typeclass(A, B), Typeclass(C, D))
is equivalent to
Typeclass(A, B, C, D)
In fact, the first will be coerced into the second, meaning that the two will compare and hash as equals.
-
Once a
Typeclassinstance has approved an object of a given type, it caches that type to speed things up in the future. This can lead to problems if you start monkeying with a type or class after it has been added to a typeclass:class A(object): def foo(self): pass class B(object): def foo(self): pass tc = Typeclass(A) b = B() # Should pass check_type(tc, b) B.foo = 5Since
Bis still cached in theTypeclassinstace as a known-good type, subsequent validations ofBinstances will be approved.The solution to this is the
recalculate_interface()method ofTypeclassinstances. Invoking this method will clear the type cache and rebuild the interface that the typeclass is looking for. If the checked object lacks an attribute specified in the
Typeclass-defined interface, theTypeclassinstance's__typecheck__will raise a_TC_MissingAttrErrorwith the name of the missing attribute. This exception is to be caught and converted to aTypeCheckErrorbefore it reaches the user.If an attribute of the checked object is not callable, the
Typeclassinstance's__typecheck__will raise a_TC_AttrErrorwith IsCallable() as the expected type and the name of the offending attribute. This exception is to be caught and converted to aTypeCheckErrorbefore it reaches the user.Typeclassinstances cache the types and classes they've found to match the interface.