Til hovedinnhold
Liten tapet sammen robot

Funksjonell programmering i Enonic XP #5 - IO

Publisert: 13. desember 2019 av Tom Arild Jakobsen

Vi kan bruke IO-monaden til å gi oss referensiell transparens når vi gjør IO-operasjoner.

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

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:

  1. Interagere med databasen
  2. HTTP-requests
  3. Be om en Random verdi
  4. 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 chainOg 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 av map, chain, fold osv. Så map for IO, er en annen funksjon enn for Either 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.