6
$\begingroup$

A problem I sometimes run into when using a language like TypeScript or C# is how they lack a perfect analogue to Haskell's typeclasses. Let's use Haskell's Functor typeclass as an example:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

The closest analogue in TypeScript would be an object conforming to an interface like:

interface Functor<T> {
  fmap <U>(func: (value: T) => U): Functor<U>
}

But here we run into a problem: TypeScript can't have a type constructor as a type parameter and the interface instructs us only to return any Functor, but this is not what we want a Functor to do. For TypeScript the following is fully conformant to the Functor interface.

class Box<T> implements Functor<T> {
  fmap <U>(func: (value: T) => U): UnrelatedFunctor<U> { /* ... */ }
}

In a simple type (without type parameter) this isn't a problem thanks to polymorphic this:

class Simple {
  doSomething (): this
}
class SimplEx extends Simple {}

const simEx: SimpleEx = // type is correct 
  new SimpleEx().doSomething() 

So for this to work we need to somehow reference our future implementing type like Haskell does it; A "forward" parameter. By referencing this parameter a type-checker would know we want an object conforming to the implementing type, not the base-type.

In pseudo-TypeScript this could look something like:

// `F`` "inherits" kind (* -> *) from `Functor`
//                ∨
interface Functor(F)<T> {
  // now we apply our argument directly to this parameter
  //                               ∨
  fmap <U>(func: (value: T) => U): F<U>
}

So the questions I have are:

  1. Is that a sound solution to my problem or am I walking into a dead-end without realizing it?
  2. Is this intuitive, particularly for people without much experience in Haskell-like languages or higher-order types?
$\endgroup$
4
  • 1
    $\begingroup$ Check out F-bounds. They're one approach to this problem in languages like Scala, but they don't go all the way (you could still do class Box<T> extends Functor<UnrelatedFunctor> (where Functor would be defined as taking a type constructor as an argument) $\endgroup$
    – user
    Jan 4 at 19:11
  • $\begingroup$ Also, I'm not sure what this has to do with syntax. Are you asking for syntax options for higher-kinded types? $\endgroup$
    – user
    Jan 4 at 19:14
  • $\begingroup$ @user You're right, it's really less of a syntax question than it is conceptual. Changed the title to reflect that. The syntax-part was mainly about how you could style this intuitively inside a more object oriented concept using classes and interfaces. I'm gonna check out F-bounds and Scala; from a short look that should be usefull, thanks. $\endgroup$ Jan 4 at 20:04
  • 1
    $\begingroup$ This question on SO might be related: stackoverflow.com/questions/35951818 $\endgroup$
    – raner
    Jan 9 at 5:43

1 Answer 1

2
$\begingroup$

If you can declare a type to be a member of a class after the type is declared, then what this means is that all of the information required by that membership (e.g. virtual dispatch tables) cannot be stored in the type itself.

At the point where the membership property is used in the program, at least one of two things must happen:

  • It is resolved at compile time.
  • It is available at run-time via some other mechanism.

An example of the first is constraints-and-concepts in C++, which is a more-strongly-typed version of something that C++ programmers have been doing with templates since the 80s.

As for the second approach, there are essentially two possibilities: pass them as function arguments (possibly implicitly), or associate them with object references. This corresponds, more or less, to type constraints and existential types in Haskell respectively.

Passing virtual dispatch tables around as function arguments has a long history. It's essentially how the C bindings for distributed object systems (e.g. COM) work.

Existential types also have a long history. There are many C APIs where a function takes an opaque void pointer argument, which is an untyped version of this. Also consider the associated "deleters" in C++ smart pointers.

Before designing this into an imperative programming language, however, you should be aware that existential types interact subtly with mutable objects. Famously, the type system of Cyclone (an ancestor of Rust) was broken for several months until the flaw was spotted.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .