¿Por qué las funciones en F # y Ocaml (y posiblemente en otros lenguajes) no son recursivas por defecto?
En otras palabras, ¿por qué los diseñadores del lenguaje decidieron que era una buena idea hacerte escribir explícitamente rec
una declaración como:
let rec foo ... = ...
y no dar a la función la capacidad recursiva por defecto? ¿Por qué la necesidad de una rec
construcción explícita ?
Respuestas:
Los descendientes franceses y británicos del ML original tomaron decisiones diferentes y sus elecciones se han heredado a lo largo de las décadas hasta las variantes modernas. Esto es solo un legado, pero afecta los modismos en estos idiomas.
Las funciones no son recursivas de forma predeterminada en la familia de idiomas CAML en francés (incluido OCaml). Esta opción facilita la sustitución de las definiciones de función (y variable) que se utilizan
let
en esos lenguajes porque puede hacer referencia a la definición anterior dentro del cuerpo de una nueva definición. F # heredó esta sintaxis de OCaml.Por ejemplo, reemplazando la función
p
al calcular la entropía de Shannon de una secuencia en OCaml:Observe cómo el argumento
p
de lashannon
función de orden superior es reemplazado por otrop
en la primera línea del cuerpo y luego por otrop
en la segunda línea del cuerpo.Por el contrario, la rama británica SML de la familia de lenguajes ML tomó la otra opción y las
fun
funciones vinculadas de SML son recursivas de forma predeterminada. Cuando la mayoría de las definiciones de funciones no necesitan acceso a enlaces previos de su nombre de función, esto da como resultado un código más simple. Sin embargo, las funciones reemplazadas están hechas para usar diferentes nombres (f1
,f2
etc.) lo que contamina el alcance y hace posible invocar accidentalmente la "versión" incorrecta de una función. Y ahora hay una discrepancia entrefun
funciones vinculadas implícitamente recursivas y funciones vinculadas no recursivasval
.Haskell hace posible inferir las dependencias entre definiciones restringiéndolas para que sean puras. Esto hace que las muestras de juguetes parezcan más simples, pero tiene un costo elevado en otros lugares.
Tenga en cuenta que las respuestas dadas por Ganesh y Eddie son pistas falsas. Explicaron por qué los grupos de funciones no se pueden colocar dentro de un gigante
let rec ... and ...
porque afecta cuando las variables de tipo se generalizan. Esto no tiene nada que ver conrec
ser predeterminado en SML pero no OCaml.fuente
Una razón crucial para el uso explícito de
rec
tiene que ver con la inferencia de tipo Hindley-Milner, que subyace a todos los lenguajes de programación funcional de tipado estático (aunque cambiado y extendido de varias formas).Si tiene una definición
let f x = x
, esperaría que tenga un tipo'a -> 'a
y sea aplicable en diferentes'a
tipos en diferentes puntos. Pero igualmente, si escribelet g x = (x + 1) + ...
, esperaríax
ser tratado comoint
en el resto del cuerpo deg
.La forma en que la inferencia de Hindley-Milner trata esta distinción es mediante un paso de generalización explícito . En ciertos puntos al procesar su programa, el sistema de tipos se detiene y dice "ok, los tipos de estas definiciones se generalizarán en este punto, de modo que cuando alguien las use, cualquier variable de tipo libre en su tipo será recién instanciada y, por lo tanto, no interferirá con ningún otro uso de esta definición ".
Resulta que el lugar sensato para hacer esta generalización es después de verificar un conjunto de funciones recursivas entre sí. Si lo hace antes, generalizará demasiado, lo que conducirá a situaciones en las que los tipos podrían colisionar. Más adelante, generalizará muy poco, haciendo definiciones que no se pueden usar con instancias de múltiples tipos.
Entonces, dado que el verificador de tipos necesita saber qué conjuntos de definiciones son recursivas entre sí, ¿qué puede hacer? Una posibilidad es simplemente hacer un análisis de dependencia de todas las definiciones en un alcance y reordenarlas en los grupos más pequeños posibles. Haskell en realidad hace esto, pero en lenguajes como F # (y OCaml y SML) que tienen efectos secundarios sin restricciones, esta es una mala idea porque también podría reordenar los efectos secundarios. Entonces, en cambio, le pide al usuario que marque explícitamente qué definiciones son recursivas entre sí y, por lo tanto, por extensión, dónde debería ocurrir la generalización.
fuente
rec
pero SML no es un contraejemplo obvio. Si la inferencia de tipos fuera el problema por las razones que describe, OCaml y SML no podrían haber elegido soluciones diferentes como lo hicieron. La razón es, por supuesto, que estás hablandoand
para hacer que Haskell sea relevante.Hay dos razones clave por las que esta es una buena idea:
Primero, si habilita las definiciones recursivas, entonces no puede hacer referencia a un enlace anterior de un valor del mismo nombre. Suele ser un modismo útil cuando se hace algo como ampliar un módulo existente.
En segundo lugar, los valores recursivos, y especialmente los conjuntos de valores recursivos entre sí, son mucho más difíciles de razonar, entonces son definiciones que proceden en orden, cada nueva definición se construye sobre lo que ya se ha definido. Al leer dicho código, es bueno tener la garantía de que, a excepción de las definiciones marcadas explícitamente como recursivas, las nuevas definiciones solo pueden referirse a definiciones anteriores.
fuente
Algunas conjeturas:
let
no solo se usa para vincular funciones, sino también otros valores regulares. La mayoría de las formas de valores no pueden ser recursivas. Se permiten ciertas formas de valores recursivos (por ejemplo, funciones, expresiones perezosas, etc.), por lo que necesita una sintaxis explícita para indicar esto.let
construcción es similar a lalet
construcción en Lisp y Scheme; que no son recursivas. Hay unaletrec
construcción separada en Scheme para recursiva Let'sfuente
let rec xs = 0::ys and ys = 1::xs
.Dado este:
Comparar:
Con este:
El primero redefine
f
para aplicar el definido previamentef
para el resultado de aplicarg
aa
. Este último redefinef
a un bucle infinito aplicandog
aa
, que por lo general no es lo que desea en las variantes ML.Dicho esto, es una cuestión de estilo de diseñador de idiomas. Sígueme el rollo.
fuente
Una gran parte de esto es que le da al programador más control sobre la complejidad de sus ámbitos locales. El espectro de
let
,let*
ylet rec
ofrecen un creciente nivel de potencia y costo.let*
ylet rec
son, en esencia, versiones anidadas de lo simplelet
, por lo que usar cualquiera de las dos es más costoso. Esta calificación le permite microgestionar la optimización de su programa, ya que puede elegir qué nivel de permiso necesita para la tarea en cuestión. Si no necesita recursividad o la capacidad de hacer referencia a enlaces anteriores, puede recurrir a un simple let para ahorrar un poco de rendimiento.Es similar a los predicados de igualdad graduados en Scheme. (es decir
eq?
,eqv?
yequal?
)fuente