Faz sentido ter o conceito de 'nulo' e 'Talvez'?

11

Ao criar um cliente para uma API da web em C #, deparei-me com um problema referente a nullum valor em que representaria duas coisas diferentes:

  • nada , por exemplo, um foopode ou não ter umbar
  • desconhecido : por padrão, a resposta da API inclui apenas um subconjunto de propriedades, é necessário indicar quais propriedades adicionais você deseja. Tão desconhecido significa que a propriedade não foi solicitada da API.

Após algumas pesquisas, descobri o tipo Maybe (ou Option), como ele é usado em linguagens funcionais e como "resolve" problemas de cancelamento de referência nulos, forçando o usuário a pensar na possível ausência de um valor. No entanto, todos os recursos que encontrei falaram sobre a substituição de null por Maybe . Eu encontrei algumas menções à lógica de três valores , mas não a entendo completamente e, na maioria das vezes, sua menção estava no contexto de "é uma coisa ruim".

Agora estou me perguntando se faz sentido ter o conceito de null e Maybe , para representar desconhecido e nada, respectivamente. Essa é a lógica de três valores sobre a qual li ou tem outro nome? Ou é a maneira pretendida de aninhar um Talvez em um Talvez?

Stijn
fonte
9
Não faz sentido ter null. É uma ideia completamente quebrada.
Andrej Bauer
7
Não faça um talvez-talvez-foo que tenha semântica diferente de um talvez-foo. Talvez seja uma mônada , e uma das leis da mônada é essa M M xe M xdeve ter a mesma semântica.
Eric Lippert
2
Você pode considerar examinar o design das versões anteriores do Visual Basic, que possuíam Nothing (referência a nenhum objeto), Null (semântica nula do banco de dados), Empty (uma variável não inicializada) e Missing (um parâmetro opcional não foi passado). Esse design era complicado e, de várias maneiras, inconsistente, mas há uma razão pela qual esses conceitos não foram conflitantes.
precisa
3
Maybe aa+1Maybe Maybe aa+1+1UserInput a
12
@ EricLippert: é falso que " M (M x)e M xdeve ter a mesma semântica". Tomemos, M = Listpor exemplo: listas de listas não são a mesma coisa que listas. Quando Mé uma mônada, há uma transformação (nomeadamente a multiplicação mônada) a partir M (M x)para M xo que explica a relação entre eles, mas eles não têm "a mesma semântica".
Andrej Bauer

Respostas:

14

O nullvalor como presente padrão em todos os lugares é apenas uma ideia realmente quebrada , então esqueça isso.

Você sempre deve ter exatamente o conceito que melhor descreve seus dados reais. Se você precisar de um tipo que indique "desconhecido", "nada" e "valor", deve ter exatamente isso. Mas se não atender às suas necessidades reais, você não deve fazê-lo. É uma boa ideia ver o que outras pessoas usam e o que elas propuseram, mas você não precisa segui-las cegamente.

As pessoas que projetam bibliotecas padrão tentam adivinhar padrões de uso comuns, e geralmente o fazem bem, mas se houver algo específico de que você precisa, você deve defini-lo. Por exemplo, em Haskell, você pode definir:

data UnknownNothing a =
     Unknown
   | Nothing
   | Value a

Pode até ser o caso de você usar algo mais descritivo e mais fácil de lembrar:

data UserInput a =
     Unknown
   | NotGiven
   | Answer a

Você pode definir 10 desses tipos para serem usados ​​em diferentes cenários. A desvantagem é que você não terá funções de biblioteca preexistentes disponíveis (como as de Maybe), mas isso geralmente acaba sendo um detalhe. Não é tão difícil adicionar sua própria funcionalidade.

Andrej Bauer
fonte
Faz todo o sentido quando você o vê assim. Estou mais interessada em idéias, apoio biblioteca existente é apenas um bônus :)
Stijn
Se ao menos a barreira para criar esses tipos fosse menor em muitos idiomas. : /
Raphael
Se ao menos as pessoas parassem de usar idiomas a partir dos anos 60 (ou suas reencarnações a partir dos anos 80).
Andrej Bauer
@AndrejBauer: what? mas então os teóricos não teriam do que reclamar!
Yttrill
Não reclamamos, estamos no ar.
Andrej Bauer 08/01
4

Até onde eu sei, o valor nullem C # é um valor possível para algumas variáveis, dependendo do seu tipo (estou certo?). Por exemplo, instâncias de alguma classe. Para o restante dos tipos (como int, booletc), você pode adicionar esse valor de exceção declarando variáveis ​​com int?ou em bool?vez disso (é exatamente isso que o Maybeconstrutor faz, como descreverei a seguir).

O Maybeconstrutor de tipos da programação funcional adiciona esse novo valor de exceção para um determinado tipo de dados. Portanto, se Inté o tipo de números inteiros ou Gameo tipo de estado de um jogo, Maybe Intpossui todos os números inteiros mais um valor nulo (às vezes chamado de nada ). O mesmo para Maybe Game. Aqui, não há tipos que acompanham o nullvalor. Você o adiciona quando precisar.

Na OMI, essa última abordagem é a melhor para qualquer linguagem de programação.

Euge
fonte
Sim, seu entendimento do C # está correto. Então, posso deduzir da sua resposta que você sugere aninhar Maybee largar nullinteiramente?
Stijn
2
Minha opinião é sobre como deve ser um idioma. Eu realmente não conheço as melhores práticas de programação em C #. No seu caso, a melhor opção seria definir um tipo de dados como @Andrej Bauer descrito, mas acho que você não pode fazer isso em C #.
quer
@Euge: os tipos de soma algébrica podem ser (aproximadamente) aproximados com subtipo clássico de estilo OO e envio de mensagem polimórfica de subtipo. É assim que é feito no Scala, por exemplo, com um toque adicional para permitir tipos fechados . O tipo se torna uma superclasse abstrata, os construtores de tipo se tornam subclasses de concreto, a correspondência de padrões sobre os construtores pode ser aproximada movendo os casos para métodos sobrecarregados nas subclasses ou isinstancetestes de concreto . Scala também possui correspondência de padrões "adequada" também, e C♯ na verdade ganhou recentemente padrões simples também.
Jörg W Mittag
O "toque adicional" que mencionei para o Scala é que, além dos modificadores "padrão" "virtuais" (para uma classe que pode ser herdada de) e final(para uma classe que não pode ser herdada), ele também possui sealed, para um classe que pode ser estendida apenas dentro da mesma unidade de compilação . Isso significa que o compilador pode conhecer estaticamente todas as subclasses possíveis (desde que sejam elas próprias sealedou final) e, assim, fazer verificações exaustivas de correspondências de padrões, o que não é possível estaticamente para um idioma com carregamento de código de tempo de execução e herança irrestrita.
Jörg W Mittag
Claro que você pode criar tipos com essas semânticas em C #. Observe que o C # não permite que o tipo interno embutido (chamado Nulável em C #) seja aninhado. int??é ilegal em c #.
Eric Lippert
3

Se você pode definir uniões de tipo, como X or Y, e se você tem um tipo nomeado Nullque representa apenas o nullvalor (e nada mais), então, para qualquer tipo T, T or Nullna verdade não é tão diferente Maybe T. O único problema nullé quando a linguagem trata um tipo de Tforma T or Nullimplícita, criando nullum valor válido para cada tipo (exceto os tipos primitivos nos idiomas que os possuem). É o que acontece, por exemplo, em Java, onde uma função que aceita a Stringtambém aceita null.

Se você tratar os tipos como conjunto de valores e orcomo união de conjuntos, (T or Null) or Nullrepresenta o conjunto de valores T ∪ {null} ∪ {null}, que é o mesmo que T or Null. Existem compiladores que realizam esse tipo de análise (SBCL). Como dito nos comentários, você não pode distinguir facilmente entre Maybe Te Maybe Maybe Tquando visualiza os tipos como domínios (por outro lado, essa visualização é bastante útil para programas de tipo dinâmico). Mas você também pode manter os tipos como expressões algébricas, onde (T or Null) or Nullrepresenta vários níveis do Maybes.

coredump
fonte
2
Na verdade, é importante que Maybe (Maybe X)não seja a mesma coisa Maybe X. Nothingnão é a mesma coisa que Just Nothing. Conflitar Maybe (Maybe X)com Maybe Xtorna impossível tratar Maybe _polimorficamente.
Gilles 'SO- stop be evil
1

Você precisa descobrir o que deseja expressar e, em seguida, encontrar uma maneira de expressá-lo.

Primeiro, você tem valores no domínio do problema (como números, seqüências de caracteres, registros de clientes, booleanos etc.). Segundo, você tem alguns valores genéricos adicionais sobre os valores do domínio do problema: por exemplo "nada" (a ausência conhecida de um valor), "desconhecido" (nenhum conhecimento sobre presença ou ausência de um valor), não mencionado, foi " alguma coisa "(existe definitivamente um valor, mas não sabemos qual). Talvez você possa pensar em outras situações.

Se você quisesse representar todos os valores mais esses três valores adicionais, criaria uma enumeração com os casos "nada", "desconhecido", "alguma coisa" e "valor" e partir daí.

Em alguns idiomas, alguns casos são mais simples. Por exemplo, em Swift, você tem para todo tipo T o tipo "opcional T" que tem como valores possíveis nulos mais todos os valores de T. Isso permite lidar com muitas situações facilmente e é perfeito se você não tiver "nada" e " valor". Você pode usá-lo se tiver "desconhecido" e "valor". Você não pode usá-lo se precisar manipular "nada", "desconhecido" e "valor". Outros idiomas podem usar "null" ou "talvez", mas isso é apenas outra palavra.

No JSON, você tem dicionários com pares de chave / valor. Aqui, uma chave pode estar ausente de um dicionário ou pode ter um valor nulo. Portanto, descrevendo cuidadosamente como as coisas são armazenadas, você pode representar valores genéricos mais dois valores adicionais.

gnasher729
fonte