Cosa significa la sintassi "Just" in Haskell?


118

Ho setacciato Internet per una spiegazione effettiva di ciò che fa questa parola chiave. Ogni tutorial Haskell che ho visto inizia a usarlo in modo casuale e non spiega mai cosa fa (e ne ho visti molti).

Ecco un pezzo di codice di base da Real World Haskell che utilizza Just. Capisco cosa fa il codice, ma non capisco quale sia lo scopo o la funzione Just.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

Da quello che ho osservato, è legato alla Maybedigitazione, ma è praticamente tutto ciò che sono riuscito a imparare.

Una buona spiegazione di ciò che Justsignifica sarebbe molto apprezzata.

Risposte:


211

In realtà è solo un normale costruttore di dati che sembra essere definito in Prelude , che è la libreria standard che viene importata automaticamente in ogni modulo.

Quello che forse è, strutturalmente

La definizione è simile a questa:

data Maybe a = Just a
             | Nothing

Quella dichiarazione definisce un tipo, Maybe ache è parametrizzato da una variabile di tipo a, il che significa semplicemente che puoi usarlo con qualsiasi tipo al posto di a.

Costruire e distruggere

Il tipo ha due costruttori Just ae Nothing. Quando un tipo ha più costruttori, significa che un valore del tipo deve essere stato costruito con uno solo dei possibili costruttori. Per questo tipo, un valore è stato costruito tramite Justo Nothing, non ci sono altre possibilità (non di errore).

Poiché Nothingnon ha alcun tipo di parametro, quando viene utilizzato come costruttore nomina un valore costante che è un membro del tipo Maybe aper tutti i tipi a. Ma il Justcostruttore ha un parametro di tipo, il che significa che quando usato come costruttore si comporta come una funzione dal tipo aa Maybe a, cioè ha il tipoa -> Maybe a

Quindi, i costruttori di un tipo costruiscono un valore di quel tipo; l'altro lato delle cose è quando vorresti usare quel valore, ed è qui che entra in gioco il pattern matching. A differenza delle funzioni, i costruttori possono essere utilizzati nelle espressioni di associazione di modelli e questo è il modo in cui è possibile eseguire l' analisi dei casi dei valori che appartengono a tipi con più di un costruttore.

Per utilizzare un Maybe avalore in una corrispondenza di pattern, è necessario fornire un pattern per ogni costruttore, in questo modo:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

In quel caso espressione, il primo modello corrisponderebbe se il valore fosse Nothing, e il secondo corrisponderebbe se il valore fosse costruito con Just. Se il secondo corrisponde, associa anche il nome valal parametro che è stato passato al Justcostruttore quando è stato costruito il valore con cui stai confrontando.

Che cosa significa forse

Forse conoscevi già come funzionava; non c'è davvero alcuna magia per i Maybevalori, è solo un normale Haskell Algebraic Data Type (ADT). Ma è usato un bel po 'perché effettivamente "solleva" o estende un tipo, come Integerdal tuo esempio, in un nuovo contesto in cui ha un valore extra ( Nothing) che rappresenta una mancanza di valore! Il sistema di tipi richiede quindi che tu controlli quel valore extra prima che ti permetta di arrivare a quello Integerche potrebbe essere lì. Ciò impedisce un numero notevole di bug.

Molte lingue oggi gestiscono questo tipo di valore "senza valore" tramite riferimenti NULL. Tony Hoare, un eminente scienziato informatico (ha inventato Quicksort ed è un vincitore del Turing Award), riconosce questo come il suo "errore da un miliardo di dollari" . Il tipo Forse non è l'unico modo per risolvere questo problema, ma ha dimostrato di essere un modo efficace per farlo.

Forse come Functor

L'idea di trasformare un tipo ad un altro in modo tale che le operazioni sul vecchio tipo possono anche essere trasformati in lavoro sul nuovo tipo è il concetto alla base della classe tipo Haskell chiamato Functor, che Maybe aha un esempio utile.

Functorfornisce un metodo chiamato fmap, che mappa le funzioni che vanno dai valori del tipo base (come Integer) a funzioni che vanno dai valori del tipo elevato (come Maybe Integer). Una funzione trasformata fmapper lavorare su un Maybevalore funziona così:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

Quindi, se hai un Maybe Integervalore m_xe una Int -> Intfunzione f, puoi fmap f m_xapplicare la funzione fdirettamente a Maybe Integersenza preoccuparti se ha effettivamente un valore o meno. In effetti, potresti applicare un'intera catena di Integer -> Integerfunzioni elevate ai Maybe Integervalori e Nothingdevi preoccuparti di controllare esplicitamente una volta sola quando hai finito.

Forse come monade

Non sono sicuro di quanto tu abbia familiarità con il concetto di Monadancora, ma almeno l'hai usato IO aprima e la firma del tipo IO asembra notevolmente simile a Maybe a. Sebbene IOsia speciale in quanto non ti espone i suoi costruttori e può quindi essere "eseguito" solo dal sistema di runtime Haskell, è anche un Functoroltre ad essere un file Monad. In effetti, c'è un senso importante in cui a Monadè solo un tipo speciale Functorcon alcune funzionalità extra, ma non è questo il posto per entrare in questo.

Ad ogni modo, alle monadi piacciono i IOtipi di mappatura su nuovi tipi che rappresentano "calcoli che si traducono in valori" e puoi elevare le funzioni in Monadtipi tramite una fmapfunzione molto simile chiamata liftMche trasforma una funzione regolare in un "calcolo che risulta nel valore ottenuto valutando il funzione."

Probabilmente hai intuito (se hai letto fin qui) che Maybeè anche un file Monad. Rappresenta "calcoli che potrebbero non riuscire a restituire un valore". Proprio come fmapnell'esempio, questo ti consente di eseguire un sacco di calcoli senza dover controllare esplicitamente gli errori dopo ogni passaggio. In effetti, il modo in cui Monadviene costruita l' istanza, un calcolo sui Maybevalori si interrompe non appena Nothingsi incontra a, quindi è un po 'come un aborto immediato o un ritorno senza valore nel mezzo di un calcolo.

Potresti aver scritto forse

Come ho detto prima, non c'è nulla di inerente al Maybetipo che è integrato nella sintassi del linguaggio o nel sistema di runtime. Se Haskell non lo ha fornito per impostazione predefinita, potresti fornire tu stesso tutte le sue funzionalità! In effetti, potresti riscriverlo comunque tu stesso, con nomi diversi, e ottenere la stessa funzionalità.

Si spera che tu capisca il Maybetipo e i suoi costruttori ora, ma se c'è ancora qualcosa di poco chiaro, fammelo sapere!


19
Che risposta eccezionale! Va detto che Haskell usa spesso Maybedove altre lingue userebbero nullo nil(con brutti messaggi in NullPointerExceptionagguato in ogni angolo). Ora anche altri linguaggi iniziano a usare questo costrutto: Scala as Option, e anche Java 8 avrà il Optionaltipo.
Landei

3
Questa è un'ottima spiegazione. Molte delle spiegazioni che ho letto in qualche modo accennano all'idea che Just sia un costruttore per il tipo Forse, ma nessuna lo ha mai reso esplicito.
reem il

@ Landei, grazie per il suggerimento. Ho apportato una modifica per fare riferimento al pericolo di riferimenti nulli.
Levi Pearson

@ Landei Il tipo di opzione esiste da ML negli anni '70, è più probabile che Scala lo abbia preso da lì perché Scala usa la convenzione di denominazione di ML, Opzione con costruttori alcuni e nessuno.
pietra miliare

@Landei Apple's Swift utilizza anche un po 'di optionals
Jamin

37

La maggior parte delle risposte attuali sono spiegazioni altamente tecniche di come Justlavorano gli amici; Ho pensato di provare a spiegare a cosa serve.

Molte lingue hanno un valore del genere nullche può essere utilizzato al posto di un valore reale, almeno per alcuni tipi. Questo ha fatto arrabbiare molte persone ed è stato ampiamente considerato una mossa sbagliata. Tuttavia, a volte è utile avere un valore come nullper indicare l'assenza di una cosa.

Haskell risolve questo problema facendoti segnare esplicitamente i punti in cui puoi avere un Nothing(la sua versione di a null). Fondamentalmente, se la tua funzione restituisce normalmente il tipo Foo, dovrebbe invece restituire il tipo Maybe Foo. Se vuoi indicare che non c'è valore, torna Nothing. Se vuoi restituire un valore bar, dovresti invece tornare Just bar.

Quindi, in pratica, se non puoi avere Nothing, non ne hai bisogno Just. Se puoi averlo Nothing, ne hai bisogno Just.

Non c'è niente di magico in Maybe; è costruito sul sistema di tipo Haskell. Ciò significa che puoi usare tutti i soliti trucchi di abbinamento dei modelli Haskell con esso.


1
Risposta molto bella accanto alle altre risposte, ma penso che trarrebbe comunque beneficio da un esempio di codice :)
PascalVKooten

13

Dato un tipo t, un valore di Just tè un valore esistente di tipo t, dove Nothingrappresenta un mancato raggiungimento di un valore o un caso in cui avere un valore non avrebbe senso.

Nel tuo esempio, avere un saldo negativo non ha senso, quindi se si verifica una cosa del genere, viene sostituito da Nothing.

Per un altro esempio, questo potrebbe essere utilizzato nella divisione, definendo una funzione di divisione che accetta ae be restituisce Just a/bse bè Nothingdiverso da zero e altrimenti. Viene spesso utilizzato in questo modo, come comoda alternativa alle eccezioni, o come il tuo esempio precedente, per sostituire i valori che non hanno senso.


1
Quindi diciamo che nel codice sopra ho rimosso Just, perché non dovrebbe funzionare? Devi avere solo ogni volta che vuoi avere un niente? Cosa succede a un'espressione in cui una funzione all'interno di quell'espressione restituisce Nothing?
reem

7
Se hai rimosso il Just, il tuo codice non verrebbe tipizzato. Il motivo Justè mantenere i tipi corretti. Esiste un tipo (una monade, in realtà, ma è più facile pensare che sia solo un tipo) Maybe t, che consiste di elementi della forma Just te Nothing. Poiché Nothingha tipo Maybe t, un'espressione che può restituire uno Nothingo un valore di tipo tnon è digitata correttamente. Se una funzione ritorna Nothingin alcuni casi, qualsiasi espressione che utilizza quella funzione deve avere un modo per verificarla ( isJusto un'istruzione case), in modo da gestire tutti i casi possibili.
qaphla

2
Quindi Just è lì per essere coerente nel tipo di Forse perché una t regolare non è nel tipo di Forse. Adesso è tutto molto più chiaro. Grazie!
reem il

3
@qaphla: il tuo commento sul fatto che sia "una monade, in realtà, [...]" è fuorviante. Maybe t è solo un tipo. Il fatto che ci sia Monadun'istanza per Maybenon lo trasforma in qualcosa che non è un tipo.
Sarah

2

Una funzione totale a-> b può trovare un valore di tipo b per ogni possibile valore di tipo a.

In Haskell non tutte le funzioni sono totali. In questo caso particolare, la funzione lendnon è totale - non è definita per il caso in cui il saldo è inferiore alla riserva (sebbene, a mio gusto, avrebbe più senso non consentire che newBalance sia inferiore alla riserva - così com'è, puoi prendere in prestito 101 da un saldo di 100).

Altri progetti che si occupano di funzioni non totali:

  • genera eccezioni dopo aver verificato che il valore di input non si adatta all'intervallo
  • restituisce un valore speciale (tipo primitivo): la scelta preferita è un valore negativo per le funzioni intere che hanno lo scopo di restituire numeri naturali (ad esempio, String.indexOf - quando una sottostringa non viene trovata, l'indice restituito è comunemente progettato per essere negativo)
  • restituisce un valore speciale (puntatore): NULL o qualcosa di simile
  • restituire silenziosamente senza fare nulla: ad esempio, lendpotrebbe essere scritto per restituire il vecchio saldo, se la condizione per il prestito non è soddisfatta
  • restituisce un valore speciale: Nothing (o Left wrapping some error description object)

Queste sono limitazioni progettuali necessarie nei linguaggi che non possono applicare la totalità delle funzioni (ad esempio, Agda può farlo, ma questo porta ad altre complicazioni, come diventare turing-incompleto).

Il problema con la restituzione di un valore speciale o la generazione di eccezioni è che è facile per il chiamante omettere per errore la gestione di tale possibilità.

Anche il problema con l'eliminazione silenziosa di un errore è ovvio: stai limitando ciò che il chiamante può fare con la funzione. Ad esempio, se viene lendrestituito il vecchio saldo, il chiamante non ha modo di sapere se il saldo è cambiato. Potrebbe essere un problema o meno, a seconda dello scopo previsto.

La soluzione di Haskell costringe il chiamante di una funzione parziale a gestire il tipo come Maybe a, oa Either error acausa del tipo restituito dalla funzione.

In questo modo, lendcome viene definita, è una funzione che non sempre calcola il nuovo equilibrio - per alcune circostanze il nuovo equilibrio non è definito. Segnaliamo questa circostanza al chiamante restituendo il valore speciale Nothing o avvolgendo il nuovo saldo in Just. Il chiamante ora ha la libertà di scegliere: gestire il mancato prestito in un modo speciale o ignorare e utilizzare il vecchio equilibrio, ad esempio maybe oldBalance id $ lend amount oldBalance.


-1

La funzione if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)deve avere lo stesso tipo di ifTruee ifFalse.

Quindi, quando scriviamo then Nothing, dobbiamo usare il Maybe atipo inelse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type

1
Sono sicuro che volevi dire qualcosa di veramente profondo qui
vedi il

1
Haskell ha inferenza di tipo. Non è necessario indicare il tipo di Nothinge in modo Just newBalanceesplicito.
destra il

Per questa spiegazione a chi non lo sapesse, i tipi espliciti chiariscono il significato dell'uso di Just.
philip
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.