4

I'm exploring the Typescript type system by implementing the Fantasy Land Spec and I ran into an issue while trying to implement the spec for Semigroup.

The spec stipulates that a Semigroup should adhere to the following type definition:

concat :: Semigroup a => a ~> a -> a

I understand this to mean that a type a, which implements Semigroup, should have a concat method that takes in a parameter of type a and returns a parameter of type a.

The only way I could think of expressing this type definition in TypeScript is this:

interface Semigroup {
    concat(other: this): this;
}

But when I try to implement this interface on a class, like this:

class Sum implements Semigroup {
    constructor(readonly num: number) {}

    concat(other: Sum): Sum {
        return new Sum(this.num + other.num);
    }
}

I get a compiler error telling me that:

Property 'concat' in type 'Sum' is not assignable to the same property in base type 'Semigroup'.
  Type '(other: Sum) => Sum' is not assignable to type '(other: this) => this'.
    Type 'Sum' is not assignable to type 'this'.
      'Sum' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Sum'.(2416)

Thanks to this S/O answer, I think I understand the problem.

I think the compiler is essentially telling me: your interface says that you should be taking a parameter that is of the concrete type this (Sum in this particular case), but a class that extends Sum could also be passed in.

However, I don't know how to fix it. That is, I don't know how to express the type definition for Semigroup in TypeScript. How to reference the implementing class from an interface?

Here is a link to a TS Playground.

Update

@Guerric P's answer made me think of a partial solution. Guerric's solution was to use a generic on the interface. This solution makes implementing the Semigroup spec possible, as shown here, but the interface doesn't really enforce it.

The fantasy land further describes the spec as follows:

s.concat(b)

/** 
 * `b` must be a value of the same `Semigroup`
 *
 * If `b` is not the same semigroup, behaviour of `concat` is 
 * unspecified.
 *
 * `concat` must return a value of the same `Semigroup`.
 */

Instead of making b a generic, I figured that we could at least restrict the type to Semigroup. That way it enforces the constraint that b must be of type Semigroup as shown here:

interface Semigroup {
    concat(other: Semigroup): Semigroup;
}

But it still doesn't enforce that it must be of the SAME Semigroup. I'm still looking for a way to do that with the TypeScript type system.

snowfrogdev
  • 5,963
  • 3
  • 31
  • 58

1 Answers1

2

I don't want to question your interpretation of that fantasy-land spec, which I admit I don't fully understand, so I'll assume your interpretation is right .

The problem is that your class could be extended, so this could refer to that extended class. There is no such thing as final class or equivalent in TypeScript.

Now let's suppose you have an ExtendedSum class which extends Sum. Your equals implementation still works because (other: Sum) => boolean is assignable to (other: ExtendedSum) => boolean. Indeed, a function that takes a Sum as parameter can take an ExtendedSum as well (structural typing principle).

However, your concat implementation doesn't work because (other: Sum) => Sum is not assignable to (other: ExtendedSum) => ExtendedSum. Indeed, a function that returns a Sum is not assignable to a function that returns an ExtendedSum because a Sum is not necessarily an ExtendedSum.

You could fix that with a generic typed interface:

interface Semigroup<T> {
    concat(other: T): T;
}

class Sum implements Setoid, Semigroup<Sum> {
    constructor(readonly num: number) {}

    equals(other: Sum): boolean {
        return this.num === other.num;
    }

    concat(other: Sum): Sum {
        return new Sum(this.num + other.num);
    }
}

TypeScript playground

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • This would make the correct implementation of the Semigroup type possible, but it wouldn't enforce its contract. With such an interface it would be possible to do `class Sum implements Semigroup { concat(other: Date) } ` https://tsplay.dev/NVgpqm – snowfrogdev Jan 06 '22 at 23:55
  • But this made me think of a slightly better, though not perfect, solution. We can at least restrict the type of `T` to `Semigroup`. https://tsplay.dev/WoJEjm – snowfrogdev Jan 07 '22 at 00:02