Implementar a classe Typkass Haskell com interface C #

13

Estou tentando comparar as classes de tipo de Haskell e as interfaces de c #. Suponha que exista um Functor.

Haskell:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Como implementar essa classe de tipo como interface em c #?

O que eu tentei:

interface Functor<A, B>
{
    F<B> fmap(Func<A, B> f, F<A> x);
}

Esta é uma implementação inválida e, na verdade, estou preso a um Ftipo genérico que deve ser retornado por fmap. Como deve ser definido e onde?

É impossível implementar Functorem C # e por quê? Ou talvez haja outra abordagem?

ДМИТРИЙ МАЛИКОВ
fonte
8
Eric Lippert fala um pouco sobre como o sistema de tipos do C # não é realmente suficiente para suportar a natureza superior dos Functors, conforme definido por Haskell nesta resposta: stackoverflow.com/a/4412319/303940
KChaloux:
1
Isso foi há cerca de 3 anos atrás. Alguma coisa mudou?
ДМИТРИЙ МАЛИКОВ
4
nada mudou para tornar isso possível em c #, nem acho que provavelmente no futuro
jk.

Respostas:

8

O sistema de tipos do C # carece de alguns recursos necessários para implementar adequadamente as classes de tipos como uma interface.

Vamos começar com o seu exemplo, mas a chave está mostrando uma conta mais completa do que uma classe de tipo é e faz e, em seguida, tentando mapeá-las para bits C #.

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Essa é a definição da classe de tipo ou semelhante à interface. Agora, vamos olhar para uma definição de um tipo e sua implementação dessa classe de tipo.

data Awesome a = Awesome a a

instance Functor Awesome where
  fmap f (Awesome a1 a2) = Awesome (f a1) (f a2)

Agora podemos ver muito obviamente um fato distinto das classes de tipos que você não pode ter com interfaces. A implementação da classe type não faz parte da definição do tipo. No C #, para implementar uma interface, você deve implementá-la como parte da definição do tipo que a implementa. Isso significa que você não pode implementar uma interface para um tipo que você não implementa, mas no Haskell você pode implementar uma classe de tipo para qualquer tipo ao qual tenha acesso.

Essa é provavelmente a maior imediatamente, mas há outra diferença bastante significativa que faz com que o equivalente em C # realmente não funcione tão bem, e você está abordando isso na sua pergunta. É sobre polimorfismo. Além disso, há algumas coisas relativamente genéricas que Haskell permite que você faça com classes de tipo que não traduzem diretamente, especialmente quando você começa a analisar a quantidade de genérico em tipos existenciais ou outras extensões do GHC, como ADTs genéricos.

Veja bem, com Haskell você pode definir os functores

data List a = List a (List a) | Terminal
data Tree a = Tree val (Tree a) (Tree a) | Terminal

instance Functor List where
  fmap :: (a -> b) -> List a -> List b
  fmap f (List a Terminal) = List (f a) Terminal
  fmap f (List a rest) = List (f a) (fmap f rest)

instance Functor Tree where
  fmap :: (a -> b) -> Tree a -> Tree b
  fmap f (Tree val Terminal Terminal) = Tree (f val) Terminal Terminal
  fmap f (Tree val Terminal right) = Tree (f val) Terminal (fmap f right)
  fmap f (Tree val left Terminal) = Tree (f val) (fmap f left) Terminal
  fmap f (Tree val left right) = Tree (f val) (fmap f left) (fmap f right)

Então, no consumo, você pode ter uma função:

mapsSomething :: Functor f, Show a => f a -> f String
mapsSomething rar = fmap show rar

Aqui reside o problema. Em C #, como você escreve essa função?

public Tree<a> : Functor<a>
{
    public a Val { get; set; }
    public Tree<a> Left { get; set; }
    public Tree<a> Right { get; set; }

    public Functor<b> fmap<b>(Func<a,b> f)
    {
        return new Tree<b>
        {
            Val = f(val),
            Left = Left.fmap(f);
            Right = Right.fmap(f);
        };
    }
}
public string Show<a>(Showwable<a> ror)
{
    return ror.Show();
}

public Functor<String> mapsSomething<a,b>(Functor<a> rar) where a : Showwable<b>
{
    return rar.fmap(Show<b>);
}

Portanto, há algumas coisas erradas na versão C #, por um lado, nem tenho certeza de que permitirá que você use o <b>qualificador como eu fiz lá, mas sem ele, tenho certeza de que não enviaria Show<>adequadamente (sinta-se à vontade para tentar e compilar para descobrir; eu não fiz).

O maior problema aqui, porém, é que, diferentemente de Haskell, onde tínhamos nossos Terminals definidos como parte do tipo e utilizáveis ​​no lugar do tipo, devido ao C # carecer de polimorfismo paramétrico apropriado (que se torna super óbvio assim que você tenta interoperar F # com C #) você não pode distinguir clara ou claramente se Direita ou Esquerda são Terminals. O melhor que você pode fazer é usar null, mas e se você estiver tentando transformar um valor em um tipo Functorou no caso de Eitherdistinguir dois tipos que carregam um valor? Agora você precisa usar um tipo e ter dois valores diferentes para verificar e alternar para modelar sua discriminação?

A falta de tipos de soma adequados, tipos de união, ADTs, o que você quiser chamá-los realmente diminui muito o que as classes de tipo oferecem, porque no final do dia elas permitem tratar vários tipos (construtores) como um único tipo, e o sistema de tipos subjacentes do .NET simplesmente não tem esse conceito.

Jimmy Hoffa
fonte
2
Eu não sou muito versado em Haskell (apenas ML Padrão), então não sei quanta diferença isso faz, mas é possível codificar tipos de soma em C # .
Doval 15/03
5

O que você precisa é de duas classes, uma para modelar o genérico de ordem superior (o functor) e outra para modelar o functor combinado com o valor livre A

interface F<Functor> {
   IF<Functor, A> pure<A>(A a);
}

interface IF<Functor, A> where Functor : F<Functor> {
   IF<Functor, B> pure<B>(B b);
   IF<Functor, B> map<B>(Func<A, B> f);
}

Portanto, se usarmos a mônada Option (porque todas as mônadas são functores)

class Option : F<Option> {
   IF<Option, A> pure<A>(A a) { return new Some<A>(a) };
}

class OptionF<A> : IF<Option, A> {
   IF<Option, B> pure<B>(B b) {
      return new Some<B>(b);
   }

   IF<Option, B> map<B>(Func<A, B> f) {
       var some = this as Some<A>;
       if (some != null) {
          return new Some<B>(f(some.value));
       } else {
          return new None<B>();
       }
   } 
}

Você pode usar métodos de extensão estáticos para converter de IF <Option, B> para Some <A> quando precisar

DetriusXii
fonte
Estou com dificuldades purena interface do functor genérico: o compilador reclama IF<Functor, A> pure<A>(A a);com "O tipo Functornão pode ser usado como parâmetro de tipo Functorno tipo genérico de método IF<Functor, A>. Não há conversão de boxe ou conversão de parâmetro de tipo de Functorpara F<Functor>". O que isto significa? E por que devemos definir pureem dois lugares? Além disso, não deve pureser estático?
Niriel
1
Oi. Eu acho que porque eu estava sugerindo mônadas e transformadores de mônada ao projetar a classe. Um transformador de mônada, como o transformador de mônada OptionT (MaybeT em Haskell) é definido em C # como OptionT <M, A> em que M é outra mônada genérica. O transformador de mônada OptionT se encaixa em uma mônada do tipo M <Opção <A>>, mas como o C # não possui tipos de classificação mais alta, é necessário instanciar a monad M de classificação superior ao chamar OptionT.map e OptionT.bind. Os métodos estáticos não funcionam porque você não pode chamar M.pure (A a) para qualquer mônada M.
DetriusXii