Considere la Functorclase de tipo en Haskell, donde fes una variable de tipo de tipo superior:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Lo que dice esta firma de tipo es que fmap cambia el parámetro de tipo de un fdesde aa b, pero lo deja fcomo estaba. Entonces, si usa fmapsobre una lista, obtiene una lista, si lo usa sobre un analizador, obtiene un analizador, y así sucesivamente. Y estas son garantías estáticas en tiempo de compilación.
No sé F #, pero consideremos qué sucede si tratamos de expresar la Functorabstracción en un lenguaje como Java o C #, con herencia y genéricos, pero sin genéricos de tipo superior. Primer intento:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
El problema con este primer intento es que una implementación de la interfaz puede devolver cualquier clase que implemente Functor. Alguien podría escribir un método FunnyList<A> implements Functor<A>cuyo mapmétodo devuelva un tipo diferente de colección, o incluso algo más que no sea una colección en absoluto pero que siga siendo un Functor. Además, cuando usa el mapmétodo, no puede invocar ningún método específico de subtipo en el resultado a menos que lo rebaje al tipo que realmente espera. Entonces tenemos dos problemas:
- El sistema de tipos no nos permite expresar el invariante de que el
mapmétodo siempre devuelve la misma Functorsubclase que el receptor.
- Por lo tanto, no existe una manera estáticamente segura de invocar un
Functormétodo no en el resultado de map.
Hay otras formas más complicadas de probar, pero ninguna de ellas funciona realmente. Por ejemplo, puede intentar aumentar el primer intento definiendo subtipos Functorque restringen el tipo de resultado:
interface Collection<A> extends Functor<A> {
Collection<B> map(Function<A, B> f);
}
interface List<A> extends Collection<A> {
List<B> map(Function<A, B> f);
}
interface Set<A> extends Collection<A> {
Set<B> map(Function<A, B> f);
}
interface Parser<A> extends Functor<A> {
Parser<B> map(Function<A, B> f);
}
Esto ayuda a evitar que los implementadores de esas interfaces más estrechas devuelvan el tipo incorrecto de Functordel mapmétodo, pero como no hay límite para la cantidad de Functorimplementaciones que puede tener, no hay límite para la cantidad de interfaces más estrechas que necesitará.
( EDITAR: Y tenga en cuenta que esto solo funciona porque Functor<B>aparece como el tipo de resultado, por lo que las interfaces secundarias pueden limitarlo. Entonces, AFAIK, no podemos limitar ambos usos de Monad<B>en la siguiente interfaz:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
En Haskell, con variables de tipo de rango superior, esto es (>>=) :: Monad m => m a -> (a -> m b) -> m b).
Otro intento más es usar genéricos recursivos para intentar que la interfaz restrinja el tipo de resultado del subtipo al subtipo en sí. Ejemplo de juguete:
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
Pero este tipo de técnica (que es bastante misteriosa para su desarrollador OOP común y corriente, diablos también para su desarrollador funcional común y corriente) tampoco puede expresar la Functorrestricción deseada :
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
El problema aquí es que esto no se limita FBa tener lo mismo Fque FA, de modo que cuando declaras un tipo List<A> implements Functor<List<A>, A>, el mapmétodo aún puede devolver un NotAList<B> implements Functor<NotAList<B>, B>.
Prueba final, en Java, usando tipos sin formato (contenedores no parametrizados):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Aquí se Fcrearán instancias en tipos no parametrizados como solo Listo Map. Esto garantiza que a FunctorStrategy<List>solo puede devolver a, Listpero ha abandonado el uso de variables de tipo para rastrear los tipos de elementos de las listas.
El meollo del problema aquí es que lenguajes como Java y C # no permiten que los parámetros de tipo tengan parámetros. En Java, si Tes una variable de tipo, puede escribir Ty List<T>, pero no T<String>. Los tipos de tipo superior eliminan esta restricción, por lo que podría tener algo como esto (no completamente pensado):
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
<B> List<B> map(Function<A, B> f) {
}
}
Y abordando este bit en particular:
(Creo) Entiendo que en lugar de myList |> List.map fo myList |> Seq.map f |> Seq.toListtipos de kinded superiores le permiten simplemente escribir myList |> map fy devolverá un List. Eso es genial (suponiendo que sea correcto), ¿pero parece un poco mezquino? (¿Y no podría hacerse simplemente permitiendo la sobrecarga de funciones?) Normalmente convierto a de Seqtodos modos y luego puedo convertir a lo que quiera después.
Hay muchos lenguajes que generalizan la idea de la mapfunción de esta manera, modelándola como si, en el fondo, el mapeo se tratara de secuencias. Esta observación suya está en ese espíritu: si tiene un tipo que admite la conversión desde y hacia Seq, obtiene la operación del mapa "gratis" mediante la reutilización Seq.map.
En Haskell, sin embargo, la Functorclase es más general que eso; no está ligado a la noción de secuencias. Puede implementar fmappara tipos que no tienen una buena asignación a secuencias, como IOacciones, combinadores de analizadores, funciones, etc.
instance Functor IO where
fmap f action =
do x <- action
return (f x)
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g)
El concepto de "mapeo" realmente no está ligado a secuencias. Es mejor comprender las leyes de functor:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Muy informalmente:
- La primera ley dice que mapear con una función de identidad / noop es lo mismo que no hacer nada.
- La segunda ley dice que cualquier resultado que pueda producir mapeando dos veces, también puede producirlo mapeando una vez.
Ésta es la razón por la que desea fmapconservar el tipo, porque tan pronto como obtenga mapoperaciones que produzcan un tipo de resultado diferente, se vuelve mucho, mucho más difícil ofrecer garantías como esta.