Cosa rende esattamente il sistema di tipi Haskell così venerato (vs, per esempio, Java)?


204

Sto iniziando a imparare l' Haskell . Sono molto nuovo, e sto solo leggendo un paio di libri online per capire meglio i suoi costrutti di base.

Uno dei "meme" di cui le persone che hanno familiarità hanno spesso parlato, è l'intera cosa "se si compila, funzionerà *" - che penso sia correlata alla forza del sistema dei tipi.

Sto cercando di capire perché esattamente Haskell è meglio di altri linguaggi staticamente tipizzati in questo senso.

Detto in altro modo, presumo in Java, potresti fare qualcosa di atroce come seppellire ArrayList<String>()per contenere qualcosa che dovrebbe davvero essere ArrayList<Animal>(). La cosa odiosa qui è che i tuoi stringcontenuti elephant, giraffe, ecc., E se qualcuno li inserisce Mercedes, il tuo compilatore non ti aiuterà.

Se ho fatto fare ArrayList<Animal>()poi, ad un certo punto più avanti nel tempo, se decido il mio programma non è realmente di animali, si tratta di veicoli, allora posso cambiare, per esempio, una funzione che produce ArrayList<Animal>per la produzione ArrayList<Vehicle>e la mia IDE mi dovrei dire ovunque ci è una pausa di compilazione.

La mia ipotesi è che questo è ciò che la gente intende con un sistema di tipo forte , ma non è ovvio per me perché Haskell sia migliore. Detto in altro modo, puoi scrivere Java buoni o cattivi, suppongo che tu possa fare lo stesso in Haskell (cioè inserire cose in stringhe / ints che dovrebbero davvero essere tipi di dati di prima classe).

Sospetto che mi manchi qualcosa di importante / di base.
Sarei molto felice di mostrarmi l'errore dei miei modi!


31
Lascerò le persone più consapevoli di quanto scrivo risposte vere, ma l'essenza è questa: i linguaggi tipicamente statici come C # hanno un sistema di tipi che tenta di aiutarti a scrivere codice difendibile ; digita sistemi come il tentativo di Haskell di aiutarti a scrivere codice corretto (cioè dimostrabile). Il principio di base sul lavoro è spostare le cose che possono essere verificate nella fase di compilazione; Haskell controlla più cose al momento della compilazione.
Robert Harvey,

8
Non so molto di Haskell, ma posso parlare di Java. Mentre appare fortemente tipizzato, ti consente comunque di fare cose "odiose" come hai detto. Per quasi tutte le garanzie che Java offre riguardo al suo sistema di tipi, c'è un modo per aggirarlo.

12
Non so perché tutte le risposte menzionino Maybesolo verso la fine. Se dovessi scegliere solo una cosa che le lingue più popolari dovrebbero prendere in prestito da Haskell, sarebbe così. È un'idea molto semplice (quindi non molto interessante da un punto di vista teorico), ma questo da solo renderebbe i nostri lavori molto più facili.
Paul,

1
Ci saranno ottime risposte qui, ma nel tentativo di aiutare, studiare le firme dei tipi. Consentono agli umani e ai programmi di ragionare sui programmi in un modo che illustrerà come Java sia nel ri-tipo medio sdolcinato.
Michael Easter,

6
Per onestà devo sottolineare che "il tutto se si compila, funzionerà" è uno slogan e non una dichiarazione di fatto. Sì, noi programmatori di Haskell sappiamo che passare la verifica del tipo offre buone probabilità di correttezza, per alcune nozioni limitate di correttezza, ma non è certamente un'affermazione letteralmente e universalmente "vera"!
Tom Ellis,

Risposte:


230

Ecco un elenco non ordinato di funzionalità del sistema di tipi disponibili in Haskell e non disponibile o meno piacevole in Java (per quanto ne sappia, che è certamente debole rispetto a Java)

  • Sicurezza . I tipi di Haskell hanno proprietà di "sicurezza del tipo" piuttosto buone. Questo è piuttosto specifico, ma essenzialmente significa che i valori di un certo tipo non possono trasformarsi in un altro tipo. Questo a volte è in contrasto con la mutabilità (vedere la limitazione del valore di OCaml )
  • Tipi di dati algebrici . I tipi di Haskell hanno essenzialmente la stessa struttura della matematica del liceo. Questo è scandalosamente semplice e coerente, eppure, a quanto pare, potente quanto potresti desiderare. È semplicemente un'ottima base per un sistema di tipi.
    • Programmazione generica del tipo di dati . Questo non è lo stesso dei tipi generici (vedi generalizzazione ). Invece, a causa della semplicità della struttura del tipo, come notato prima, è relativamente facile scrivere codice che opera genericamente su quella struttura. Più tardi parlerò di come qualcosa come l' Eqidentità potrebbe essere derivato automaticamente da un compilatore Haskell per un tipo definito dall'utente. In sostanza il modo in cui lo fa è camminare sulla struttura semplice e comune alla base di qualsiasi tipo definito dall'utente e abbinarlo tra valori: una forma molto naturale di uguaglianza strutturale.
  • Tipi reciprocamente ricorsivi . Questa è solo una componente essenziale della scrittura di tipi non banali.
    • Tipi nidificati . Ciò consente di definire tipi ricorsivi su variabili che ricorrono a tipi diversi. Ad esempio, un tipo di alberi bilanciati è data Bt a = Here a | There (Bt (a, a)). Pensa attentamente ai valori validi di Bt ae osserva come funziona quel tipo. È difficile!
  • La generalizzazione . Questo è quasi troppo sciocco per non avere un sistema di tipi (ehm, ti guarda, vai). È importante avere nozioni di variabili di tipo e la capacità di parlare di codice che è indipendente dalla scelta di quella variabile. Hindley Milner è un sistema di tipi derivato dal sistema F. Il sistema di tipi di Haskell è un'elaborazione della tipizzazione HM e il sistema F è essenzialmente il cuore della generalizzazione. Quello che intendo dire è che Haskell ha un'ottima storia di generalizzazione.
  • Tipi astratti . La storia di Haskell qui non è grandiosa, ma anche inesistente. È possibile scrivere tipi con un'interfaccia pubblica ma un'implementazione privata. Questo ci consente sia di ammettere modifiche al codice di implementazione in un secondo momento e, soprattutto perché è alla base di tutte le operazioni in Haskell, scrivere tipi "magici" che hanno interfacce ben definite come IO. Java probabilmente in realtà ha una storia di tipo astratto più bella, a dire il vero, ma non credo che fino a quando le interfacce non siano diventate più popolari fosse davvero vero.
  • Parametricity . Valori Haskell non hanno alcun operazioni universali. Java viola questo con cose come l'uguaglianza di riferimento e l'hashing e ancor più palesemente con le coercizioni. Ciò significa che ottieni teoremi gratuiti sui tipi che ti consentono di conoscere il significato di un'operazione o di un valore in modo del tutto notevole dal suo tipo --- alcuni tipi sono tali che ci può essere solo un numero molto piccolo di abitanti.
  • I tipi di tipo più elevato mostrano tutto il tipo quando si codificano cose più difficili. Functor / Applicativo / Monade, Pieghevole / Trasversabile, l'intero mtlsistema di tipizzazione degli effetti, punti di correzione funzionali generici. La lista potrebbe continuare all'infinito. Ci sono molte cose che sono meglio espresse a tipi superiori e relativamente pochi tipi di sistemi consentono persino all'utente di parlare di queste cose.
  • Digitare le classi . Se pensi ai sistemi di tipi come logiche — il che è utile — allora spesso ti viene richiesto di provare le cose. In molti casi si tratta essenzialmente di un rumore di linea: potrebbe esserci una sola risposta giusta ed è una perdita di tempo e fatica per il programmatore affermarlo. Le macchine da scrivere sono un modo per Haskell di generare le prove per te. In termini più concreti, questo ti consente di risolvere semplici "sistemi di equazioni di tipo" come "A quale tipo intendiamo fare le (+)cose insieme? Oh Integer, ok! Ora incorporiamo il codice giusto!". In sistemi più complessi potresti stabilire vincoli più interessanti.
    • Calcolo del vincolo . I vincoli in Haskell - che sono il meccanismo per entrare nel sistema di prologio della classe di caratteri - sono strutturati tipicamente. Ciò fornisce una forma molto semplice di relazione di sottotipo che consente di assemblare vincoli complessi da quelli più semplici. L'intera mtlbiblioteca si basa su questa idea.
    • Derivando . Per guidare la canonicità del sistema di typeclass è necessario scrivere molto spesso codice banale per descrivere i vincoli che i tipi definiti dall'utente devono creare un'istanza. Esegui la struttura molto normale dei tipi di Haskell, spesso è possibile chiedere al compilatore di fare questa piastra per te.
    • Digitare class prolog . Il risolutore di classi di tipo Haskell - il sistema che sta generando quelle "prove" a cui ho fatto riferimento in precedenza - è essenzialmente una forma paralizzata di Prolog con proprietà semantiche più belle. Questo significa che puoi codificare cose veramente pelose in tipo prologo e aspettarti che vengano gestite tutte in fase di compilazione. Un buon esempio potrebbe essere la risoluzione di una prova che due elenchi eterogenei sono equivalenti se si dimentica l'ordine: sono "set" eterogenei equivalenti.
    • Classi di tipi multiparametriche e dipendenze funzionali . Questi sono solo perfezionamenti di enorme utilità per basare il prologo della tabella dei tipi. Se conosci Prolog, puoi immaginare quanto aumenta la potenza espressiva quando puoi scrivere predicati di più di una variabile.
  • Inferenza abbastanza buona . Le lingue basate sui sistemi di tipo Hindley Milner hanno una buona deduzione. HM stesso ha un'inferenza completa, il che significa che non è mai necessario scrivere una variabile di tipo. Haskell 98, la forma più semplice di Haskell, lo getta già in alcune circostanze molto rare. In generale, il moderno Haskell è stato un esperimento per ridurre lentamente lo spazio dell'inferenza completa, aggiungendo più potenza a HM e vedendo quando gli utenti si lamentano. Le persone si lamentano molto raramente: l'inferenza di Haskell è piuttosto buona.
  • Sottotipo molto, molto, molto debole . Ho menzionato in precedenza che il sistema di vincoli del prologo tipoclass ha una nozione di sottotipo strutturale. Questa è l'unica forma di sottotipo in Haskell . Il sottotipo è terribile per ragionamento e inferenza. Rende ciascuno di questi problemi significativamente più difficile (un sistema di disuguaglianze anziché un sistema di uguaglianze). È anche molto facile fraintendere (la sottoclasse equivale al sottotipo? Certo che no! Ma la gente molto spesso lo confonde e molte lingue aiutano in quella confusione! Come siamo finiti qui? Suppongo che nessuno esamini mai l'LSP.)
    • Nota di recente (inizio 2017) Steven Dolan ha pubblicato la sua tesi su MLsub , una variante di inferenza di tipo ML e Hindley-Milner che ha una storia di sottotitoli molto bella ( vedi anche ). Questo non ovvia a ciò che ho scritto sopra - la maggior parte dei sistemi di sottotitolo sono rotti e hanno una cattiva inferenza - ma suggerisce che proprio oggi potremmo aver scoperto alcuni modi promettenti per avere un'inferenza completa e un sottotitolo che suonino bene insieme. Ora, per essere del tutto chiari, le nozioni di sottotipizzazione di Java non sono in grado di sfruttare gli algoritmi e i sistemi di Dolan. Richiede un ripensamento del significato del sottotipo.
  • Tipi di rango superiore . Ho parlato prima della generalizzazione, ma più che una semplice generalizzazione è utile poter parlare di tipi che hanno variabili generalizzate al loro interno . Ad esempio, una mappatura tra strutture di ordine superiore che è ignara (vedi parametria ) a ciò che tali strutture "contengono" ha un tipo simile (forall a. f a -> g a). In HM dritto è possibile scrivere una funzione in questo tipo, ma con i tipi più alto rango si esigere una tale funzione come un argomento in questo modo: mapFree :: (forall a . f a -> g a) -> Free f -> Free g. Si noti che la avariabile è associata solo all'interno dell'argomento. Ciò significa che il definitore della funzione mapFreepuò decidere a cosa aviene istanziata quando la usano, non l'utente di mapFree.
  • Tipi esistenziali . Mentre i tipi più alto rango ci permettono di parlare di quantificazione universale, tipi esistenziali parliamo di quantificazione esistenziale: l'idea che ci si limita esiste un certo tipo sconosciuto soddisfare alcune equazioni. Questo finisce per essere utile e continuare più a lungo richiederebbe molto tempo.
  • Digitare le famiglie . A volte i meccanismi della typeclass sono scomodi poiché non sempre pensiamo in Prolog. Le famiglie di tipi ci consentono di scrivere relazioni funzionali dirette tra i tipi.
    • Famiglie di tipo chiuso . Le famiglie di tipi sono aperte per impostazione predefinita, il che è fastidioso perché significa che mentre puoi estenderle in qualsiasi momento, non puoi "invertirle" con alcuna speranza di successo. Questo perché non puoi dimostrare l' iniettività , ma con le famiglie di tipo chiuso puoi farlo.
  • Tipi indicizzati gentili e promozione dei tipi . Sto diventando davvero esotico a questo punto, ma questi hanno un uso pratico di volta in volta. Se desideri scrivere un tipo di handle aperti o chiusi, puoi farlo molto bene. Si noti nel frammento seguente che Stateè un tipo algebrico molto semplice che aveva i suoi valori promossi anche a livello di tipo. Poi, in seguito, si può parlare di costruttori di tipo simile Handlecome prendere argomenti a specifici tipi come State. È fonte di confusione capire tutti i dettagli, ma anche molto bene.

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
    
  • Rappresentazioni del tipo di runtime che funzionano . Java è noto per avere la cancellazione del tipo e avere quella caratteristica pioggia sulle sfilate di alcune persone. La cancellazione del tipo è la strada giusta da percorrere, tuttavia, come se si avesse una funzione, getRepr :: a -> TypeRepralmeno si violerebbe la parametricità. Quel che è peggio è che se questa è una funzione generata dall'utente che viene utilizzata per innescare coercizioni non sicure in fase di esecuzione ... allora hai un grosso problema di sicurezza . Il Typeablesistema di Haskell consente la creazione di una cassaforte coerce :: (Typeable a, Typeable b) => a -> Maybe b. Questo sistema si basa Typeablesull'implementazione nel compilatore (e non su userland) e inoltre non potrebbe essere data una semantica così piacevole senza il meccanismo di typeclass di Haskell e le leggi che è garantito seguire.

Più di questi, tuttavia, il valore del sistema di tipi di Haskell si riferisce anche al modo in cui i tipi descrivono la lingua. Ecco alcune funzionalità di Haskell che guidano il valore attraverso il sistema di tipi.

  • Purezza . Haskell non consente effetti collaterali per una definizione molto, molto, molto ampia di "effetto collaterale". Questo ti costringe a inserire più informazioni nei tipi poiché i tipi governano input e output e senza effetti collaterali tutto deve essere preso in considerazione negli input e output.
    • IO . Successivamente, Haskell aveva bisogno di un modo per parlare degli effetti collaterali - dal momento che qualsiasi programma reale deve includerne alcuni - così una combinazione di caratteri tipografici, tipi di tipo superiore e tipi astratti ha dato l'idea di usare un tipo particolare, super speciale chiamato IO aper rappresentare calcoli con effetti collaterali che danno come risultato valori di tipo a. Questo è il fondamento di un sistema di effetti molto bello incorporato in un linguaggio puro.
  • Mancanza dinull . Tutti sanno che nullè l'errore di miliardi di dollari dei moderni linguaggi di programmazione. I tipi algebrici, in particolare la possibilità di aggiungere uno stato "non esiste" ai tipi che hai trasformando un tipo Anel tipo Maybe A, mitigano completamente il problema null.
  • Ricorsione polimorfa . Ciò consente di definire funzioni ricorsive che generalizzano le variabili di tipo nonostante le utilizzino in tipi diversi in ciascuna chiamata ricorsiva nella propria generalizzazione. È difficile parlarne, ma è particolarmente utile per parlare di tipi nidificati. Guardare indietro al Bt atipo da prima e cercare di scrivere una funzione per calcolare le dimensioni: size :: Bt a -> Int. Sembrerà un po 'come size (Here a) = 1e size (There bt) = 2 * size bt. Dal punto di vista operativo non è troppo complesso, ma si noti che la chiamata ricorsiva sizenell'ultima equazione si verifica in un tipo diverso , ma la definizione generale ha un bel tipo generalizzato size :: Bt a -> Int. Si noti che questa è una funzionalità che interrompe l'inferenza totale, ma se si fornisce una firma del tipo, Haskell lo consentirà.

Potrei continuare, ma questo elenco dovrebbe farti iniziare e poi alcuni.


7
Null non è stato un "errore" da miliardi di dollari. Ci sono casi in cui non è possibile verificare staticamente che il puntatore non verrà sottoposto a dereferenziazione prima che possa esistere un significato ; avere un tentativo di trappola della dereferenza in tal caso è spesso meglio che richiedere che il puntatore identifichi inizialmente un oggetto insignificante. Penso che il più grande errore correlato a null sia stato avere implementazioni che, dato , intrappoleranno , ma non intrappoleranno néchar *p = NULL;*p=1234char *q = p+5678;*q = 1234;
supercat

37
Questo è semplicemente citato da Tony Hoare: en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions . Mentre sono sicuro che ci sono volte in cui nullè necessario nell'aritmetica del puntatore, interpreto invece che dire che l'aritmetica del puntatore è un brutto posto per ospitare la semantica della tua lingua, non che null sia ancora un errore.
J. Abrahamson,

18
@supercat, puoi davvero scrivere una lingua senza null. Se permetterlo o no è una scelta.
Paul Draper,

6
@supercat - Questo problema esiste anche in Haskell, ma in una forma diversa. Haskell è normalmente pigro e immutabile, e quindi ti permette di scrivere p = undefinedfino a quando pnon viene valutato. Più utilmente, puoi inserire undefineduna sorta di riferimento mutabile, sempre a condizione che tu non lo valuti. La sfida più seria è rappresentata da calcoli pigri che potrebbero non terminare, il che è ovviamente indecifrabile. La differenza principale è che questi sono tutti errori di programmazione inequivocabili e non vengono mai utilizzati per esprimere la logica ordinaria.
Christian Conkle,

6
@supercat Haskell manca del tutto la semantica di riferimento (questa è la nozione di trasparenza referenziale che implica che tutto viene preservato sostituendo i riferimenti con i loro riferimenti). Quindi, penso che la tua domanda sia mal posta.
J. Abrahamson,

78
  • Inferenza del tipo completo. In realtà puoi usare tipi complessi onnipresentemente senza pensare: "Sacra merda, tutto ciò che faccio è scrivere firme di tipo".
  • I tipi sono completamente algebrici , il che rende molto semplice esprimere alcune idee complesse.
  • Haskell ha classi di tipi, che sono una sorta di interfacce simili, tranne per il fatto che non devi mettere tutte le implementazioni per un tipo nello stesso posto. Puoi creare implementazioni delle tue classi di tipi per tipi di terze parti esistenti, senza bisogno di accedere alla loro fonte.
  • Le funzioni di ordine superiore e ricorsive hanno la tendenza a mettere più funzionalità nell'ambito del controllo del tipo. Prendi il filtro , ad esempio. In un linguaggio imperativo, potresti scrivere un forciclo per implementare la stessa funzionalità, ma non avrai le stesse garanzie di tipo statico, perché un forciclo non ha il concetto di un tipo restituito.
  • La mancanza di sottotipi semplifica notevolmente il polimorfismo parametrico.
  • I tipi di tipo superiore (tipi di tipi) sono relativamente facili da specificare e utilizzare in Haskell, il che consente di creare astrazioni attorno a tipi completamente insondabili in Java.

7
Bella risposta - potresti darmi un semplice esempio di un tipo più gentile, pensi che mi aiuterebbe a capire perché è impossibile farlo in Java.
phatmanace,

3
Ci sono alcuni buoni esempi qui .
Karl Bielefeldt,

3
Anche la corrispondenza dei motivi è molto importante, significa che puoi usare il tipo di un oggetto per prendere decisioni in modo super facile.
Benjamin Gruenbaum,

2
@BenjaminGruenbaum Non credo che definirei una funzionalità di sistema di tipo.
Doval,

3
Mentre gli ADT e gli HKT sono sicuramente parte della risposta, dubito che chiunque faccia questa domanda saprà perché sono utili, suggerisco che entrambe le sezioni debbano essere ampliate per spiegarlo
jk.

62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

In Haskell: un numero intero, un numero intero che potrebbe essere nullo, un numero intero il cui valore proviene dal mondo esterno e un numero intero che potrebbe essere invece una stringa, sono tutti tipi distinti - e il compilatore lo imporrà . Non è possibile compilare un programma Haskell che non rispetta queste distinzioni.

(Tuttavia, è possibile omettere le dichiarazioni di tipo. Nella maggior parte dei casi, il compilatore può determinare il tipo più generale per le variabili che si tradurrà in una compilazione corretta. Non è pulito?)


11
+1 mentre questa risposta non è completa, penso che sia molto più preciso a livello di domanda
jk.

1
+1 Anche se aiuterebbe a spiegare che hanno anche altre lingue Maybe(ad esempio Java Optionale Scala Option), ma in quelle lingue è una soluzione semi-cotta, dal momento che puoi sempre assegnare nulla una variabile di quel tipo e far esplodere il tuo programma a run- tempo. Questo non può accadere con Haskell [1], perché non esiste un valore null , quindi semplicemente non puoi imbrogliare. ([1]: in realtà, puoi generare un errore simile a NullPointerException usando funzioni parziali come fromJustquando hai un Nothing, ma quelle funzioni sono probabilmente disapprovate).
Andres F.

2
"un numero intero il cui valore proviene dal mondo esterno" - non IO Integersarebbe più vicino al "sottoprogramma che, quando eseguito, fornisce un numero intero"? Poiché a) main = c >> cil valore restituito per primo cpuò essere diverso quindi per secondo cmentre aavrà lo stesso valore indipendentemente dalla sua posizione (purché siamo in ambito unico) b) ci sono tipi che indicano valori provenienti da un mondo esterno per far rispettare la sua sanificazione (cioè non metterli direttamente ma prima controlla se l'input dell'utente è corretto / non malevolo).
Maciej Piechotka,

4
Maciej, sarebbe più preciso. Stavo cercando la semplicità.
WolfeFan,

30

Molte persone hanno elencato cose positive su Haskell. Ma in risposta alla tua domanda specifica "perché il sistema dei tipi rende i programmi più corretti?", Sospetto che la risposta sia "polimorfismo parametrico".

Considera la seguente funzione di Haskell:

foobar :: x -> y -> y

Esiste letteralmente un solo modo per implementare questa funzione. Solo con la firma del tipo, posso dire esattamente cosa fa questa funzione, perché c'è solo una cosa possibile che può fare. [OK, non del tutto, ma quasi!]

Fermati e pensaci un momento. Questo è davvero un grosso problema! Significa che se scrivo una funzione con questa firma, in realtà è impossibile per la funzione fare qualcosa di diverso da quello che intendevo. (La stessa firma del tipo può comunque essere errata, ovviamente. Nessun linguaggio di programmazione potrà mai prevenire tutti i bug.)

Considera questa funzione:

fubar :: Int -> (x -> y) -> y

Questa funzione è impossibile . Non puoi letteralmente implementare questa funzione. Posso dirlo solo dalla firma del tipo.

Come puoi vedere, una firma di tipo Haskell ti dice molto!


Confronta con C #. (Mi dispiace, il mio Java è un po 'arrugginito.)

public static TY foobar<TX, TY>(TX in1, TY in2)

Ci sono un paio di cose che questo metodo potrebbe fare:

  • Ritorna in2come risultato.
  • Ripeti per sempre e non restituire mai nulla.
  • Lancia un'eccezione e non restituire mai nulla.

In realtà, Haskell ha anche queste tre opzioni. Ma C # ti offre anche le opzioni aggiuntive:

  • Restituisce null. (Haskell non ha null.)
  • Modifica in2prima di restituirlo. (Haskell non ha modifiche sul posto.)
  • Usa la riflessione. (Haskell non ha riflessione.)
  • Eseguire più azioni I / O prima di restituire un risultato. (Haskell non ti consentirà di eseguire l'I / O a meno che tu non dichiari di eseguire l'I / O qui.)

La riflessione è un martello particolarmente grande; usando la riflessione, posso costruire un nuovo TYoggetto dal nulla e restituirlo! Posso ispezionare entrambi gli oggetti e fare diverse azioni a seconda di ciò che trovo. Posso apportare modifiche arbitrarie a entrambi gli oggetti passati.

L'I / O è un martello altrettanto grande. Il codice potrebbe essere la visualizzazione di messaggi all'utente, l'apertura di connessioni al database o la riformattazione del disco rigido o qualsiasi altra cosa.


La foobarfunzione Haskell , al contrario, può solo prendere alcuni dati e restituirli invariati. Non può "guardare" i dati, perché il suo tipo è sconosciuto al momento della compilazione. Non può creare nuovi dati, perché ... beh, come si costruiscono i dati di qualsiasi tipo possibile? Avresti bisogno di riflessioni per quello. Non può eseguire alcun I / O, poiché la firma del tipo non dichiara che l'I / O viene eseguito. Quindi non può interagire con il filesystem o la rete, o persino eseguire thread nello stesso programma! (Vale a dire, è sicuro al 100%.

Come puoi vedere, non permettendoti di fare un sacco di cose, Haskell ti consente di fornire garanzie molto forti su ciò che il tuo codice effettivamente fa. Così stretto, infatti, che (per un codice davvero polimorfico) di solito c'è solo un modo possibile che i pezzi possano stare insieme.

(Per essere chiari: è ancora possibile scrivere funzioni di Haskell in cui la firma del tipo non ti dice molto. Int -> IntPotrebbe essere praticamente qualsiasi cosa. Ma anche allora, sappiamo che lo stesso input produrrà sempre lo stesso output con certezza al 100%. Java non lo garantisce nemmeno!)


4
+1 Ottima risposta! Questo è molto potente e spesso sottovalutato dai nuovi arrivati ​​a Haskell. A proposito, una funzione "impossibile" più semplice sarebbe fubar :: a -> b, no? (Sì, ne sono consapevole unsafeCoerce. Presumo che non stiamo parlando di nulla con "non sicuro" nel suo nome, e nemmeno i nuovi arrivati ​​dovrebbero preoccuparsene!: D)
Andres F.

Ci sono molte firme di tipo più semplice che non puoi scrivere, sì. Ad esempio, foobar :: xè abbastanza inattuabile ...
MathematicalOrchid

In realtà, non puoi rendere sicuro il thread puro del codice, ma puoi comunque renderlo multi-thread. Le tue opzioni sono "prima di valutarlo, valutalo", "quando lo valuti, potresti voler valutare anche questo in un thread separato" e "quando lo valuti, potresti voler valutare anche questo, possibilmente in un thread separato ". L'impostazione predefinita è "fai come desideri", che significa essenzialmente "valuta il più tardi possibile".
John Dvorak,

Più prosaicamente puoi chiamare metodi di istanza su in1 o in2 che hanno effetti collaterali. Oppure puoi modificare lo stato globale (che, garantito, è modellato come un'azione IO in Haskell, ma potrebbe non essere quello che la maggior parte della gente considera IO).
Doug McClean,

2
@isomorphismes Il tipo x -> y -> yè perfettamente implementabile. Il tipo (x -> y) -> ynon lo è. Il tipo x -> y -> yaccetta due input e restituisce il secondo. Il tipo (x -> y) -> yha una funzione attiva xe in qualche modo deve farcela y...
MathematicalOrchid

17

Una domanda SO correlata .

Presumo che tu possa fare lo stesso in haskell (cioè sistemare cose in stringhe / ints che dovrebbero essere davvero tipi di dati di prima classe)

No, davvero non puoi - almeno non allo stesso modo di Java. In Java, questo genere di cose succede:

String x = (String)someNonString;

e Java proverà felicemente a lanciare il tuo non-String come String. Haskell non consente questo genere di cose, eliminando un'intera classe di errori di runtime.

nullfa parte del sistema dei tipi (as Nothing), quindi deve essere esplicitamente richiesto e gestito, eliminando un'intera altra classe di errori di runtime.

Ci sono anche molti altri vantaggi sottili - specialmente riguardo al riutilizzo e alle classi di tipo - che non ho le competenze per conoscere abbastanza bene da comunicare.

Soprattutto, però, è perché il sistema di tipi di Haskell consente molta espressività. Puoi fare un sacco di cose con solo poche regole. Considera l'albero sempre presente di Haskell:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

Hai definito un intero albero binario generico (e due costruttori di dati) in una riga di codice abbastanza leggibile. Tutto usando solo alcune regole (con tipi di somma e tipi di prodotto ). Cioè 3-4 file di codice e classi in Java.

Soprattutto tra quelli inclini a venerare i sistemi di tipo, questo tipo di concisione / eleganza è molto apprezzato.


Ho capito solo NullPointerExceptions dalla tua risposta. Potresti includere altri esempi?
Jesvin Jose,

2
Non necessariamente vero, JLS §5.5.1 : Se T è un tipo di classe, allora | S | <: | T | o | T | <: | S |. Altrimenti, si verifica un errore in fase di compilazione. Quindi il compilatore non ti permetterà di lanciare tipi inconvertibili - ci sono ovviamente modi per aggirarlo.
Boris the Spider,

A mio avviso, il modo più semplice di mettere i vantaggi delle classi di tipo è che sono come quelli interfaceche possono essere aggiunti dopo il fatto, e non "dimenticano" il tipo che li sta implementando. Cioè, puoi assicurarti che due argomenti per una funzione abbiano lo stesso tipo, a differenza di interfaces dove due List<String>s possono avere implementazioni diverse. Tecnicamente potresti fare qualcosa di molto simile in Java aggiungendo un parametro di tipo a ogni interfaccia, ma il 99% delle interfacce esistenti non lo fa e confonderai i tuoi colleghi.
Doval,

2
@BoristheSpider Vero, ma il lancio di eccezioni comporta quasi sempre il downcasting da una superclasse a una sottoclasse o da un'interfaccia a una classe, e non è insolito che la superclasse sia Object.
Doval,

2
Penso che il punto nella domanda sulle stringhe non abbia a che fare con errori di cast e di runtime, ma il fatto che se non si desidera utilizzare i tipi, Java non ti farà - come in, archiviare effettivamente i dati in serie forma, abusando di stringhe come tipo ad hoc any. Neanche Haskell ti impedirà di farlo, dato che ... beh, ha delle stringhe. Haskell può dare gli strumenti, non può forzatamente fermare dal fare cose stupide, se ti ostini a Greenspunning abbastanza di un interprete di reinventare nullin un contesto nidificato. Nessuna lingua può.
Leushenko,

0

Uno dei "meme" di cui le persone che hanno familiarità hanno spesso parlato, è l'intera cosa "se si compila, funzionerà *" - che penso sia correlata alla forza del sistema dei tipi.

Questo è principalmente vero con piccoli programmi. Haskell ti impedisce di fare errori che sono facili in altre lingue (ad esempio confrontando an Int32e a Word32e qualcosa esplode), ma non ti impedisce di tutti gli errori.

Haskell in realtà rende il refactoring molto più semplice. Se il tuo programma era precedentemente corretto e verificava i problemi, ci sono buone possibilità che rimanga corretto anche dopo piccole modifiche.

Sto cercando di capire perché Haskell sia meglio di altre lingue tipicamente statiche in questo senso.

I tipi in Haskell sono abbastanza leggeri, in quanto è facile dichiarare nuovi tipi. Questo è in contrasto con un linguaggio come Rust, dove tutto è solo un po 'più ingombrante.

La mia ipotesi è che questo è ciò che la gente intende con un sistema di tipo forte, ma non è ovvio per me perché Haskell sia migliore.

Haskell ha molte caratteristiche oltre alla semplice somma e ai tipi di prodotto; ha anche tipi universalmente quantificati (ad esempio id :: a -> a). Puoi anche creare tipi di record contenenti funzioni, che è abbastanza diverso da un linguaggio come Java o Rust.

GHC può anche derivare alcune istanze basandosi solo sui tipi e, poiché l'avvento dei generici, è possibile scrivere funzioni generiche tra i tipi. Questo è abbastanza conveniente ed è più fluido dello stesso in Java.

Un'altra differenza è che Haskell tende ad avere errori di tipo relativamente buoni (almeno per quanto riguarda la scrittura). L'inferenza di tipo di Haskell è sofisticata ed è abbastanza raro che sia necessario fornire annotazioni di tipo per ottenere qualcosa da compilare. Ciò è in contrasto con Rust, dove talvolta l'inferenza del tipo può richiedere annotazioni anche quando il compilatore potrebbe in linea di principio dedurne il tipo.

Infine, Haskell ha le macchine da scrivere, tra cui la famosa monade. Le monadi sono un modo particolarmente piacevole per gestire gli errori; in pratica ti danno quasi tutta la comodità di nullsenza il terribile debug e senza rinunciare alla sicurezza del tuo tipo. Quindi la capacità di scrivere funzioni su questi tipi conta davvero un po 'quando si tratta di incoraggiarci ad usarli!

Detto in altro modo, puoi scrivere Java buono o cattivo, presumo che tu possa fare lo stesso in Haskell

Questo è forse vero, ma manca un punto cruciale: il punto in cui inizi a spararti nel piede in Haskell è più lontano del punto in cui inizi a spararti nel piede in Java.

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.