Skip to content

Instantly share code, notes, and snippets.

@ulve
Last active May 29, 2018 11:36
Show Gist options
  • Save ulve/31015fedf83efcc44262f16c221dee48 to your computer and use it in GitHub Desktop.
Save ulve/31015fedf83efcc44262f16c221dee48 to your computer and use it in GitHub Desktop.
representable fnuctors in typescript
// Jag har den här enkla map-klassen. Det är egentligen inte så stora fördelar
//med den men den har en map-funktion och det använder vi en hel del i vår
// lösning. Den har inte så mycket att göra med början på den här historien
// men den är rätt viktig senare!
class SimpleMap<T> {
private values: { [index: string]: T };
constructor() {
this.values = {};
}
map<U>(f: (x: T) => U): SimpleMap<U> {
const newMap = new SimpleMap<U>();
Object.keys(this.values).forEach(e => {
newMap.set(e, f(this.values[e]));
});
return newMap;
}
get(key: string): T {
return this.values[key];
}
set(key: string, value: T): SimpleMap<T> {
this.values[key] = value;
return this;
}
}
// Så här va. Jag har ett gäng komponenter som jag vill göra en hel massa
// saker med. Det här behövs ibland men inte alltid. Problemet är att det
// tar en del tid att göra sakerna när dom inte alltid behövs. Hur vore
// det om man kunde göra om en icke lazy implementation till lazy? Fins det
// något enkelt sätt?
// Vi börjar med exemplet. Det här är dom olika komponenter som finns
// Egentligen hade man kunnat ha dom som en enum eller ännu hellre en
// type Departments = "Finance" | "HR" och så vidare men det senare
// fungerar inte i typescript eftersom type inte finns kvar efter kompilering
// Det här duger utmärkt som vår indextyp
const departmentIds: string[] = [
"Finance",
"HR",
"Development",
"Costodial" /* etc */
];
// Här är då en avdelning som vi kommer att fylla på. Det är egentligen inget
// krav att alla departments ser likadana ut, vi kan göra en union type av det
// men för enkehletsskull ser allt likadant ut
interface IDepartment {
name: string;
fullyLoaded: boolean;
employees?: string[];
boss?: string;
// etc
}
// Sen gör jag om vår lista med avdelningsnamn till en lista med avdelningar.
// Alla är tomma just nu det är bara för att vi skall ha en startpunkt.
const empty = departmentIds.reduce((acc, key) => {
acc.set(key, key);
return acc;
}, new SimpleMap<string>());
// sen har vi då några funktioner här som bygger ut datan lite. Dom kan ta lite
// tid. Dom slår mot databasen och gör tunga beräkningar.
const loadDepartment = (name: string): IDepartment => ({
name: `id: ${name} fullName: ${name}`,
fullyLoaded: false
});
const loadEmployees = (o: IDepartment): IDepartment => ({
...o,
employees: ["Sven", "Erik", "Torkel"]
});
const loadBoss = (o: IDepartment): IDepartment => ({ ...o, boss: "Olov" });
const fullyLoaded = (o: IDepartment): IDepartment => ({
...o,
fullyLoaded: true
});
// nu kan vi mappa över vår struktur och göra allt som behövs. Rätt smidigt!
const fullLoad = empty
.map(loadDepartment)
.map(loadBoss)
.map(loadEmployees)
.map(fullyLoaded);
// Problemet med det här är att det kan ta ganska lång tid i vår lösning. Vi
// vill helst inte ladda saker i onödan även om alla skall finnas där när
// vi behöver dom så vore det ju toppen om man bara laddade dom en och en
// Här kommer representables in. En functor är representable ifall den är
// isomorfisk till en funktion. Vad betyder det? Ja kan man konvertera från
// en funktor till en funktion och sen konvertera tillbaks utan att tappa
// information så är fuctorn isomorfisk till funktionen. En functor är ju
// isomorfisk mot en annan functor om man kan konvertera from och tillbaks
// utan att förlora data och funktioner kan vara functorer dom med.
// När inträffar det här då? Ja det är svårt med functorer som innehåller
// variabel mängd data som t.ex. en lista. Det skall vara fast längd, då
// fungerar det bäst.
// Vi börjar från botten. Funktionen Id kan representeras av undefined
// Id innehåller ett värde och undefined är ett enda värde. Går det här
// att förstå?
// Id bör ha det här interfacet. Vi lägger på en map också annars blir det
// ingen functor av det
interface IId<T> {
x: T;
map: <U>(f: (x1: T) => U) => IId<U>;
}
// Och en data-constructor för Id
const Id = <T>(x: T): IId<T> => ({
x,
map: f => Id(f(x))
});
// Alltså är en functor som innehåller ett värde isomorfisk mot id. Det är
// kanske inte så svårt att förstå. Det är inte heller så himla viktigt
// eller användbart.
// Vad skall då to göra? Jo kollar man på signaturen så ser man vad vi vill ha
// to :: Id<T> -> (() -> T)
// Id för a borde returnera en funktion som rakt av ger tillbaks
// värdet på id. Här är det lite manuell curry då va men det ser bra ut
const idTo = <T>(thId: IId<T>) => () => thId.x;
// Sen så kollar vi på hur from skall se ut
// from :: (() -> T) -> Id<T>
// Jaha den tar alltså en funktion som tar noll argument och ger ett värde
// sen får man ett Id på det värde
const idFrom = <T>(f: () => T) => Id(f());
// Vi testar. Vi gör ett Id värde på fiskrens och sen kör vi till och from
// så bör vi få tillbaks fiskrens
const a1 = idFrom(idTo(Id("Fiskrens")));
a1.x === "Fiskrens";
// => true
// Vi testar att köra andra hållet. Vi gör en funktion som returnerar fiskrens
// sen from och sen to på den så bör vi ha en funktion som returnerar fiskrens
const a2 = idTo(idFrom(() => "Fiskrens")); // () => ‘hi’
a2() === "Fiskrens";
// => true
// Nu rullar vi på här! Trevligt! Men man kommer inte långt på en funktion som
// bara kan hålla ett värde va? Och hur i hela fridens dar har nått av det här
// med grundproblemet att göra?
// Vill vi ha fler värden i vår funktion så fungerar ju inte den här versionen
// som kör på Id-functionen. Två t.ex. hur gör man det?
// Ett par kan då encodas av en boolean. Vi gör en sån före vi går vidare
interface IPair<T> {
p1: T;
p2: T;
map: (f: (p: T) => T) => IPair<T>;
}
// och en data-constructor
const Pair = <T>(x: T, y: T): IPair<T> => ({
p1: x,
p2: y,
map: f => Pair(x, f(y))
});
// Hur skall då en pairTo se ut?
// pairTo :: Pair<T> -> (boolean -> T)
const pairTo = <T>(p: IPair<T>) => bool => (bool ? p.p1 : p.p2);
// Och en pairFrom
// pairFrom :: (boolean -> T) -> Pair<T>
const pairFrom = <T>(f: (x: boolean) => T) => Pair(f(true), f(false));
const a3 = pairFrom(pairTo(Pair("Katt", "Hund")));
// => Pair("Katt", "Hund")
const a4 = pairTo(pairFrom(x => (x ? "Hund" : "Katt")));
a4(true);
// => "Hund"
const a = a4(false);
// => "Katt"
// Okej det här är ju superfräsigt men ingen på hela jorden kan hitta någon
// användning för det här! Kom till poängen!
// Vad är då poängen? Jo vi vill kunna encoda mer data. Kanske inte fast antal
// värden bara. Hur vore det om vi kunde encoda till en SimpleMap?
// mapFrom :: (string -> T) -> SimpleMap<T>
// Vi har alltså en funktion som tar en sträng och ger tillbaks i vårt fall
// en IDepartment och som resultat så får vi tillbaks en SimpleMap<IDepartment>
// Vi bygger upp vår SimpleMap genom att göra om vår indexeringstyp till en map.
// Precis som vi gjorde först. Det här är ju precis som med Pair eller Id men
// vi har en SimpleMap istället. Den rymmer ju många fler värden. Prick så många
// som vi har i vår "indextyp"
const mapFrom = <T>(f: (d: string) => T) =>
departmentIds.reduce(
(acc: SimpleMap<T>, key: string) => acc.set(key, f(key)),
new SimpleMap()
);
// och med kraften i curry så skickar vi in en SimpleMap får tillbaks en funktion
// som helt enkelt hämtar ut värdet ur SimpleMap
// mapTo :: SimpleMap<T> -> (string -> T)
const mapTo = <T>(structure: SimpleMap<T>) => (key: string) =>
structure.get(key);
// Och då provar vi använda det!
const mapEmpty = mapFrom(x => x);
// const result = mapTo(mapEmpty).map(loadDepartment).map(loadBoss).map(loadEmployees).map(fullyLoaded)
// va tusan! Funktioner är inte en functor i Typescript och det
// betyder att det inte finns någon map!
// för att fixa det så tar vi hjälp av en gammal kompis som ingen
// trodde skulle användas. Reader-monaden!
const Reader = f => ({
run: f,
map: g => Reader(x => g(f(x))),
chain: g => Reader(x => g(f(x)).run(x))
});
// Det är ju så att map för en funktion egentligen bara är composition
// och istället för att lägga till map på Function.prototype så skulle
// man kunna vara fiffig och använda vår vän Reader-monaden
const katt = Reader(mapTo(mapEmpty))
.map(loadDepartment)
.map(loadBoss)
.map(loadEmployees)
.map(fullyLoaded);
// då körs inget när vi sätter upp sakerna. Vi kan fortfarande köra map
// för att bygga upp den tänkta strukturen men vi har i själva verket bara
// avnänt funktionskomposition. För att hämta ut vårt värde så kan vi köra
// den resulterande funktionen med run
const a5 = katt.run("HR");
a5;
// => { name: 'HR', loaded: true, boss: 'Olov', employees: [ 'Sven', 'Erik', 'Torkel' ] }
// Det är ju prick vad vi vill ha! Ännu en seger för superabstrakta lösningar!
// Okej i typscript är det ju inte direkt superenkelt att skriva allt det här
// Det är mer lämmpat för språk som har allt redan. Men råkar man ha sakerna så
// är det inte direkt några problem att använda det.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment