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 effettovalidate
:Either
getEnv
:IO
Questo mi dice che il mioprogram
girerà con la "somma" di tutti gli effetti, cioèIO
+Either
perciò torno su e modifico la firma diprogram
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