2013-10-30 16 views
22

Ho visto un video di recente su come si poteva venire con la monade IO, il discorso era in scala. In realtà mi sto chiedendo quale sia il motivo per cui le funzioni restituiscono IO [A] al di fuori di esse. Le espressioni lambda avvolte nell'oggetto IO sono ciò che le mutazioni sono e, a un certo punto, più in alto nel cambiamento che devono essere osservate, voglio dire eseguito, in modo che qualcosa accada. Non stai solo spingendo il problema più in alto nell'albero da qualche altra parte?Scala IO monade: qual è il punto?

L'unico vantaggio che posso vedere è che consente una valutazione lenta, nel senso che se non si chiama l'operazione unsafePerformIO non si verificano effetti collaterali. Immagino anche che altre parti del programma possano usare/condividere codice e deciede quando vuole che si verifichino gli effetti collaterali.

Mi chiedevo se questo è tutto? C'è qualche vantaggio nella testabilità? Sto assumendo non come si dovrebbe osservare gli effetti che nega questo tipo. Se hai usato tratti/interfacce puoi controllare le dipendenze ma non quando gli effetti si verificano su queste dipendenze.

Ho messo insieme il seguente esempio nel codice.

case class IO[+A](val ra:() => A){ 
    def unsafePerformIO() : A = ra(); 
    def map[B](f: A => B) : IO[B] = IO[B](() => f(unsafePerformIO())) 
    def flatMap[B](f: A => IO[B]) : IO[B] = { 
    IO(() => f(ra()).unsafePerformIO()) 
    } 
} 



case class Person(age: Int, name: String) 

object Runner { 

    def getOlderPerson(p1: Person,p2:Person) : Person = 
    if(p1.age > p2.age) 
     p1 
     else 
     p2 

    def printOlder(p1: Person, p2: Person): IO[Unit] = { 
    IO(() => println(getOlderPerson(p1,p2))).map(x => println("Next")) 
    } 

    def printPerson(p:Person) = IO(() => { 
    println(p) 
    p 
    }) 

    def main(args: Array[String]): Unit = { 

    val result = printPerson(Person(31,"Blair")).flatMap(a => printPerson(Person(23,"Tom")) 
            .flatMap(b => printOlder(a,b))) 

    result.unsafePerformIO() 
    } 

} 

Si può vedere come gli effetti vengono differiti fino al main che immagino sia freddo. L'ho inventato dopo averlo provato dal video.

La mia implementazione è corretta e la mia comprensione è corretta.

Mi chiedo anche se per ottenere milage dovrebbe essere combinato con ValidationMonad, come in ValidationMonad [IO [Person]] in modo da poter cortocircuitare quando si verificano eccezioni? Pensieri per favore.

Blair

risposta

27

È utile per la firma del tipo di una funzione per registrare se ha o meno effetti collaterali. La tua implementazione di IO ha valore perché fa così tanto. Rende il tuo codice meglio documentato; e se si refactoring il codice per separare, per quanto possibile, la logica che coinvolge IO dalla logica che non lo fa, hai reso le funzioni non-IO-coinvolgenti più compositivi e più testabili. Si potrebbe fare lo stesso refactoring senza un tipo di IO esplicito; ma usando un tipo esplicito significa che il compilatore può aiutarti a fare la separazione.

Ma questo è solo l'inizio. Nel codice della tua domanda, le azioni IO sono codificate come lambda e quindi sono opache; non c'è nulla che tu possa fare con un'azione IO, tranne eseguirlo, e il suo effetto quando viene eseguito è hardcoded.

Questo non è l'unico modo possibile per implementare la monade IO.

Ad esempio, potrei rendere le mie case classi di azioni IO che estendono un tratto comune. Quindi, ad esempio, posso scrivere un test che esegue una funzione e vede se restituisce il tipo corretto dell'azione IO.

In questi casi le classi che rappresentano diversi tipi di azioni IO, potrebbero non includere implementazioni codificate di ciò che le azioni eseguono quando corro. Invece, potrei disaccoppiare quello usando il modello typeclass. Ciò consentirebbe lo swapping in diverse implementazioni di ciò che fanno le azioni IO. Ad esempio, potrei avere una serie di implementazioni che comunicano con un database di produzione e un altro insieme che comunica con un database in memoria fittizio a scopo di test.

C'è un buon trattamento di questi problemi nel Capitolo 13 ("effetti esterni e di I/O") del libro programmazione funzionale di Bjarnason & Chiusano a Scala. Vedi in particolare 13.2.2, "Vantaggi e svantaggi del semplice tipo di IO".

UPDATE (dicembre 2015): re "scambia in diverse implementazioni di ciò che fanno le azioni IO", in questi giorni sempre più persone usano la "monade libera" per questo genere di cose; vedere per es. Il post sul blog di John De Goes "A Modern Architecture for FP".

+0

Grazie. Grande risposta darò un'occhiata a quelle idee stasera. –

+0

Hai uno snippet di codice con sottoclassi e classi di tipi in mente? –

+2

Vedere le diapositive di Runar a cui Drexin si collega, in particolare le cose 'ConsoleIO'. Dimostra la separazione della dichiarazione di quali azioni IO esistono ('caso caso GetLine ...', 'case class PutLine ... ') dalla definizione di cosa potrebbe accadere se esegui quelle azioni (' oggetto implicito ConsoleEffect .. .'). Ma nota che ci sono anche altre cose lì dentro; non è un codice minimale che dimostra solo _ quello che ho detto. –

18

Il vantaggio di utilizzare il monade IO sta avendo programmi puri. Non spingere gli effetti collaterali più in alto nella catena, ma eliminali. Se si dispone di una funzione impura come la seguente:

def greet { 
    println("What is your name?") 
    val name = readLine 
    println(s"Hello, $name!") 
} 

è possibile rimuovere l'effetto collaterale riscrivendo a:

def greet: IO[Unit] = for { 
    _ <- putStrLn("What is your name?") 
    name <- readLn 
    _ <- putStrLn(s"Hello, $name!") 
} yield() 

La seconda funzione è referenzialmente trasparente.

Una spiegazione molto buona del motivo per cui utilizzare le monade IO porta a programmi puri è possibile trovare in Rúnar Bjarnason's slides da scala.io (il video può essere trovato here).

+0

Programmatore non FP qui. Perché il secondo è referenzialmente trasparente? La stampa dipenderà dall'input dell'utente contenuto nella funzione di benvenuto, no? – nawfal

+0

@nawfal È referenzialmente trasparente perché non ha alcun effetto sul mondo esterno e non dipende dallo stato esterno. Ogni volta che lo esegui ottieni la stessa cosa: un'azione che, quando viene eseguita, stampa del testo e chiede l'input. Se si volesse farlo due volte, si potrebbe chiamare la funzione due volte per ottenere due azioni identiche oppure chiamare la funzione una volta e riutilizzare l'azione che restituisce e il programma sarebbe semanticamente identico. Puoi anche chiamarlo e non usare mai il risultato che sarebbe semanticamente identico a non chiamare mai la funzione. – puhlen

+0

@puhlen L'ho capito, ma poi quando chiami quell'azione restituita, hai degli effetti collaterali, giusto? Fondamentalmente stai spostando la parte con effetti collaterali in qualche altra parte? – nawfal