2015-05-19 19 views
5

Ho deciso di provare la programmazione funzionale e Purescript. Dopo aver letto "Learn you a Haskell for great good" e "PureScript by Example" e giocando un po 'con il codice penso di poter dire di aver capito le basi, ma una cosa mi preoccupa molto: il codice sembra molto accoppiato. È normale che io cambi le librerie molto spesso e in OOP posso usare l'architettura di cipolla per disaccoppiare il mio codice da quello specifico della libreria, ma non ho idea di come farlo in Purescript.Come strutturare l'app in purescript

Ho cercato di scoprire come le persone lo fanno in Haskell, ma tutto quello che ho potuto trovare erano risposte come "Nessuno ha mai creato app complesse in Haskell, quindi nessuno sa come farlo" o "Hai inserito e tu hai prodotto, tutto nel fratempo sono solo funzioni pure ". Ma in questo momento ho un'app giocattolo che usa dom virt, segnali, memoria web, librerie router e ognuno di essi ha i propri effetti e strutture dati, quindi non sembra un input e un output.

Quindi la mia domanda è come dovrei strutturare il mio codice o quale tecnica dovrei usare in modo da poter cambiare le mie librerie senza riscrivere metà della mia app?

Aggiornamento:

suggerimento di utilizzare diversi strati e mantenere gli effetti nel modulo principale è abbastanza comune troppo e capisco perché dovrei farlo.
Ecco un semplice esempio che si spera illustrare il problema che sto parlando:

btnHandler :: forall ev eff. (MouseEvent ev) => ev -> Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace | eff) Unit 
btnHandler e = do 
    btn <- getTarget e 
    Just btnId <- getAttribute "id" btn 
    Right clicks <- (getItem localStorage btnId) >>= readNumber 
    let newClicks = clicks + 1 
    trace $ "Button #" ++ btnId ++ " has been clicked " ++ (show newClicks) ++ " times" 
    setText (show newClicks) btn 
    setItem localStorage btnId $ show newClicks 
    -- ... maybe some other actions 
    return unit 

-- ... other handlers for different controllers 

btnController :: forall e. Node -> _ -> Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace | e) Unit 
btnController mainEl _ = do 
    delegateEventListener mainEl "click" "#btn1" btnHandler 
    delegateEventListener mainEl "click" "#btn2" btnHandler 
    delegateEventListener mainEl "click" "#btn3" btnHandler 
    -- ... render buttons 
    return unit 

-- ... other controllers 

main :: forall e. Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace, router :: Router | e) Unit 
main = do 
    Just mainEl <- body >>= querySelector "#wrapper" 
    handleRoute "/" $ btnController mainEl 
    -- ... other routes each with it's own controller 
    return unit 

Qui abbiamo semplice applicazione contatore con il routing, WSS, manipolazioni dom e la registrazione della console. Come puoi vedere non c'è un singolo input e una singola uscita. Possiamo ottenere input dagli ascoltatori di router o eventi e utilizzare console o dom come output, quindi diventa un po 'più complicato.

Avere tutto questo codice effectful nel modulo principale si sente male per me per due motivi:

  1. Se voglio continuare ad aggiungere percorsi e controllori di questo modulo si trasformerà rapidamente in un migliaio di linea di casino.
  2. Keeping routing, manipolazioni dom e memorizzazione dei dati nello stesso modulo viola principio di singola responsabilità (e presumo che è importante in FP troppo)

Possiamo dividere questo modulo in altri più, ad esempio un modulo per controller e creare un qualche tipo di strato efficace. Ma poi quando avrò dieci moduli controller e voglio cambiare la mia specifica lib di dom, dovrei modificarli tutti.

Entrambi questi approcci sono lontani dall'ideale, quindi la domanda è la prima che dovrei scegliere? O forse c'è un altro modo per andare?

risposta

6

Non c'è motivo per cui non sia possibile avere un livello intermedio per l'astrazione delle dipendenze. Supponiamo che tu voglia utilizzare un router per la tua applicazione. È possibile definire una libreria "router astrazione" che avrebbe il seguente aspetto:

module App.Router where 

import SomeRouterLib 

-- Type synonym to make it easy to change later 
type Route = SomeLibraryRouteType 

-- Just an alias to the Router library 
makeRoute :: String -> Route -> Route 
makeRoute = libMakeRoute 

E poi il nuovo fiammante esce, e si desidera cambiare la vostra libreria di routing. Dovrai creare un nuovo modulo conforme alla stessa API, ma con le stesse funzioni: un adattatore, se vuoi.

module App.RouterAlt where 

import AnotherRouterLib 

type Route = SomeOtherLibraryType 

makeRoute :: String -> Route -> Route 
makeRoute = otherLibMakeRoute 

Nell'applicazione principale, è ora possibile scambiare le importazioni e tutto dovrebbe funzionare correttamente. Probabilmente ci sarà più massaggio che deve accadere per far funzionare i tipi e le funzioni come ci si aspetterebbe da loro, ma questa è l'idea generale.

Il codice di esempio è di natura molto imperativa. Non è un codice funzionale idiomatico, e penso che tu abbia ragione nel notare che non è sostenibile. Gli idiomi più funzionali includono purescript-halogen e purescript-thermite.

Considerare l'interfaccia utente come una pura funzione dello stato dell'applicazione corrente. In altre parole, dato il valore attuale delle cose, come appare la mia app? Inoltre, si consideri che lo stato attuale dell'applicazione può essere derivato dall'applicazione di una serie di funzioni pure ad uno stato iniziale.

Qual è lo stato dell'applicazione?

data AppState = AppState { buttons :: [Button] } 
data Button = Button { numClicks :: Integer } 

Che tipo di eventi stai guardando?

data Event = ButtonClick { buttonId :: Integer } 

Come gestirlo?

handleEvent :: AppState -> Event -> AppState 
handleEvent state (ButtonClick id) = 
    let newButtons = incrementButton id (buttons state) 
    in AppState { buttons = newButtons } 

incrementButton :: Integer -> [Button] -> [Button] 
incrementButton _ []  = [] 
incrementButton 0 (b:bs) = Button (1 + numClicks b) : bs 
incrementButton i (b:bs) = b : incrementButton (i - 1) buttons 

Come si esegue il rendering dell'applicazione, in base allo stato corrente?

render :: AppState -> Html 
render state = 
    let currentButtons = buttons state 
     btnList = map renderButton currentButtons 
     renderButton btn = "<li><button>" ++ show (numClicks btn) ++ "</button></li>" 
    in "<div><ul>" ++ btnList ++ "</ul></div>" 
3

Questa è una domanda un po 'aperta, quindi è difficile rispondere in modo specifico senza esempi concreti.

aver inserito e si dispone di uscita, tutto il resto sono solo funzioni pure

dichiarazioni come questa sono in realtà piuttosto vicino alla verità.Dato che non ci sono oggetti stateful in Haskell e PureScript, la maggior parte del codice in un'app si baserà su funzioni pure e tipi di dati semplici (o record), e quindi non è strettamente accoppiato a nessuna libreria particolare (a parte cose come Maybe, Either, Tuple e così via, che non sono realmente librerie nel senso in cui si sta parlando).

Per quanto possibile, è necessario provare a inserire il codice che utilizza gli effetti "all'esterno". È qui che si interlacciano le varie librerie necessarie per elaborare qualunque input e produrre qualsiasi output richiesto dall'app. Questa stratificazione semplifica il passaggio e l'estensione delle librerie, poiché in questo caso si solleva principalmente il codice core puro nella monade Eff per "collegarlo" agli ingressi e alle uscite esterne.

Un modo di guardarlo, è che se ti ritrovi a usare Eff molto al di fuori del modulo principale o del primo strato della tua app, probabilmente stai "facendo male".

Se stai scrivendo Haskell, sostituisci ovunque io menzioni lo Eff con IO.

+0

Grazie per la risposta. Ho modificato la mia domanda e ho aggiunto un semplice esempio. – starper