Til hovedinnhold
Bord med laptop, kaffekaraffel og kopp

Funksjonell programmering i Enonic XP #4 - Either

Publisert: 13. desember 2019 av Tom Arild Jakobsen

I stedet for å kaste Exceptions – noe som er en sideeffekt – kan vi heller pakke resultatet inn i en Either.

Artikkelnummer Andre artikler i denne serien
1 Funksjonell programmering i Enonic XP #1 - Intro
2 Funksjonell programmering i Enonic XP #2 - TypeScript
3 Funksjonell programmering i Enonic XP #3 - Option
4 Funksjonell programmering i Enonic XP #4 - Either
5 Funksjonell programmering i Enonic XP #5 - IO
6 Funksjonell programmering i Enonic XP #6 - Eksempler

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.