Come praticante, perché dovrei preoccuparmi di Haskell? Che cos'è una monade e perché ne ho bisogno? [chiuso]


9

Semplicemente non capisco quale problema risolvono.



2
Penso che questa modifica sia un po 'estrema. Penso che la tua domanda sia stata essenzialmente buona. È solo che alcune parti erano un po '... polemiche. Che probabilmente è solo il risultato della frustrazione di provare a imparare qualcosa di cui non stavi vedendo il punto.
Jason Baker,

@SnOrfus, sono stato io a bastardare la domanda. Ero troppo pigro per modificarlo correttamente.
Giobbe

Risposte:


34

Le monadi non sono né buone né cattive. Lo sono e basta. Sono strumenti che vengono utilizzati per risolvere problemi come molti altri costrutti di linguaggi di programmazione. Un'applicazione molto importante è quella di rendere la vita più facile per i programmatori che lavorano in un linguaggio puramente funzionale. Ma sono utili in linguaggi non funzionali; è solo che le persone raramente si rendono conto che stanno usando una Monade.

Che cos'è una monade? Il modo migliore per pensare a una Monade è come un modello di progettazione. Nel caso dell'I / O, probabilmente potresti pensare che sia poco più di una pipeline glorificata in cui lo stato globale è ciò che viene passato tra le fasi.

Ad esempio, prendiamo il codice che stai scrivendo:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

C'è molto di più da fare qui di quanto non sembri. Per esempio, si noterà che putStrLnha la seguente firma: putStrLn :: String -> IO (). Perchè è questo?

Pensaci in questo modo: facciamo finta (per semplicità) che stdout e stdin siano gli unici file su cui possiamo leggere e scrivere. In un linguaggio imperativo, questo non è un problema. Ma in un linguaggio funzionale, non puoi mutare lo stato globale. Una funzione è semplicemente qualcosa che accetta un valore (o valori) e restituisce un valore (o valori). Un modo per aggirare questo è usare lo stato globale come valore che viene passato dentro e fuori da ogni funzione. Quindi potresti tradurre la prima riga di codice in qualcosa del genere:

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

... e il compilatore saprebbe stampare tutto ciò che è stato aggiunto al secondo elemento di global_state. Ora non so te, ma odierei programmare così. Il modo in cui ciò è stato reso più semplice è stato l'uso delle Monadi. In una Monade, passi un valore che rappresenta un tipo di stato da un'azione all'altra. Questo è il motivo per cui putStrLnha un tipo di ritorno IO (): sta restituendo il nuovo stato globale.

Allora perché ti importa? Bene, i vantaggi della programmazione funzionale rispetto al programma imperativo sono stati discussi a morte in diversi punti, quindi non risponderò a questa domanda in generale (ma vedi questo documento se vuoi ascoltare il caso della programmazione funzionale). Per questo caso specifico, tuttavia, potrebbe essere utile capire cosa Haskell sta cercando di realizzare.

Molti programmatori pensano che Haskell cerchi di impedire loro di scrivere codice imperativo o usare effetti collaterali. Questo non è del tutto vero. Pensaci in questo modo: un linguaggio imperativo è uno che consente effetti collaterali per impostazione predefinita, ma ti consente di scrivere codice funzionale se lo desideri (e sei disposto a gestire alcune delle contorsioni che richiederebbero). Haskell è puramente funzionale per impostazione predefinita, ma ti consente di scrivere codice imperativo se lo desideri (cosa che fai se il tuo programma deve essere utile). Il punto non è rendere difficile la scrittura di codice con effetti collaterali. È per assicurarti di essere esplicito sull'avere effetti collaterali (con il sistema dei tipi che lo impone).


6
L'ultimo paragrafo è oro. Per estrarlo e parafrasarlo un po ': "Un linguaggio imperativo è uno che consente effetti collaterali per impostazione predefinita, ma consente di scrivere codice funzionale se proprio lo si desidera. Un linguaggio funzionale è puramente funzionale per impostazione predefinita, ma consente di scrivere codice imperativo se vuoi davvero ".
Frank Shearar il

Vale la pena notare che il documento a cui ti sei collegato rifiuta specificamente l'idea di "immutabilità come virtù della programmazione funzionale" proprio all'inizio.
Mason Wheeler,

@MasonWheeler: ho letto quei paragrafi, non come respingere l'importanza dell'immutabilità, ma respingendolo come argomento convincente per dimostrare la superiorità della programmazione funzionale. In effetti, dice la stessa cosa sull'eliminazione di goto(come argomento per la programmazione strutturata) un po 'più tardi nel documento, caratterizzando tali argomenti come "infruttuosi". Eppure nessuno di noi desidera segretamente gotoil ritorno. È semplicemente che non si può sostenere che gotonon è necessario per le persone che lo usano ampiamente.
Robert Harvey,

7

Mi mordo !!! Le monadi da sole non sono in realtà una ragion d'essere per Haskell (le prime versioni di Haskell non le avevano nemmeno).

La tua domanda è un po 'come dire "C ++ quando guardo la sintassi, mi annoio così tanto. Ma i modelli sono una caratteristica molto pubblicizzata di C ++, quindi ho visto un'implementazione in qualche altro linguaggio".

L'evoluzione di un programmatore Haskell è uno scherzo, non è pensato per essere preso sul serio.

Una Monade ai fini di un programma in Haskell è un'istanza della classe di tipo Monad, vale a dire, è un tipo che capita a supporto di un certo piccolo insieme di operazioni. Haskell ha un supporto speciale per i tipi che implementano la classe di tipo Monad, in particolare il supporto sintattico. Praticamente ciò che ne risulta è ciò che è stato definito un "punto e virgola programmabile". Quando si combina questa funzionalità con alcune delle altre funzionalità di Haskell (funzioni di prima classe, pigrizia di default) ciò che si ottiene è la capacità di implementare determinate cose come librerie tradizionalmente considerate funzionalità linguistiche. Ad esempio, è possibile implementare un meccanismo di eccezione. È possibile implementare il supporto per continuazioni e coroutine come libreria. Haskell, la lingua non supporta le variabili mutabili:

Chiedete "Forse / Identità / Monadi della divisione sicura ???". La monade Maybe è un esempio di come è possibile implementare (molto semplice, solo un'eccezione) la gestione delle eccezioni come libreria.

Hai ragione, scrivere messaggi e leggere l'input dell'utente non è univoco. IO è un pessimo esempio di "monadi come caratteristica".

Ma per iterare, una "caratteristica" da sola (ad es. Monadi) in isolamento dal resto della lingua non appare necessariamente immediatamente utile (una nuova grande funzionalità di C ++ 0x sono riferimenti di valore, non significa che puoi prendere li fuori dal contesto C ++ perché la sua sintassi ti annoia e necessariamente vede l'utilità). Un linguaggio di programmazione non è qualcosa che si ottiene gettando un sacco di funzioni in un secchio.


In realtà, haskell supporta le variabili mutabili tramite la monade ST (una delle poche strane parti magiche impure del linguaggio che gioca secondo le sue stesse regole).
Sara

4

Tutti i programmatori scrivono programmi, ma le somiglianze finiscono qui. Penso che i programmatori differiscano molto più di quanto la maggior parte dei programmatori possa immaginare. Prendi qualsiasi "battaglia" di vecchia data, come la digitazione di variabili statiche vs tipi di solo runtime, script vs compilato, stile C vs orientato agli oggetti. Scoprirai impossibile argomentare razionalmente che un campo è inferiore, perché alcuni sfornano codice eccellente in un sistema di programmazione che mi sembra inutile o addirittura del tutto inutile.

Penso che persone diverse la pensino diversamente, e se non sei tentato dallo zucchero sintattico o soprattutto dalle astrazioni che esistono solo per comodità e che in realtà hanno un costo di runtime significativo, allora stai lontano da tali lingue.

Ti consiglierei comunque di provare almeno a familiarizzare con i concetti a cui ti stai arrendendo. Non ho nulla contro qualcuno che è veemente pro-puro-C, a patto che capiscano davvero qual è il grosso problema delle espressioni lambda. Sospetto che la maggior parte non diventerà immediatamente un fan, ma almeno sarà lì in fondo alle loro menti quando troveranno il problema perfetto quale sarebbe stato oh molto più facile da risolvere con lambda.

E, soprattutto, cerca di evitare di essere infastidito dai fanboy, soprattutto da persone che non sanno di cosa stanno parlando.


4

Haskell applica la Trasparenza referenziale : dati gli stessi parametri, ogni funzione restituisce sempre lo stesso risultato, indipendentemente da quante volte la chiamate.

Ciò significa, ad esempio, che su Haskell (e senza Monadi) non è possibile implementare un generatore di numeri casuali. In C ++ o Java puoi farlo usando variabili globali, memorizzando il valore intermedio "seed" del generatore casuale.

Su Haskell la controparte delle variabili globali sono Monadi.


Quindi ... e se volessi un generatore di numeri casuali? Non è anche una funzione? Anche se no, come posso procurarmi un generatore di numeri casuali?
Giobbe

@Job Puoi creare un generatore di numeri casuali all'interno di una monade (è fondamentalmente uno stato-tracker), oppure puoi usare unsafePerformIO, il diavolo di Haskell che non dovrebbe mai essere usato (e in effetti probabilmente spezzerà il tuo programma se usi la casualità al suo interno!)
alternativa il

In Haskell, o si passa intorno a un "RandGen" che è fondamentalmente lo stato attuale dell'RNG. Quindi la funzione che genera un nuovo numero casuale prende un RandGen e restituisce una tupla con il nuovo RandGen e il numero prodotto. L'alternativa è specificare da qualche parte che si desidera un elenco di numeri casuali tra un valore minimo e uno massimo. Ciò restituirà un flusso infinito di numeri valutati pigramente, quindi possiamo semplicemente scorrere questo elenco ogni volta che abbiamo bisogno di un nuovo numero casuale.
Qqwy,

Allo stesso modo in cui li ottieni in qualsiasi altra lingua! ti procuri un algoritmo pseudo-casuale di generatore di numeri e poi lo semini con un certo valore e raccogli i numeri "casuali" che spuntano fuori! L'unica differenza è che linguaggi come C # e Java eseguono automaticamente il seeding del PRNG usando l'orologio di sistema o cose del genere. Questo e il fatto che in haskell si ottenga anche un nuovo PRNG che è possibile utilizzare per ottenere il numero "successivo", mentre in C # / Java, tutto ciò avviene internamente utilizzando variabili mutabili Randomnell'oggetto.
Sara

4

Una specie di vecchia domanda, ma è davvero una buona domanda, quindi risponderò.

Puoi pensare alle monadi come blocchi di codice per i quali hai il controllo completo su come vengono eseguiti: cosa dovrebbe restituire ogni riga di codice, se l'esecuzione dovrebbe fermarsi in qualsiasi momento, se qualche altra elaborazione dovrebbe avvenire tra ciascuna riga.

Darò alcuni esempi di cose che le monadi consentono che altrimenti sarebbe difficile. Nessuno di questi esempi è in Haskell, solo perché la mia conoscenza di Haskell è un po 'traballante, ma sono tutti esempi di come Haskell abbia ispirato l'uso delle monadi.

parser

Normalmente, se volessi scrivere un parser di qualche tipo, dire di implementare un linguaggio di programmazione, dovresti leggere la specifica BNF e scrivere un sacco di codice loopy per analizzarlo, o dovresti usare un compilatore del compilatore come Flex, Bison, Yacc ecc. Ma con le monadi, puoi creare una specie di "compilatore di compilatori" proprio in Haskell.

I parser non possono davvero essere fatti senza monadi o linguaggi speciali come yacc, bisonte ecc.

Ad esempio, ho preso la specifica del linguaggio BNF per il protocollo IRC :

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

E lo ho ridotto a circa 40 righe di codice in F # (che è un'altra lingua che supporta le monadi):

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

La sintassi della monade di F # è piuttosto brutta rispetto a quella di Haskell e probabilmente avrei potuto migliorarlo un po ', ma il punto da portare a casa è che strutturalmente, il codice parser è identico al BNF. Non solo questo avrebbe richiesto molto più lavoro senza le monadi (o un generatore di parser), ma non avrebbe avuto quasi nessuna somiglianza con le specifiche, e quindi sarebbe stato terribile sia da leggere che da mantenere.

Multitasking personalizzato

Normalmente, il multitasking è pensato come una funzionalità del sistema operativo - ma con le monadi, è possibile scrivere il proprio scheduler in modo tale che dopo ogni monade delle istruzioni, il programma passerebbe il controllo allo scheduler, che quindi sceglierebbe un'altra monade da eseguire.

Un ragazzo ha creato una monade "compito" per controllare i loop di gioco (sempre in F #), in modo che invece di dover scrivere tutto come una macchina a stati che agisca su ogni Update()chiamata, potrebbe semplicemente scrivere tutte le istruzioni come se fossero un'unica funzione .

In altre parole, invece di dover fare qualcosa del genere:

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

Potresti fare qualcosa del tipo:

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ to SQL

LINQ to SQL è in realtà un esempio di monade e funzionalità simili potrebbero essere facilmente implementate in Haskell.

Non entrerò nei dettagli poiché non ricordo tutto così accuratamente, ma Erik Meijer lo spiega abbastanza bene .


1

Se hai familiarità con i modelli GoF, le monadi sono come il modello Decorator e il modello Builder messi insieme, su steroidi, morsi da un tasso radioattivo.

Ci sono risposte migliori sopra, ma alcuni dei vantaggi specifici che vedo sono:

  • le monadi decorano un tipo di nucleo con proprietà aggiuntive senza cambiare il tipo di nucleo. Ad esempio, una monade potrebbe "sollevare" la stringa e aggiungere valori come "isWellFormed", "isProfanity" o "isPalindrome" ecc.

  • allo stesso modo, le monadi consentono di conglomerare un tipo semplice in un tipo di raccolta

  • le monadi consentono l'associazione tardiva delle funzioni a questo spazio di ordine superiore

  • le monadi consentono di mescolare funzioni e argomenti arbitrari con un tipo di dati arbitrario, nello spazio di ordine superiore

  • le monadi consentono di fondere funzioni pure e apolidi con una base impura e piena di stato, in modo da poter tenere traccia di dove si trova il problema

Un esempio familiare di monade in Java è Elenco. Prende alcune classi principali, come String, e le "solleva" nello spazio monade di List, aggiungendo informazioni sull'elenco. Quindi lega nuove funzioni in quello spazio come get (), getFirst (), add (), empty (), ecc.

Su larga scala, immagina che invece di scrivere un programma, hai appena scritto un grosso Builder (come il modello GoF), e il metodo build () alla fine sputava qualsiasi risposta il programma avrebbe dovuto produrre. E che potresti aggiungere nuovi metodi a ProgramBuilder senza ricompilare il codice originale. Ecco perché le monadi sono un potente modello di design.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.