IO Codice monadico: composizione di funzioni standard vs capovolte

7

Di seguito c'è una linea che gestisce le connessioni socket in un semplice programma Haskell.

mainLoop :: Socket -> IO ()
mainLoop sock = accept sock >>= forkIO . handle . fst >> mainLoop sock

Il "flusso" di dati nella funzione non è coerente: è lasciato a destra quando si usa bind >>= o >> (sequenza?), ma da destra a sinistra quando si usa la composizione della funzione . .

Il sotto è migliore? Utilizzando una forma semplificata / modificata di >>> da Control.Arrow, che è una composizione di funzione capovolta.

infixr 2 >>>
(>>>) = flip (.)

mainLoop :: Socket -> IO ()
mainLoop sock = accept sock >>= fst >>> handle >>> forkIO >> mainLoop sock

Ha un flusso di dati consistente, da sinistra a destra. Ci sono degli svantaggi nell'usare questo come pattern per la maggior parte del codice?

Un'alternativa, come discusso nei commenti, è di usare =<< . Ma sembra che anche la forma capovolta di >> non sia definita.

infixr 1 <<
(<<) = flip (>>)

mainLoop :: Socket -> IO ()
mainLoop sock = mainLoop sock << forkIO . handle . fst =<< accept sock

Quindi in realtà ho la stessa domanda per questo modulo come per il modulo che usa >>>

    
posta Michal Charemza 10.03.2017 - 23:56
fonte

1 risposta

4

Penso che la vera risposta sia

  1. Se hai qualche tipo di colleghi su questo progetto, esegui questo da loro per ottenere la loro opinione.
  2. Se è solo per te, scegli quello che più ti piace.

Mentre Haskell supporta molto bene la programmazione point-free, la lingua e la comunità sono più o meno standardizzate attorno alla composizione da destra a sinistra basata sulla matematica con (.) e ($) . Sfortunatamente come hai notato, queste catene sono spesso meno leggibili rispetto alla controparte ruotata da sinistra a destra.

Ora è abbastanza facile definire le versioni capovolte degli operatori di cui sopra, ma la difficoltà è che non esiste una vera convenzione. Puoi utilizzare (>>>) da Control.Arrow . Ho anche visto (#) in diagrams o (&) in Data.Functions per flip ($) . Ed è ovviamente solo un paio di righe per definire le tue versioni. Perché non c'è una sola risposta standard qui incontrerai un paio di problemi:

  1. Precedenza poco chiara. (.) e ($) hanno una precedenza molto chiara e ben nota: rispettivamente più alta e più bassa. La precedenza di qualsiasi versione capovolta non sarà così familiare (i lettori del tuo codice non sapranno immediatamente come analizzare l'espressione).
  2. Onere cognitivo. C'è un piccolo ma preciso costo per l'uso di notazioni non familiari: i lettori devono ricordare a se stessi cosa stanno leggendo.
  3. La poca consuetudine. Mentre il codice che utilizza qualsiasi versione capovolta potrebbe essere totalmente valido e funzionante con codice Haskell, non sembrerà esattamente come il normale codice Haskell.

Mi imbatto in questi problemi in entrambi i casi. Nel primo caso, la compresenza di (>>>) e (>>) rende difficile l'analisi visiva della linea. Poiché (>>) è più tranquillo, voglio raggruppare insieme le ultime due espressioni anche se ciò non è corretto. La seconda versione ha il problema che la lettura di una catena monadica da destra a sinistra è solo strana. Anche in questo caso non c'è nulla di sbagliato in entrambe le versioni, ma penso che rendano il tuo codice meno idiomatico per un piccolo guadagno.

Penso che la soluzione reale (oltre a prendere una macchina del tempo e riprogettare la libreria standard di Haskell) sia che ogni volta che si imbocca una catena (o in questo caso una catena di catene) che è difficile da leggere; piuttosto che cercare le indicazioni o qualsiasi cosa è meglio riscrivere / scomporre quell'espressione. Qui potrei suggerire

mainLoop :: Socket -> IO ()
mainLoop sock = do
  contents <- accept sock
  forkIO . handle . fst $ contents
  mainLoop sock

O anche meglio

mainLoop sock = forever (accept sock >>= handleContents)      
  where
    handleContents (h, _) = forkIO . handle $ h
    
risposta data 11.03.2017 - 23:33
fonte

Leggi altre domande sui tag