Penso che il breve riassunto del perché nulla sia indesiderabile è che gli stati insignificanti non dovrebbero essere rappresentabili .
Supponiamo che stia modellando una porta. Può essere in uno dei tre stati: aperto, chiuso ma sbloccato, chiuso e bloccato. Ora potrei modellarlo sulla falsariga di
class Door
private bool isShut
private bool isLocked
ed è chiaro come mappare i miei tre stati in queste due variabili booleane. Ma questo foglie quarto, Stato indesiderato disponibili: isShut==false && isLocked==true
. Poiché i tipi che ho selezionato come mia rappresentazione ammettono questo stato, devo dedicare uno sforzo mentale per garantire che la classe non entri mai in questo stato (forse codificando esplicitamente un invariante). Al contrario, se stessi usando una lingua con tipi di dati algebrici o enumerazioni verificate che mi permettono di definire
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
allora potrei definire
class Door
private DoorState state
e non ci sono più preoccupazioni. Il sistema di tipi garantirà che ci siano solo tre stati possibili per cui class Door
deve trovarsi un'istanza . Questo è ciò che i sistemi di tipo sono bravi - escludendo esplicitamente un'intera classe di errori in fase di compilazione.
Il problema null
è che ogni tipo di riferimento ottiene questo stato extra nel suo spazio che in genere è indesiderato. Una string
variabile può essere qualsiasi sequenza di caratteri, o potrebbe essere questo folle null
valore extra che non si associa al mio dominio problematico. Un Triangle
oggetto ha tre Point
s, che hanno essi stessi X
e Y
valori, ma sfortunatamente la Point
s o la Triangle
stessa potrebbe essere questo pazzo valore nullo che non ha senso per il dominio grafico in cui sto lavorando. Ecc.
Quando si intende modellare un valore eventualmente inesistente, è necessario attivarlo esplicitamente. Se il modo in cui intendo modellare le persone è che ognuno Person
ha a FirstName
e a LastName
, ma solo alcune persone hanno MiddleName
s, allora vorrei dire qualcosa come
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
dove string
qui si presume che sia un tipo non annullabile. Quindi non ci sono invarianti difficili da stabilire e nessun inaspettato NullReferenceException
quando si cerca di calcolare la lunghezza del nome di qualcuno. Il sistema dei tipi garantisce che qualsiasi codice che si occupa dei MiddleName
conti possa essere None
considerato possibile , mentre qualsiasi codice che si occupa di FirstName
può presumere che vi sia un valore.
Quindi, ad esempio, usando il tipo sopra, potremmo creare questa stupida funzione:
let TotalNumCharsInPersonsName(p:Person) =
let middleLen = match p.MiddleName with
| None -> 0
| Some(s) -> s.Length
p.FirstName.Length + middleLen + p.LastName.Length
senza preoccupazioni. Al contrario, in una lingua con riferimenti nullable per tipi come stringa, quindi assumendo
class Person
private string FirstName
private string MiddleName
private string LastName
finisci per creare cose come
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length + p.MiddleName.Length + p.LastName.Length
che esplode se l'oggetto Persona in arrivo non ha l'invariante di tutto ciò che è non nullo, oppure
let TotalNumCharsInPersonsName(p:Person) =
(if p.FirstName=null then 0 else p.FirstName.Length)
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ (if p.LastName=null then 0 else p.LastName.Length)
o forse
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ p.LastName.Length
supponendo che si p
assicuri che il primo / ultimo ci siano, ma che il mezzo può essere nullo, o forse fai controlli che generano diversi tipi di eccezioni o chissà cosa. Tutte queste folli scelte di implementazione e cose su cui pensare sorgono perché c'è questo stupido valore rappresentabile che non vuoi o di cui non hai bisogno.
Null in genere aggiunge complessità inutile. La complessità è nemica di tutto il software e dovresti cercare di ridurre la complessità ogni volta che sia ragionevole.
(Nota bene che c'è anche più complessità anche in questi semplici esempi. Anche se a FirstName
non può essere null
, a string
può rappresentare ""
(la stringa vuota), che probabilmente non è nemmeno un nome di persona che intendiamo modellare. Come tale, anche con non- stringhe nullable, potrebbe comunque essere il caso che "rappresentiamo valori insignificanti". Ancora una volta, potresti scegliere di combatterlo tramite invarianti e codice condizionale in fase di esecuzione, oppure usando il sistema dei tipi (ad esempio per avere un NonEmptyString
tipo). quest'ultimo è forse sconsigliato (i tipi "buoni" sono spesso "chiusi" su una serie di operazioni comuni, e ad esempio NonEmptyString
non sono chiusi.SubString(0,0)
), ma mostra più punti nello spazio di progettazione. Alla fine della giornata, in ogni dato sistema di tipi, c'è una certa complessità che sarà molto brava a sbarazzarsi di, e un'altra complessità che è intrinsecamente più difficile da eliminare. La chiave di questo argomento è che in quasi tutti i sistemi di tipi, il passaggio da "riferimenti annullabili di default" a "riferimenti non annullabili di default" è quasi sempre una semplice modifica che rende il sistema di tipi molto migliore nella lotta contro la complessità e escludendo alcuni tipi di errori e stati insignificanti. Quindi è abbastanza folle che così tante lingue continuino a ripetere questo errore ancora e ancora.)