Last active
January 24, 2020 13:36
-
-
Save ulve/82c015db253ab96245d03a438e5bd6b6 to your computer and use it in GitHub Desktop.
Typescript catamorphism
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface Book { | |
kind: "book"; | |
price: Number; | |
title: String; | |
} | |
interface Chocolate { | |
kind: "chocolate"; | |
taste: String; | |
price: Number; | |
} | |
interface Wrapping { | |
kind: "wrapping"; | |
pattern: String; | |
} | |
interface Wrapped { | |
kind: "wrapped"; | |
wrapping: Wrapping; | |
contains: Gift; | |
} | |
interface Boxed { | |
kind: "boxed"; | |
contains: Gift; | |
} | |
type Gift = Book | Chocolate | Wrapped | Boxed; | |
let book1: Gift = { | |
kind: "book", | |
title: "The Life of a Lumberjack", | |
price: 123 | |
}; /*?*/ | |
let chocolate1: Gift = { | |
kind: "chocolate", | |
price: 22, | |
taste: "Strawberry" | |
}; /*?*/ | |
let wrapping1: Wrapping = { kind: "wrapping", pattern: "diamonds" }; | |
// Data constructor for wrapped gifts | |
const wrapGift = (g: Gift, w: Wrapping): Gift => ({ | |
kind: "wrapped", | |
wrapping: w, | |
contains: g | |
}); | |
// Data constructor for boxed gifts | |
const boxGift = (g: Gift): Gift => ({ kind: "boxed", contains: g }); | |
let wrapped1: Gift = wrapGift(book1, wrapping1); /*?*/ | |
let boxed1: Gift = boxGift(chocolate1); /*?*/ | |
let wrapped2: Gift = wrapGift(boxed1, wrapping1); /*?*/ | |
const whatsInside = (g: Gift): String => { | |
switch (g.kind) { | |
case "chocolate": | |
return `Delicious ${g.taste} chocolate`; | |
case "book": | |
return `An interesting book named ${g.title}`; | |
case "wrapped": | |
return `wrapped in ${g.wrapping.pattern}...` + whatsInside(g.contains); | |
case "boxed": | |
return "boxed..." + whatsInside(g.contains); | |
} | |
}; | |
const totalCost = (g: Gift): Number => { | |
switch (g.kind) { | |
case "chocolate": | |
return g.price; | |
case "book": | |
return g.price; | |
case "wrapped": | |
return totalCost(g.contains); | |
case "boxed": | |
return totalCost(g.contains); | |
} | |
}; | |
console.log(whatsInside(book1)); | |
console.log(whatsInside(chocolate1)); | |
console.log(whatsInside(wrapped1)); | |
console.log(whatsInside(wrapped2)); | |
console.log(whatsInside(boxed1)); | |
console.log(`$ ${totalCost(book1)}`); | |
console.log(`$ ${totalCost(chocolate1)}`); | |
console.log(`$ ${totalCost(wrapped1)}`); | |
console.log(`$ ${totalCost(wrapped2)}`); | |
console.log(`$ ${totalCost(boxed1)}`); | |
const cataGift = <T>( | |
fBook: (a: Book) => T, | |
fChocolate: (a: Chocolate) => T, | |
fWrapped: (a: T, p: Wrapping) => T, | |
fBoxed: (a: T) => T, | |
g: Gift | |
): T => { | |
switch (g.kind) { | |
case "chocolate": | |
return fChocolate(g); | |
case "book": | |
return fBook(g); | |
case "wrapped": | |
return fWrapped( | |
cataGift(fBook, fChocolate, fWrapped, fBoxed, g.contains), | |
g.wrapping | |
); | |
case "boxed": | |
return fBoxed(cataGift(fBook, fChocolate, fWrapped, fBoxed, g.contains)); | |
} | |
}; | |
// Not the identity for real but close enough | |
const Id = (x, ...y) => x; | |
// Prints the content of a gift | |
const prettyPrint = (g: Gift): String => | |
cataGift( | |
b => `An interesting book named ${b.title}`, | |
b => `Delicious ${b.taste} chocolate`, | |
(c, p) => `wrapped in ${p.pattern}...` + c, | |
b => "boxed..." + b, | |
g | |
); | |
console.log(prettyPrint(book1)); | |
console.log(prettyPrint(chocolate1)); | |
console.log(prettyPrint(wrapped1)); | |
console.log(prettyPrint(wrapped2)); | |
console.log(prettyPrint(boxed1)); | |
console.log(cataGift(b => b.price, b => b.price, Id, Id, book1)); | |
console.log(cataGift(b => b.price, b => b.price, Id, Id, chocolate1)); | |
console.log(cataGift(b => b.price, b => b.price, Id, Id, wrapped1)); | |
console.log(cataGift(b => b.price, b => b.price, Id, Id, wrapped2)); | |
console.log(cataGift(b => b.price, b => b.price, Id, Id, boxed1)); | |
// A function that removes any wrapping | |
const unwrapper = (gift: Gift): Gift => | |
cataGift( | |
Id, // do nothing | |
Id, | |
Id, // This will throw away the wrapping | |
boxGift, // rebox | |
gift | |
); /*?*/ | |
unwrapper(wrapped1); /*?*/ | |
unwrapper(wrapped2); /*?*/ | |
unwrapper(chocolate1); /*?*/ | |
unwrapper(boxed1); /*?*/ | |
// A functions that removes any boxes (but rewraps any gifts) | |
const unboxer = (gift: Gift): Gift => | |
cataGift( | |
Id, | |
Id, | |
wrapGift, // rewrap | |
Id, | |
gift | |
); | |
unboxer(wrapped1); /*?*/ | |
unboxer(wrapped2); /*?*/ | |
unboxer(chocolate1); /*?*/ | |
unboxer(boxed1); /*?*/ | |
const nibble = c => ({ | |
kind: "chocolate", | |
taste: "half eaten " + c.taste, | |
price: c.price / 2 | |
}); | |
const nibbler = (gift: Gift): Gift => | |
cataGift(Id, nibble, wrapGift, boxGift, gift); /*?*/ | |
nibbler(wrapped2); /*?*/ | |
// Look! They compose! | |
nibbler(unboxer(unwrapper(wrapped1))); /*?*/ | |
nibbler(unboxer(unwrapper(wrapped2))); /*?*/ | |
unboxer(unwrapper(chocolate1)); /*?*/ | |
unboxer(unwrapper(boxed1)); /*?*/ | |
// and in any order! | |
nibbler(unboxer(unwrapper(wrapped2))); /*?*/ | |
unboxer(nibbler(unwrapper(wrapped2))); /*?*/ | |
unboxer(unwrapper(nibbler(wrapped2))); /*?*/ | |
// A list of gifts | |
let gifts: [Gift] = [book1, chocolate1, wrapped1, boxed1, wrapped2]; /*?*/ | |
// The total cost of all the gifts | |
gifts.reduce( | |
(a, x) => a + cataGift(b => b.price, b => b.price, Id, Id, x), | |
0 | |
); /*?*/ | |
// A list of the content | |
gifts.map(x => | |
cataGift( | |
b => `An interesting book named ${b.title}`, | |
b => `Delicious ${b.taste} chocolate`, | |
Id, | |
Id, | |
x | |
) | |
); /*?*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Types */ | |
interface Div { | |
kind: "div"; | |
class: string; | |
contains: [Html]; | |
} | |
interface Span { | |
kind: "span"; | |
class: string; | |
contains: [Html]; | |
} | |
interface TextElement { | |
kind: "textelement"; | |
value: string; | |
} | |
interface Image { | |
kind: "image"; | |
src: string; | |
} | |
type Html = Div | Span | TextElement | Image; | |
/* Test data */ | |
let img1: Html = { | |
kind: "image", | |
src: "/images/cat1.gif" | |
}; | |
let text1: Html = { | |
kind: "textelement", | |
value: "Interesting text" | |
}; | |
let div1: Html = { | |
kind: "div", | |
class: "imgContainer", | |
contains: [img1] | |
} | |
let span1: Html = { | |
kind: "span", | |
class: "fancyText", | |
contains: [text1] | |
} | |
let div2 : Html = { | |
kind: "div", | |
class: "superContainer", | |
contains: [div1, span1] | |
} | |
let div3 : Html = { | |
kind: "div", | |
class: "superContainer", | |
contains: [div2] | |
} | |
/* Catamorphism */ | |
const cataHtml = <T>( | |
fImg: (a: Image) => T, | |
fText: (a: TextElement) => T, | |
fSpan: (a: T[], b: string) => T, | |
fDiv: (a: T[], b: string) => T, | |
a : Html | |
) : T => { | |
switch(a.kind) { | |
case "image": | |
return fImg(a); | |
case "textelement": | |
return fText(a); | |
case "span": | |
return fSpan(a.contains.map(b => cataHtml(fImg, fText, fSpan, fDiv, b)), a.class); | |
case "div": | |
return fDiv(a.contains.map(b => cataHtml(fImg, fText, fSpan, fDiv, b)), a.class); | |
} | |
} | |
/* Test */ | |
const Id = (x, ...y) => x; | |
const prettyPrint = (a : Html) : string => | |
cataHtml( | |
b => `An image pointing to ${b.src}`, | |
b => `A text element continaing ${b.value}`, | |
(b, c) => b +` inside a span styled with ` + c, | |
(b, c) => b + ` inside a div styled with ` + c, | |
a | |
) | |
console.log(prettyPrint(div2)) | |
/* | |
An image pointing to /images/cat1.gif inside a div styled with imgContainer,A text element continaing Interesting text inside a span styled with fancyText inside a div styled with superContainer | |
*/ | |
const addCssClass = (a : Html, newClass: string) : Html => | |
cataHtml( | |
Id, | |
Id, | |
(b, c) => ({kind: "span", class: c + " newClass", contains: b }), | |
(b, c) => ({kind: "div", class: c + " newClass", contains: b }), | |
a) | |
console.log(addCssClass(div1, "NewClass")) | |
console.log(addCssClass(div2, "NewClass")) | |
console.log(addCssClass(div2, "NewClass")) | |
/* | |
{ kind: 'div', | |
class: 'superContainer newClass', | |
contains: | |
[ { kind: 'div', | |
class: 'imgContainer newClass', | |
contains: [{kind: 'img', src: '/images/cat1.gif'}] }, | |
{ kind: 'span', class: 'fancyText newClass', contains: [{ kind: 'textelement', value: 'Interesting text'}] } ] } | |
*/ | |
const allStylesUsed = (a: Html) : string[] => { | |
const withDuplicates = cataHtml( | |
b => [''], | |
b => [''], | |
(b, c) => | |
{ | |
return [].concat.apply([c], b) | |
}, | |
(b, c) => { | |
return [].concat.apply([c], b) | |
}, | |
a) | |
.filter(b => b !== '') | |
.sort() | |
return Array.from(new Set([...withDuplicates])); | |
} | |
console.log(allStylesUsed(div3)) | |
/* | |
['fancyText', 'imageContainer', 'superContainer'] | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment