Haskell: Typeclass vs passar uma função

16

Para mim, parece que você sempre pode passar argumentos de função em vez de usar uma classe de tipo. Por exemplo, em vez de definir a classe de classe de igualdade:

class Eq a where 
  (==)                  :: a -> a -> Bool

E usá-lo em outras funções para indicar o argumento de tipo deve ser uma instância de Eq:

elem                    :: (Eq a) => a -> [a] -> Bool

Não podemos simplesmente definir nossa elemfunção sem usar uma classe de tipo e passar um argumento de função que faz o trabalho?

mahdix
fonte
2
isso é chamado de passagem de dicionário. Você pode pensar nas restrições da classe de tipo como argumentos implícitos.
Poscat 16/04
2
Você poderia fazer isso, mas obviamente é muito mais conveniente não ter que passar uma função e apenas usar uma função "padrão", dependendo do tipo.
Robin Zigmond
2
Você poderia colocar assim, sim. Mas eu argumentaria que há pelo menos uma outra vantagem importante: a capacidade de escrever funções polimórficas que funcionam em qualquer tipo que implemente uma "interface" específica ou conjunto de recursos. Penso que as restrições de tipo de letra expressam isso muito claramente de uma maneira que a transmissão de argumentos de função extra não. Em particular por causa das (leis implícitas) (tristemente implícitas) que muitas classes de tipos precisam satisfazer. Uma Monad mrestrição me diz mais do que passar argumentos de função adicionais dos tipos a -> m ae m a -> (a -> m b) -> m b.
Robin Zigmond
11
Veja também as tipologias são essenciais?
luqui 16/04
11
A TypeApplicationsextensão permite explicitar o argumento implícito. (==) @Int 3 5compara 3e 5especificamente como Intvalores. Você pode pensar @Intcomo uma chave no dicionário de funções de igualdade específicas de tipo, em vez da Intfunção de comparação específica em si.
chepner 16/04

Respostas:

19

Sim. Isso é chamado de "estilo de passagem de dicionário". Às vezes, quando estou fazendo algumas coisas especialmente complicadas, preciso descartar uma classe e transformá-la em dicionário, porque a passagem de dicionário é mais poderosa 1 , mas muitas vezes bastante complicada, tornando o código conceitualmente simples bastante complicado. Às vezes, uso o estilo de passagem de dicionário em idiomas que não são Haskell para simular classes tipográficas (mas aprendi que geralmente não é uma idéia tão boa quanto parece).

Obviamente, sempre que houver uma diferença no poder expressivo, haverá uma troca. Embora você possa usar uma determinada API de mais maneiras, se for escrita usando DPS, a API obtém mais informações, se você não puder. Uma das maneiras pelas Data.Setquais isso aparece na prática é o fato de existir apenas um Orddicionário por tipo. Ele Setarmazena seus elementos classificados de acordo com Ord, e se você criar um conjunto com um dicionário e, em seguida, inserir um elemento usando outro, como seria possível com o DPS, poderá quebrar Seta invariante e causar a falha. Esse problema de exclusividade pode ser atenuado usando um método existencial fantasmadigite para marcar o dicionário, mas, novamente, à custa de um pouco de complexidade irritante na API. Isso também aparece da mesma maneira na TypeableAPI.

A parte da singularidade não aparece com muita frequência. O que as classes tipográficas são ótimas é escrever código para você. Por exemplo,

catProcs :: (i -> Maybe String) -> (i -> Maybe String) -> (i -> Maybe String)
catProcs f g = f <> g

que usa dois "processadores" que recebem uma entrada e podem dar uma saída e os concatenam, achatando Nothing, teriam que ser escritos no DPS, algo assim:

catProcs f g = (<>) (funcSemi (maybeSemi listSemi)) f g

Basicamente, tivemos que especificar o tipo em que o estamos usando novamente, mesmo que já o tenhamos explicado na assinatura do tipo, e isso foi redundante porque o compilador já conhece todos os tipos. Como existe apenas uma maneira de construir um dado Semigroupem um tipo, o compilador pode fazer isso por você. Isso tem um efeito de tipo "juros compostos" quando você começa a definir várias instâncias paramétricas e a usar a estrutura de seus tipos para calcular para você, como nos Data.Functor.*combinadores, e isso é usado com grande efeito, deriving viaonde você pode obter basicamente todas as estrutura algébrica "padrão" do seu tipo, escrita para você.

E nem me inicie nos MPTC e nos fundeps, que alimentam as informações de volta em digitação e inferência. Eu nunca tentei converter uma coisa dessas para DPS - suspeito que envolva repassar muitas provas de igualdade de tipo - mas, de qualquer forma, tenho certeza de que seria muito mais trabalho para o meu cérebro do que seria confortável com.

-

1 U NLES você usa reflectioncaso em que eles se tornam equivalentes em poder -, mas reflectiontambém pode ser complicado de usar.

luqui
fonte
11
Talvez valha a pena mencionar Winant e Devriese 2018: aplicativo de dicionário explícito coerente para Haskell
leftaroundabout 16/04
Estou muito interessado nos fundeps expressos através do DPS. Você conhece alguns recursos recomendáveis ​​sobre esse assunto? Enfim, explicação muito compreensível.
bob
@ Bob, não de improviso, mas seria uma exploração interessante. Talvez faça uma nova pergunta sobre isso?
luqui 16/04
5

Sim. Isso (chamado passagem de dicionário) é basicamente o que o compilador faz para as aulas de qualquer maneira. Para essa função, feita literalmente, ficaria assim:

elemBy :: (a -> a -> Bool) -> a -> [a] -> Bool
elemBy _ _ [] = False
elemBy eq x (y:ys) = eq x y || elemBy eq x ys

Chamar elemBy (==) x xsagora é equivalente a elem x xs. E, neste caso específico, você pode ir um passo além: sempre eqtem o mesmo primeiro argumento, para que seja responsabilidade do chamador aplicá-lo e acabar com isso:

elemBy2 :: (a -> Bool) -> [a] -> Bool
elemBy2 _ [] = False
elemBy2 eqx (y:ys) = eqx y || elemBy2 eqx ys

Chamar elemBy2 (x ==) xsagora é equivalente a elem x xs.

...Oh espere. Isso é só any. (E, de fato, na biblioteca padrãoelem = any . (==) ,.)

Joseph Sible-Restabelecer Monica
fonte
A passagem do dicionário AFAIU é a abordagem do Scala para codificar classes de tipos. Esses argumentos extras podem ser declarados como implicite o compilador os injeta no escopo.
michid 16/04