Polymorphic functions

home

 

Polymorphic functions all work the same. We'll get back to that. For now, here's a Python function which finds the total price of 2 items:

def priceSum(a1, a2):
  p : int = 0
  if a1.isValid(): p += a1.price()
  
  if a2.isValid(): p += a2.price()
  return p

Inside, it looks up a1.isValid() and a1.price(), and again for a2. As long as it can do that, it doesn't otherwise care what they are. a1 could be a Car object, with 30 parts, while a2 could be a simple GarageSaleItem. We can't normally even mix those, but priceSum can. The system is often called Duck Typing, based on the saying: "if it walks and talks like a Duck". In this case, the function takes 2 "Ducks", and walking and talking are isValid() and price(). If you have those, you're a Duck, or close enough, and you can be an input to priceSum.

Let's try to write priceSum in C++. It's about the same, except for how it won't work:

int priceSum(Car a1, Car a2) {
  int p=0;
  if(a1.isValid()) p += a1.price();
  if(a2.isValid()) p += a2.price();
  return p;
}

Clearly, this only works on Cars, but it's not our fault. In C++, and most mainstream languages, we're required to provide a type. We could have used GarageSaleItem and it would only work with them. But now we know why polymorphism is a thing. It's a really sweet feature in a language that normally requires a set type. C++ can make polymorphic functions (it calls them template functions):

int priceSum<T>(T a1, T a2) { // <-- added <T>
  int p=0;
  if(a1.isValid()) p+=a1.price();
  if(a2.isValid()) p+=a2.price();
  return p;
}

We needed to write a little extra code to "turn on" polymorphism. But now it works for Cars and garage sales and anything else. It's not quite right, since that version can't take 1 Car and one GarageSaleItem. This next one is the real version:

int priceSum<S,T>(S a1, T a2) { // the 2 input types can be different
// the rest is the same

C++ lets us have completely open Duck-Type style polymorphism, but makes us work a little to get it.

Meanwhile, Java and C# think it needs more rules. They don't like how the function heading doesn't formally require isValid() and price(). If it did, two related nice things would happen: tooltips could help us more; and we'd get helpful errors sooner. Here's the same polymorphic function in Java. Step one is a fake type listing those functions:

// this means "anything with isValid() and price()"
interface priceable {
  bool isValid();
  int price();
}

It does what it looks like. priceable is now a shortcut for "anything which has an isValid() and price()". It's a nice trick. But now we have to remember that extra rule: sometimes when you see something like Car or GarageSaleItem, it's really one of these shortcuts. Using priceable, we can now write a the polymorphic Java version:

int priceSum(priceable a1, priceable a2) { // <-- priceable is a stand-in for Car, etc...
  int p=0;
  if(a1.isValid()) p+=a1.price();
  if(a2.isValid()) p+=a2.price();
  return p;  
}

The heading now officially has the rules for a1 and a2. A mouse-over will show us the requirements, and the system can more easily see what errors to give. We had to write a little more, but this seems better. Imagine there are other functions like this, also using priceable, and it seems even better.

But we're not done. Each class needs to register itself as a priceable:

class Car implements priceable { // <-we needed to add "implements priceable"
  // We had price() and isValid() in here before
  // They're still here, unchanged
}

At first, this seems horrible. Our simple polymorphic function is useless by itself. We need to go around to every single class and specifically give it permission to use us. Bleah.

But for real, that's not how it works. Let's go back to the Python version. It's no accident we have lots of classes with isValid() and price(). We planned it. We even had to agree ahead of time that price() is always in Euros or whatever. We may as well mark them with implements priceable as we get every class in shape. In fact, we can mark them at the start. Then the computer can yell at us until we add those two functions. Needing to specially register each class isn't all that different.

The drawback with that system is when we want to mix&match. If we need price() (but not isValid) and isWhite() (but not isBlack) and so on, it's going to be a problem. A problem C++ or Python wouldn't have. Hopefully we won't need to do that much.

Scala dials it back. It has the interface method, but has another where you don't need to register it with each class, called Structural Types:

type priceable = { def isValid():Boolean, def price():Integer }

// can be called using any type which happens to have isValid() and price():
def priceSum(a1 : priceable, a2 : priceable) = {
  var p : Integer = 0
  if(a1.isValid()) p+=a1.price()
  if(a2.isValid()) p+=a2.price()
  p
}

It looks the same -- there are only so many ways to say "must have these 2 functions". But it works as-is. Classes don't need to be marked in any special way. In fact, we can skip priceable and list the requirements right in the heading:

// same thing, version #2:
def priceSum(
        a1 : { def isValid():Boolean; def price():Integer }, // describe what a1 needs
        a2 : { def isValid():Boolean; def price():Integer }  // ditto for a2
      ) {
  var p : Integer = 0
  if(a1.isValid()) p+=a1.price()
  if(a2.isValid()) p+=a2.price()
  p
}

This sounds exactly like what we wanted. But we were wrong. Listing everything out like this turns out to be a huge pain and not all that useful for other technical stuff.

Another way of getting polymorphic functions is with inheritance. That seems like cheating, since inheritance is a whole big topic that includes polymorphic functions and a lot more. But it works. Here's a start, making Car:

abstract class BasicItem {
  // this fully has isValid written, including a helper variable:
  bool isValid() { return usableRating>0; }
  int usableRating;
  
  // a back-up price, which most people will replace w/their own:
  virtual int price() { return 0; } // many items have no price
}

class Car : BasicItem {
  override int price() { return x+y-z; } // some car-cost formula I made up
}

Then we'll make GarageSaleItems and so on, inheriting from BasicItem. Now we can use BasicItem as an input type. This takes Cars and/or GarageSaleItems

int priceSum(BasicItem a1, BasicItem a2) {
  int p=0;
  if(a1.isValid()) p+=a1.price();
  if(a2.isValid()) p+=a2.price();
  return p;  
}

But wait a minute! That looks exactly the same as the interface method. And not just looks. It is the same. Interfaces are just a form of inheritance. The inheritance method for polymorphic functions looks different, but it's not anything new. In fact, in C++ you inherit from several things and decide for yourself which seem interface-y.

This next thing is a detour, but related. What about sort functions? Are they polymorphic? A typical one, Sort(A), would be. It takes a list of any type, as long as it has a function like a1.comesBefore(a2).

What about something like IntSort(A, compareHow)? That only takes a list of ints. The extra compareHow function lets us adapt it, but that's just the normal thing extra inputs do. It's not polymorphic. But the real version is: Sort(A, compareHow). Once again, A is a list of anything. The extra input is still just nice.

 

So what do we have, altogether? Pretty much every language can write priceSum, to have it run on any type that could possibly work. There's a few ways, but we end up with the same polymorphic functions in every case.

Like everything else in computer science, the rest is just fast vs. slow but with better errors. Python likes to write short programs. Quick and overly flexible is fine. Java wants to be for massive industrial applications -- an extra 20 minutes setting things up will in theory pay off during debugging.

Even though I know they're the same, they feel very different. The Python way seems like a "real" polymorphic function. The interface method -- interfaces serve one purpose and one purpose only, which is to enable polymorphic functions. But it still feels like I'm merely using inheritance.

 

 

 

Comments. or email adminATtaxesforcatses.com