Por que a comunidade Lisp prefere acumular todos os parênteses no final da função?

26

Por que a comunidade Lisp prefere acumular todos os parênteses no final da função:

(defn defer-expensive [cheap expensive]
  (if-let [good-enough (force cheap)]
    good-enough
    (force expensive)))

Por que não empregar uma convenção como C ou Java?
Bem, ok, o Lisp é muito mais antigo que esses idiomas, mas estou falando dos Lispers contemporâneos.

(defn defer-expensive [cheap expensive]
  (if-let [good-enough (force cheap)]
    good-enough
    (force expensive)
  )
)

Nota: o snippet de código é do livro "The Joy of Clojure".

Quíron
fonte
13
Disseram-me que nos dias de cartões perfurados, era prática comum encerrar um programa Lisp com um cartão extra com 80 parênteses, apenas para garantir que houvesse o suficiente para corresponder a todos os parênteses abertos no programa .
Alger
2
Wow eu gosto disso. Eu sempre odiei o parêntese, mas o seu segundo exemplo me parece agradável. Não é tão bom quanto Python, mas melhora.
Eric Wilson
5
porque eles podem?
Muad'Dib
Seria bom se um dialeto de Clojure promovesse o recuo para ser uma estrutura de código. Isso eliminaria completamente os parênteses (como F #, etc.). Obviamente, parênteses ainda seriam legais, se desejado.
Intrepidis

Respostas:

28

Uma das razões pelas quais as linguagens baseadas em Algol incentivam os chavetas em sua própria linha é incentivar a adição de mais linhas entre os chaveadores delimitadores sem precisar movê-los. Ou seja, se alguém começa com

if (pred)
{
  printf("yes");
}

é fácil aparecer e adicionar outra declaração dentro do aparelho:

if (pred)
{
  printf("yes");
  ++yes_votes;
}

A forma original tinha sido

if (pred)
{ printf("yes"); }

então teríamos que "mover" duas chaves, mas meu exemplo está mais preocupado com a segunda. Aqui, os chavetas estão delimitando o que pretende ser uma sequência de instruções , principalmente invocadas para efeito colateral.

Por outro lado, o Lisp não possui declarações; toda forma é expressão , produzindo algum valor - mesmo que em alguns casos raros (pensando no Common Lisp), esse valor seja deliberadamente escolhido como "sem valor" por meio de um (values)formulário vazio . É menos comum encontrar seqüências de expressões , em vez de expressões aninhadas . O desejo de "abrir uma sequência de etapas até o delimitador de fechamento" não surge com tanta frequência, porque, quando as instruções desaparecem e os valores de retorno se tornam mais comuns, é mais raro ignorar o valor de retorno de uma expressão e, portanto, mais É raro avaliar uma sequência de expressões apenas para efeito colateral.

No Common Lisp, o prognformulário é uma exceção (assim como seus irmãos):

(progn
  (exp-ignored-return-1)
  (exp-ignored-return-2)
  (exp-taken-return))

Aqui, prognavalia as três expressões em ordem, mas descarta os valores de retorno das duas primeiras. Você pode imaginar escrever esse último parêntese de fechamento em sua própria linha, mas observe novamente que, como a última forma é especial aqui ( embora não seja no sentido comum de Lisp de ser especial ), com tratamento distinto, é mais provável que alguém adicione novos expressões no meio da sequência, em vez de apenas "adicionar outra até o fim", pois os chamadores seriam afetados não apenas por quaisquer novos efeitos colaterais, mas por uma provável alteração no valor de retorno.

Fazendo uma simplificação grosseira, os parênteses na maioria das partes de um programa Lisp estão delimitando argumentos passados ​​para funções - assim como em linguagens C - e não delimitando blocos de instrução. Pelas mesmas razões, tendemos a manter os parênteses delimitando uma chamada de função em C perto dos argumentos, assim como fazemos no Lisp, com menos motivação para desviar desse agrupamento próximo.

O fechamento dos parênteses é muito menos importante do que o recuo do formulário onde eles são abertos. Com o tempo, aprende-se a ignorar os parênteses e a escrever e ler pela forma - como fazem os programadores de Python. No entanto, não deixe que essa analogia o leve a pensar que a remoção completa dos parênteses valeria a pena. Não, esse é um debate mais bem guardado comp.lang.lisp.

seh
fonte
2
eu acho que adicionar no final não é tão raro, por exemplo (let ((var1 expr1) more-bindings-to-be-added) ...), ou(list element1 more-elements-to-be-added)
Alexey
14

Porque isso não ajuda. Usamos recuo para mostrar a estrutura do código. Se queremos separar blocos de código, usamos linhas realmente vazias.

Como a sintaxe do Lisp é tão consistente, os parênteses são o guia definitivo para o recuo, tanto para o programador quanto para o editor.

(Para mim, a questão é: por que os programadores C e Java gostam de usar o aparelho?)

Apenas para demonstrar, assumindo que esses operadores estavam disponíveis em uma linguagem semelhante a C:

Foo defer_expensive (Thunk cheap, Thunk expensive) {
    if (Foo good_enough = force (cheap)) {
        return good_enough; }
    else {
        return force (expensive); }}

Duas chaves de fechamento fecham dois níveis de aninhamento. A sintaxe está obviamente correta. Em analogia à sintaxe do Python, os chavetas são apenas tokens INDENT e DEDENT explícitos.

Obviamente, esse pode não ser o TM "estilo único" , mas acredito que seja apenas um acidente e um hábito histórico.

Svante
fonte
2
Além disso, quase todos os editores lisp marcar o parêntese correspondente ao fechar (alguns também correta )para ]ou vice-versa), então você sabe que você fechou a quantidade certa, mesmo se você não verificar os níveis de recuo.
configurator
6
WRT "the question", porque "jogar ao redor" fechando tokens no estilo do segundo exemplo permite alinhá-los facilmente com os olhos e ver o que fecha o que, mesmo se você estiver apenas em um editor de texto sem correspondência automática / recursos de destaque.
Mason Wheeler
1
@Mason Wheeler Sim, exatamente.
Quíron
3
TBH, o que isso me diz é que os parênteses no LISP são redundantes, com a possibilidade (como no C ++) de que o recuo possa enganar - se o recuo indicar o que você precisa saber, o mesmo deve dizer ao compilador, como em Haskell e Python. BTW - ter as chaves no mesmo nível de recuo que o if / switch / while / qualquer coisa no C ++ ajuda a evitar casos em que a indentação é enganosa - uma varredura visual no LHS informa que todas as chaves abertas são correspondentes a uma chave próxima e esse recuo é consistente com esses aparelhos.
Steve314
1
@ Steve314: Não, o recuo é redundante. A indentação também pode induzir em erro em Python e Haskell (pense em indentação ou tabulares únicos), mas o analisador também é enganoso. Penso que os parênteses são uma ferramenta para o escritor e o analisador, enquanto o recuo é uma ferramenta para o escritor e o leitor (humano). Em outras palavras, recuo é um comentário, parênteses são sintaxe.
Svante
11

O código é muito mais compacto então. O movimento no editor é por expressões s de qualquer maneira, para que você não precise desse espaço para edição. O código é lido principalmente pela estrutura e frases - não seguindo delimitadores.

Rainer Joswig
fonte
Que honra receber uma resposta sua :) Obrigado.
Quíron
Que editor se move por expressões s? Mais especificamente, como faço vimpara fazer isso?
hasen
2
@HasenJ Você pode precisar do vimacs.vim plugin . : P
Mark C
5

Lispers, você sabe, com ódio bletcherous que seja, não é um certo je ne sais quoi para o segundo exemplo:

(defn defer-expensive [cheap expensive]
  (if-let [good-enough (force cheap)]
    good-enough
    (force expensive)
  )
)

No começo, eu não conseguia identificar a fonte do fascínio, por assim dizer, e então percebi que faltava apenas um gatilho:

(defn defer-expensive [cheap expensive]      
  (if-let [good-enough (force cheap)]
    good-enough   ;   )
    (force expensive) 
  )
)

Voilà!

Vou praticar agora o meu domínio sobre gangster Lisp!

Kaz
fonte
2
Essa é uma das respostas mais engraçadas e subestimadas que eu já vi neste site. Eu tiro meu chapéu.
Bydjr
0

No meu caso, considero as linhas dedicadas aos delimitadores um desperdício de espaço na tela e, quando você escreve código em C, há também o estilo de

if (pred) {
   printf("yes");
   ++yes_votes;
}

Por que as pessoas colocam a chave de abertura na mesma linha do "se" para economizar espaço e porque parece redundante ter sua própria linha quando o "se" já tem sua própria linha.

Você alcança o mesmo resultado reunindo os parênteses no final. Em C pareceria estranho porque as instruções terminam em ponto-e-vírgula e o par de colchetes não abre assim

{if (pred)
    printf("yes");
}

é como aquela chave de fechamento no final, parece fora de lugar. abre assim

if (pred) {
    printf("yes");
}

fornecendo uma visão clara do bloco e seus limites com '{' & '}'

E com a ajuda de um editor que corresponde aos parênteses destacando-os como o vim, você pode ir até o final, onde todos os parênteses estão agrupados e movê-los facilmente, combinar todas as parênteses de abertura e ver o formulário lisp aninhado

(defn defer-expensive [cheap expensive]
    _(if-let [good-enough (force cheap)]
    good-enough
    (force expensive)_))

você pode colocar o cursor no ponto de fechamento no meio e destacar o ponto de abertura do if-let.

kisai
fonte