
Funksjonell programmering i Enonic XP #5 - IO
Vi kan bruke IO-monaden til å gi oss referensiell transparens når vi gjør IO-operasjoner.
Problemet med å håndtere sideeffekter i rene funksjoner?
I første kapittel så vi på tre typer sideeffekter, og vi har sett på håndtering av null-verdier og exceptions. Det som gjenstår er rett og slett alle de andre sideeffektene.
Det inkluderer f.eks:
- Interagere med databasen
- HTTP-requests
- Be om en Random verdi
- Logging
Målet vårt er ikke å unngå å utføre sideefeekter, for det må vi.
Målet er å isolere den delen av koden som består av rene funksjoner fra den delen av koden som utfører sideeffekter. Vi ønsker dessuten at så stor del av koden som mulig bare består av rene funksjoner.
Trikset
I stedet for å utføre sideeffektene, returnerer vi bare en callback med koden som skal gjøre sideeffekter. Da kan sideeffektene utføres senere fra en del av kodebasen som ikke består av rene funksjoner.
Vi gir callbacken typen IO<A>
, hvor A
er typen av verdien som returneres av callbacken. Litt forenklet kan vi si de følgende: type IO<A> = () => A;
.
Men ved å returnere callbacken som en IO<A>
kan vi gjøre operasjoner fra fp-ts på IO<A>
, som map
og chain
. Og det er spesielt chain
som gir oss muligheten å slå sammen (flatten) flere IO
, slik at vi kan uttrykke et program som mange sideeffekter med ett nivå IO
i returtypen.
Hver type i fp-ts har egne implementasjoner avmap
,chain
,fold
osv. Såmap
forIO
, er en annen funksjon enn forEither
eller Array. Men de er konseptuelt like.
Random eksempel
I dette eksempelet utsetter vi å gjøre sideeffekten Math.random()
ved å enkode den som en IO<number>
.
Vi gjør deretter en map
på den, hvor vi tar random-nummeret og gjør det til en string
.
Til slutt eksekverer vi IO
'en – og utfører sideeffekten – og funksjonen er ikke ren lenger.
Eksempel:
// hjelpefunksjon
const numberToString = (value: number) => value.toString();
const random: IO<number> = () => Math.random();
const randomAsString: IO<string> = map(numberToString)(random);
// Vi har ikke utført sideeffekten Math.random() enda, og er enda "ren"
const result: string = randomAsString();
// Der utførte vi sideeffekten, og vi er ikke "ren" lenger
IO + Either = IOEither
Hvis vi for eksempel har en funksjon som skal gjøre et databasekall, ønsker vi å ha en returtype som enkoder alle effektene som utføres. Det gjøres rett og slett ved å returnere et resultat av type IO<Either<E, A>>
. Denne typen kan være litt tricky å jobbe med, så fp-ts har laget et alias som gjør akkurat samme jobben, nemlig IOEither<E, A>
.
IOEither<E, A>
er den vanligste returtypen i enonic-fp biblioteket. Når vi gjør en fold()
over en IOEither
, folder vi egentlig over Either
-delen, og vi sitter igjen med IO<A>
. Denne må da kjøres for å utføre sideeffektene, slik at vi sitter igjen med et resultat av type A
.
Eksempel:
I dette eksempelet har vi oppdatert getEmployee()
fra forrige kapittel, slik at det returnerer IOEither
i stedet for Either
. Om du sammenligner koden er den helt lik. Men funksjonene vi bruker kommer fra fp-ts/lib/IOEither i stedet for fp-ts/lib/Either.
Den andre forskjellen er at funksjonene i fold()
for IOEither
må returnere IO<Response>
, og at fold()
returnerer IO<Response>
(som vi lagrer i const program
) som må kjøres for å utføre sideeffektene, og returnere Response
.
import {pipe} from "fp-ts/pipeable";
import {IO, io} from "fp-ts/IO";
import {chain, fold, IOEither, tryCatch} from "fp-ts/IOEither";
import {Request, Response} from "enonic-types/controller";
import {Content, ContentLibrary} from "enonic-types/content";
import {EnonicError, internalServerError, notFoundError} from "enonic-fp/errors";
import {fromNullable} from "enonic-fp/utils";
import {Employee} from "../../content-types/employee/employee";
const contentLib: ContentLibrary = __non_webpack_require__('/lib/xp/content');
function getEmployee(key: string): IOEither<EnonicError, Content<Employee>> {
return pipe(
tryCatch(
() => contentLib.get<Employee>({ key }),
(e) => (
{
...internalServerError,
title: String(e)
} as EnonicError
)
),
chain(
fromNullable(notFoundError)
)
);
}
export function get(req: Request): Response {
const employee = getEmployee(req.params.key!);
const program: IO<Response> = fold(
(err: EnonicError) => io.of<Response>(
{
status: err.status,
body: err
}
),
(employeeContent: Content<Employee>) => io.of<Response>(
{
status: 200,
body: employeeContent
}
)
)(employee);
return program();
}
Veien videre
Vi har så langt sett på byggestenene for å jobbe med FP og Enonic. I neste kapittel skal vi se på hva man kan bygge med det.