¿Por qué hay tantas funciones `map` para diferentes tipos en F #?

9

Estoy aprendiendo F #. Comencé FP con Haskell, y tengo curiosidad por esto.

Como F # es lenguaje .NET, parece más razonable para mí declarar una interfaz como Mappable, al igual que la Functorclase de tipo haskell .

ingrese la descripción de la imagen aquí

Pero como en la imagen de arriba, las funciones de F # se separan y se implementan por sí mismas. ¿Cuál es el propósito del diseño de dicho diseño? Para mí, presentar Mappable.mape implementar esto para cada tipo de datos sería más conveniente.

MyBug18
fuente
Esta pregunta no pertenece a SO. No es un problema de programación. Le sugiero que pregunte en F # Slack o en algún otro foro de discusión.
Doblado Tranberg
55
@BentTranberg Leído generosamente, The community is here to help you with specific coding, algorithm, or language problems.también abarcaría preguntas de diseño de lenguaje, siempre que se cumplan los demás criterios.
kaefer
3
Para resumir, F # no tiene clases de tipo y, por lo tanto, debe volver a implementarse mapy otras funciones comunes de orden superior para cada tipo de colección. Una interfaz ayudaría poco, ya que todavía requiere que cada tipo de colección proporcione una implementación separada.
dumetrulo

Respuestas:

19

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 festá el tipo que implementa la interfaz? ¡Oh espera! ¡F # tampoco tiene eso! Aquí fhay 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 Mappableque puede ser implementado por List, Array, Seqy 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 .mapimplementació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 nestedListsí 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, #Mappablees un atajo para "cualquier tipo 'atal 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 nestedListmá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 mapcomo 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 .mapser? ¡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 lety 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.

Fyodor Soikin
fuente