Skip to content

Instantly share code, notes, and snippets.

@lierdakil
Last active September 19, 2022 13:28
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lierdakil/2ece55b7684c5923b1ea4c36df643455 to your computer and use it in GitHub Desktop.
Save lierdakil/2ece55b7684c5923b1ea4c36df643455 to your computer and use it in GitHub Desktop.
An example of Functor in TypeScript. You can run this on https://www.typescriptlang.org/play/
interface Functor<T> {
map<U>(f: (x: T) => U): Functor<U>
}
class Box<T> implements Functor<T> {
value: T
constructor(x: T) {
this.value = x
}
map<U>(f: (x: T) => U): Box<U> {
return new Box(f(this.value))
}
toString(): string {
return `Box(${this.value.toString()})`
}
}
const box = <T> (x: T): Box<T> => new Box(x)
const log = (msg: string, x: any) => {
const pre = document.createElement('pre')
pre.innerText = `${msg} ==> ${x.toString()}`
document.body.appendChild(pre)
}
const trim = (x: string) => x.trim()
const len = (x: string) => x.length
const inc = (x: number) => x+1
function compose<T,U,V>(x: (arg: T) => U, y: (arg: U) => V): (arg: T) => V {
return arg => y(x(arg))
}
box(' 42 ') // contains a string
.map(trim)
.map(len)
.map(inc)
// vs
box([' 42 ', 'ABC ', 'x']) // contains an array
.map(array => array.map(trim))
.map(array => array.map(len))
.map(array => array.map(inc))
// is the same as
box(' 42 ') // contains a string
.map(compose(compose(trim, len), inc))
box([' 42 ', 'ABC ', 'x']) // contains an array
.map(array => array.map(compose(compose(trim, len), inc)))
// looks better if we do it like this
function fmap<T, U>(f: (arg: T) => U): (Fx: Functor<T>) => Functor<U> {
return Fx => Fx.map(f)
}
log(
"fmap(compose(compose(trim, len), inc))(box(' 42 '))",
fmap(compose(compose(trim, len), inc))(box(' 42 '))
)
log(
"fmap(fmap(compose(compose(trim, len), inc)))(box([' 42 ', 'ABC ', 'x']))",
fmap(fmap(compose(compose(trim, len), inc)))(box([' 42 ', 'ABC ', 'x']))
)
// the true powef of fmap is being applicable to any structure that can be mapped over
class Maybe<T> implements Functor<T>{
value: {val: T, type: 'just'} | {type: 'nothing'}
constructor(x?: T) {
if (x != null) this.value = { val: x, type: 'just' }
else this.value = { type: 'nothing' }
}
map<U>(f: (x: T | undefined) => U): Functor<U> {
if (this.value.type === 'just') return just(f(this.value.val))
else return nothing()
}
toString(): string {
if (this.value.type === 'just')
return `just(${this.value.val.toString()})`
else
return `nothing()`
}
}
const just = <T> (x: T) => new Maybe<T>(x)
const nothing = <T> () => new Maybe<T>() // bad style, I know, <T> is just a type assertion in disguise
// this is fmap over array
log(
"fmap(compose(compose(trim, len), inc))([' 42 ', 'ABC ', 'x'])",
fmap(compose(compose(trim, len), inc))([' 42 ', 'ABC ', 'x'])
)
//this is fmap over Maybe
log(
"fmap(compose(compose(trim, len), inc))(just(' 42 '))",
fmap(compose(compose(trim, len), inc))(just(' 42 '))
)
//it won't break if we don't provide a value though!
log(
"fmap(compose(compose(trim, len), inc))(nothing())",
fmap(compose(compose(trim, len), inc))(nothing())
)
// another simple example
class Either<T> implements Functor<T>{
value: {msg: string, type: 'left'} | {type: 'right', val: T}
constructor(msg: string)
constructor(msg: null, x: T)
constructor(msg: string | null, x?: T) {
if (msg === null) this.value = { val: x, type: 'right' }
else this.value = { type: 'left', msg }
}
map<U>(f: (x: T | undefined) => U): Functor<U> {
if (this.value.type === 'right') return right(f(this.value.val))
else return left(this.value.msg)
}
toString(): string {
if (this.value.type === 'right')
return `right(${this.value.val.toString()})`
else
return `left(${this.value.msg.toString()})`
}
}
const left = <T>(msg: string) => new Either<T>(msg)
const right = <T>(val: T) => new Either<T>(null, val)
//this is fmap over Either
log(
"fmap(compose(compose(trim, len), inc))(right(' 42 '))",
fmap(compose(compose(trim, len), inc))(right(' 42 '))
)
//this will return argument verbatim
log(
"fmap(compose(compose(trim, len), inc))(left('Something went wrong'))",
fmap(compose(compose(trim, len), inc))(left('Something went wrong'))
)
@alvaro-cuesta
Copy link

alvaro-cuesta commented Apr 8, 2019

For anyone that, like me, stumbles upon this through Google: this does not really adhere to the functor definition, because it can return a different Functor<T> from map (I was actually searching for a way to express the constraint when I arrived here).

class BadBox<T> implements Functor<T> {
  value: T
  constructor(x: T) {
    this.value = x
  }
  map<U>(f: (x: T) => U): Box<U> {
    return new Box(f(this.value))
  }
  toString(): string {
    return `BadBox(${this.value.toString()})`
  }
}

console.log(new BadBox(3).toString()); // -> BadBox(3)
console.log(new BadBox(3).map(x => 2 * x).toString()); // -> Box(3)  --  oops!

There doesn't seem to be any way to refer to the actual type of Functor<T> (something akin to Self) so I'm not sure it can be implemented in TypeScript.

@fvilante
Copy link

fvilante commented Apr 18, 2019

=========================
Updated: 09/05/2019
=========================

Type script must implement Higher-Kinded types to make this possible.

There exist some non-native forms to make HKT available in TS, but none of than is as easy as would be.

See the main issue on github here. For more details.

Vote bellow to incentive TS Team to solve this issue.

Note: I think you may ignore my original post bellow

=========================
ORIGINAL POST:
=========================

@alvaro-cuesta I was thinking about the problem you presented.

Basically I see two root causes:

  1. Typescript structural type-system See here
  2. The ausence of a Unit construct.

I started a solution from scratch. I think this solution can be mapped to a class/interface idiom, but unfortunatelly I do not have time to take this further step. Would appreciate if someone in internet can.

Code bellow is just a prove-of-concept.

// ==============================================
//  PROPOSAL OF SOLUTION
// ==============================================

// ================================
// Emulate nominal type-system
// ================================

type Kind<K> = { kind: K }
type Value<T> = { value: T }
type Container<K, T> = Kind<K> & Value<T> 

type Unit<K> = <T>(value: T) => Container<K, T>
const Unit = 
    <K extends string>(kind: K):Unit<K> => <T>(value: T) => 
        ({kind, value})

// ================================
//  The map (aka: Functor)
// ================================

// curied version
const map_ =
    <KB extends string>(unit: Unit<KB>) =>
    <A, B>(fn: (_: A) => B) =>
    <KA extends string>(m: Container<KA, A>): Container<KB, B> =>
        unit(fn(m.value))

// uncuried version
const map =
    <KB extends string, KA extends string, A, B>
    (unit: Unit<KB>,
    fn: (_: A) => B,
    m: Container<KA, A>): Container<KB, B> =>
        unit(fn(m.value))


// ================================
//  Use
// ================================

// Box now is a polimorphic nominal type constructor 
const Box = Unit("Box")
const OtherBox = Unit("OtherBox")

// example
const boxedString = Box("hello world") 
const boxedNumber = Box(10) 

// Functor map can't fail now
const mapbox = map(Box, (x:number) => x * 2, Box(3))
const mapOtherBox = map(OtherBox, (x:number) => x * 2, Box(3))

// You can use partially aplied version
const mapBox_ = map_(Box)
const mapOtherBox_ = map_(OtherBox)

// Use of curried version
const mapOtherBox_ = mapBox_((x:string) => x + "world")(Box("hello"))

Code written in Typescript 3.4.

Thank you @lierdakil for the original post.

@lierdakil
Copy link
Author

Huh. This is unexpectedly popular.

FWIW the original code was only intended to show the power of a given functional idiom to a colleague of mine using a familiar syntax. The intention wasn't ever to have a perfectly correct Functor implementation, which I doubt is entirely possible in TypeScript in the first place.

Neither I think one actually needs a Functor idiom in TypeScript per se -- when you find yourself looking for functional idioms in TypeScript, perhaps it's time to switch to PureScript, or GHCJS, or Elm, or something else I'm not aware of, which natively supports (or even enforces) the functional style, instead of trying to fit square pegs into round holes. Just my opinion though, which anyone's free to disagree with.

@lierdakil
Copy link
Author

lierdakil commented Apr 20, 2019

Actually, this made me curious, so I spent an hour or two on Google. So here's a couple references.

Basically, for the approach used in the OP to be completely sound, we need to have higher-kinded types in TS, which we don't. See microsoft/TypeScript#1213. And before that could happen, type inference would likely have to become significantly better (see discussion in microsoft/TypeScript#23809 and microsoft/TypeScript#24626)

Until we have native HKT support, we can emulate those in a couple different ways (a few of those are outlined in microsoft/TypeScript#1213 discussion), but all of those require concessions. One option, somewhat similar to what @fvilante suggested here in that it essentially makes HKTs nominal via tagging, is what fp-ts library does. Here's a brief overview: https://gist.github.com/gcanti/2b455c5008c2e1674ab3e8d5790cdad5#file-fp-ts-technical-overview-md

@fvilante
Copy link

fvilante commented May 9, 2019

It seems that the issue bellow is agregating all the demand to for Higher-Kinded types in Typescript:

issue#1213

I'm asking for my TS collegues to vote bellow to make some additional incentivel to TS Team address this issue:

Vote here
Thumbs Up, Hearts, etc !!

Thank you @lierdakil for yours considerations !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment