Implemente la clase de tipo Haskell con la interfaz C #

13

Estoy tratando de comparar las clases de tipos de Haskell y las interfaces de C #. Supongamos que hay un Functor.

Haskell

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

¿Cómo implementar esta clase de tipo como interfaz en C #?

Lo que he intentado:

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

Esta es una implementación no válida y en realidad estoy atrapado con un Ftipo genérico que debería devolver fmap. ¿Cómo debe definirse y dónde?

¿Es imposible implementar Functoren C # y por qué? ¿O tal vez hay otro enfoque?

ДМИТРИЙ МАЛИКОВ
fuente
8
Eric Lippert habla un poco acerca de cómo el sistema de tipos de C # no es realmente suficiente para soportar la naturaleza de tipo superior de Functors como lo define Haskell en esta respuesta: stackoverflow.com/a/4412319/303940
KChaloux
1
Esto fue hace unos 3 años. ¿Algo cambió?
ДМИТРИЙ МАЛИКОВ
44
nada ha cambiado para que esto sea posible en C #, ni creo que sea probable en el futuro
jk.

Respuestas:

8

El sistema de tipos de C # carece de un par de características necesarias para implementar correctamente las clases de tipos como una interfaz.

Comencemos con su ejemplo, pero la clave es mostrar una cuenta más completa de lo que es y hace una clase de tipos, y luego tratar de asignarlos a bits C #.

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

Esta es la definición de clase de tipo, o similar a la interfaz. Ahora veamos una definición de un tipo y su implementación de esa clase de tipo.

data Awesome a = Awesome a a

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

Ahora podemos ver muy obviamente un hecho distinto de las clases de tipos que no puede tener con las interfaces. La implementación de la clase de tipo no forma parte de la definición del tipo. En C #, para implementar una interfaz, debe implementarla como parte de la definición del tipo que la implementa. Esto significa que no puede implementar una interfaz para un tipo que no implementa usted mismo, sin embargo, en Haskell puede implementar una clase de tipo para cualquier tipo al que tenga acceso.

Esa es probablemente la más grande de inmediato, pero hay otra diferencia bastante significativa que hace que el equivalente de C # realmente no funcione tan bien, y lo está abordando en su pregunta. Se trata de polimorfismo. También hay algunas cosas relativamente genéricas que Haskell le permite hacer con clases de tipos que no se traducen directamente, especialmente cuando comienza a observar la cantidad de generismo en los tipos existenciales u otras extensiones de GHC como ADT genéricos.

Verás, con Haskell puedes definir los 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)

Luego, en el consumo puede tener una función:

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

Aquí yace el problema. En C #, ¿cómo se escribe esta función?

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>);
}

Entonces, hay un par de cosas mal con la versión C #, por una cosa, ni siquiera estoy seguro de que te permita usar el <b>calificador como lo hice allí, pero sin él, estoy seguro de que no se despachará Show<>adecuadamente (siéntete libre de intentarlo y compilar para averiguarlo; no lo hice).

Sin embargo, el problema más grande aquí es que, a diferencia de lo anterior en Haskell, donde definimos nuestros Terminals como parte del tipo y luego los usamos en lugar del tipo, debido a que C # carece del polimorfismo paramétrico apropiado (que se vuelve súper obvio tan pronto como intentas interoperar) F # con C #) no puede distinguir clara o limpiamente si Derecha o Izquierda son Terminals. Lo mejor que puede hacer es usar null, pero ¿qué sucede si está tratando de hacer un tipo de valor ao Functoren el caso de Eitherque distinga dos tipos que tienen un valor? ¿Ahora tiene que usar un tipo y tener dos valores diferentes para verificar y cambiar para modelar su discriminación?

La falta de tipos de suma adecuados, tipos de unión, ADT, como quiera llamarlos realmente hace mucho de lo que las clases de tipos le dan, porque al final del día le permiten tratar múltiples tipos (constructores) como un solo tipo, y el sistema de tipos subyacente de .NET simplemente no tiene ese concepto.

Jimmy Hoffa
fuente
2
No estoy muy versado en Haskell (solo ML estándar), así que no sé cuánta diferencia hace esto, pero es posible codificar tipos de suma en C # .
Doval
5

Lo que necesita son dos clases, una para modelar el genérico de orden superior (el functor) y otra para modelar el functor combinado con el valor libre 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);
}

Entonces, si usamos la opción mónada (porque todas las mónadas son 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>();
       }
   } 
}

Luego puede usar métodos de extensión estática para convertir de IF <Opción, B> a Algunos <A> cuando necesite

DetriusXii
fuente
Tengo dificultades con purela interfaz genérica de functor: el compilador se queja IF<Functor, A> pure<A>(A a);con "El tipo Functorno se puede usar como parámetro de tipo Functoren el tipo de método genérico IF<Functor, A>. No hay conversión de boxeo o conversión de parámetro de tipo de Functora F<Functor>". ¿Qué significa esto? ¿Y por qué debemos definir pureen dos lugares? Además, ¿no debería pureser estático?
Niriel
1
Hola. Creo que porque estaba insinuando mónadas y transformadores de mónada cuando diseñaba la clase. Un transformador de mónada, como el transformador de mónada OptionT (MaybeT en Haskell) se define en C # como OptionT <M, A> donde M es otra mónada genérica. Las cajas de transformador de mónada OptionT en una mónada de tipo M <Opción <A>>, pero dado que C # no tiene tipos de tipo superior, necesita una forma de instanciar la mónada de tipo superior M cuando llama a OptionT.map y OptionT.bind. Los métodos estáticos no funcionan porque no puedes llamar a M.pure (A a) para ninguna mónada M.
DetriusXii