
Funksjonell programmering i Enonic XP #2 - TypeScript
Vi anbefaler på det sterkeste å bruke TypeScript på serveren når du jobber med funksjonell programmering i XP. Typeskjekking av en kompilator vil fange opp mange feil før de havner i produksjon.
Hvordan kompilere TypeScript i Enonic
- Bruk webpack-starter for å sette opp et Enonic-prosjekt. TypeScript-støtte er inkludert (anbefalt).
- Alternativt kan man installere TypeScript fra npm og bruke
tsc
for å kompilere TS til JS.npm install -g typescript tsc my-file.ts
Hva er TypeScript?
TypeScript er JavaScript pluss statiske typer. Man kompilerer TypeScript til JavaScript, slik at vi kan kjøre det backend i Enonic XP, eller i nettleseren.
Variabler med typer
For å uttrykke at en variabel har en type kan man gjøre følgende:
// Slik gir vi variablene typer:
let myNumber: number = 8;
let myString: string = "hei";
// TypeScript-kompilatoren vil ikke tillate dette, siden vi prøver å sette en 'number'-variabel til en 'string':
myNumber = myString;
Interfaces
For å begrense formen på et objekt i TypeScript definerer vi et interface
. Det er mye tryggere og enklere å programmere mot et objekt, hvis du kan være helt sikker på formen på objektet.
// Alle objekter av typen Employee, må følge denne formen
interface Employee {
id: number;
name: string;
email?: string; // email har "?" som gjør at 'undefined' er en lovlig verdi
}
const employee1: Employee = {
id: 007,
name: "James Bond",
email: "jbond@universal-exports.com"
};
// TypeScript-kompilatoren vil ikke tillate dette siden 'id' og 'name' er obligatoriske felter
const employee2: Employee = {};
Fra Content Type til Interface
En Content Type i Enonic beskriver formen på json-data som lagres. Og det er jo akkurat det Interfaces også gjør.
Derfor har vi laget en open source Gradle-plugin som brukes som leser content type-xmler, og genererer Typescript-interfaces.
Eksempel:
Gitt at vi har en content type beskrevet i content-types/article/article.xml:
<?xml version="1.0" encoding="UTF-8"?>
<content-type>
<display-name>Article</display-name>
<super-type>base:structured</super-type>
<form>
<input name="title" type="TextLine">
<label>Title of the article</label>
<occurrences minimum="1" maximum="1"/>
</input>
<input name="body" type="HtmlArea">
<label>Main text body</label>
<occurrences minimum="0" maximum="1"/>
</input>
</form>
</content-type>
Så vil det genereres en fil (i samme mappe) som heter content-types/article/article.ts:
export interface Article {
/**
* Title of the article
*/
title: string;
/**
* Main text body
*/
body?: string;
}
Dette TypeScript-interfacet kan nå brukes serverside i Enonic. Dette gir oss en sikkerhet på at koden vår alltid er kompatibel med content som den får fra Enonic. Skrivefeil vil rett og slett ikke kompilere, og man vil ikke klare å bygge prosjekter før feilen har blitt rettet.
Det er også veldig deilig at den gir oss code completion i IDEer, noe som gjør at vi kan kode raskere og vite at koden er korrekt.
Og hvis man kjører genereringa av TypeScript hver gang man bygger i Enonic, så tvinges TypeScript-koden til å bli oppdatert til å følge oppdateringer i Content typen, ellers vil bygging feile.
Eksempel på bruk av interface i service/article/article.ts:
import {Request, Response} from 'enonic-types/controller';
import {Content, ContentLibrary} from 'enonic-types/content';
import {Article} from '../../site/content-types/article/article'
const contentLib: ContentLibrary = __non_webpack_require__("/lib/xp/content");
export function get(req: Request): Response {
// Resultatet er av type "Content", og content.data er av type "Article"
const content: Content<Article> = contentLib.get({ key: req.params.key! })!;
return {
status: 200,
body: {
id: content._id,
title: content.data.title,
text: content.data.text // Denne linja vil feile kompilatoren, siden "Article" fra forrige eksempel ikke har feltet "text"
}
}
}
Det regnes som dårlig praksis å bruke !
for å fjerne nullability. Vi gjør her for å holde disse eksemplene så enkle som mulig.
Fra site, page og part til interface
Det er også mulig å generere interfaces for sites, pages, parts, layouts og id-providers.
Prinsippene er det samme som for content types, så vi blir ikke å gå mer i detalj på disse i denne artikkelen. Men det viktige er at vi får den samme tryggheten og tette koblingen mellom xml og TypeScript for all data i Enonic som baserer seg på xml-konfigurasjon.
Typer for Enonic biblioteker
Vi har laget et bibliotek med TypeScript definisjoner for Enonics standardbiblioteker som heter enonic-types. Følgende standard biblioteker er støttet:
- AuthLibrary
- CommonLibrary
- ContentLibrary
- ContextLibrary
- EncodingLibrary
- EventLibrary
- HttpLibrary
- I18nLibrary
- IOLibrary
- MailLibrary
- MenuLibrary
- NodeLibrary
- PortalLibrary
- RecaptchaLibrary
- RepoLibrary
- RouterLibrary
- SessionLibrary
- ThymeleafLibrary
- ValueLibrary
- WebsocketLibrary
Funksjonell programmering med fp-ts
Siden funksjonell programmering i stor grad handler om å ta output fra en funksjon, og bruke det som input i neste funksjon (funksjonskomposisjon), er det ekstra viktig å ha kontroll på typene. Dette gjelder spesielt når vi begynner å bruke mer komplekse typer fra fp-ts.
Vi skal her se på noen funksjoner som går igjen i mange typer. Vi finner map
, chain
og fold
, i for eksepel Array
, Option
og Either
.
Vi skal her se på hvordan vi bruker dem med Array
, mens påfølgende artikler vil ta for seg de andre typene. Ja, de samme metodene (eller tilsvarende) finnes i vanilla-JS/vanilla-TS, så bruk gjerne de vanlige i den daglige programmeringa. Her bruker vi fp-ts-funksjonene kun for å demonstrere det konseptuelle.
map
Vi bruker map
for å kjøre en funksjon på alle elementene i et array og returnere resultatet.
import { map } from 'fp-ts/lib/Array';
const arr: Array<string> = ["1", "2", "3"];
const result: Array<number> = map(parseInt)(arr);
// [1, 2, 3]
Vi kan se at map
fra fp-ts tar parameterene på en litt annen måte enn vi er vant med fra Array.prototype.map. I fp-ts tar map én parameter – som er funksjonen som skal kjøres – og returnerer en ny funksjon som tar én parameter, og det er selve arrayet.
Dette er en teknikk som heter currying, og hvis dette er ukjent for deg anbefaler jeg at du leser Professor Frisby's Mostly Adequate Guide to Functional Programming kapittel 4.
chain
En chain
er veldig lik en map
. Et annet navn på chain er flatMap
(se Array.prototype.flatMap). Vi bruker chain
, når callback
funksjonen også retunerer et Array
, og vi ønsker å flate ut (flatten
) de to nivåene av Arrays
, så det bare blir ett nivå. Husk at chain
bare flater ut på to nivåer, selv om du har enda flere nivåer med nested Array
.
import { chain, map } from 'fp-ts/lib/Array';
const arr: Array<string> = ["Functional Programming", "Enonic XP", "in TypeScript"];
const searchTerms: Array<string> = chain(str => str.split(" "))(arr);
// ["Functional", "Programming", "Enonic", "XP", "in", "TypeScript"]
// USING THE SAME FUNCTION IN "map"
const fromNormalMap: Array<Array<string>> = map(str => str.split(" "))(arr);
// [["Functional", "Programming"], ["Enonic", "XP"], ["in", "TypeScript"]]
For andre typer som Option<A>
eller Either<E, A>
ønsker man alltid bare ha ett nivå dybde (altså ikke noe av type Option<Option<string>>
), derfor er chain
veldig nyttig for å slå sammen flere nivåer, når callback
funksjonen returnere et resultat av samme type.
fold
Vi bruker fold
for å pakke ut verdien som er inni en type. En nært beslektet funksjon er Array.prototype.reduce, med den forskjellen at reduce
tar en initsiell verdi, og fold
ikke gjøre det.
For Array
kan man bestemme hvilken vei man ønsker å iterere, for å generere et resultat, så det eksisterer foldLeft
og foldRight
. I eksempelet under ser vi på foldLeft
.
foldLeft
gir oss en mulighet til å bruke rekursjon til å iterere gjennom hele arrayet, og foldLeft
tar to argumenter.
onNil
brukes til å terminere rekursjonen når man sender inn et tomtArray
og tar en funksjon med 0 argumenter.onCons
tar en funksjon med 2 argumenter, hvorhead
er det første elementet i arrayet ogtail
er resten av arrayet (som kan brukes rekursivt med samme funksjon).
import { foldLeft } from 'fp-ts/lib/Array'; // For array har vi "foldLeft" og "foldRight" i stedet for bare "fold"
const arr: Array<number> = [1, 2, 3];
Сode
const getSum = foldLeft(
() => 0,
(head, tail) => head + getSum(tail) // 1 + 2 + 3 + 0
);
const sum = getSum(arr);
// sum = 6
For andre typer enn Array
, bruker vi gjerne fold
som siste steg av det vi gjør, for å generere det vi skal returnere. Hvis det er i en controller vi er, brukes gjerne fold
til å gjøre en IOEither
om til en Response
.
Pipe
Vi kan bruke pipe
til å sette opp en kjede, der output fra en funksjon blir tatt som input i neste funksjon.
Eksempel:
import {pipe} from "fp-ts/lib/pipeable";
const result = pipe( // 1
1, // 2
(num) => num + 5, // 3
(num) => num * 2,
);
// result = 12
-
pipe
er en funksjon som tar en verdi som sitt første argument, og alle påfølgende argumenter er funksjoner som kun tar ett argument. - Den første parameteren er verdien
1
.
(Det er vanlig å liste ut parameterene tilpipe
nedover i stedet for inline). - Alle påfølgende parametere til
pipe
er nå funksjoner som som tar resultatet fra linja over som input.
Pipe med fp-ts
pipe
blir fort veldig nyttig når man ønsker å gjøre en rekke operasjoner fra fp-ts på rekke.
Eksempel:
import { pipe } from "fp-ts/lib/pipeable";
import { map, foldLeft } from "fp-ts/lib/Array";
// getSum is a function(input: Array<number): number;
const getSum: (input: Array<number>) => number // 1
= foldLeft(
() => 0,
(head, tail) => head + getSum(tail)
);
function increment(num: number) {
return num + 1;
}
const sum: number = pipe(
[1, 2, 3], // 2
map(increment), // 3
getSum // 4
);
// sum = 9
-
Samme
getSum
som i eksempelet over -
Verdien som er første parameter i
pipe
er her av typeArray<number>
-
Vi bruker
map
fra fp-ts/lib/Array til å kalleincrement
funksjonen på hver verdi i arrayet, og så returnere resultatene som etArray<number>
. -
En
pipe
vil ofte avsluttes med enfold
, som her skjer inni den rekursivegetSum
funksjonen. For en Enonic XP controller vil sistefold
inni enpipe
ofte returnere enResponse
, som videre returneres av controlleren.
Veien videre
Option
-typen.