He estado haciendo desarrollo en F # por un tiempo y me gusta. Sin embargo, una palabra de moda que sé que no existe en F # es tipos de tipo superior. He leído material sobre tipos de clase alta y creo que entiendo su definición. No estoy seguro de por qué son útiles. ¿Alguien puede proporcionar algunos ejemplos de lo que los tipos de tipo superior facilitan en Scala o Haskell, que requieren soluciones en F #? También para estos ejemplos, ¿cuáles serían las soluciones alternativas sin tipos de tipo superior (o viceversa en F #)? Tal vez estoy tan acostumbrado a solucionarlo que no me doy cuenta de la ausencia de esa función.
(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. De nuevo, tal vez estoy demasiado acostumbrado a solucionarlo. Pero, ¿hay algún ejemplo en el que los tipos de tipo superior realmente le ahorren en pulsaciones de teclas o en seguridad de tipos?

IMonad<T>y luego volver a convertirla en, por ejemplo,IEnumerable<int>oIObservable<int>cuando hayas terminado? ¿Es todo esto solo para evitar el casting?returnfuncionaría ya que realmente pertenece al tipo de mónada, no a una instancia en particular, por lo que no querrá ponerlo en laIMonadinterfaz en absoluto.bindaka,SelectManyetc. Lo que significa que alguien podría usar la API debindunaIObservablea otraIEnumerabley asumir que funcionaría, lo que sí, si ese es el caso y no hay forma de evitarlo. No estoy 100% seguro de que no haya forma de evitarlo.Respuestas:
Entonces, el tipo de tipo es su tipo simple. Por ejemplo,
Inttiene kind, lo*que significa que es un tipo base y se puede instanciar mediante valores. Según una definición imprecisa de tipo de tipo superior (y no estoy seguro de dónde F # dibuja la línea, así que incluyémosla ), los contenedores polimórficos son un gran ejemplo de un tipo de tipo superior.data List a = Cons a (List a) | NilEl constructor de
Listtipos tiene tipo, lo* -> *que significa que se debe pasar un tipo concreto para que resulte en un tipo concreto:List Intpuede tener habitantes como[1,2,3]pero élListmismo no.Voy a suponer que los beneficios de los contenedores polimórficos son obvios, pero
* -> *existen tipos más útiles que solo los contenedores. Por ejemplo, las relacionesdata Rel a = Rel (a -> a -> Bool)o analizadores
data Parser a = Parser (String -> [(a, String)])Ambos también tienen amabilidad
* -> *.Sin embargo, podemos llevar esto más lejos en Haskell al tener tipos con tipos de orden incluso superior. Por ejemplo, podríamos buscar un tipo con kind
(* -> *) -> *. Un ejemplo simple de esto podría ser elShapeque intenta llenar un recipiente de algún tipo* -> *.data Shape f = Shape (f ()) [(), (), ()] :: Shape ListEsto es útil para caracterizar
Traversables en Haskell, por ejemplo, ya que siempre se pueden dividir en su forma y contenido.split :: Traversable t => t a -> (Shape t, [a])Como otro ejemplo, consideremos un árbol que está parametrizado según el tipo de rama que tiene. Por ejemplo, un árbol normal podría ser
data Tree a = Branch (Tree a) a (Tree a) | LeafPero podemos ver que el tipo de rama contiene una
PairdeTree as, por lo que podemos extraer esa pieza del tipo de forma paramétricadata TreeG f a = Branch a (f (TreeG f a)) | Leaf data Pair a = Pair a a type Tree a = TreeG Pair aEste
TreeGconstructor de tipos tiene kind(* -> *) -> * -> *. Podemos usarlo para hacer otras variaciones interesantes como unRoseTreetype RoseTree a = TreeG [] a rose :: RoseTree Int rose = Branch 3 [Branch 2 [Leaf, Leaf], Leaf, Branch 4 [Branch 4 []]]O patológicos como un
MaybeTreedata Empty a = Empty type MaybeTree a = TreeG Empty a nothing :: MaybeTree a nothing = Leaf just :: a -> MaybeTree a just a = Branch a EmptyO un
TreeTreetype TreeTree a = TreeG Tree a treetree :: TreeTree Int treetree = Branch 3 (Branch Leaf (Pair Leaf Leaf))Otro lugar donde esto aparece es en "álgebras de functores". Si dejamos caer algunas capas de abstracción, esto podría considerarse mejor como un pliegue, como
sum :: [Int] -> Int. Las álgebras se parametrizan sobre el functor y el portador . El functor tiene amabilidad* -> *y el portador es*tan buenodata Alg f a = Alg (f a -> a)tiene amabilidad
(* -> *) -> * -> *.Algútil debido a su relación con los tipos de datos y los esquemas de recursividad construidos sobre ellos.-- | The "single-layer of an expression" functor has kind `(* -> *)` data ExpF x = Lit Int | Add x x | Sub x x | Mult x x -- | The fixed point of a functor has kind `(* -> *) -> *` data Fix f = Fix (f (Fix f)) type Exp = Fix ExpF exp :: Exp exp = Fix (Add (Fix (Lit 3)) (Fix (Lit 4))) -- 3 + 4 fold :: Functor f => Alg f a -> Fix f -> a fold (Alg phi) (Fix f) = phi (fmap (fold (Alg phi)) f)Finalmente, aunque son teóricamente posibles, nunca he visto un constructor de tipos de tipo aún superior. A veces vemos funciones de ese tipo como
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b, pero creo que tendrás que profundizar en el prólogo de tipos o en la literatura escrita de forma dependiente para ver ese nivel de complejidad en los tipos.fuente
TreeTreees simplemente patológico, pero de manera más práctica significa que tienes dos tipos diferentes de árboles entrelazados entre sí; llevar esa idea un poco más lejos puede darte algunas nociones muy poderosas de tipo seguro, como rojo estáticamente seguro / árboles negros y el pulcro y equilibrado tipo FingerTree.Considere la
Functorclase de tipo en Haskell, dondefes una variable de tipo de tipo superior:class Functor f where fmap :: (a -> b) -> f a -> f bLo que dice esta firma de tipo es que fmap cambia el parámetro de tipo de un
fdesdeaab, pero lo dejafcomo estaba. Entonces, si usafmapsobre 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étodoFunnyList<A> implements Functor<A>cuyomapmé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 unFunctor. Además, cuando usa elmapmé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:mapmétodo siempre devuelve la mismaFunctorsubclase que el receptor.Functormétodo no en el resultado demap.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
Functordelmapmétodo, pero como no hay límite para la cantidad deFunctorimplementaciones 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 deMonad<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:
/** * A semigroup is a type with a binary associative operation. Law: * * > x.append(y).append(z) = x.append(y.append(z)) */ interface Semigroup<T extends Semigroup<T>> { T append(T arg); } class Foo implements Semigroup<Foo> { // Since this implements Semigroup<Foo>, now this method must accept // a Foo argument and return a Foo result. Foo append(Foo arg); } class Bar implements Semigroup<Bar> { // Any of these is a compilation error: 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 mismoFqueFA, de modo que cuando declaras un tipoList<A> implements Functor<List<A>, A>, elmapmétodo aún puede devolver unNotAList<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 soloListoMap. Esto garantiza que aFunctorStrategy<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 escribirTyList<T>, pero noT<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> { // Since F := List, F<B> := List<B> <B> List<B> map(Function<A, B> f) { // ... } }Y abordando este bit en particular:
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 haciaSeq, obtiene la operación del mapa "gratis" mediante la reutilizaciónSeq.map.En Haskell, sin embargo, la
Functorclase es más general que eso; no está ligado a la noción de secuencias. Puede implementarfmappara tipos que no tienen una buena asignación a secuencias, comoIOacciones, combinadores de analizadores, funciones, etc.instance Functor IO where fmap f action = do x <- action return (f x) -- This declaration is just to make things easier to read for non-Haskellers newtype Function a b = Function (a -> b) instance Functor (Function a) where fmap f (Function g) = Function (f . g) -- `.` is function compositionEl 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) xsMuy informalmente:
Ésta es la razón por la que desea
fmapconservar el tipo, porque tan pronto como obtengamapoperaciones que produzcan un tipo de resultado diferente, se vuelve mucho, mucho más difícil ofrecer garantías como esta.fuente
fmapencendidoFunction acuando ya tiene una.operación? Entiendo por qué.tiene sentido ser la definición de lafmapoperación, pero no llego a donde alguna vez necesitarías usar enfmaplugar de.. Tal vez si pudiera dar un ejemplo en el que sea útil, me ayudaría a comprender.doublede un funtor, dondedouble [1, 2, 3]da[2, 4, 6]ydouble sinda un fn que es el doble del pecado. Puedo ver dónde si empiezas a pensar con esa mentalidad, cuando ejecutas un mapa en una matriz, esperas una matriz de regreso, no solo una secuencia, porque, bueno, estamos trabajando en matrices aquí.Functory dejar que el cliente de la biblioteca lo elija. La respuesta de J. Abrahamson proporciona un ejemplo: los pliegues recursivos se pueden generalizar mediante el uso de functores. Otro ejemplo son las mónadas libres; puede pensar en ellos como una especie de biblioteca de implementación de intérpretes genéricos, donde el cliente proporciona el "conjunto de instrucciones" de forma arbitrariaFunctor.Functoro aSemiGroup. ¿Dónde utilizan más los programas reales esta función de lenguaje?No quiero repetir información en algunas respuestas excelentes que ya están aquí, pero hay un punto clave que me gustaría agregar.
Por lo general, no necesita tipos de tipo superior para implementar una mónada o un funtor (o un funtor aplicativo, una flecha o ...) en particular. Pero hacerlo es principalmente perder el punto.
En general, he descubierto que cuando la gente no ve la utilidad de los functores / mónadas / lo que sea, a menudo es porque están pensando en estas cosas una a la vez . Las operaciones de functor / monad / etc realmente no agregan nada a ninguna instancia (en lugar de llamar a bind, fmap, etc., podría simplemente llamar a las operaciones que usé para implementar bind, fmap, etc.). Para lo que realmente desea estas abstracciones es para poder tener un código que funcione genéricamente con cualquier functor / mónada / etc.
En un contexto donde dicho código genérico se usa ampliamente, esto significa que cada vez que escribe una nueva instancia de mónada, su tipo obtiene acceso inmediatamente a una gran cantidad de operaciones útiles que ya se han escrito para usted . Ese es el punto de ver mónadas (y functores, y ...) en todas partes; no para que pueda usar en
bindlugar deconcatemapimplementarmyFunkyListOperation(lo que no me gana nada en sí mismo), sino para que cuando lo necesitemyFunkyParserOperationymyFunkyIOOperationpueda reutilizar el código que vi originalmente en términos de listas porque en realidad es monad-genérico .Pero para abstraer un tipo parametrizado como una mónada con seguridad de tipos , necesita tipos de tipo superior (como se explica en otras respuestas aquí).
fuente
Para una perspectiva más específica de .NET, escribí una publicación de blog sobre esto hace un tiempo. El quid de esto es que, con tipos de tipo superior, podría potencialmente reutilizar los mismos bloques LINQ entre
IEnumerablesyIObservables, pero sin tipos de tipo superior esto es imposible.Lo más cerca que se puede conseguir (que me di cuenta después de publicar el blog) es para hacer su propia
IEnumerable<T>yIObservable<T>, y los dos se extendía desde unaIMonad<T>. Esto le permitiría reutilizar sus bloques LINQ si están indicadosIMonad<T>, pero ya no es seguro para los tipos porque le permite mezclar y combinarIObservablesyIEnumerablesdentro del mismo bloque, lo que si bien puede sonar intrigante habilitar esto, debería Básicamente, obtengo un comportamiento indefinido.Escribí una publicación posterior sobre cómo Haskell hace esto fácil. (Una operación no operativa, en realidad: restringir un bloque a un cierto tipo de mónada requiere código; habilitar la reutilización es el valor predeterminado).
fuente
IObservablesen código de producción.IObservable, y usa eventos en el capítulo WinForms de su propio libro.El ejemplo más utilizado de polimorfismo de tipo de tipo superior en Haskell es la
Monadinterfaz.FunctoryApplicativeson de clase superior de la misma manera, por lo que mostraréFunctorpara mostrar algo conciso.class Functor f where fmap :: (a -> b) -> f a -> f bAhora, examine esa definición y observe cómo
fse usa la variable de tipo . Verá quefno puede significar un tipo que tenga valor. Puede identificar valores en esa firma de tipo porque son argumentos y resultados de funciones. Entonces, las variables de tipoaybson tipos que pueden tener valores. También lo son las expresiones de tipof ayf b. Pero no enfsí mismo.fes un ejemplo de una variable de tipo superior. Dado que*es el tipo de tipos que pueden tener valores,fdebe tener el tipo* -> *. Es decir, toma un tipo que puede tener valores, porque sabemos por examen previo queaybdebe tener valores. Y también sabemos esof ayf bdebe tener valores, por lo que devuelve un tipo que debe tener valores.Esto hace que se
fuse en la definición deFunctoruna variable de tipo superior.Las interfaces
ApplicativeyMonadagregan más, pero son compatibles. Esto significa que también trabajan en variables de tipo con kind* -> *.Trabajar en tipos de tipo superior introduce un nivel adicional de abstracción: no está restringido a crear abstracciones sobre tipos básicos. También puede crear abstracciones sobre tipos que modifican otros tipos.
fuente