Considere isso uma pergunta "acadêmica". Eu estive pensando sobre como evitar NULLs de tempos em tempos e este é um exemplo em que não consigo encontrar uma solução satisfatória.
Vamos supor que eu armazene medições onde, às vezes, é sabido que a medição é impossível (ou está ausente). Eu gostaria de armazenar esse valor "vazio" em uma variável, evitando NULL. Outras vezes, o valor pode ser desconhecido. Portanto, tendo as medidas para um determinado período de tempo, uma consulta sobre uma medida nesse período pode retornar três tipos de respostas:
- A medida real naquele momento (por exemplo, qualquer valor numérico incluindo
0
) - Um valor "ausente" / "vazio" (ou seja, uma medição foi feita e o valor é conhecido por estar vazio nesse ponto).
- Um valor desconhecido (ou seja, nenhuma medida foi feita nesse ponto. Pode estar vazio, mas também pode haver qualquer outro valor).
Esclarecimentos importantes:
Supondo que você tivesse uma função get_measurement()
retornando uma de "vazio", "desconhecido" e um valor do tipo "número inteiro". Ter um valor numérico implica que certas operações podem ser feitas no valor de retorno (multiplicação, divisão, ...), mas o uso dessas operações em NULLs causará um travamento no aplicativo se não for capturado.
Gostaria de poder escrever código, evitando verificações NULL, por exemplo (pseudocódigo):
>>> value = get_measurement() # returns `2`
>>> print(value * 2)
4
>>> value = get_measurement() # returns `Empty()`
>>> print(value * 2)
Empty()
>>> value = get_measurement() # returns `Unknown()`
>>> print(value * 2)
Unknown()
Observe que nenhuma das print
instruções causou exceções (como nenhum NULL foi usado). Portanto, os valores vazios e desconhecidos se propagariam conforme necessário e a verificação se um valor é realmente "desconhecido" ou "vazio" pode ser adiada até realmente necessário (como armazenar / serializar o valor em algum lugar).
Nota lateral: A razão pela qual eu gostaria de evitar NULLs é principalmente um quebra-cabeças. Se eu quiser fazer as coisas, não sou contra o uso de NULLs, mas descobri que evitá-los pode tornar o código muito mais robusto em alguns casos.
0
,[]
, ou{}
(o escalar 0, a lista vazia, e o mapa vazio, respectivamente). Além disso, esse valor "ausente" / "desconhecido" é basicamente exatamente o quenull
serve - representa que poderia haver um objeto lá, mas não existe.Respostas:
A maneira comum de fazer isso, pelo menos com linguagens funcionais, é usar uma união discriminada. Este é um valor que é um de um int válido, um valor que indica "ausente" ou um valor que indica "desconhecido". No F #, pode parecer algo como:
Um
Measurement
valor será então aReading
, com um valor int, ou aMissing
, ou umUnknown
com os dados brutos, comovalue
(se necessário).No entanto, se você não estiver usando uma linguagem que ofereça suporte a uniões discriminadas ou equivalente, esse padrão provavelmente não será muito útil para você. Portanto, você poderia, por exemplo, usar uma classe com um campo enum que denota qual dos três contém os dados corretos.
fonte
std::variant
(e seus predecessores espirituais).Se você ainda não sabe o que é uma mônada, hoje seria um ótimo dia para aprender. Eu tenho uma introdução suave para programadores de OO aqui:
https://ericlippert.com/2013/02/21/monads-part-one/
Seu cenário é uma pequena extensão da "talvez mônada", também conhecida como
Nullable<T>
C # eOptional<T>
em outros idiomas.Vamos supor que você tenha um tipo abstrato para representar a mônada:
e depois três subclasses:
Precisamos de uma implementação do Bind:
A partir disso, você pode escrever esta versão simplificada do Bind:
E agora você está pronto. Você tem uma
Measurement<int>
na mão. Você quer dobrar:E siga a lógica; se
m
éEmpty<int>
entãoasString
éEmpty<String>
, excelente.Da mesma forma, se tivermos
e
então podemos combinar duas medidas:
e novamente, se
First()
éEmpty<int>
entãod
éEmpty<double>
e assim por diante.A etapa principal é obter a operação de ligação correta . Pense bem sobre isso.
fonte
Null
porNullable
+ algum código padrão? :)Measurement<T>
é o tipo monádico.Eu acho que, neste caso, uma variação em um padrão de objeto nulo seria útil:
Você pode transformá-lo em uma estrutura, substituir Equals / GetHashCode / ToString, adicionar conversões implícitas de ou para
int
e, se desejar um comportamento semelhante ao NaN, também poderá implementar seus próprios operadores aritméticos, de modo que, por exemplo.Measurement.Unknown * 2 == Measurement.Unknown
.Dito isto, o C #
Nullable<int>
implementa tudo isso, com a única ressalva de que você não pode diferenciar entre diferentes tipos denull
s. Eu não sou uma pessoa Java, mas meu entendimento é que o JavaOptionalInt
é semelhante e outras linguagens provavelmente têm suas próprias instalações para representar umOptional
tipo.fonte
Value
getter, que absolutamente deve falhar, pois você não pode converter um deUnknown
volta em umint
. Se a medida tivesse um, digamos,SaveToDatabase()
método, uma boa implementação provavelmente não executaria uma transação se o objeto atual for um objeto nulo (por comparação com um singleton ou por uma substituição de método).Se você literalmente DEVE usar um número inteiro, existe apenas uma solução possível. Use alguns dos valores possíveis como 'números mágicos' que significam 'ausente' e 'desconhecido'
por exemplo, 2.147.483.647 e 2.147.483.646
Se você só precisa do int para medições 'reais', crie uma estrutura de dados mais complicada
Esclarecimentos importantes:
Você pode obter os requisitos de matemática sobrecarregando os operadores da classe
fonte
Option<Option<Int>>
type Measurement = Option<Int>
para um resultado que era um número inteiro ou uma leitura vazia está ok, e tambémOption<Measurement>
para uma medição que pode ter sido feita ou não .Se suas variáveis são números de ponto flutuante, o IEEE754 (o padrão de número de ponto flutuante suportado pela maioria dos processadores e idiomas modernos) tem o seu apoio: é um recurso pouco conhecido, mas o padrão define não um, mas uma família inteira de Valores de NaN (não um número), que podem ser usados para significados arbitrários definidos pelo aplicativo. Em flutuadores de precisão única, por exemplo, você tem 22 bits livres que podem ser usados para distinguir entre 2 ^ {22} tipos de valores inválidos.
Normalmente, as interfaces de programação expõem apenas uma delas (por exemplo, da Numpy
nan
); Não sei se existe uma maneira integrada de gerar outras que não sejam a manipulação explícita de bits, mas é apenas uma questão de escrever algumas rotinas de baixo nível. (Você também precisará de um para diferenciá-los, porque, por design,a == b
sempre retorna falso quando um deles é um NaN.)Usá-los é melhor do que reinventar seu próprio "número mágico" para sinalizar dados inválidos, porque eles se propagam corretamente e sinalizam invalidez: por exemplo, você não corre o risco de se dar um tiro no pé se usar uma
average()
função e esquecer de procurar seus valores especiais.O único risco é que as bibliotecas não as suportem corretamente, pois são um recurso bastante obscuro: por exemplo, uma biblioteca de serialização pode 'achatá-las' da mesma forma
nan
(o que parece equivalente a ela para a maioria dos propósitos).fonte
Seguindo a resposta de David Arno , você pode fazer algo como uma união discriminada no OOP e em um estilo funcional de objeto como o fornecido pelo Scala, pelos tipos funcionais do Java 8 ou por uma biblioteca Java FP como o Vavr ou o Fugue . natural escrever algo como:
impressão
( Implementação completa como uma essência .)
Uma linguagem ou biblioteca FP fornece outras ferramentas como
Try
(akaMaybe
) (um objeto que contém um valor ou um erro) eEither
(um objeto que contém um valor de sucesso ou um valor de falha) que também podem ser usadas aqui.fonte
A solução ideal para o seu problema dependerá do motivo pelo qual você se preocupa com a diferença entre uma falha conhecida e uma medição não confiável conhecida e com quais processos posteriores você deseja dar suporte. Observe que 'processos a jusante' neste caso não exclui operadores humanos ou colegas desenvolvedores.
Simplesmente criar um "segundo sabor" nulo não fornece ao conjunto de processos a jusante informações suficientes para derivar um conjunto razoável de comportamentos.
Se você se basear em suposições contextuais sobre a origem de maus comportamentos sendo feitos pelo código a jusante, eu chamaria essa arquitetura ruim.
Se você souber o suficiente para distinguir entre uma razão para falha e uma falha sem uma razão conhecida, e essas informações vão informar comportamentos futuros, você deve comunicar esse conhecimento a jusante ou manipulá-lo em linha.
Alguns padrões para lidar com isso:
null
fonte
Se eu estivesse preocupado em "fazer algo" em vez de uma solução elegante, o truque rápido e sujo seria simplesmente usar as strings "desconhecido", "ausente" e 'representação de strings do meu valor numérico', que seria então convertido de uma string e usado conforme necessário. Implementado mais rápido do que escrever isso e, pelo menos em algumas circunstâncias, totalmente adequado. (Agora estou formando um pool de apostas no número de votos negativos ...)
fonte
A essência se a pergunta parece ser "Como eu retorno duas informações não relacionadas de um método que retorna um único int? Eu nunca quero verificar meus valores de retorno, e os nulos são ruins, não os use".
Vejamos o que você deseja passar. Você está passando um raciocínio int ou não-int por que não pode dar o int. A pergunta afirma que haverá apenas duas razões, mas quem já fez um enum sabe que qualquer lista aumentará. O escopo de especificar outras justificativas apenas faz sentido.
Inicialmente, portanto, parece que pode ser um bom argumento para lançar uma exceção.
Quando você deseja dizer ao chamador algo especial que não está no tipo de retorno, as exceções geralmente são o sistema apropriado: as exceções não são apenas para estados de erro e permitem que você retorne muito contexto e lógica para explicar por que você pode hoje não.
E este é o sistema ONLY que permite retornar ints com garantia garantida e garantir que todo operador int e método que recebe ints possam aceitar o valor de retorno desse método sem precisar verificar valores inválidos, como valores nulos ou mágicos.
Mas as exceções são realmente apenas uma solução válida se, como o nome indica, esse for um caso excepcional , não o curso normal dos negócios.
E um try / catch and handler é tão clichê quanto uma verificação nula, que foi o que foi contestado em primeiro lugar.
E se o chamador não contiver a tentativa / captura, o chamador precisará, e assim por diante.
Um segundo passe ingênuo é dizer "É uma medida. Medições negativas de distância são improváveis". Portanto, para algumas medições Y, você pode apenas ter consts para
É assim que é feito em muitos sistemas C antigos, e mesmo em sistemas modernos, onde há uma restrição genuína ao int, e você não pode envolvê-lo em uma estrutura ou mônada de algum tipo.
Se as medições puderem ser negativas, você apenas aumentará seu tipo de dados (por exemplo, int longo) e fará com que os valores mágicos sejam maiores que o intervalo da int e, idealmente, comece com algum valor que aparecerá claramente em um depurador.
Existem boas razões para tê-los como uma variável separada, em vez de apenas ter números mágicos. Por exemplo, digitação estrita, manutenção e conformidade com as expectativas.
Em nossa terceira tentativa, analisamos os casos em que é normal o negócio ter valores não int. Por exemplo, se uma coleção desses valores puder conter várias entradas não inteiras. Isso significa que um manipulador de exceções pode ser a abordagem errada.
Nesse caso, parece um bom argumento para uma estrutura que passa pelo int e pela lógica. Novamente, esse raciocínio pode ser apenas uma constante como o descrito acima, mas em vez de manter os dois no mesmo int, você os armazena como partes distintas de uma estrutura. Inicialmente, temos a regra de que, se a lógica for definida, o int não será definido. Mas não estamos mais vinculados a essa regra; também podemos fornecer justificativas para números válidos, se necessário.
De qualquer maneira, toda vez que você o chama, você ainda precisa de um clichê, para testar a justificativa para ver se o int é válido e, em seguida, retire e use a parte int se a justificativa permitir.
É aqui que você precisa investigar seu raciocínio por trás de "não usar nulo".
Como exceções, nulo significa um estado excepcional.
Se um chamador está chamando esse método e ignorando completamente a parte "lógica" da estrutura, esperando um número sem nenhum tratamento de erro e obtém um zero, ele manipulará o zero como um número e estará errado. Se obtiver um número mágico, tratará isso como um número e estará errado. Mas se obtiver um valor nulo, ele cairá , como deve acontecer.
Portanto, toda vez que você chama esse método, deve verificar o valor de retorno; no entanto, lida com os valores inválidos, dentro ou fora da banda, try / catch, verificando a estrutura para um componente "racional", verificando o int para um número mágico ou verificando um int para um nulo ...
A alternativa, lidar com a multiplicação de uma saída que pode conter um int inválido e uma lógica como "Meu cachorro comeu essa medida", é sobrecarregar o operador de multiplicação para essa estrutura.
... E sobrecarregue todos os outros operadores em seu aplicativo que possam ser aplicados a esses dados.
... E, em seguida, sobrecarregue todos os métodos que podem receber ints.
... E todas essas sobrecargas ainda precisam conter verificações de entradas inválidas, apenas para que você possa tratar o tipo de retorno desse método como se ele fosse sempre um int válido no momento em que você está chamando.
Portanto, a premissa original é falsa de várias maneiras:
fonte
Não entendo a premissa da sua pergunta, mas aqui está a resposta do valor nominal. Para ausente ou vazio, você pode fazer
math.nan
(não é um número). Você pode executar qualquer operação matemáticamath.nan
e ela permanecerámath.nan
.Você pode usar
None
(nulo do Python) para um valor desconhecido. De qualquer forma, você não deve manipular um valor desconhecido e algumas linguagens (Python não é uma delas) possuem operadores nulos especiais, de modo que a operação só é executada se o valor for nulo; caso contrário, o valor permanecerá nulo.Outros idiomas têm cláusulas de guarda (como Swift ou Ruby), e Ruby tem um retorno antecipado condicional.
Eu já vi isso resolvido no Python de várias maneiras diferentes:
__mult__
modo que nenhuma exceção seja gerada quando seus valores Desconhecido ou Faltando aparecerem. Numpy e pandas podem ter essa capacidade neles.Unknown
ou -1 / -2) e uma instrução iffonte
Como o valor é armazenado na memória depende do idioma e dos detalhes da implementação. Eu acho que o que você quer dizer é como o objeto deve se comportar para o programador. (É assim que eu leio a pergunta, me diga se estou errado.)
Você já propôs uma resposta para isso em sua pergunta: use sua própria classe que aceite qualquer operação matemática e retorne a si mesma sem gerar uma exceção. Você diz que deseja isso porque deseja evitar verificações nulas.
Solução 1: não evite verificações nulas
Missing
pode ser representado comomath.nan
Unknown
pode ser representado comoNone
Se você tiver mais de um valor, poderá
filter()
aplicar a operação apenas em valores que não sãoUnknown
ouMissing
, ou em quaisquer valores que deseja ignorar para a função.Não consigo imaginar um cenário em que você precise de uma verificação nula de uma função que atue em um único escalar. Nesse caso, é bom forçar verificações nulas.
Solução 2: use um decorador que captura exceções
Nesse caso,
Missing
pode aumentarMissingException
eUnknown
pode aumentarUnknownException
quando as operações são executadas nele.A vantagem dessa abordagem é que as propriedades de
Missing
eUnknown
são suprimidas somente quando você solicita explicitamente que elas sejam suprimidas. Outra vantagem é que essa abordagem é auto-documentada: toda função mostra se espera ou não um desconhecido ou um desaparecido e como a função.Quando você chama uma função que não espera que Missing receba Missing, a função aumentará imediatamente, mostrando exatamente onde ocorreu o erro, em vez de silenciosamente falhar e propagar uma Missing up the chain call. O mesmo vale para Desconhecido.
sigmoid
ainda pode ligarsin
, mesmo que não espere umMissing
ouUnknown
, já quesigmoid
o decorador pegará a exceção.fonte
Ambas soam como condições de erro, então eu julgaria que a melhor opção aqui é simplesmente
get_measurement()
lançar ambas como exceções imediatamente (comoDataSourceUnavailableException
ouSpectacularFailureToGetDataException
, respectivamente). Em seguida, se algum desses problemas ocorrer, o código de coleta de dados poderá reagir a ele imediatamente (como tentar novamente no último caso) eget_measurement()
só precisará retornar umint
caso que possa obter os dados com êxito. fonte - e você sabe que issoint
é válido.Se sua situação não suportar exceções ou não puder fazer muito uso delas, então uma boa alternativa é usar códigos de erro, talvez retornados por uma saída separada para
get_measurement()
. Esse é o padrão idiomático em C, onde a saída real é armazenada em um ponteiro de entrada e um código de erro é passado de volta como valor de retorno.fonte
As respostas dadas são boas, mas ainda não refletem a relação hierárquica entre valor, vazio e desconhecido.
Feio (por sua abstração falha), mas totalmente operacional seria (em Java):
Aqui, linguagens funcionais com um sistema de tipos agradáveis são melhores.
De fato: Os vazios / ausentes e desconhecidos * não-valores parecem bastante parte de algum estado do processo, alguns pipeline de produção. Como o Excel, espalhe células de planilha com fórmulas que referenciam outras células. Ali alguém poderia pensar em armazenar lambdas contextuais. Alterar uma célula reavaliaria todas as células dependentes recursivamente.
Nesse caso, um valor int seria obtido por um fornecedor int. Um valor vazio daria a um fornecedor int lançando uma exceção vazia ou avaliando como vazio (recursivamente para cima). Sua fórmula principal conectaria todos os valores e possivelmente também retornaria um vazio (valor / exceção). Um valor desconhecido desativaria a avaliação lançando uma exceção.
Os valores provavelmente seriam observáveis, como uma propriedade vinculada a java, notificando os ouvintes sobre alterações.
Em resumo: o padrão recorrente de necessidade de valores com estados adicionais vazios e desconhecidos parece indicar que uma planilha mais semelhante ao modelo de dados de propriedades encadernadas pode ser melhor.
fonte
Sim, o conceito de vários tipos diferentes de NA existe em alguns idiomas; mais ainda nos estatísticos, onde é mais significativo (a grande distinção entre Missing-Random, Missing-Completely-Random, Missing-Not-Random-Random ).
se estivermos apenas medindo comprimentos de widgets, não será crucial distinguir entre 'falha do sensor' ou 'corte de energia' ou 'falha de rede' (embora 'excesso numérico' transmita informações)
mas, por exemplo, na mineração de dados ou em uma pesquisa, solicitando aos entrevistados, por exemplo, sua renda ou status de HIV, um resultado de 'Desconhecido' é distinto de 'Recusar responder', e você pode ver que nossas suposições anteriores sobre como imputar o último tendem a ser diferente do anterior. Portanto, idiomas como SAS suportam vários tipos diferentes de NA; a linguagem R não, mas os usuários muitas vezes precisam burlar isso; As NAs em diferentes pontos de um pipeline podem ser usadas para denotar coisas muito diferentes.
Quanto à forma como você representa diferentes tipos de NA em linguagens de uso geral que não as suportam, geralmente as pessoas invadem coisas como NaN de ponto flutuante (requer conversão de números inteiros), enumerações ou sentinelas (por exemplo, 999 ou -1000) para números inteiros ou valores categóricos. Geralmente não há uma resposta muito limpa, desculpe.
fonte
R possui suporte de valor ausente incorporado. https://medium.com/coinmonks/dealing-with-missing-data-using-r-3ae428da2d17
Edit: porque eu fui derrotado, vou explicar um pouco.
Se você vai lidar com estatísticas, recomendo que você use uma linguagem de estatísticas como R porque R é escrito por estatísticos para estatísticos. A falta de valores é um tópico tão grande que eles ensinam um semestre inteiro. E há livros grandes apenas sobre valores ausentes.
No entanto, você pode marcar os dados ausentes, como um ponto ou "ausente" ou o que for. Em R, você pode definir o que você quer dizer com falta. Você não precisa convertê-los.
A maneira normal de definir o valor ausente é marcá-los como
NA
.Então você pode ver quais valores estão faltando;
E então o resultado será;
Como você pode ver,
""
não está faltando. Você pode ameaçar""
como desconhecido. ENA
está faltando.fonte
Existe uma razão para que a funcionalidade do
*
operador não possa ser alterada?A maioria das respostas envolve algum tipo de valor de pesquisa, mas pode ser mais fácil alterar o operador matemático nesse caso.
Você, então, ser capaz de ter semelhante
empty()
/unknown()
funcionalidade em todo o seu projeto.fonte