Skip to content

Instantly share code, notes, and snippets.

@ulve
Last active January 24, 2020 13:36
Show Gist options
  • Save ulve/82c015db253ab96245d03a438e5bd6b6 to your computer and use it in GitHub Desktop.
Save ulve/82c015db253ab96245d03a438e5bd6b6 to your computer and use it in GitHub Desktop.
Typescript catamorphism
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
)
); /*?*/
/* 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