Skip to content

Commit

Permalink
Merge pull request #20 from gcanti/amazing
Browse files Browse the repository at this point in the history
New technique to fake higher kinded types (v0.2)
  • Loading branch information
gcanti committed Mar 31, 2017
2 parents 5808c9b + e3cc231 commit 1a9d071
Show file tree
Hide file tree
Showing 52 changed files with 978 additions and 1,552 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,11 @@
**Note**: Gaps between patch versions are faulty/broken releases.
**Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.

# 0.2

- **Breaking Change**
- complete refactoring: new technique to get higher kinded types and typeclasses

# 0.1.1

- **New Feature**
Expand Down
326 changes: 31 additions & 295 deletions README.md
Expand Up @@ -5,8 +5,6 @@ A mix of
- PureScript
- Scala

The idea (faking higher kinded types in TypeScript) is based on the paper [Lightweight higher-kinded polymorphism](https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf), [elm-brands](https://github.com/joneshf/elm-brands) and [flow-static-land](https://github.com/gcanti/flow-static-land).

See the section "Technical overview" below for an explanation of the technique.

# Algebraic types
Expand Down Expand Up @@ -53,331 +51,69 @@ See the section "Technical overview" below for an explanation of the technique.

# Technical overview

## A basic `Option` type

File: Option.ts

```ts
// definition
type None = {
__tag: 'None'
}

type Some<A> = {
__tag: 'Some',
value: A
}

type Option<A> = None | Some<A>

// helpers
const none: None = { __tag: 'None' }

function some<A>(a: A): Option<A> {
return { __tag: 'Some', value: a }
}

// a specialised map for Option
function map<A, B>(f: (a: A) => B, fa: Option<A>): Option<B> {
switch (fa.__tag) {
case 'None' :
return fa
case 'Some' :
return some(f(fa.value))
}
}
```

Usage

```ts
const double = (n: number): number => n * 2
const length = (s: string): number => s.length

console.log(map(double, some(1))) // { __tag: 'Some', value: 2 }
console.log(map(double, none)) // { __tag: 'None' }
console.log(map(length, some(2))) // <= error
```

## Adding static land support

TypeScript doesn't support higher kinded types

```ts
interface StaticFunctor {
map<A, B>(f: (a: A) => B, fa: ?): ?
}
```

but we can fake them with an interface

```ts
interface HKT<F, A> {
__hkt: F
__hkta: A
}
```

where `F` is a unique identifier representing the type constructor and `A` its type parameter.

Now we can define a generic `StaticFunctor` interface

```ts
interface StaticFunctor<F> {
map<A, B>(f: (a: A) => B, fa: HKT<F, A>): HKT<F, B>
}
```

and a new `Option` type

```ts
// unique identifier
type URI = 'Option'

type None = {
__tag: 'None'
__hkt: URI
__hkta: any
}

type Some<A> = {
__tag: 'Some',
__hkt: URI
__hkta: A
value: A
}

type Option<A> = None | Some<A>

const none: None = {
__tag: 'None',
__hkt: 'Option',
__hkta: undefined as any
}

function some<A>(a: A): Option<A> {
return {
__tag: 'Some',
__hkt: 'Option',
__hkta: a,
value: a
}
}

function map<A, B>(f: (a: A) => B, fa: Option<A>): Option<B> {
switch (fa.__tag) {
case 'None' :
return fa
case 'Some' :
return some(f(fa.value))
}
}
```

Let's check the implementation

```ts
// if this type-checks the signature is likely correct
;({ map } as StaticFunctor<URI>)
```

Usage

```ts
console.log(map(double, some(1))) // { __tag: 'Some', __hkt: 'Option', __hkta: 2, value: 2 }
console.log(map(double, none)) // { __tag: 'None', __hkt: 'Option', __hkta: undefined }
console.log(map(length, some(2))) // <= error
```

Exports can be directly used as a static land dictionary

```ts
import * as option from './Option' // option contains map
```

There's a problem though. Let's define a generic `lift` function based on the `StaticFunctor` interface

```ts
class FunctorOps {
lift<F, A, B>(functor: StaticFunctor<F>, f: (a: A) => B): (fa: HKT<F, A>) => HKT<F, B> {
return fa => functor.map(f, fa)
}
}

const ops = new FunctorOps()
```

If we try to use `lift` and `map` together TypeScript raises an error

```ts
const maybeLength = ops.lift({ map }, length)

map(double, maybeLength(some('hello')))
/*
Argument of type 'HKT<"Option", number>' is not assignable to parameter of type 'Option<number>'.
Type 'HKT<"Option", number>' is not assignable to type 'Some<number>'.
Property '__tag' is missing in type 'HKT<"Option", number>'
*/
```

Every `Option<A>` is a `HKT<"Option", A>` but the converse is not true. In order to fix this (we **know** that `Option<A> = HKT<"Option", A>`) functions like `map` should accept the more general version `HKT<"Option", A>` and return the more specific version `Option<A>`

```ts
type HKTOption<A> = HKT<URI, A>

function map<A, B>(f: (a: A) => B, fa: HKTOption<A>): Option<B> {
const option = fa as Option<A>
switch (option.__tag) {
case 'None' :
return option
case 'Some' :
return some(f(option.value))
}
}
## Higher kinded types and type classes

map(double, maybeLength(some('hello'))) // ok
```
Higher kinded types are represented by a unique string literal (called `URI`).

We can do even better. Note that `maybeLength` has the following signature
There's a central type dictionary where a mapping `URI` -> concrete type is stored

```ts
(fa: HKT<"Option", string>) => HKT<"Option", number>
// file ./HTK.ts
export interface HKT<A> {}
```

We'd like to have `(fa: Option<string>) => Option<number>` instead.

We'll use a feature called [Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) for that

Let's move the `Functor` definition to its own file

File: Functor.ts

```ts
export interface StaticFunctor<F> {
map<A, B>(f: (a: A) => B, fa: HKT<F, A>): HKT<F, B>
}

export class FunctorOps {
// base signature
lift<F, A, B>(functor: StaticFunctor<F>, f: (a: A) => B): (fa: HKT<F, A>) => HKT<F, B>
lift<F, A, B>(functor: StaticFunctor<F>, f: (a: A) => B): (fa: HKT<F, A>) => HKT<F, B> {
return fa => functor.map(f, fa)
}
}

export const ops = new FunctorOps()
```
Instances can be defined (everywhere) using a feature called [Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) so there's no danger of name conflict (the typechecker checks for duplicates).

File: Option.ts
Here's a mapping between the string literal `'Option'` and the concrete type `Option<A>`

```ts
declare module './Functor' {
interface FunctorOps {
// specialized signature for Option
lift<A, B>(functor: StaticFunctor<URI>, f: (a: A) => B): (fa: Option<A>) => Option<B>
// file ./Option.ts
declare module './HKT' {
interface HKT<A> {
Option: Option<A>
}
}
```

That means that `lift` is truly polimophic and may have a specialized signature for any higher kinded type.

## Adding fantasy land support

We can define a generic `FantasyFunctor` interface

```ts
interface FantasyFunctor<F, A> extends HKT<F, A> {
map<B>(f: (a: A) => B): FantasyFunctor<F, B>
}
```

And now let's change the implementation of `None` and `Some`
export const URI = 'Option'
export type URI = typeof URI
export type Option<A> = None<A> | Some<A>

```ts
type URI = 'Option'

class None<A> implements FantasyFunctor<URI, A> {
__tag: 'None'
__hkt: URI
__hkta: any
export class None<A> {
readonly _URI: URI
map<B>(f: (a: A) => B): Option<B> {
return none
}
...
}

class Some<A> implements FantasyFunctor<URI, A> {
__tag: 'Some'
__hkt: URI
__hkta: A
constructor(public value: A) { }
export Some<A> {
readonly _URI: URI
// fantasy-land implementation
map<B>(f: (a: A) => B): Option<B> {
return some(f(this.value))
return new Some(f(this.value))
}
...
}

type Option<A> = None<A> | Some<A>

const none = new None<any>()

function some<A>(a: A): Option<A> {
return new Some(a)
}
```

Note that `None` has a type parameter, because the signature of `map` (the method) must be the same for both `None` and `Some` otherwise TypeScript will complain.

The implementation of `map` (the static function) is now trivial.

```ts
function map<A, B>(f: (a: A) => B, fa: HKTOption<A>): Option<B> {
return (fa as Option<A>).map(f)
// static-land implementation
export function map<A, B>(f: (a: A) => B, fa: Option<A>): Option<B> {
return fa.map(f)
}
```

## Faking Haskell's type classes

Let's add to `FunctorOps` a polimorphic `map` function

```ts
export class FunctorOps {
lift<F, A, B>(functor: StaticFunctor<F>, f: (a: A) => B): (fa: HKT<F, A>) => HKT<F, B>
lift<F, A, B>(functor: StaticFunctor<F>, f: (a: A) => B): (fa: HKT<F, A>) => HKT<F, B> {
return fa => functor.map(f, fa)
}
Concrete types can be retrieved by their `URI` using a feature called [Index types](https://www.typescriptlang.org/docs/handbook/advanced-types.html).

map<F, A, B>(f: (a: A) => B, fa: FantasyFunctor<F, A>): FantasyFunctor<F, B>
map<F, A, B>(f: (a: A) => B, fa: FantasyFunctor<F, A>): FantasyFunctor<F, B> {
return fa.map(f)
}
}
```
Type classes are implemented following (when possible) both the [static-land](https://github.com/rpominov/static-land) spec and the [fantasy-land](https://github.com/fantasyland/fantasy-land) spec.

And the corresponding module augmentation in the `Option.ts` file
Here's the definition of the type class `Functor` (following the static-land spec)

```ts
declare module './Functor' {
interface FunctorOps {
lift<A, B>(functor: StaticFunctor<URI>, f: (a: A) => B): (fa: Option<A>) => Option<B>
map<A, B>(f: (a: A) => B, fa: HKTOption<A>): Option<B>
}
export interface StaticFunctor<F extends HKTS> {
URI: F
map<A, B>(f: (a: A) => B, fa: HKT<A>[F]): HKT<B>[F]
}
```

> Roughly speaking the `map` definition in `FunctorOps` corresponds to a type class definition, while the module augmentation part corresponds to declaring an instance
Now `map` is a truly polimorphic function with prefect type inference

```ts
import * as option from 'fp-ts/lib/Option'
import * as io from 'fp-ts/lib/IO'
const length = (s: string): number => s.length

// x :: Option<number>
const x = map(length, option.of('hello'))
// y :: IO<number>
const y = map(length, io.of('hello'))
```

# License

The MIT License (MIT)

0 comments on commit 1a9d071

Please sign in to comment.