DEV Community

loading...

Polymorphism and why use it?

ivaaaan profile image Ivan ・4 min read

Intro

In this article, I want to introduce you to different types of polymorphism. I'll try to answer questions about what it is, why, and how to use it with different examples. I wrote this article, to sum up, and structure my knowledge after reading On understanding types, data abstraction, and polymorphism paper. I'll use PHP, Go, and Typescript for code examples.

What is polymorphism?

Simply put, polymorphism allows your functions to have values of multiple types. It's opposed to monomorphism, where functions only work on one type.

For example, consider the following function:

func length(list []int) int {}
Enter fullscreen mode Exit fullscreen mode

This function is monomorphic. It allows you to pass list only as only type []int.

On the other hand:

func length(list []interface) int {}
Enter fullscreen mode Exit fullscreen mode

is polymorphic. Your list can be []int, []string, and etc.

How many types of polymorphism are there?

According to the paper, there are four. We will discuss only two of them in this article, which the paper names as "true polymorphism".

types of polymorphism

Inclusion

Let's start with the one that we all know about. Inclusion polymorphism says that your types can have subtypes, and your function will work with any subtype as well as with the supertype.

For example:

interface Writer {
    write(value: string): void;
}

class ConsoleWriter implements Writer {
    public write(value: string): void {
        console.log(value);
    }
}

class FileWriter implements Writer {
    public write(value: string): void {
        // write to file here
    }
}

function write(writer: Writer, value: string): void {
    writer.write(value);
}
Enter fullscreen mode Exit fullscreen mode

Although this is a bad example for real-world programming (composition over inheritance), it's good for demonstrating the idea.

You can pass any object of type Writer, and its subtypes to the write function. What does it give to us? It simply allows you to program by contract and not to care about the implementation. You always know that the $writer object has a defined set of methods that you can run.

Subclasses and interfaces are not the only way to achieve this type of polymorphism. There's also a concept that's called duck typing which says that if an object has a set of methods that are defined in type A, then it's type A. If it behaves like a duck, then it's a duck.

A common example is Go:

type Writer interface {
    Write(p []byte) (n int, err error)
}

type FileWriter struct {}

func (w FileWriter) Write(p []byte) (n int, err error) {}

func write(w Writer, data []byte) {}
Enter fullscreen mode Exit fullscreen mode

For those who are not familiar with Go, the code above defines an interface Writer and the struct(think about this as a class) FileWriter with the method Write. Here, the FileWriter is a subtype of Writer, although it doesn't implement any interface or anything.

In Go, any type is a subtype of the type named interface which is an inclusion kind of polymorphism, but when generics are released, the language will support parametric as well. Let's jump to this one.

Parametric

Parametric polymorphism allows you to define generic functions that will work with any type. A common technique that allows you to achieve this is generic programming. Consider this method in Typescript:

function length<T>(list: T) int {}
Enter fullscreen mode Exit fullscreen mode

Users of your function can pass a list of any type here.

As I wrote above, Go doesn't not support this yet. You still can create a function like this:

func length(list []interface) {}
Enter fullscreen mode Exit fullscreen mode

But this is inclusion polymorphism because any type in the Go is a subtype of the type interface.

I won't dive into the details about why generics are better, but you can watch this video if you're interested.

What does it give to us?

One type, many implementations

Polymorphism gives you IoC(Inversion Of Control) power. To simplify, you can create some complicated function that accepts two values:

type Writer interface {
    func Write(data string) string
}

func transformAndWriteData(w Writer, data string) string {
    // transform data
    w.Write(data);

    return data
}
Enter fullscreen mode Exit fullscreen mode

Inside that function, you can do whatever you want and then call w.Write. You don't care how Write behaves. It could write to some remote file, disk, or stdout.

Unit testing

This concept actually allows you to unit test your program. This is a common technique when you test only one part of the system at a time. It would be hard and unnecessary to use the implementation of FileWriter when you want to test transformAndWriteData. So you can create a mock:

package main

import (
    "testing"
    "reflect"
    "strings"
)

type Writer interface {
    Write(data string)
}

func transformAndWriteData(w Writer, data string) string {
    data = strings.ToUpper(data) // let's pretend this is useful
    w.Write(data);

    return data
}

type MockWriter struct {
    Data string
}

func (w *MockWriter) Write(data string) {
    w.Data = data
}

func TestTransformAndWrite(t *testing.T) {
    data := "abc"
    writer := &MockWriter{}

    expected := "ABC"
    actual := transformAndWriteData(writer, data);

    if !reflect.DeepEqual(actual, expected) {
        t.Errorf("expected %q, got %q", expected, actual)
    }
}
Enter fullscreen mode Exit fullscreen mode

Generic programming

What about parametric polymorphism? It also gives you the power of IoC. For example, you want to implement your own map function in TypeScript:

function map<T, K>(list: Array<T>, f: ((_: T) => K)): Array<K> {}
Enter fullscreen mode Exit fullscreen mode

Or, you want to build a library that provides some sort of authentication:

interface User {
  id: int;
  username: string;
}

function authenticate<T extends User>(user: T) {
  //...
}
Enter fullscreen mode Exit fullscreen mode

Now, programmers who use your tiny auth library can create their own implementation of user and then use the authenticate method. This is a super-simplified example, but you get the idea.

Summary

We've defined two types of polymorphism: inclusion and parametric. Both could be used separately, or together, and allow you to define beautiful abstractions which give you the power of IoC. And both make more sense in statically typed languages, where types could be checked on compile, which will save you time debugging errors during runtime.

Discussion (0)

pic
Editor guide