Considere la Functor
clase de tipo en Haskell, donde f
es 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 f
desde a
a b
, pero lo deja f
como estaba. Entonces, si usa fmap
sobre 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 Functor
abstracció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 map
mé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 map
mé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
map
método siempre devuelve la misma Functor
subclase que el receptor.
- Por lo tanto, no existe una manera estáticamente segura de invocar un
Functor
mé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 Functor
que 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 Functor
del map
método, pero como no hay límite para la cantidad de Functor
implementaciones 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 Functor
restricció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 FB
a tener lo mismo F
que FA
, de modo que cuando declaras un tipo List<A> implements Functor<List<A>, A>
, el map
mé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 F
crearán instancias en tipos no parametrizados como solo List
o Map
. Esto garantiza que a FunctorStrategy<List>
solo puede devolver a, List
pero 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 T
es una variable de tipo, puede escribir T
y 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 f
o myList |> Seq.map f |> Seq.toList
tipos de kinded superiores le permiten simplemente escribir myList |> map f
y 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 Seq
todos modos y luego puedo convertir a lo que quiera después.
Hay muchos lenguajes que generalizan la idea de la map
funció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 Functor
clase es más general que eso; no está ligado a la noción de secuencias. Puede implementar fmap
para tipos que no tienen una buena asignación a secuencias, como IO
acciones, 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 fmap
conservar el tipo, porque tan pronto como obtenga map
operaciones que produzcan un tipo de resultado diferente, se vuelve mucho, mucho más difícil ofrecer garantías como esta.