
Funksjonell programmering i Enonic XP #4 - Either
I stedet for å kaste Exceptions – noe som er en sideeffekt – kan vi heller pakke resultatet inn i en Either.
Håndtere exceptions
Dette kanskje det viktigste kapittelet i denne serien, siden IOEither (som er en slektning av Either) er den vanligste typen vi jobber med.
Vi så i kapittel 3 hvordan man kan bruke Option<A>
for å representere en verdi av type A
som potensielt kan mangle. Det vi skal se på nå, er hvordan man kan bruker Either<E, A>
for å representere et resultat som enten er en Exception/Error av type E
, eller en verdi av type A
.
Konvensjonen i funksjonell progammering er at error-verdien er på venstesiden, og gjøres til en Either
ved å kalle left()
, og resultverdien er på høyresiden, og pakkes inn ved å bruke right()
.
Either
i fp-ts er det som kalles "right biased". Og det betyr at map
og chain
opererer på verdien på høyresiden, mens venstresiden blir ignorert av de funksjonene.
Eksempel:
Som tidligere nevnt så er det å kaste en exception en sideeffekt. For å gjøre funksjonen getEmployee()
til en ren funksjon, fanger vi derfor exceptionen med en try/catch
, og retunerer heller en Either<EnonicError, Content<Employee>>
. Det vil si at resultatet av funksjonen enten er en EnonicError
, eller en Content<Employee>
.
I get
funksjonen bruker vi fold()
for å gjøre Either
om til Result
, som kan returneres. fold()
tar to funksjonener som omformer de to resultattypene om til Response
.
import {Either, right, left, fold} from 'fp-ts/Either';
import {Request, Response} from "enonic-types/controller";
import {Content, ContentLibrary} from "enonic-types/content";
import {EnonicError, internalServerError} from "enonic-fp/errors";
import {Employee} from "../../content-types/employee/employee";
const contentLib: ContentLibrary = __non_webpack_require__('/lib/xp/content');
function getEmployee(key: string): Either<EnonicError, Content<Employee>> {
try {
const content = contentLib.get<Employee>({ key });
return right(content!); // For simplicity here, we use "!" to ignore the null case
} catch(e) {
return left(internalServerError);
}
}
/**
* Controllers get function
*/
export function get(req: Request): Response {
const eitherEmployee = getEmployee(req.params.key!);
return fold(
(err: EnonicError) => (
{
status: 500,
body: err
} as Response
),
(employeeContent: Content<Employee>) => (
{
status: 200,
body: employeeContent
}
)
)(eitherEmployee);
}
Hjelpefunksjoner
Eksempel:
Man kan bruke hjelpefunksjonen tryCatch
, i Either-pakken i fp-ts, for å gjøre det samme litt mer elegant. tryCatch
funksjonen tar to funksjoner som parametere. Den første funksjonen er bare en callback som gjør operasjonen som kan kaste en exception. Den andre funksjonen tar exceptionen som kastes som input, og returnerer et resultat med type E
(den venstre typen).
import { Either, tryCatch } from 'fp-ts/Either';
import { Content, ContentLibrary } from "enonic-types/content";
import { EnonicError, internalServerError } from "enonic-fp/errors";
import { Employee } from "../../content-types/employee/employee";
const contentLib: ContentLibrary = __non_webpack_require__('/lib/xp/content');
function getEmployee(key: string) : Either<EnonicError, Content<Employee>> {
return tryCatch(
() => contentLib.get<Employee>({ key })!, // For simplicity here, we use "!" to ignore the null case
(e) => internalServerError
)
}
Null-håndtering med Either
I forrige kapittel så vi hvordan vi kunne bruke Option<A>
for å representere en nullable verdi, og i dette kapittelet ser vi på hvordan vi bruker Either<E, A>
for å representere en mulig exception.
I praksis vil jo getEmployee()
funksjonen vår kunne gjøre begge deler, og vi kunne representert resultatet som Either<EnonicError, Option<Content<Employee>>>
. Det er litt av en munnfull av en return type.
En bedre løsning er å slå sammen Option
og Either
ved å la None representeres av en EnonicError
i stedet. Vi kan nå bruke fold()
fra Either
til å håndtere en manlende verdi, sammen med de andre tilfellene som ikke gir http status=200.
Eksempel:
import {Either, fold, tryCatch, fromNullable, chain} from 'fp-ts/Either';
import {Request, Response} from "enonic-types/controller";
import {Content, ContentLibrary} from "enonic-types/content";
import {EnonicError, internalServerError, notFoundError} from "enonic-fp/errors";
import {Employee} from "../../content-types/employee/employee";
import {pipe} from "fp-ts/pipeable";
const contentLib: ContentLibrary = __non_webpack_require__('/lib/xp/content');
function getEmployee(key: string): Either<EnonicError, Content<Employee>> {
return pipe(
tryCatch(
() => contentLib.get<Employee>({ key }),
(e) => internalServerError
),
chain(
fromNullable(notFoundError)
)
);
}
export function get(req: Request): Response {
const eitherEmployee = getEmployee(req.params.key!);
return fold(
(err: EnonicError) => (
{
status: err.status,
body: err
} as Response
),
(employeeContent: Content<Employee>) => (
{
status: 200,
body: employeeContent
}
)
)(eitherEmployee);
}
Veien videre
Når du leser koden over tenker du kanskje at koden ser mye mer komplisert og krøkkete ut enn den du er vant til. Så hvorfor skal du bytte til dette?
Svaret ligger i den definisjonen vi prøvde oss på øverst i første artikkel. FP handler om "komposisjon av rene funksjoner".
En del av mønstrene som brukes i eksempelet vi akkurat så kan trekkes ut og gjenbrukes. Og til slutt sitter vi igjen med noen små veldig gjenbrukbare funksjoner som kan sys sammen til superrobuste kontrollere.
Vi skal se eksempler på dette i kapittel 6, og da vil du forhåpentligvis se hvor deilig FP-kode i Enonic kan være. Men først skal vi ta en snartur innom IO
-monaden.