Search code examples
ooplanguage-agnosticpolymorphism

Is polymorphism exclusive to OOP?


I am a student, so I don't know if my question is wrongly placed. I know that polymorphism is commonly achieved in OOP through method overriding, function overloading, and so on. But is it a concept exclusive to OOP languages? If not, could you provide examples on how to achieve it in non-OOP languages, including C but not exclusive to it? (So far I have some Java, C++, C, Javascript, and PHP experience)

Thanks.


Solution

  • Polymorphism is not exclusive to OOP, or even OOP-like systems, but as you point out, they were conceived to give a standard pattern to what was considered to be a universal idea in software. It can be simulated in languages that don't have first class support for it (and has syntactic help in some, for example lua).

    In old procedural languages, one uses the same tricks C++ does, just more or less explicitly; often a void * to some dynamically allocated data along with a number of function pointers for operations on it. It's exemplified to a degree by the standard library qsort function.

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    typedef struct Sortable {
        char *(*toString)(struct Sortable *p);
    } Sortable;
    
    typedef struct AThing {
        Sortable p;
        int value;
    } AThing;
    
    typedef struct BThing {
        Sortable p;
        const char *str;
    } BThing;
    
    int sortableCompare(const void *a, const void *b) {
        int res;
        Sortable *aa = *((Sortable **)a);
        Sortable *bb = *((Sortable **)b);
        char *astr = aa->toString(aa);
        char *bstr = bb->toString(bb);
    
        res = strcmp(astr, bstr);
    
        free(astr);
        free(bstr);
    
        return res;
    }
    
    char *AThing_toString(Sortable *p) {
        AThing *self = (AThing*)p;
        char *res = malloc(15);
        snprintf(res, 15, "%d", self->value);
        return res;
    }
    
    char *BThing_toString(Sortable *p) {
        BThing *self = (BThing*)p;
        return strdup(self->str);
    }
    
    AThing *createIntSortable(int i) {
        AThing *self = malloc(sizeof(*self));
        self->p.toString = AThing_toString;
        self->value = i;
        return self;
    }
    
    BThing *createStringSortable(const char *str) {
        BThing *self = malloc(sizeof(*self));
        self->p.toString = BThing_toString;
        self->str = str;
        return self;
    }
    
    int main() {
        Sortable *array[3];
    
        array[0] = &createIntSortable(1)->p;
        array[1] = &createStringSortable("hi")->p;
        array[2] = &createIntSortable(3)->p;
    
        qsort
            ( array // auto decay and coerce to void*
            , sizeof(array) / sizeof(array[0]) // nelem
            , sizeof(array[0]) // elem size
            , &sortableCompare // compare function
            );
        for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
            char *p = array[i]->toString(array[i]);
            printf("%d %s\n", i, p);
            free(p);
        }
    
        for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
            // To be more fancy, make a destructor in p                                                                                                                       
            free(array[i]);
        }
    }
    

    go and rust are not strictly object oriented, but use interface and duck-type conversions to give all types a lot of the same properties as objects. You can decorate anything with methods and choose whether the self parameter is by reference or by value.

    Haskell has type classes, which is kind of similar to the go and rust style of polymorphism in that a person who provides a type provides an implementation of an interface on that type (for example, an object that can act as a list might implement Monoid). Any function that takes objects implementing monoid can then receive an automatic coercion of the object to the Monoid typeclass implementation.

    In functional languages that don't have type classes (and even much of the time in haskell), people do polymorphism by writing generic higher-order functions and then passing in sets of operations on the objects they care about. In this way, you can have containers, filters, transformers and such that don't know what kind of thing they work on, but undertake a set of operations the user provides based on their own objective. A good full example of this is elm-astar because it implements A* in a language that doesn't have any polymorphism facilities of its own. It gains some terseness compared to C because while polymorphism isn't builtin, generic types are.