Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Dump di quando detto da @gcanti nel canale #fp di italiajs in data 1 marzo 2019

Questa non è farina del mio sacco, io ho solo copia incollato la discussione. Kudos to Giulio Canti

@vncz ho un consiglio su come procedere a strutturare un programma funzionale, con me ha funzionato bene, vediamo se funziona anche per te

In linea teorica e generale procedo così

  • dominio
  • firma del programma
  • firme delle operazioni di base
  • implementazione del programma
  • implementazioni delle operazioni di base

adesso provo ad adattare questo schema generale al tuo caso (semplificandolo, mi serve solo come spunto) Il dominio è semplice (almeno in prima battuta)

export type Config = {
  eventPath: string
  token: string
  sha: string
  workspace: string
  filePath: string
}

Sulla firma del programma non sono ancora sicuro, mi limiterò ad abbozzarla, ma da quello che ho visto finora diciamo che non ha un input e supponiamo che debba restituire una stringa

declare function program(): string

uso declare perchè in una prima fase non mi importa nulla dell'implementazione Adesso penso a tutte le operazioni di base che mi servono nel programma, con due importanti punti in testa

  • ogni operazione che definisco deve avere meno effetti possibile
  • non mi preoccupo minimamente di come collegarle tra loro

La ragione di quei due punti è che:

  • non voglio pensare ad effetti che non riguardano la singola operazione che sto modellando
  • comporle insieme non sarà un problema, so già che la programmazione funzionale mi da una serie di strumenti per farlo Dunque per il mio programma avrò bisogno di recuperare un Config, come faccio? Non lo faccio, adesso non mi importa. Suppongo che qualcuno me lo dia già bello che pronto e scrivo il core del programma
declare function core(config: Config): string

Solo adesso mi preoccupo di come fare a recuperarla: devo prenderla da process.env ma dovrò validarla prima. Pensiamo prima a questo prima di come recuperarlo: perciò adesso suppongo di avere già process.env e devo solo validarla

import { Either } from 'fp-ts/lib/Either'

declare function validate(env: NodeJS.ProcessEnv): Either<string, Config>

Qui ho deciso di modellare l'errore con una stringa per semplicità, in un secondo momento posso fare il raffinato, ma cominciamo schisci (edited) Finalmente mi pongo il problema di come recuperare un NodeJS.ProcessEnv

import { IO } from 'fp-ts/lib/IO'

declare const getEnv: IO<NodeJS.ProcessEnv>

Adesso mi metto li e faccio i conti degli effetti

  • core: nessun effetto
  • validate: Either
  • getEnv: IO Questo mi dice che il mio program girerà con la "somma" di tutti gli effetti, cioè IO + Either perciò torno su e modifico la firma di program
declare function program(): IO<Either<string, string>>

Ora ho modellato il problema completamente

import { Either } from 'fp-ts/lib/Either'
import { IO } from 'fp-ts/lib/IO'

export type Config = {
  eventPath: string
  token: string
  sha: string
  workspace: string
  filePath: string
}

declare function core(config: Config): string

declare function validate(env: NodeJS.ProcessEnv): Either<string, Config>

declare const getEnv: IO<NodeJS.ProcessEnv>

declare function program(): IO<Either<string, string>>

abbiamo finito il punto 3) dalla lista passiamo al 4) "implementazione del programma"

function program(): IO<Either<string, string>> {
  ???
}

tolgo il declare e cerco di usare le operazioni di base, senza implementarle, non mi interessa in questo momento (edited)

function program(): IO<Either<string, string>> {
  return getEnv.map(env => validate(env).map(config => core(config)))
}

fatto Qui per mettere insieme i sotto-programmi ho dovuto sfruttare solo le istanze di funtore di IO e Either, aka ho mappato alla grande (edited) Il compilatore mi sta dando l'OK, tutto verde. Se sono soddisfatto del modello allora posso affrontare l'ultimo punto, il più noioso francamente: le implementazioni dei sotto-programmi getEnv è facile

const getEnv: IO<NodeJS.ProcessEnv> = new IO(() => process.env)

core me lo sono inventato di sana pianta che restituisce una string quindi non è molto significativo in questo dialogo

function core(config: Config): string {
  return config.eventPath + config.token // così tanto per dire...
}

validate è un po' rognosa ma se usiamo io-ts ce la si cava

import * as t from 'io-ts'

const Config = t.type({
  GITHUB_EVENT_PATH: t.string,
  GITHUB_TOKEN: t.string,
  GITHUB_SHA: t.string,
  GITHUB_WORKSPACE: t.string,
  SPECTRAL_FILE_PATH: t.string
})

function validate(env: NodeJS.ProcessEnv): Either<string, Config> {
  return Config.decode(env).bimap(
    () => 'Invalid env variables',
    a => ({
      eventPath: a.GITHUB_EVENT_PATH,
      token: a.GITHUB_TOKEN,
      sha: a.GITHUB_SHA,
      workspace: a.GITHUB_WORKSPACE,
      filePath: a.SPECTRAL_FILE_PATH
    })
  )
}

Fine.

// Esempio completo 
import { Either } from 'fp-ts/lib/Either'
import { IO } from 'fp-ts/lib/IO'
import * as t from 'io-ts'

export type Config = {
  eventPath: string
  token: string
  sha: string
  workspace: string
  filePath: string
}

function core(config: Config): string {
  return config.eventPath + config.token // così tanto per dire...
}

const Config = t.type({
  GITHUB_EVENT_PATH: t.string,
  GITHUB_TOKEN: t.string,
  GITHUB_SHA: t.string,
  GITHUB_WORKSPACE: t.string,
  SPECTRAL_FILE_PATH: t.string
})

function validate(env: NodeJS.ProcessEnv): Either<string, Config> {
  return Config.decode(env).bimap(
    () => 'Invalid env variables',
    a => ({
      eventPath: a.GITHUB_EVENT_PATH,
      token: a.GITHUB_TOKEN,
      sha: a.GITHUB_SHA,
      workspace: a.GITHUB_WORKSPACE,
      filePath: a.SPECTRAL_FILE_PATH
    })
  )
}

const getEnv: IO<NodeJS.ProcessEnv> = new IO(() => process.env)

function program(): IO<Either<string, string>> {
  return getEnv.map(env => validate(env).map(config => core(config)))
}

Naturalmente in un programma più complesso ci sono più cose da tenere in considerazione, ma il processo mentale generale (se non altro per ogni sottosistema) è lo stesso.

Se vuoi aggiungere la gestione degli errori devi decidere cosa fare in caso di un Left. Ancora una volta non mi preoccupo di come comporre la mia gestione degli errori con quello che già ho, un modo lo troverò. Pensiamo solo a come gestire l'errore, che vogliamo fare con questo Either<string, string> che mi arriva? Facciamo un classicone per semplicità di esposizione? console.error per l'errore e console.log per il risultato di successo?

import { log, error } from 'fp-ts/lib/Console'

function handleError(ma: Either<string, string>): IO<void> {
  return ma.fold(error, log)
}

Adesso mi preoccupo di combinarlo con il programma che ho già

function programWithErrorHandling(): IO<void> {
  return program().chain(handleError)
}

Notare che adesso il tipo di ritorno è cambiato in IO<void> perchè ho gestito l'eventuale errore program l'ho lasciato inalterato che magari voglio averne un'altra versione con un error handling diverso Ultima considerazione

un modo lo troverò

Io sono certo di poter trovare un modo. E non per via di una questione empirica.

Buon week-end, e viva la programmazione funzionale

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment