Como devo armazenar valores "desconhecidos" e "ausentes" em uma variável, mantendo a diferença entre "desconhecido" e "ausente"?


57

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 printinstruçõ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.


19
Por que você deseja distinguir "medição feita, mas valor vazio" vs. "sem medição"? De fato, o que significa "medição feita, mas valor vazio"? O sensor falhou ao produzir um valor válido? Nesse caso, como isso é diferente de "desconhecido"? Você não poderá voltar no tempo e obter o valor correto.
DaveG

3
@DaveG Suponha que você busque o número de CPUs em um servidor. Se o servidor estiver desligado ou tiver sido descartado, esse valor simplesmente não existe. Será uma medida que não faz sentido (talvez "ausente" / "vazio" não sejam os melhores termos). Mas o valor é "conhecido" por ser absurdo. Se o servidor existir, mas o processo de busca do valor travar, medi-lo é válido, mas falha, resultando em um valor "desconhecido".
Exhuma

2
@exhuma Eu descreveria como "não aplicável", então.
Vincent Vincent

6
Por curiosidade, que tipo de medida você está fazendo onde "vazio" não é simplesmente igual ao zero de qualquer escala? "Desconhecido" / "ausente" Posso ser útil, por exemplo, se um sensor não estiver conectado ou se a saída bruta do sensor for lixo por um motivo ou outro, mas "vazio" em todos os casos em que posso pensar, pode ser mais consistente representado por 0, [], ou {}(o escalar 0, a lista vazia, e o mapa vazio, respectivamente). Além disso, esse valor "ausente" / "desconhecido" é basicamente exatamente o que nullserve - representa que poderia haver um objeto lá, mas não existe.
22818 Nic Hartley

7
Qualquer que seja a solução usada para isso, pergunte a si mesmo se ela sofre de problemas semelhantes aos que fizeram você querer eliminar o NULL em primeiro lugar.
Ray

Respostas:


85

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:

type Measurement =
    | Reading of value : int
    | Missing
    | Unknown of value : RawData

Um Measurementvalor será então a Reading, com um valor int, ou a Missing, ou um Unknowncom os dados brutos, como value(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.


7
você pode fazer tipos de soma nos idiomas OO, mas há um bom número de placas da caldeira para fazê-los funcionar stackoverflow.com/questions/3151702/…
jk.

11
“[Em idiomas de idiomas não funcionais] esse padrão provavelmente não é muito útil para você” - é um padrão bastante comum no OOP. O GOF tem uma variação desse padrão e linguagens como o C ++ oferecem construções nativas para codificá-lo.
Konrad Rudolph

14
@jk. Sim, eles não contam (bem, acho que sim; são muito ruins nesse cenário devido à falta de segurança). Eu quis dizer std::variant(e seus predecessores espirituais).
Konrad Rudolph

2
@Ewan Não, está dizendo "A medição é um tipo de dados que é ... ou ...".
Konrad Rudolph

2
@DavidArno Bem, mesmo sem DUs, existe uma solução "canônica" para isso no OOP, que consiste em ter uma superclasse de valores com subclasses para valores válidos e inválidos. Mas isso provavelmente está indo longe demais (e, na prática, parece que a maioria das bases de código evita o polimorfismo da subclasse em favor de um sinalizador para isso, como mostrado em outras respostas).
Konrad Rudolph

58

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 # e Optional<T>em outros idiomas.

Vamos supor que você tenha um tipo abstrato para representar a mônada:

abstract class Measurement<T> { ... }

e depois três subclasses:

final class Unknown<T> : Measurement<T> { ... a singleton ...}
final class Empty<T> : Measurement<T> { ... a singleton ... }
final class Actual<T> : Measurement<T> { ... a wrapper around a T ...}

Precisamos de uma implementação do Bind:

abstract class Measurement<T>
{ 
    public Measurement<R> Bind(Func<T, Measurement<R>> f)
  {
    if (this is Unknown<T>) return Unknown<R>.Singleton;
    if (this is Empty<T>) return Empty<R>.Singleton;
    if (this is Actual<T>) return f(((Actual<T>)this).Value);
    throw ...
  }

A partir disso, você pode escrever esta versão simplificada do Bind:

public Measurement<R> Bind(Func<A, R> f) 
{
  return this.Bind(a => new Actual<R>(f(a));
}

E agora você está pronto. Você tem uma Measurement<int>na mão. Você quer dobrar:

Measurement<int> m = whatever;
Measurement<int> doubled = m.Bind(a => a * 2);
Measurement<string> asString = m.Bind(a => a.ToString());

E siga a lógica; se mé Empty<int>então asStringé Empty<String>, excelente.

Da mesma forma, se tivermos

Measurement<int> First()

e

Measurement<double> Second(int i);

então podemos combinar duas medidas:

Measurement<double> d = First().Bind(Second);

e novamente, se First()é Empty<int>então dé Empty<double>e assim por diante.

A etapa principal é obter a operação de ligação correta . Pense bem sobre isso.


4
Mônadas (felizmente) são muito mais fáceis de usar do que entender. :)
Guran

11
@leftaroundabout: Precisamente porque eu não queria entrar nessa distinção de cortar o cabelo; como observa o pôster original, muitas pessoas não têm confiança quando se trata de mônadas. As caracterizações da teoria das categorias carregadas de jargões de operações simples trabalham contra o desenvolvimento de um senso de confiança e compreensão.
Eric Lippert

2
Portanto, seu conselho é substituir Nullpor Nullable+ algum código padrão? :)
Eric Duminil

3
@ Claude: Você deve ler o meu tutorial. Uma mônada é um tipo genérico que segue certas regras e fornece a capacidade de vincular uma cadeia de operações, portanto, nesse caso, Measurement<T>é o tipo monádico.
Eric Lippert

5
@daboross: Embora eu concorde que mônadas com estado são uma boa maneira de introduzir mônadas, não penso em carregar estado como a coisa que caracteriza uma mônada. Penso que você pode unir uma sequência de funções é algo atraente; o estado é apenas um detalhe de implementação.
Eric Lippert

18

Eu acho que, neste caso, uma variação em um padrão de objeto nulo seria útil:

public class Measurement
{
    private int value;
    private bool isUnknown = false;
    private bool isMissing = false;

    private Measurement() { }
    public Measurement(int value) { this.value = value; }

    public int Value {
        get {
            if (!isUnknown && !isMissing)
            {
                return this.value;
            }
            throw new SomeException("...");
        }                   
    }

    public static readonly Measurement Unknown = new Measurement
    {
        isUnknown = true
    };

    public static readonly Measurement Missing = new Measurement
    {
        isMissing = true
    };
}

Você pode transformá-lo em uma estrutura, substituir Equals / GetHashCode / ToString, adicionar conversões implícitas de ou para inte, 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 de nulls. Eu não sou uma pessoa Java, mas meu entendimento é que o Java OptionalInté semelhante e outras linguagens provavelmente têm suas próprias instalações para representar um Optionaltipo.


6
A implementação mais comum que eu já vi desse padrão envolve herança. Pode haver um caso para duas subclasses: MissingMeasurement e UnknownMeasurement. Eles poderiam implementar ou substituir métodos na classe de medição pai. 1
Greg Burghardt

2
Não é o ponto do Padrão de Objeto Nulo que você não falha em valores inválidos, mas não faz nada?
precisa saber é o seguinte

2
@ChrisWohlert, neste caso, o objeto realmente não possui nenhum método, exceto o Valuegetter, que absolutamente deve falhar, pois você não pode converter um de Unknownvolta em um int. 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).
Maciej Stachowski 15/08/1918

3
@MaciejStachowski Sim, não estou dizendo que não deve fazer nada, estou dizendo que o Padrão de Objeto Nulo não é um bom ajuste. Sua solução pode ser boa, mas eu não chamaria isso de Padrão de Objeto Nulo .
Chris Wohlert

14

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

class Measurement {
    public bool IsEmpty;
    public bool IsKnown;
    public int Value {
        get {
            if(!IsEmpty && IsKnown) return _value;
            throw new Exception("NaN");
            }
        }
}

Esclarecimentos importantes:

Você pode obter os requisitos de matemática sobrecarregando os operadores da classe

public static Measurement operator+ (Measurement a, Measurement b) {
    if(a.IsEmpty) { return b; }
    ...etc
}

10
@KakturusOption<Option<Int>>
Bergi

5
@ Bergi Você não pode achar que seja remotamente aceitável .. #
BlueRaja - Danny Pflughoeft

8
@ BlueRaja-DannyPflughoeft Na verdade, ele se encaixa muito bem na descrição dos OPs, que também tem uma estrutura aninhada. Para se tornar aceitável, é claro que introduziríamos um alias de tipo apropriado (ou "newtype") - mas um type Measurement = Option<Int>para um resultado que era um número inteiro ou uma leitura vazia está ok, e também Option<Measurement>para uma medição que pode ter sido feita ou não .
Bergi 14/08/1918

7
@arp "Inteiros perto de NaN"? Você poderia explicar o que você quer dizer com isso? Parece um pouco contra-intuitivo dizer que um número está "próximo" do próprio conceito de algo não ser um número.
22818 Nic Hartley

3
@Nic Hartley Em nosso sistema, um grupo do que "naturalmente" seria o menor número inteiro negativo possível foi reservado como NaN. Usamos esse espaço para codificar várias razões pelas quais esses bytes representavam algo diferente de dados legítimos. (isso foi há décadas e eu posso ter confundido alguns detalhes, mas definitivamente havia um conjunto de bits que você poderia colocar em um valor inteiro para fazê-lo disparar NaN se você tentasse fazer contas com ele.
arp

11

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 == bsempre 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).


6

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:

var value = Measurement.of(2);
out.println(value.map(x -> x * 2));

var empty = Measurement.empty();
out.println(empty.map(x -> x * 2));

var unknown = Measurement.unknown();
out.println(unknown.map(x -> x * 2));

impressão

Value(4)
Empty()
Unknown()

( Implementação completa como uma essência .)

Uma linguagem ou biblioteca FP fornece outras ferramentas como Try(aka Maybe) (um objeto que contém um valor ou um erro) e Either(um objeto que contém um valor de sucesso ou um valor de falha) que também podem ser usadas aqui.


2

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:

  • Tipos de soma
  • Sindicatos discriminados
  • Objetos ou estruturas que contêm uma enumeração que representa o resultado da operação e um campo para o resultado
  • Cordas mágicas ou números mágicos impossíveis de alcançar através da operação normal
  • Exceções, nos idiomas em que esse uso é idiomático
  • Percebendo que não há realmente nenhum valor em diferenciar esses dois cenários e apenas usar null

2

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 ...)


Promovido por mencionar "fazer algo".
Adeus Ms Chipps

4
Algumas pessoas podem observar que isso sofre os mesmos problemas do uso do NULL, ou seja, ele apenas muda da necessidade de verificações NULL para precisar de verificações "desconhecidas" e "ausentes", mas mantém o tempo de execução travado pela corrupção de dados silenciosa e feliz para o azarado como os únicos indicadores que você esqueceu de um cheque. Mesmo as verificações NULL ausentes têm a vantagem de que os linters podem pegá-las, mas isso perde isso. Ele faz adicionar uma distinção entre "desconhecido" e "ausente", embora, por isso bate NULL lá ...
8bittree

2

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

  • -1 = desconhecido,
  • -2 = impossível de medir,
  • -3 = recusou-se a responder,
  • -4 = conhecido mas confidencial,
  • -5 = varia dependendo da fase da lua, consulte a tabela 5a,
  • -6 = quadridimensional, medidas indicadas no título,
  • -7 = erro de leitura do sistema de arquivos,
  • -8 = reservado para uso futuro,
  • -9 = quadrado / cúbico, então Y é igual a X,
  • -10 = é uma tela do monitor, portanto, não usa as medidas X, Y: use X como a diagonal da tela,
  • -11 = anotou as medidas no verso de um recibo e foi lavado em ilegibilidade, mas acho que eram 5 ou 17,
  • -12 = ... você entendeu a ideia.

É 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:

  1. Se você tiver valores inválidos, não poderá evitar a verificação desses valores em nenhum momento do código em que está lidando com os valores.
  2. Se você está retornando algo diferente de um int, não está retornando um int, portanto não pode tratá-lo como um int. A sobrecarga do operador permite fingir , mas isso é apenas fingir.
  3. Um int com números mágicos (incluindo NULL, NAN, Inf ...) não é mais realmente um int, é uma estrutura de pobre.
  4. Evitar nulos não tornará o código mais robusto, apenas ocultará os problemas com ints ou os moverá para uma estrutura complexa de tratamento de exceções.

1

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ática math.nane 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:

  • com uma estrutura de dados do wrapper, pois as informações numéricas geralmente estão prestes a uma entidade e têm um tempo de medição. O wrapper pode substituir métodos mágicos, de __mult__modo que nenhuma exceção seja gerada quando seus valores Desconhecido ou Faltando aparecerem. Numpy e pandas podem ter essa capacidade neles.
  • com um valor sentinela (como o seu Unknownou -1 / -2) e uma instrução if
  • com um sinalizador booleano separado
  • com uma estrutura de dados lenta - sua função executa alguma operação na estrutura e, em seguida, ela retorna, a função mais externa que precisa do resultado real avalia a estrutura de dados lenta
  • com um pipeline lento de operações - semelhante ao anterior, mas este pode ser usado em um conjunto de dados ou em um banco de dados

1

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

Missingpode ser representado como math.nan
Unknownpode ser representado comoNone

Se você tiver mais de um valor, poderá filter()aplicar a operação apenas em valores que não são Unknownou Missing, 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, Missingpode aumentar MissingExceptione Unknownpode aumentar UnknownExceptionquando as operações são executadas nele.

@suppressUnknown(value=Unknown) # if an UnknownException is raised, return this value instead
@suppressMissing(value=Missing)
def sigmoid(value):
    ...

A vantagem dessa abordagem é que as propriedades de Missinge Unknownsã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.

sigmoidainda pode ligar sin, mesmo que não espere um Missingou Unknown, já que sigmoido decorador pegará a exceção.


11
pergunto-me qual é o sentido de postar duas respostas para a mesma pergunta (esta é a sua resposta anterior , algo de errado com ela?) #
286

@gnat Esta resposta fornece um raciocínio sobre por que não deve ser feito da maneira que o autor mostra, e eu não queria passar pelo incômodo de integrar duas respostas com idéias diferentes - é mais fácil escrever duas respostas que podem ser lidas independentemente . Não entendo por que você se importa tanto com o raciocínio inofensivo de outra pessoa.
noɥʇʎԀʎzɐɹƆ

0

Suponha buscar o número de CPUs em um servidor. Se o servidor estiver desligado ou tiver sido descartado, esse valor simplesmente não existe. Será uma medida que não faz sentido (talvez "ausente" / "vazio" não sejam os melhores termos). Mas o valor é "conhecido" por ser absurdo. Se o servidor existir, mas o processo de busca do valor travar, medi-lo é válido, mas falha, resultando em um valor "desconhecido".

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 (como DataSourceUnavailableExceptionou SpectacularFailureToGetDataException, 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) e get_measurement()só precisará retornar um intcaso que possa obter os dados com êxito. fonte - e você sabe que isso inté 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.


0

As respostas dadas são boas, mas ainda não refletem a relação hierárquica entre valor, vazio e desconhecido.

  • O mais alto é desconhecido .
  • Antes de usar um valor primeiro vazio, deve ser esclarecido.
  • Por último, vem o valor com o qual calcular.

Feio (por sua abstração falha), mas totalmente operacional seria (em Java):

Optional<Optional<Integer>> unknowableValue;

unknowableValue.ifPresent(emptiableValue -> ...);
Optional<Integer> emptiableValue = unknowableValue.orElse(Optional.empty());

emptiableValue.ifPresent(value -> ...);
int value = emptiableValue.orElse(0);

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.


0

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.

  • também existe o caso em que temos várias variáveis ​​de NA para uma única entrada ("imputação múltipla"). Exemplo: se eu não souber a idade, o CEP, o nível de escolaridade ou a renda de uma pessoa, será mais difícil atribuir sua renda.

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.


0

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.

x <- c(1, 2, NA, 4, "")

Então você pode ver quais valores estão faltando;

is.na(x)

E então o resultado será;

FALSE FALSE  TRUE FALSE FALSE

Como você pode ver, ""não está faltando. Você pode ameaçar ""como desconhecido. E NAestá faltando.


@ Hulk, que outras linguagens funcionais suportam valores ausentes? Mesmo que eles suportem valores ausentes, tenho certeza de que você não pode preenchê-los com métodos estatísticos em apenas uma linha de código.
ilhan

-1

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.


4
Isso significa que você teria que sobrecarregar todos os operadores
canalizar
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.