Que problema os tipos de dados algébricos resolvem?

18

Aviso justo, eu sou novo em programação funcional, por isso posso ter muitas suposições ruins.

Eu tenho aprendido sobre tipos algébricos. Muitas linguagens funcionais parecem tê-las e são bastante úteis em conjunto com a correspondência de padrões. No entanto, que problema eles realmente resolvem? Eu posso implementar um tipo algébrico aparentemente (mais ou menos) em C # assim:

public abstract class Option { }
public class None : Option { }
public class Some<T> : Option
{
    public T Value { get; set; }
}

var result = GetSomeValue();
if(result is None)
{
}
else
{
}

Mas acho que a maioria concorda que isso é uma bastardização da programação orientada a objetos, e você nunca deve fazê-lo. Então, a programação funcional apenas adiciona uma sintaxe mais limpa que faz com que esse estilo de programação pareça menos grosseiro? O que mais estou perdendo?

ConditionRacer
fonte
6
A programação funcional é um paradigma diferente da programação orientada a objetos.
Basile Starynkevitch
@BasileStarynkevitch eu percebo, mas existem idiomas como o F # que são um pouco dos dois. A questão não é tanto sobre linguagens funcionais, mas mais sobre qual problema os tipos de dados algébricos resolvem.
ConditionRacer
7
O que acontece quando eu defino class ThirdOption : Option{}e dou a você new ThirdOption()onde você esperava Someou None?
amon
1
Os idiomas @amon que possuem tipos de soma geralmente têm maneiras de impedi-lo. Por exemplo, Haskell define data Maybe a = Just a | Nothing(equivalente a data Option a = Some a | Noneno seu exemplo): você não pode adicionar um terceiro caso post-hoc. Embora você possa emular tipos de soma em C # da maneira que você mostrou, não é a mais bonita.
Martijn
1
Eu acho que é menos "que problema os ADTs resolvem" e mais como "ADTs são uma maneira diferente de abordar as coisas".
MathematicalOrchid

Respostas:

44

Classes com interfaces e herança apresentam um mundo aberto: qualquer pessoa pode adicionar um novo tipo de dados. Para uma determinada interface, pode haver classes implementando-a em todo o mundo, em diferentes arquivos, em diferentes projetos, em diferentes empresas. Eles facilitam a adição de casos às estruturas de dados, mas como as implementações da interface são descentralizadas, é difícil adicionar um novo método à interface. Uma vez que uma interface é pública, ela fica basicamente congelada. Ninguém conhece todas as implementações possíveis.

Os tipos de dados algébricos são duplos, estão fechados . Todos os casos dos dados estão listados em um único local e as operações não apenas podem listar exaustivamente as variantes, mas são incentivadas a fazê-lo. Consequentemente, escrever uma nova função operando em um tipo de dados algébrico é trivial: basta escrever a função maldita. Em troca, adicionar novos casos é complicado porque você precisa revisar basicamente toda a base de código e estender cada match. Semelhante à situação com interfaces, na biblioteca padrão do Rust, a adição de uma nova variante é uma mudança de última hora (para tipos públicos).

Estes são os dois lados do problema de expressão . Os tipos de dados algébricos são uma solução incompleta para eles, mas o OOP também. Ambos têm vantagens, dependendo de quantos casos de dados existem, com que frequência esses casos mudam e com que frequência as operações são estendidas ou alteradas. (É por isso que muitas linguagens modernas fornecem ambos, ou algo semelhante, ou vão direto para mecanismos mais poderosos e mais complicados que tentam incluir as duas abordagens.)


fonte
Você recebe um erro do compilador se não conseguir atualizar uma correspondência ou é apenas no tempo de execução que você descobre?
Ian
6
@Ian A maioria das linguagens funcionais é tipicamente estática e verifica a exaustividade da correspondência de padrões. No entanto, se houver um padrão "pegar tudo", o compilador ficará feliz mesmo que a função precise lidar com o novo caso para fazer seu trabalho. Além disso, é necessário recompilar todo o código dependente, não é possível compilar apenas uma biblioteca e vinculá-la novamente a um aplicativo já criado.
Além disso, verificar estaticamente se um padrão é exaustivo é geralmente caro. Ele resolve o problema do SAT na melhor das hipóteses e na pior das hipóteses a linguagem permite predicados arbitrários, o que torna isso indecidível.
usr
@usr Nenhum idioma que eu conheço tenta uma solução perfeita, ou o compilador entende que é exaustivo ou você é forçado a adicionar um caso abrangente em que você falha e queima. Eu não sei de uma relação com o SAT, você tem um link para uma redução? Independentemente disso, para código real escrito em programas reais, a verificação exaustiva é uma gota no balde.
Imagine que você está combinando com N booleanos. Em seguida, você adiciona cláusulas de correspondência como (a, b, _, d, ...). O caso geral é então! Clause1 &&! Clause2 && .... Parece-me SAT.
usr
12

Então, a programação funcional apenas adiciona uma sintaxe mais limpa que faz com que esse estilo de programação pareça menos grosseiro?

Talvez seja uma simplificação, mas sim.

O que mais estou perdendo?

Sejamos claros sobre o que são tipos de dados algébricos (resumindo este link fino de Aprenda você como Haskell):

  • Um tipo de soma que diz "esse valor pode ser um A ou um B".
  • Um tipo de produto que diz "esse valor é um A e um B".

seu exemplo realmente funciona apenas com o primeiro.

O que talvez você esteja perdendo é que, ao fornecer essas duas operações básicas, as linguagens funcionais permitem criar todo o resto. O C # possui estruturas, classes, enumerações, genéricos e pilhas de regras para governar como essas coisas se comportam.

Combinadas com alguma sintaxe para ajudar, as linguagens funcionais podem decompor as operações nesses dois caminhos, fornecendo uma abordagem limpa, simples e elegante dos tipos.

Que problema os tipos de dados algébricos resolvem?

Eles resolvem o mesmo problema que qualquer outro sistema de tipos: "que valores são legais para usar aqui?" - eles apenas adotam uma abordagem diferente.

Telastyn
fonte
4
Sua última frase é como dizer a alguém que "os navios resolvem o mesmo problema que os aviões: transporte - eles apenas adotam uma abordagem diferente". É uma afirmação completamente correta e também bastante inútil.
Mehrdad 22/06
@ mehrdad - Eu acho que isso está exagerando um pouco.
Telastyn
1
Sem dúvida, você entende bem os tipos de dados algébricos, mas sua lista com marcadores ("Um tipo de soma é ..." e "Um tipo de produto é ...") se parece mais com uma descrição dos tipos de união e interseção, que não são exatamente a mesma coisa que os tipos de soma e produto.
pyon 23/07
6

Pode ser uma surpresa saber que a correspondência de padrões não é considerada a maneira mais idiomática de trabalhar com Opções. Veja a documentação das Opções do Scala para mais informações. Não sei por que tantos tutoriais de FP incentivam esse uso.

Principalmente o que está faltando é que existem várias funções criadas para facilitar o trabalho com o Options. Considere o exemplo principal dos documentos do Scala:

val name: Option[String] = request getParameter "name"
val upper = name map { _.trim } filter { _.length != 0 } map { _.toUpperCase }
println(upper getOrElse "")

Observe como mape filterpermita que você encadeie operações nas Opções, sem precisar verificar em cada ponto se você tem Noneou não. Então, no final, você usa getOrElsepara especificar um valor padrão. Em nenhum momento você está fazendo algo "nojento", como verificar tipos. Qualquer verificação de tipo inevitável é feita internamente na biblioteca. Haskell tem seu próprio conjunto de funções análogas, sem mencionar o grande conjunto de funções que funcionará em qualquer mônada ou functor.

Outros tipos de dados algébricos têm suas próprias maneiras idiomáticas de trabalhar com eles, e a correspondência de padrões é baixa no totem na maioria dos casos. Da mesma forma, ao criar seus próprios tipos, espera-se que você forneça funções semelhantes para trabalhar com eles.

Karl Bielefeldt
fonte