Question

I have to admit that I have only basic knowledge of Python and currently learning Haskell.

I was wondering if the concept of type classes exist/makes sense in Python or in Clojure (or some other dynamically-strongly typed language) ?

In other words, if I have a function name f then depending on the run time parameter fed to f a different function implementation will be invoked (like the == function for types that belong to the Eq type class in Haskell). Does such a concept exist in dynamic languages such as Clojure/Python ?

Was it helpful?

Solution 2

You can get pretty close to this with multimethods or protocols in clojure, or with simple member functions (class methods) in python. There's one important feature missing in each of these that's present in haskell, though: return-type polymorphism.

The compiler knows what type you "expect" a function to return, and can dispatch to a different implementation accordingly. This means that the same function, called on the same arguments, can do something entirely different, depending on what's done with its return value. For example:

Prelude> read "[5,6,7]" :: [Int]
[5,6,7]
Prelude> read "[5,6,7]" :: [Double]
[5.0,6.0,7.0]

Likewise, you can even have polymorphic constants, which have a different value for each typeclass instance:

Prelude Data.Word> (minBound, minBound, minBound) :: (Int, Bool, Word8)
(-9223372036854775808,False,0)

You can't really do this in a dynamic language because there's no type inference. You can fake it a little bit by passing around objects that represent "the type of result that I want", and use those as your dispatcher, but it's not really the same.

OTHER TIPS

Multimethods seem to do the trick in Clojure. For example, let's define a plus function that adds numbers but concatenates the string representation of anything else.

(defmulti plus (fn [& xs] (every? number? xs)))

(defmethod plus true [& xs] (apply + xs))
(defmethod plus false [& xs] (apply str xs))

(plus 1 8) ;9
(plus 1 \8) ;"18"

Multimethods are functions ((ifn? plus) is true), so are as first-class as you could wish:

(let [z (partial plus 5)] (z \3)) ;"53"

Multiple dispatch (example in Julia language) has similar purposes with type classes. Multiple dispatch provides compile-time polymorphism (just like type classes) while object interfaces in typical dynamic languages (i.e. Python) are usually limited to runtime polymorphism. Multiple dispatch provides better perfomance than usual interfaces you can see in dynamic object-oriented languages so it makes perfect sense in dynamic languages.

There are some multiple dispatch implementations for Python but I don't sure if they provide compile-time polymorpism.

You can't define multiple functions with the same name in Python (in the same scope). If you do, the second definition will overwrite the first and be the only one called (at least when both are in the same scope--obviously you can have class methods in different classes that share a name). The parameter list is also type-ignorant, so even if you could define functions twice the interpreter would only distinguish them based on number of parameters, not the type. What you need to do is write a single function that can handle several different argument lists, and then if necessary check their type inside that function.

The simplest way to do this is to use default parameters and keyword arguments.

Default Parameters

Say you had a function like so:

def BakePie(crust, filling="apple", oventemp=(375,F), universe=1):
    ...

You can call this function using positional arguments like so:

BakePie("graham cracker")
BakePie("buttery", "cherry")
BakePie("fluffy", "lemon meringue", (400,F))
BakePie("fluffy", "key lime", (350,F), 7)

Keyword Arguments

Those will all work, but you may not always want to change every default. What if you want to bake a standard Apple Pie, just in a different universe? Well, you can call it using keyword arguments:

BakePie("buttery", universe=42)

In this case the default arguments for filling and oventemp will be used, and only the argument for universe (and crust, which must always be given since there is no default) will be changed. The one rule here is that any keyword arguments must be to the right of any positional arguments when calling the function. The order of the keyword arguments does not matter, e.g. this will also work:

BakePie("fluffy", oventemp=(200, C), filling="pecan")

But this will not:

BakePie(filling="boysenberry", "crumb")

More with Keyword Arguments

Now, what if the behaviour of your function is completely different depending on which arguments get passed? For example, you have a multiply function that takes either two integers, or it takes an integer and a list of integers, or else two list of integers, and multiplies these. This situation is one in which you, the caller, will want to use just keyword arguments. You can set up the function definition like so:

def GenericMultiply(int1=False, int2=False, ilist1=False, ilist2=False):
    # check which parameters have values, then do stuff

(Or use None instead of False.)

When you need to multiply two integers, call like so:

GenericMultiply(int1=6, int2=7)

NOTE: You could also perform the above with just two positional arguments, and manually check their type inside the function, either by using the type() function or by using try:except: blocks and e.g. calling methods that only work on lists or integers on each argument.

Further Reading

There are many other ways you could go about this in Python, I've just tried to describe the simplest one. If you want to learn more, I recommend the official Python tutorial's section on Defining Functions and the next section, More on Defining Functions (this will get into detail about positional and keyword arguments, as well as the *args and **kwargs syntax which can allow you to define functions with variable length parameter lists without needing to use default values).

To some extent, this functionality is replicated by class methods. For example, the __repr__ method is approximately the same as the Show type class in Haskell:

$ ghci
>>> x = 1
>>> show x
"1"
>>> x = [1,2,3]
>>> show x
"[1,2,3]"

or in Python

$ python
>>> x = 1
>>> x.__repr__()
'1'
>>> x = [1,2,3]
>>> x.__repr__()
'[1,2,3]'

Clearly, a different function is being called in each case depending on the type (in the case of Haskell) or class (in the case of Python) of the thing that show / __repr__ is being applied to / called on.

A closer approximation, for languages that support them, is interfaces - abstract classes with all of their methods also abstract (they're called interfaces in Java, and virtual classes in C++). You don't tend to see them as much in dynamically typed languages, because the main point of an interface is to declare a set of methods and their associated types that an implementing class must conform to. Without static types, there isn't much point declaring what the types should be.

Yes, typeclasses exist in Python. You can model them using dry-python/classes.

Example:

from classes import AssociatedType, Supports, typeclass

class Greet(AssociatedType):
    """Special type to represent that some instance can `greet`."""

@typeclass(Greet)
def greet(instance) -> str:
    """No implementation needed."""

@greet.instance(str)
def _greet_str(instance: str) -> str:
    return 'Hello, {0}!'.format(instance)

def greet_and_print(instance: Supports[Greet]) -> None:
    print(greet(instance))

greet_and_print('world')
# Hello, world!

It also has full mypy support.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top