Por que as funções em Ocaml / F # não são recursivas por padrão?

104

Por que as funções em F # e Ocaml (e possivelmente em outras linguagens) não são recursivas por padrão?

Em outras palavras, por que os designers da linguagem decidiram que era uma boa ideia fazer você digitar explicitamente recuma declaração como:

let rec foo ... = ...

e não dá a função recursiva capacidade por padrão? Por que a necessidade de uma recconstrução explícita ?

nsantorello
fonte
Consulte também stackoverflow.com/questions/3739628/…
Brian de

Respostas:

87

Os descendentes franceses e britânicos do ML original fizeram escolhas diferentes e suas escolhas foram herdadas ao longo das décadas pelas variantes modernas. Portanto, isso é apenas um legado, mas afeta os idiomas nessas linguagens.

As funções não são recursivas por padrão na família de idiomas CAML francesa (incluindo OCaml). Essa escolha torna mais fácil substituir as definições de função (e variável) usando letnessas linguagens, porque você pode consultar a definição anterior dentro do corpo de uma nova definição. F # herdou essa sintaxe de OCaml.

Por exemplo, substituindo a função pao calcular a entropia de Shannon de uma sequência em OCaml:

let shannon fold p =
  let p x = p x *. log(p x) /. log 2.0 in
  let p t x = t +. p x in
  -. fold p 0.0

Observe como o argumento ppara a shannonfunção de ordem superior é substituído por outro pna primeira linha do corpo e depois por outro pna segunda linha do corpo.

Por outro lado, a ramificação SML britânica da família de linguagens ML escolheu a outra escolha e as funfunções de ligação da SML são recursivas por padrão. Quando a maioria das definições de função não precisa de acesso às ligações anteriores de seu nome de função, isso resulta em um código mais simples. No entanto, as funções superada são feitos para usar nomes diferentes ( f1, f2etc.) que polui o alcance e torna possível acidentalmente invocar a "versão" errada de uma função. E agora há uma discrepância entre funfunções vinculadas implicitamente recursivas e funções vinculadas não recursivas val.

Haskell torna possível inferir as dependências entre as definições, restringindo-as a serem puras. Isso faz com que as amostras de brinquedos pareçam mais simples, mas tem um custo alto em outro lugar.

Observe que as respostas dadas por Ganesh e Eddie são pistas falsas. Eles explicaram por que grupos de funções não podem ser colocados dentro de um gigante, let rec ... and ...porque isso afeta quando as variáveis ​​de tipo são generalizadas. Isso não tem nada a ver com recser padrão no SML, mas não no OCaml.

JD
fonte
3
Não acho que eles sejam uma pista falsa: se não fosse pelas restrições à inferência, é provável que programas ou módulos inteiros fossem automaticamente tratados como recursivos mutuamente, como a maioria das outras linguagens. Isso tornaria a decisão de design específico se "rec" deveria ou não ser necessário discutível.
GS - Peça desculpas a Monica
"... automaticamente tratado como recursivo como a maioria das outras linguagens". BASIC, C, C ++, Clojure, Erlang, F #, Factor, Forth, Fortran, Groovy, OCaml, Pascal, Smalltalk e Standard ML não.
JD,
3
C / C ++ só requer protótipos para definições de encaminhamento, o que não é realmente sobre marcar recursão explicitamente. Java, C # e Perl certamente têm recursão implícita. Poderíamos entrar em um debate interminável sobre o significado de "mais" e a importância de cada idioma, então vamos nos contentar com "muitos" outros idiomas.
GS - Peça desculpas a Monica em
3
"C / C ++ só requer protótipos para definições futuras, o que não é realmente sobre marcar recursão explicitamente". Apenas no caso especial de auto-recursão. No caso geral de recursão mútua, as declarações de encaminhamento são obrigatórias em C e C ++.
JD
2
Na verdade, declarações de encaminhamento não são necessárias em C ++ em escopos de classe, ou seja, métodos estáticos podem chamar uns aos outros sem quaisquer declarações.
polkovnikov.ph
52

Uma razão crucial para o uso explícito de recé fazer com a inferência de tipo de Hindley-Milner, que fundamenta todas as linguagens de programação funcional de tipo estático (embora alterado e estendido de várias maneiras).

Se você tem uma definição let f x = x, espera que ela tenha um tipo 'a -> 'ae seja aplicável a diferentes 'atipos em diferentes pontos. Mas, da mesma forma, se você escrever let g x = (x + 1) + ..., esperaria xser tratado como um intno resto do corpo de g.

A maneira como a inferência de Hindley-Milner lida com essa distinção é por meio de uma etapa de generalização explícita . Em certos pontos ao processar seu programa, o sistema de tipo para e diz "ok, os tipos dessas definições serão generalizados neste ponto, de modo que quando alguém as usar, quaisquer variáveis ​​de tipo livre em seu tipo serão novamente instanciadas, e assim não interferirá com nenhum outro uso desta definição. "

Acontece que o melhor lugar para fazer essa generalização é verificar um conjunto recursivo de funções mutuamente. Qualquer coisa antes, e você generalizará demais, levando a situações em que os tipos podem realmente colidir. Posteriormente, você generalizará muito pouco, fazendo definições que não podem ser usadas com instanciações de vários tipos.

Portanto, visto que o verificador de tipo precisa saber quais conjuntos de definições são recursivos mutuamente, o que ele pode fazer? Uma possibilidade é simplesmente fazer uma análise de dependência em todas as definições em um escopo e reordená-las nos menores grupos possíveis. Haskell realmente faz isso, mas em linguagens como F # (e OCaml e SML), que têm efeitos colaterais irrestritos, é uma má ideia porque pode reordenar os efeitos colaterais também. Em vez disso, ele pede ao usuário para marcar explicitamente quais definições são recursivas mutuamente e, portanto, por extensão, onde a generalização deve ocorrer.

GS - Peça desculpas a Monica
fonte
3
Err, não. Seu primeiro parágrafo está errado (você está falando sobre o uso explícito de "e" e não de "rec") e, conseqüentemente, o resto é irrelevante.
JD
5
Nunca fiquei satisfeito com esse requisito. Obrigada pelo esclarecimento. Outra razão pela qual Haskell é superior em design.
Bent Rasmussen
9
NÃO!!!! COMO ISSO PÔDE ACONTECER?! Esta resposta está completamente errada! Leia a resposta de Harrop abaixo ou confira The Definition of Standard ML (Milner, Tofte, Harper, MacQueen - 1997) [p.24]
lambdapower
9
Como eu disse em minha resposta, o problema de inferência de tipo é um dos motivos para a necessidade de rec, ao invés de ser o único motivo. A resposta de Jon também é uma resposta muito válida (além do comentário sarcástico usual sobre Haskell); Eu não acho que os dois estão em oposição.
GS - Peça desculpas a Monica em
16
“o problema de inferência de tipo é uma das razões para a necessidade de rec”. O fato de que OCaml exige, recmas SML não, é um contra-exemplo óbvio. Se a inferência de tipo fosse o problema pelos motivos que você descreve, OCaml e SML não poderiam ter escolhido soluções diferentes como fizeram. A razão, claro, é que você está falando andpara tornar Haskell relevante.
JD
10

Existem dois motivos principais pelos quais esta é uma boa ideia:

Primeiro, se você habilitar definições recursivas, não poderá se referir a uma ligação anterior de um valor com o mesmo nome. Geralmente, esse é um idioma útil quando você está fazendo algo como estender um módulo existente.

Em segundo lugar, os valores recursivos, e especialmente os conjuntos de valores recursivos mutuamente, são muito mais difíceis de raciocinar do que as definições que procedem em ordem, cada nova definição construída sobre o que já foi definido. É bom, ao ler esse código, ter a garantia de que, exceto para as definições marcadas explicitamente como recursivas, as novas definições só podem se referir a definições anteriores.

zrr
fonte
4

Algumas suposições:

  • letnão é usado apenas para vincular funções, mas também outros valores regulares. A maioria das formas de valores não podem ser recursivas. Certas formas de valores recursivos são permitidas (por exemplo, funções, expressões lazy, etc.), portanto, é necessária uma sintaxe explícita para indicar isso.
  • Pode ser mais fácil otimizar funções não recursivas
  • O encerramento criado quando você cria uma função recursiva precisa incluir uma entrada que aponta para a própria função (para que a função possa se chamar recursivamente), o que torna os encerramentos recursivos mais complicados do que os não recursivos. Então, pode ser bom ser capaz de criar fechamentos não recursivos mais simples quando você não precisa de recursão
  • Ele permite que você defina uma função em termos de uma função definida anteriormente ou valor com o mesmo nome; embora eu ache que isso é uma prática ruim
  • Segurança extra? Certifique-se de que você está fazendo o que pretendia. por exemplo, se você não pretende que seja recursivo, mas acidentalmente usou um nome dentro da função com o mesmo nome da própria função, provavelmente irá reclamar (a menos que o nome tenha sido definido antes)
  • A letconstrução é semelhante à letconstrução em Lisp e Scheme; que são não recursivos. Há uma letrecconstrução separada no Scheme para recursiva vamos
newacct
fonte
"A maioria das formas de valores não podem ser recursivas. Certas formas de valores recursivos são permitidas (por exemplo, funções, expressões preguiçosas, etc.), portanto, é necessária uma sintaxe explícita para indicar isso". Isso é verdade para F #, mas não tenho certeza de como isso é verdade para OCaml, onde você pode fazer let rec xs = 0::ys and ys = 1::xs.
JD
4

Dado isso:

let f x = ... and g y = ...;;

Comparar:

let f a = f (g a)

Com isso:

let rec f a = f (g a)

O primeiro redefine fpara aplicar o definido anteriormente fao resultado da aplicação ga a. Este último redefine fa aplicação de loop para sempreg aa , o que geralmente não é o que você quer em ML variantes.

Dito isso, é uma coisa do estilo do designer de linguagem. Apenas vá em frente.

James Woodyatt
fonte
1

Uma grande parte disso é que ele dá ao programador mais controle sobre a complexidade de seus escopos locais. O espectro de let, let*e let recoferta de um nível crescente de poder e custo. let*e let recsão, em essência, versões aninhadas do simpleslet , portanto, usar qualquer um deles é mais caro. Essa classificação permite que você microgerencie a otimização do seu programa, pois você pode escolher o nível de permissão de que precisa para a tarefa em questão. Se você não precisa de recursão ou da capacidade de se referir a vínculos anteriores, pode recorrer a um simples let para economizar um pouco de desempenho.

É semelhante aos predicados de igualdade graduada em Scheme. (ou seja eq?, eqv?e equal?)

Evan Meagher
fonte