Last active
May 29, 2018 11:36
-
-
Save ulve/31015fedf83efcc44262f16c221dee48 to your computer and use it in GitHub Desktop.
representable fnuctors in typescript
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
// 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