De vez em quando, quando os programadores estão reclamando de erros / exceções nulos, alguém pergunta o que fazemos sem nulo.
Tenho uma idéia básica da frieza dos tipos de opção, mas não tenho o conhecimento ou a habilidade de idiomas para melhor expressar isso. Qual é uma ótima explicação do seguinte, escrita de maneira acessível ao programador médio para o qual podemos apontar essa pessoa?
- A indesejabilidade de ter referências / ponteiros ser anulável por padrão
- Como os tipos de opção funcionam, incluindo estratégias para facilitar a verificação de casos nulos, como
- correspondência de padrões e
- compreensões monádicas
- Solução alternativa como mensagem comendo nada
- (outros aspectos que eu perdi)
programming-languages
functional-programming
null
nullpointerexception
non-nullable
Roman A. Taycher
fonte
fonte
Respostas:
Penso que o resumo sucinto de por que nulo é indesejável é que estados sem sentido não devem ser representáveis .
Suponha que eu esteja modelando uma porta. Pode estar em um dos três estados: aberto, fechado, mas desbloqueado, e fechado e bloqueado. Agora eu poderia modelá-lo ao longo das linhas de
e está claro como mapear meus três estados nessas duas variáveis booleanas. Mas isso deixa uma quarta, estado indesejado disponíveis:
isShut==false && isLocked==true
. Como os tipos que selecionei como minha representação admitem esse estado, devo dedicar esforço mental para garantir que a classe nunca entre nesse estado (talvez codificando explicitamente um invariante). Por outro lado, se eu estivesse usando uma linguagem com tipos de dados algébricos ou enumerações verificadas que me permitissem definirentão eu poderia definir
e não há mais preocupações. O sistema de tipos garantirá a existência de apenas três estados possíveis para uma instância
class Door
. É nisso que os sistemas de tipos são bons - excluindo explicitamente toda uma classe de erros no tempo de compilação.O problema
null
é que todo tipo de referência obtém esse estado extra em seu espaço que normalmente é indesejável. Umastring
variável pode ser qualquer sequência de caracteres ou essenull
valor extra maluco que não é mapeado no domínio do meu problema. UmTriangle
objeto tem trêsPoint
s, que possuemX
eY
valores, mas infelizmente oPoint
s ou oTriangle
próprio pode ser esse valor nulo louco que não faz sentido para o domínio gráfico em que estou trabalhando. Etc.Quando você pretende modelar um valor possivelmente inexistente, opte explicitamente por ele. Se a maneira que pretendo modelar as pessoas é que todos
Person
têm aFirstName
e aLastName
, mas apenas algumas pessoas têmMiddleName
s, então eu gostaria de dizer algo comoonde
string
aqui é assumido como um tipo não anulável. Então não há invariantes complicados para estabelecer e nenhumNullReferenceException
s inesperado ao tentar calcular o tamanho do nome de alguém. O sistema de tipos garante que qualquer código que lide com asMiddleName
contas pela possibilidade de existirNone
, enquanto qualquer código que lide com oFirstName
possa assumir com segurança que existe um valor lá.Por exemplo, usando o tipo acima, poderíamos criar esta função boba:
sem preocupações. Por outro lado, em um idioma com referências anuláveis para tipos como string, assumindo
você acaba criando coisas como
que explode se o objeto Person recebido não tiver o invariante de tudo ser não nulo ou
ou talvez
assumindo que o
p
primeiro / o último esteja lá, mas o meio pode ser nulo, ou talvez você faça verificações que geram tipos diferentes de exceções, ou quem sabe o que. Todas essas loucas opções de implementação e coisas para se pensar surgem porque existe esse valor representável estúpido que você não quer ou precisa.Nulo normalmente adiciona complexidade desnecessária. A complexidade é inimiga de todos os softwares e você deve se esforçar para reduzir a complexidade sempre que possível.
(Observe bem que há mais complexidade até para esses exemplos simples. Mesmo que a
FirstName
não possa sernull
, astring
pode representar""
(a sequência vazia), que provavelmente também não é um nome de pessoa que pretendemos modelar. Como tal, mesmo com não- seqüências anuláveis, ainda pode ser o caso de estarmos "representando valores sem sentido". Novamente, você pode optar por combater isso por meio de invariantes e código condicional em tempo de execução ou usando o sistema de tipos (por exemplo, para ter umNonEmptyString
tipo). este último talvez seja desaconselhável (tipos "bons" geralmente são "fechados" em um conjunto de operações comuns e, por exemplo,NonEmptyString
não são fechados em.SubString(0,0)
), mas demonstra mais pontos no espaço de design. No final do dia, em qualquer sistema de tipos, existe alguma complexidade da qual será muito bom se livrar, e outra complexidade que é intrinsecamente mais difícil de se livrar. A chave para este tópico é que, em quase todos os sistemas de tipos, a mudança de "referências anuláveis por padrão" para "referências não anuláveis por padrão" é quase sempre uma mudança simples que torna o sistema de tipos muito melhor na luta contra a complexidade e descartando certos tipos de erros e estados sem sentido. Portanto, é muito louco que tantos idiomas continuem repetindo esse erro repetidamente.)fonte
O bom dos tipos de opções não é que eles são opcionais. É que todos os outros tipos não são .
Às vezes , precisamos ser capazes de representar um tipo de estado "nulo". Às vezes, precisamos representar uma opção "sem valor", bem como os outros valores possíveis que uma variável pode assumir. Portanto, uma linguagem que não permita isso será um pouco prejudicada.
Mas , muitas vezes , não precisamos disso, e permitir um estado "nulo" apenas leva a ambiguidade e confusão: toda vez que eu acesso uma variável de tipo de referência no .NET, devo considerar que ela pode ser nula .
Freqüentemente, nunca será realmente nulo, porque o programador estrutura o código para que isso nunca aconteça. Mas o compilador não pode verificar isso e, toda vez que o vê, você deve se perguntar "isso pode ser nulo? Preciso verificar aqui nulo?"
Idealmente, nos muitos casos em que nulo não faz sentido, não deve ser permitido .
Isso é difícil de obter no .NET, onde quase tudo pode ser nulo. Você precisa confiar no autor do código que está chamando para ser 100% disciplinado e consistente e documentar claramente o que pode e o que não pode ser nulo, ou você deve ser paranóico e verificar tudo .
No entanto, se os tipos não são anuláveis por padrão , você não precisa verificar se eles são nulos. Você sabe que eles nunca podem ser nulos, porque o compilador / verificador de tipos impõe isso a você.
E, em seguida, só precisamos de uma porta dos fundos para os casos raros em que fazer necessidade de lidar com um estado nulo. Em seguida, um tipo de "opção" pode ser usado. Em seguida, permitimos nulo nos casos em que tomamos uma decisão consciente de que precisamos ser capazes de representar o caso "sem valor" e, em todos os outros casos, sabemos que o valor nunca será nulo.
Como outros já mencionaram, em C # ou Java, por exemplo, null pode significar uma de duas coisas:
O segundo significado deve ser preservado, mas o primeiro deve ser totalmente eliminado. E mesmo o segundo significado não deve ser o padrão. É algo em que podemos optar por se e quando precisamos . Mas quando não precisamos que algo seja opcional, queremos que o verificador de tipos garanta que nunca será nulo.
fonte
Até agora, todas as respostas se concentram no motivo de
null
ser uma coisa ruim e em como é útil se uma linguagem garantir que determinados valores nunca serão nulos.Eles então sugerem que seria uma idéia bastante interessante se você aplicasse a nulidade para todos os valores, o que pode ser feito se você adicionar um conceito como
Option
ouMaybe
representar tipos que nem sempre têm um valor definido. Essa é a abordagem adotada por Haskell.É tudo de bom! Mas isso não impede o uso de tipos explicitamente anuláveis / não nulos para obter o mesmo efeito. Por que, então, a Option ainda é uma coisa boa? Afinal, Scala suporta valores anuláveis (é tem de, para que ele possa trabalhar com bibliotecas Java), mas suporta
Options
bem.P. Então, quais são os benefícios além de poder remover nulos de um idioma completamente?
A. Composição
Se você fizer uma tradução ingênua do código com reconhecimento de nulo
para código com reconhecimento de opção
não há muita diferença! Mas também é uma maneira terrível de usar Opções ... Essa abordagem é muito mais limpa:
Ou até:
Quando você começa a lidar com a Lista de opções, fica ainda melhor. Imagine que a
people
própria lista seja opcional:Como é que isso funciona?
O código correspondente com verificações nulas (ou mesmo elvis?: Operadores) seria dolorosamente longo. O verdadeiro truque aqui é a operação flatMap, que permite a compreensão aninhada de Opções e coleções de uma maneira que valores nulos nunca podem atingir.
fonte
flatMap
seria chamado(>>=)
, isto é, o operador "bind" de mônadas. É isso mesmo, os haskellers gostam tanto deflatMap
pingar que colocamos no logotipo da nossa linguagem.Option<T>
nunca, jamais seja nula. Infelizmente, Scala é uhh, ainda ligada ao Java :-) (Por outro lado, se Scala não jogar bonito com Java, que o usariam Oo?)Desde que as pessoas parecem estar perdendo:
null
é ambígua.A data de nascimento de Alice é
null
. O que isso significa?A data da morte de Bob é
null
. O que isso significa?Uma interpretação "razoável" pode ser que a data de nascimento de Alice existe, mas é desconhecida, enquanto a data de morte de Bob não existe (Bob ainda está vivo). Mas por que chegamos a respostas diferentes?
Outro problema:
null
é um caso extremo.null = null
?nan = nan
?inf = inf
?+0 = -0
?+0/0 = -0/0
?As respostas são geralmente "sim", "não", "sim", "sim", "não", "sim", respectivamente. Os "matemáticos" loucos chamam NaN de "nulidade" e dizem que se compara igual a si mesmo. O SQL trata nulos como não iguais a nada (portanto, eles se comportam como NaNs). Alguém se pergunta o que acontece quando você tenta armazenar ± ∞, ± 0 e NaNs na mesma coluna do banco de dados (existem 2 53 NaNs, metade dos quais é "negativo").
Para piorar a situação, os bancos de dados diferem na maneira como tratam NULL, e a maioria deles não é consistente (consulte Manipulação NULL no SQLite para obter uma visão geral). Isso é horrível.
E agora a história obrigatória:
Projetei recentemente uma tabela de banco de dados (sqlite3) com cinco colunas
a NOT NULL, b, id_a, id_b NOT NULL, timestamp
. Como é um esquema genérico projetado para resolver um problema genérico para aplicativos bastante arbitrários, existem duas restrições de exclusividade:id_a
existe apenas para compatibilidade com um design de aplicativo existente (em parte porque eu não encontrei uma solução melhor) e não é usado no novo aplicativo. Devido à forma como NULL trabalha em SQL, posso inserir(1, 2, NULL, 3, t)
e(1, 2, NULL, 4, t)
e não violar a primeira restrição de exclusividade (porque(1, 2, NULL) != (1, 2, NULL)
).Isso funciona especificamente por causa de como o NULL funciona com uma restrição de exclusividade na maioria dos bancos de dados (presumivelmente, é mais fácil modelar situações do "mundo real", por exemplo, duas pessoas não podem ter o mesmo número de seguridade social, mas nem todas as pessoas têm um).
FWIW, sem primeiro chamar um comportamento indefinido, as referências do C ++ não podem "apontar para" nulas e não é possível construir uma classe com variáveis-membro de referência não inicializadas (se uma exceção for lançada, a construção falhará).
Nota: Ocasionalmente, você pode querer ponteiros mutuamente exclusivos (ou seja, apenas um deles pode ser não NULL), por exemplo, em um iOS hipotético
type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed
. Em vez disso, sou forçado a fazer coisas assimassert((bool)actionSheet + (bool)alertView == 1)
.fonte
assert(actionSheet ^ alertView)
? Ou o seu idioma não pode XOR bools?A indesejabilidade de ter referências / ponteiros é anulável por padrão.
Eu não acho que esse seja o principal problema com nulos, o principal problema com nulos é que eles podem significar duas coisas:
Os idiomas que suportam os tipos de opção geralmente também proíbem ou desencorajam o uso de variáveis não inicializadas.
Como os tipos de opção funcionam, incluindo estratégias para facilitar a verificação de casos nulos, como a correspondência de padrões.
Para serem eficazes, os tipos de opção precisam ser suportados diretamente no idioma. Caso contrário, é necessário muito código da placa da caldeira para simulá-los. A correspondência de padrões e a inferência de tipos são dois recursos principais do idioma, facilitando o trabalho dos tipos de opção. Por exemplo:
Em F #:
No entanto, em uma linguagem como Java sem suporte direto para os tipos de opção, teríamos algo como:
Solução alternativa como mensagem comendo nada
A "mensagem que come nada" do Objective-C não é tanto uma solução, mas uma tentativa de aliviar a dor de cabeça da verificação nula. Basicamente, em vez de lançar uma exceção de tempo de execução ao tentar chamar um método em um objeto nulo, a expressão é avaliada como nula. Suspendendo a descrença, é como se cada método de instância começasse
if (this == null) return null;
. Mas há perda de informações: você não sabe se o método retornou nulo porque é um valor de retorno válido ou porque o objeto é realmente nulo. É como engolir exceção e não progride para resolver os problemas com nulo descrito anteriormente.fonte
A Assembléia nos trouxe endereços também conhecidos como ponteiros não digitados. C os mapeou diretamente como ponteiros digitados, mas introduziu o null de Algol como um valor exclusivo de ponteiro, compatível com todos os ponteiros digitados. O grande problema com null em C é que, como todo ponteiro pode ser nulo, nunca se pode usar um ponteiro com segurança sem uma verificação manual.
Em idiomas de nível superior, ter nulo é estranho, pois realmente transmite duas noções distintas:
Ter variáveis indefinidas é praticamente inútil e gera comportamento indefinido sempre que elas ocorrem. Suponho que todos concordarão que ter coisas indefinidas deve ser evitado a todo custo.
O segundo caso é opcional e é melhor fornecido explicitamente, por exemplo, com um tipo de opção .
Digamos que estamos em uma empresa de transporte e precisamos criar um aplicativo para ajudar a criar uma programação para nossos motoristas. Para cada motorista, armazenamos algumas informações, tais como: as cartas de condução que eles possuem e o número de telefone para ligar em caso de emergência.
Em C, poderíamos ter:
Como você observa, em qualquer processamento da nossa lista de drivers, teremos que verificar se há indicadores nulos. O compilador não irá ajudá-lo, a segurança do programa depende de seus ombros.
No OCaml, o mesmo código ficaria assim:
Digamos agora que queremos imprimir os nomes de todos os motoristas, juntamente com seus números de licença de caminhão.
Em C:
No OCaml, isso seria:
Como você pode ver neste exemplo trivial, não há nada complicado na versão segura:
Considerando que em C, você poderia ter esquecido uma verificação nula e boom ...
Nota: esses exemplos de código não foram compilados, mas espero que você tenha entendido as idéias.
fonte
NULL
que em "referência que pode não apontar para nada" foi inventada para alguma linguagem Algol (a Wikipedia concorda, consulte en.wikipedia.org/wiki/Null_pointer#Null_pointer ). Mas é claro que é provável que os programadores de montagem inicializem seus ponteiros para um endereço inválido (leia-se: Nulo = 0).O Microsoft Research possui um projeto interessante chamado
É uma extensão C # com tipo não nulo e algum mecanismo para verificar se seus objetos não são nulos , embora, IMHO, a aplicação do princípio do design por contrato possa ser mais apropriada e mais útil para muitas situações problemáticas causadas por referências nulas.
fonte
Vindo do background do .NET, sempre achei que o null tinha um ponto, é útil. Até eu conhecer as estruturas e como era fácil trabalhar com elas, evitando muito código clichê. Tony Hoare falando na QCon London em 2009, pediu desculpas por inventar a referência nula . Para citá-lo:
Veja esta pergunta também em programadores
fonte
Robert Nystrom oferece um belo artigo aqui:
http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/
descrevendo seu processo de pensamento ao adicionar suporte para ausência e falha em sua linguagem de programação Magpie .
fonte
Eu sempre olhei para Nulo (ou nulo) como sendo a ausência de um valor .
Às vezes você quer isso, às vezes não. Depende do domínio com o qual você está trabalhando. Se a ausência for significativa: sem nome do meio, seu aplicativo poderá agir de acordo. Por outro lado, se o valor nulo não estiver presente: o primeiro nome é nulo, o desenvolvedor recebe a proverbial ligação telefônica às duas da manhã.
Também vi código sobrecarregado e complicado demais com verificações de nulo. Para mim, isso significa uma das duas coisas:
a) um bug mais alto na árvore de aplicativos
b) design incorreto / incompleto
Do lado positivo - Nulo é provavelmente uma das noções mais úteis para verificar se algo está ausente, e linguagens sem o conceito de nulo acabarão complicando demais as coisas na hora de validar os dados. Nesse caso, se uma nova variável não for inicializada, os ditos idiomas geralmente definirão variáveis para uma sequência vazia, 0 ou uma coleção vazia. No entanto, se uma sequência vazia ou 0 ou coleção vazia forem valores válidos para o seu aplicativo - você terá um problema.
Às vezes, isso é contornado, inventando valores especiais / estranhos para os campos para representar um estado não inicializado. Mas o que acontece quando o valor especial é inserido por um usuário bem-intencionado? E não vamos entrar na bagunça que isso fará das rotinas de validação de dados. Se a linguagem suportasse o conceito nulo, todas as preocupações desapareceriam.
fonte
Às vezes, os idiomas de vetor podem se safar por não ter um nulo.
O vetor vazio serve como um nulo digitado neste caso.
fonte