Haskell’s type system currently doesn’t allow for partial functions, despite compilers allowing them. While you’re allowed to write a function that doesn’t accept certain arguments, the type system has no way to indicate this. This is an area where Haskell’s type system isn’t comprehensive. Now, I know a lot of people would scoff at the idea that Haskell’s type system isn’t comprehensive, but I suggest that while the generics and polymorphism are great, the concrete types themselves could be given more depth.

A type is a set of values. For example, Int is the set of all numbers that are divisible by 1. Bool is the set {True, False}. This isn’t a new idea. But while it is often used to explain type systems, it’s rarely used in practice. As an example of how it might be used, consider a function that takes two numbers and divides them.

Straightforward, right? But what if zero is passed in as b? Haskell evaluates that to Infinity. But why should it be allowed at all? If Int is a set, why can’t a type be a set of Ints that aren’t zero? Here’s some psuedo-haskell that displays this concept.

Here, only numbers that are not zero are members of n. In order to call div, your parameter must be either a constant, or of a type that is a subset of n.

This only gets us so far. What if we start chaining divisions? div (div a b) (div c d), for example, could produce a 0 denominator and be disallowed. Type comprehension becomes necessary.

A few things are being showcased here. First, two type parameters are declared. ia and ib represent the types that the user has for their a and b arguments. With these Int subsets given, we can return a proper subset of possible Ints using a type comprehension as the return type. Type comprehensions look similar to list comprehensions. In this case, the return type is defined as the set of a / b for all a in ia and all b in ib.

This gives the type system quite a bit of knowledge. For example, it knows at a type level that (div (div 8 2) (div 6 3)) has a return type of {2}, which guarantees that it will return 2. That expression is allowed because the type system knows that div 6 3 can’t return 0. So its return type is in fact a subset of the set of non-zero integers.

Note: This all assumes the division operator has roughly the same type signature. In which case, div = (/).

Now, this all works great for Ints. What about other types? Well suppose that the Int type is just an algebraic data type where every number is a constructor. Then, the same logic can be used for all algebraic data types.

By excluding the C constructor entirely, the function only needs to reason about the A and B constructors. If totality were enforced, this type system would be a correct way to allow partial functions, by simply disallowing certain constructors in the type.

Going back to the division example, you can see that you could write the return type of a function as essentially a mirror of the computation to get an accurate type. Theoretically, a pure programming language could be designed where the return type alone is sufficient as the computation. A good example of this would be the length function.

The return type is enough to express this computation. It is a set that represents the length of all lists. If you pass a subset of all lists to the function, it’s able to narrow the return type down to the correct subset. At runtime, the subset that is passed has only the list that you want the length of. Therefore, the return type is a subset with only one element, the return value.

If functions can be represented entirely using just their type, the compiler can have all the knowledge it will ever need about a thing’s type. It’s able to know exactly what that value could or couldn’t be.

There are some cool consequences. For example, if we allow sum-types on a type-set level, the Either type becomes unnecessary.

Ignoring that SomeData type is basically Either, the return type of “get” is a subset of a+b, where a and b are the SomeData type parameters. It’s a heterogenous type. Since in x, an A is passed, and the type parameter is Int, the return type is known to contain only Ints (a subset indeed of Int+b), specifically 3. Therefore x is allowed to have the type Int (although the type system knows that x’s actual type is {3}).

Another interesting example, where a heterogeneous type can be used to compute a homogenous return.

Additionally, this can be used to help the infamous records problem in haskell. The generated getter functions can have their type specialized to a subset of ADT values that have that record name. This lets any ADTs in the same module have records with values of the same name, without generating a partial getter function.

The type signature here specifies that the passed value can be either ADT, as long as it’s not the wrong constructor, and the return type will return something of a subset of String+Int. Except, by passing in a value whose type is just one of the ADTs, the type system is able to determine if the return value is an Int or a String.

The biggest issue with this idea that I can see is that calculating sets and determining whether A subsets B could be very straining on a compiler. It’s nearly running your program for you at compile time. I’m not sure how this could be mitigated, but I imagine that the compiler can be lazy, and only evaluate sets when it needs to.

Finishing up, I think this idea could solve a few problems, while opening the door for a lot of new concepts. It’s much easier for the system to know precisely and accurately what values it’s dealing with. If nothing else, I find it an interesting direction to go in.