Sí, una pregunta muy directa en la superficie. Pero si te tomas el tiempo para pensarlo hasta el final, te sumerges en las profundidades de la teoría de tipos inconmensurable. Y la teoría de los tipos también te mira a ti.
Primero, por supuesto, ya has descubierto correctamente que F # no tiene clases de tipo, y es por eso. Pero propones una interfaz Mappable
. Ok, veamos eso.
Digamos que podemos declarar tal interfaz. ¿Te imaginas cómo se vería su firma?
type Mappable =
abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>
¿Dónde f
está el tipo que implementa la interfaz? ¡Oh espera! ¡F # tampoco tiene eso! Aquí f
hay una variable de tipo de clase más alta, y F # no tiene clase de clase más alta. No hay forma de declarar una función f : 'm<'a> -> 'm<'b>
o algo así.
Pero bueno, digamos que también superamos ese obstáculo. Y ahora tenemos una interfaz Mappable
que puede ser implementado por List
, Array
, Seq
y el fregadero de la cocina. ¡Pero espera! ¡Ahora tenemos un método en lugar de una función, y los métodos no componen bien! Veamos cómo agregar 42 a cada elemento de una lista anidada:
// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))
// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))
Mira: ¡ahora tenemos que usar una expresión lambda! No hay forma de pasar esta .map
implementación a otra función como valor. Efectivamente, el final de "funciones como valores" (y sí, lo sé, usar un lambda no se ve muy mal en este ejemplo, pero créanme, se pone muy feo)
Pero espera, todavía no hemos terminado. ¡Ahora que es una llamada al método, la inferencia de tipos no funciona! Debido a que la firma de tipo de un método .NET depende del tipo de objeto, el compilador no puede inferir ambos. Este es en realidad un problema muy común que enfrentan los novatos cuando interactúan con bibliotecas .NET. Y la única cura es proporcionar una firma de tipo:
add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))
¡Oh, pero esto todavía no es suficiente! Aunque he proporcionado una firma para nestedList
sí mismo, no he proporcionado una firma para el parámetro de lambda l
. ¿Cuál debería ser esa firma? ¿Dirías que debería ser fun (l: #Mappable) -> ...
? Ah, y ahora finalmente llegamos a los tipos de rango N, ya ves, #Mappable
es un atajo para "cualquier tipo 'a
tal que 'a :> Mappable
", es decir, una expresión lambda que es genérica.
O, alternativamente, podríamos volver a la mayor amabilidad y declarar el tipo de nestedList
más precisamente:
add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...
Pero bueno, dejemos de lado la inferencia de tipos por ahora y volvamos a la expresión lambda y cómo ahora no podemos pasar map
como valor a otra función. Digamos que extendemos un poco la sintaxis para permitir algo como lo que hace Elm con los campos de registro:
add42 nestedList = nestedList.map (.map ((+) 42))
¿Cuál sería el tipo de .map
ser? ¡Tendría que ser un tipo restringido , como en Haskell!
.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>
Wow, esta bién. Dejando a un lado el hecho de que .NET ni siquiera permite que tales tipos existan, efectivamente, ¡acabamos de recuperar las clases de tipos!
Pero hay una razón por la cual F # no tiene clases de tipo en primer lugar. Muchos aspectos de esa razón se describen anteriormente, pero una forma más concisa de expresarlo es: simplicidad .
Para ver, esta es una bola de hilo. Una vez que tenga clases de tipo, debe tener restricciones, mayor amabilidad, rango-N (o al menos rango-2), y antes de darse cuenta, está solicitando tipos impredecibles, funciones de tipo, GADT y todos los El resto
Pero Haskell paga un precio por todas las golosinas. Resulta que no hay una buena manera de inferir todas esas cosas. Los tipos de clase superior funcionan, pero las restricciones ya no. Rango N: ni siquiera sueñe con eso. E incluso cuando funciona, obtienes errores de tipo que debes tener un doctorado para entender. Y es por eso que en Haskell se le anima a poner firmas tipográficas en todo. Bueno, no todo , todo , pero realmente casi todo. Y donde no pones firmas tipográficas (por ejemplo, adentro let
y where
) - sorpresa-sorpresa, esos lugares están realmente monomorfizados, por lo que esencialmente estás de vuelta en la simplista tierra F #.
En F #, por otro lado, las firmas de tipo son raras, principalmente solo para documentación o para interoperabilidad .NET. Fuera de esos dos casos, puede escribir un programa complejo grande en F # y no usar una firma de tipo una vez. La inferencia de tipos funciona bien, porque no hay nada demasiado complejo o ambiguo para que lo maneje.
Y esta es la gran ventaja de F # sobre Haskell. Sí, Haskell te permite expresar cosas súper complejas de una manera muy precisa, eso es bueno. Pero F # te permite ser muy flojo, casi como Python o Ruby, y aún así el compilador te atrapará si tropiezas.