Function flexibility considered harmful 
Tags: javascript, typescript, elm
Published: 2020-01-16 15:46:51

What I would like to talk about is polymorphism, exactly [ad-hoc polymorphism](https://en.wikipedia.org/wiki/Ad_hoc_polymorphism), and more exactly the wrong usage of ad-hoc polymorphism. Ad-hoc polymorphism in used when some function `f` has different behavior for given argument `a` being different type. To show what I mean, I will show example of monomorphic and polymorphic function:

```ts
[Pseudo Code TS flavor]
function monoF(a: number): number => { /* implement. */ }
// overloaded function with two implementations:
function poliF(a: string): string => { /* implement. */ }
function poliF(a: number): number => { /* implement. */ }

```
As you can see `monoF` allows only number to be passed, and this function also returns one type - `number`. The `poliF` has two implementations, it is overloaded for `string` and `number` type.

## The issues with ad-hoc polymorphism

What is then problem with such ad-hoc polymorphism? The problem is that it **often** leads to wrong design. In TypeScript function overloads is even more difficult as TS not allows on many implementations, implementation can be one and single, what force us into function with multiple branches.

```js
[JS]
function isAdult(u){
    if (typeof u === 'number') {
      return u >= 18;
    } else {
      return u.age >= 18;
    }
}
```
From deduction of the implementation we can understand that it works for two possible types, one is `number` and second `object` with `age` property. To see it more clear let's add TypeScript types. 

```ts
[TS]
function isAdult(u: number | {age: number}): boolean {
    if (typeof u === 'number') {
      return u >= 18;
    } else {
      return u.age >= 18;
    }
}

isAdult(19)
isAdult(user)
```
Ok now we see more, our function in [hindley milner notation](https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system) has a type `number | {age: number} -> boolean`.

Consider that our `isAdult` function is able to cover two separated types and map them to `boolean`. Because of these two types we were forced to append condition inside the implementation, as the function is rather simple this is still additional complexity. I can say `isAdult` is a function merged from two `number -> string` and `{age: number} -> string`. And what is the purpose of this? Ah - flexibility, this function can be used in two different cases. But let's consider simpler version.

```ts
[TS]
function isAdult(u: number): boolean {
    return u >= 18;
}
// usage
isAdult(19)
isAdult(user.age)
```
The only difference is the need to pass `user.age` instead of `user`. But such approach removed most of the code inside the function, also from beginning the only thing this function cared for was the age represented as `number`.

Let's take a look at ad-hoc polimorhism which includes also return type.

```ts
[TS]
function add(a: string, b: string): number
function add(a: number, b: number): number
function add(a: string | number, b: string | number) {
    if (typeof a === 'string' && typeof b === 'string') {
        return parseInt(a) + parseInt(b)
    }
    if (typeof a === 'number' && typeof b === 'number'){
        return a + b;
    }
    return a; // the dead code part
}
const a = add(1, 2)
const b = add("1", "2")
```

As it is visible code is quite terrible. We need to check variables types by runtime `typeof`, also we introduced the dead code part, taking overloads into consideration there really is no other case then pair `(number, number)` and `(string, string)`, but our implementation sees all possible cases so also pairs `(string, number)` and `(number, string)`. 

> Ts is not able to have function overloads in form of own implementations as for example C++ has, because TS has no way to check what really type do we have, as type is only annotation not existing in the runtime

To be fair we can little bit change the implementation, but the only way is to use here type assertion.

```ts
function add(a: string | number, b: string | number) {
    if (typeof a === 'string') {
        return parseInt(a) + parseInt(b as string) // type assertion
    }
    return a + (b as number); // type assertion
}
```
Is it better, not sure. Type assertion are always risky, type safety loose here.

Lets now think why do we at all do that, why we need two input types? We abstract from developer the need of parsing a string to int. Is this game worth the candle? No it is not. 

> Some of you can say that union creates a new type, as we can say `type T = string | number`, yes that is true, and in that term function `T -> T` is not polymorphic

The smaller monomorphic version
```ts
function add(a: string, b: string) {
    return parseInt(a) + parseInt(b)
}
```
And for numbers u have already `+` operator. Nothing more is needed.

## The real example of wrong design

Next example is from the real code and the question from stackoverflow - [How to ensure TypeScript that string|string[] is string without using as?](https://stackoverflow.com/questions/59729108/how-to-ensure-typescript-that-stringstring-is-string-without-using-as/59729563#59729563)

We want to have a function which is overloaded in such way, that for `string` returns `string` and for `array of strings`, return `array of strings`. The real purpose of having this duality is - to give developers better experience, probably better ...

Its also very common in JS world to give ad-hoc polymorphism in every place in order to simplify the interface. This historical practice I consider as wrong.

```ts
function f(id: string[]): string[];
function f(id: string): string;
function f(id: string | string[]): string | string[] {
    if (typeof id === 'string') {
        return id + '_title';
    }
    return id.map(x => x + '_title');
}

const title = f('a'); // const title: string
const titles = f(['a', 'b', 'c']); // const titles: string[]
```
What we gain here, ah yes developer can put one element in form of plain string, or many inside an array. Because of that we have introduced complexity in the form of:
- conditions inside implementations
- three function type definitions

What we gain is:
- use string for one element :)

Ok, but what wrong will happen if the function will be refactored into monomorphic form:

```ts
function f(id: string[]): string[] {
    return id.map(x => x + '_title');
}
const title = f(['a']); // brackets oh no :D
const titles = f(['a', 'b', 'c']);
```
The real difference is that we need to add brackets around our string, is it such big issue? Don't think so. We have predictable monomorphic function which is simple and clean in implementation.

## What about Elm

Let's switch the language to [Elm](https://elm-lang.org/), Elm is language which is simple and follow very strict rules. How ad-hoc polymorphism is resolved here? And the answer is - there is no such thing. Elm allows for parametric polymorphism, which should be familiar for you in form of generic types in many languages, but **there is no way to overload functions in Elm**.

Additionally such unions like `string | string[]` are not possible in Elm type system, the only way how we can be close to such is custom sum type. Consider following Elm example:

```elm
[ELM]
type UAge = Age Int | UAge { age: Int } -- custom type
isAdult : UAge -> Bool
isAdult str = case str of
    Age age -> age >= 18
    UAge u -> u.age >= 18

-- using
isAdult (UAge {age = 19})
isAdult (Age 19)  
``` 
In order to achieve the same in Elm, we need to introduce custom type, the custom type is simulating `number | {age: number}` from TypeScript. This custom type is a [sum type](https://dev.to/macsikora/algebraic-structures-explained-part-1-base-definitions-2576/#sum-type), in other words we can consider that our function really is monomorphic as the type is defined as  `UAge -> Bool`. Such practice in Elm is just a burden, and it is a burden because its not preferable to follow such ideas. The whole implementation should look like:

```elm
[ELM]
isAdult : Int -> Bool
isAdult age = age >= 18
-- using
isAdult user.age
isAdult 19 
```
And if you really have a need to call `isAdult` for user record, then use function composition
```elm
[ELM]
isUserAdult: { age: Int } -> Bool
isUserAdult u = isAdult u.age 
```
Function `isUserAdult` is just calling `isAdult`. The original function is user context free, it is more flexible to be used, is ideal component, and we can use `isAdult` for other objects not only with `age` property.

## Is ad-hoc polymorphism always wrong

No, but we need to be careful with this tool. Such polymorphism gives a lot of flexibility, we can overload functions to work with different type of objects. The whole Haskell type system is based on parametric and ad-hoc polymorphism, the later is implemented there in form of [typeclasses](https://en.wikipedia.org/wiki/Type_class). Thanks to such you can for example use operators like `<$>` or `>>=` for different instances. It is very powerful tool, but also one of main reasons why Haskell code is so difficult to grasp, level of abstraction is often very high and this is also because when you look at functions or operators, they can have different implementation for different types.

More low level and very usable example of ad-hoc polymorphism  is C++ function like [`to_string`](http://www.cplusplus.com/reference/string/to_string/) function which has many overloads for many types. That kind of usage is very useful. Consider what a burden it would be if you would need to create a different name for your `log` utility function for every different type. 

Functions and operators overloads is also very handy tool for introducing own algebras, if you want more information about this topic consider the series of articles about [algebraic structures](https://dev.to/macsikora/algebraic-structures-explained-part-1-base-definitions-2576/).

Conclusion. Use function overloads carefully, don't put complexity were it is not needed, there is no issue in putting value into brackets, function flexibility is not always the good thing. Consider composition over multi-purposes functions.

PS. Sorry for clickbait title