Elmord's Magic Valley

Software, lingüística, mitologia nórdica e rock'n'roll

Lisp stares at PHP

2015-09-02 22:11 -0300. Tags: comp, prog, pldesign, lisp, php, lows

No último post, eu falei sobre o lows, um Lisp que compila para PHP 5.2 que eu comecei a desenvolver, e o que eu fiz até agora nele. Neste post, eu pretendo discutir algumas questões de design da linguagem, mais ou menos na mesma idéia da série Blueprints for a shell (só que bem menor (or so I hope)). Como já mencionei, não sei quando vou mexer no lows de novo (afinal, eu tenho certos afazeres mundanos, tais como um mestrado para terminar), mas deixo aqui documentadas algumas idéias. Comentários são sempre bem-vindos.

Tipos de dados

Os tipos de dados do PHP não casam muito bem (leia-se: não casam praticamente nada) com os tipos convencionais do mundo Lisp. O plano é tentar projetar a linguagem para permitir um estilo de programação suficientemente Lisp-like com os tipos existentes, ao invés de tentar recriar os tipos líspicos tradicionais, até porque isso não seria muito viável em termos de performance em PHP.

Arrays

Basicamente o único tipo de coleção que o PHP possui é o array, que faz tanto o papel de lista quanto de dicionário (e mesmo como dicionário, ele preserva a ordem das inserções). Eu não vejo nenhuma maneira eficiente de implementar um cons em PHP (adiciona um elemento a uma lista, criando uma lista nova, em espaço/tempo O(1)). Daria para criar uma classe Cons, mas o overhead seria muito grande, e além do mais o objetivo da linguagem é interagir facilmente com código PHP. So, arrays.

As operações map, filter, reduce e afins são independentes da representação da coleção e funcionam igualmente bem com arrays. O idioma de criar listas usando cons e recursão, por outro lado, cai por terra, mas ao mesmo tempo ele já não seria uma boa escolha em PHP porque chamadas de função são mais custosas, e não há tail call optimization. Na verdade tail call optimization não nos ajudaria nesse caso anyway, porque a recursão não fica in tail position; nessa situação, quando a performance é relevante, a galera costuma acumular os elementos da lista como um argumento da chamada recursiva e chamar reverse! no final do loop, mas se é para fazer isso, podemos passar um array como argumento e adicionar elementos com push. Anyway.

No futuro, map e afins podem ser inlined, o que torna essas construções basicamente equivalentes a loops. Até mesmo a closure passada como argumento para o map não precisa ser construída, e ao invés disso o corpo do lambda pode ser inserido diretamente no loop.

Há que se pensar na sintaxe equivalente ao Array(...) do PHP. Para listas simples, (array 1 2 3) é suficiente. O problema é o equivalente da forma Array("foo"=>1, "bar"=>2). Quando a chave é uma string literal simples, uma possibilidade seria usar keywords do Chicken: (array foo: 1 bar: 2). Quando a chave não é literal, algo como (array (=> key1 val1) (=> key2 val2)) poderia ser usado, mas isso é meio verboso. I don't know.

Uma bizarrice de arrays do PHP é que elas são passadas por cópia. Ao que parece, simplesmente não existe uma maneira de passar um array "by sharing", como os valores costumam ser passados em Lisp ou em Python. É possível criar uma referência (que na verdade é um alias) para uma variável que contém um array, mas a referência é à variável, não ao array; se outro valor é atribuído à variável, a referência/alias reflete a modificação. Acho que teremos que conviver com isso.

Símbolos

Não existe nada equivalente a símbolos em PHP. A principal vantagem de símbolos sobre strings é o fato de que eles podem ser comparados em tempo constante, mas o PHP não possui nada realmente equivalente ao operador eq dos Lisps, então acho que não teria muito propósito implementar símbolos em lows. 'foo poderia ser usado como abreviação para "foo", talvez.

Pharen tem um "operador" #foo, que gera a string correspondente ao nome da função foo. A razão desse operador é que Pharen converte hífens em nomes de função para underlines (o que é uma boa idéia em um Lisp→PHP), e o operador # produz uma string com o nome convertido. Talvez ' pudesse ser usado com o mesmo propósito em lows.

By the way, lows não tem um operador quote de verdade, pois o código de um programa lows não é representado por uma estrutura de dados de lows (i.e., do PHP). Yeah, lows não é uma linguagem homoicônica, e talvez nem merecesse o título de Lisp por conta disso. Macros em lows não seriam escritas em lows, mas sim em (Chicken) Scheme, mas veremos isso mais adiante.

(Qual seria então o significado de '(+ 1 2) em lows? Erro de sintaxe? Uma abreviação de (array '+ '1 '2)?)

Miscelânea

Não há muito o que dizer sobre os demais tipos (números, strings, booleanos, e NULL). PHP converte loucamente entre tipos, o que é meio desagradável, mas não há muito o que fazer, a menos que queiramos inserir checks antes do uso de qualquer operação. (Isso poderia ser uma opção de compilação, útil para debugging, talvez.)

Operadores

Operações aritméticas e afins em Lisp costumam ser funções, e não operadores especiais. Porém, não queremos ter que chamar uma função em lows para realizar operações aritméticas, pois o custo de uma chamada de função em PHP é alto. (Mesmo Lisps costumam otimizar essas chamadas quando é possível determinar os tipos dos argumentos em tempo de compilação.) Por outro lado, seria bom podermos passar + e afins como argumento para outras funções. Uma possível solução seria expandir '+ para (lambda (x y) (+ x y)), mas isso fixa o número de argumentos do operador, enquanto o ideal é que (+ 1 2 3) seja possível e seja traduzido para 1+2+3. Outra possibilidade seria ter tanto uma função, definida na biblioteca padrão, quanto um operador especial, e o compilador decidiria qual usar dependendo do caso. Mas não tem por que incluir uma função para cada operador na biblioteca padrão, se o próprio compilador pode gerar o lambda com o código apropriado automaticamente quando necessário.

A maioria dos operadores do lows seriam equivalentes diretos, e teriam o mesmo nome, dos operadores do PHP. As exceções seriam:

Os operadores binários têm um detalhe extra: x || y em PHP sempre retorna um booleano, enquanto em Lisp (or x y) costuma retornar x se ele é verdadeiro, e y caso contrário. Daria para traduzir (or x y) para x? x : y (com o detalhe de que x não pode ser avaliado duas vezes), mas isso meio que estraga a idéia de tradução direta (os condicionais de ifs teriam uma cara bizarra no código resultante). Talvez o compilador pudesse detectar quando o and/or está sendo usado como condicional de um if e traduzi-lo para &&/|| nesses casos.

De qualquer forma, o que o PHP acha que é verdadeiro é diferente do que um Lisp normalmente acha que é verdadeiro: em Lisp, normalmente o valor booleano falso (ou a lista vazia) é considerada como falso, e tudo o mais é considerado verdadeiro (inclusive 0, a string vazia, etc.). Esse comportamento inclusive faria mais sentido em PHP, que possui funções como strpos que devolvem um índice numa string (que pode ser 0) ou false. Mas não adiantaria mudar só o and, teria que mudar o comportamento do if também. Tem que ver isso aí...

Funções

Funções em PHP não verificam se o número de argumentos passados corresponde ao número de parâmetros declarados. Poderia haver uma opção de compilação para inserir um check no começo de cada função.

While we are at it, poderíamos permitir declarar os tipos dos argumentos e adicionar checks no começo da função, ou verificar e emitir warnings em tempo de compilação quando possível. Mas isso ficaria para o futuro distante.

Muito me alegraria ter keyword arguments em lows. Eles poderiam ser passados como um array, i.e., (f x y a: 1 b: 2) seria equivalente a (f x y (array a: 1 b: 2)), e no corpo da função referências a a seriam traduzidas para referências à chave no array.

Variáveis globais e constantes

O plano original era que um acesso a uma variável não declarada localmente seria interpretado como um acesso à variável global de nome correspondente. Porém, estive pensando se não seria melhor usar um prefixo ou outra convenção para nomear variáveis globais (e.g., *foo*, a la Common Lisp, ou $foo, a la Ruby). A vantagem seria poder detectar erros de digitação em tempo de compilação, ao invés de assumir que o nome não declarado se trata de uma variável global.

Outro problema são as constantes globais. O Pharen aparentemente decide se um nome é uma constante ou uma variável exigindo que constantes sejam escritas em maiúsculas. ($_SERVER e afins também são escritos em maiúsculas, mas o Pharen usa um operador especial $ para acessar essas variáveis.) Nada no PHP exige que o nome de uma constante seja totalmente em maiúsculas, entretanto. Uma possibilidade seria usar +nome+, uma convenção usada por alguns programadores Common Lisp. I don't know. (error-reporting +E-ALL+)? Talvez (error-reporting #E_ALL)? (error-reporting <E_ALL>)?

Namespaces

Um dialeto de Lisp é classificado como Lisp-1 ou Lisp-2 dependendo de se nomes de funções e de variáveis vivem em um mesmo namespace (Lisp-1), ou se dois namespaces separados são usados (Lisp-2). PHP é mais próximo de um Lisp-2 do que de um Lisp-1, mas a distinção não é bem a mesma, pois em PHP variáveis possuem um prefixo que as identifica ($), enquanto em Lisp a posição do nome em uma chamada determina o namespace a ser utilizado (se o nome aparece como primeiro elemento em uma lista entre parênteses, ele é um nome de função; caso contrário, é uma variável).

Para o lows ser um Lisp-1, seria necessário determinar se (f x y) deveria ser traduzido para f($x, $y) ou $f($x, $y). No geral, isso não é possível (o compilador lows não tem conhecimento dos nomes definidos fora do arquivo que está compilando). Assim, um Lisp-2 é uma escolha mais natural: (f x y) sempre traduz para f($x, $y). Para obter o equivalente de $f($x, $y), i.e., para usar uma variável como a função a ser chamada, deve-se usar um operador especial, normalmente chamado funcall nos Lisps: (funcall f x y).

Em PHP 5.2, $f() só funciona se $f é uma string contendo o nome da função a ser chamada; não é possível chamar closures criadas com (lambda ...) dessa maneira. Por outro lado, call_user_func($f, ...) funciona tanto quando $f é uma string quanto com outros valores chamáveis. Como queremos que funcall funcione em ambos os casos, funcall deve ser traduzido para call_user_func.

A situação contrária ao funcall, i.e., quando se quer usar uma função nomeada como argumento para outra, é coberta pelo nosso operador 'foo, que produz uma string com o nome (convertido) da função foo. Como PHP não possui funções de primeira classe nem declarações locais de função, a string com o nome da função é suficiente para identificar a que função está se referindo.

Classes, métodos e bagulheiras

Definições de classes e métodos são relativamente straightforward. (defclass Foo atributos/métodos...) não tem por que ser muito diferente da construção equivalente em PHP. Podemos adicionar algumas abreviações, e.g., permitir definir um construtor padrão ao invés do costumeiro $this->x = $x; $this->y = $y; … do PHP.

A bagunça começa na hora de escolher uma sintaxe para chamada de métodos e acesso a atributos. Pharen usa algo como (-> objeto (método args...)), mas isso é meio verboso. Uma solução seria permitir (objeto->método args...) como uma abreviação, no caso mais comum em que objeto é uma variável, mas eu prefiro . para isso ao invés de ->. By the way, e se eu quiser um método cujo nome está em uma variável, i.e., $objeto->$método(args...)? Suponho que em Pharen seja possível escrever (-> objeto ($método args...)), que é como eles resolvem o problema do funcall, mas meh, não é assim que lows funciona – e se eu quiser uma expressão ao invés de uma variável para computar o nome do método?. Que tal (-> objeto (funcall método args...))? Mas agora eu vejo que toda essa sintaxe conflita com a sintaxe para acesso de atributos. Em Pharen, (-> objeto nome) é equivalente a $objeto->nome. Novamente, para usar um nome vindo de uma variável, pode-se usar $nome, mas, novamente, isso não permite usar uma expressão para calcular o nome do atributo; (-> objeto (computa-nome)) seria interpretado como $objeto->computa_nome().

So. Estou com a sensação de que o adequado em lows seria que $obj->nome fosse algo como (-> obj 'nome), com o nome quoted. Sem o quote, nome seria avaliado como uma expressão que produz um nome. Quanto a chamada de métodos, talvez o jeito seja usar um operador distinto do usado para atributos. Meh. Independentemente dos operadores escolhidos, obj.nome seria considerado uma abreviação para (-> obj 'nome), já que acredito que acesso com um nome fixo a um objeto em uma variável pré-determinada seja o caso mais comum. E (obj.método args...) seria uma abreviação para $obj->método(args...). Isso tudo requer mais pensação.

Analogamente, o argumento do operador new também deveria ser quoted: (new 'Foo), não (new Foo). Nesse sentido, as operações do lows seriam mais parecidas com o make-instance e slot-value do CLOS do que com o Pharen. Now, o CLOS usa (método objeto args...) para chamadas de métodos (que na verdade são multi-métodos; o objeto é um argumento como os demais, que tambem podem ser especializados para diferentes classes). O problema de adotar essa sintaxe em lows é que é necessário saber que método é um método e não uma função para decidir que código PHP gerar. Uma possiblidade seria usar (.método objeto args...). Tem que ver isso aí. Outra situação a ser considerada é quando queremos passar um método como argumento para uma função (i.e., o equivalente do Array($obj, "método") do PHP). Hmmrgh...

Falando de abreviações, acho que seria uma boa adotar @nome como uma abreviação de $this->nome, a la Ruby. (@método args...) poderia ser abreviação de $this->método(args...).

[Um problema de usar (. objeto 'nome) é que . é sintaxe especial em Scheme, e atualmente eu uso a função read do Scheme para ler o código-fonte lows de um arquivo. Mais adiante, é de se pensar escrever um reader próprio para o lows. Isso também teria a vantagem de permitir manter informação de linha e coluna nas formas lidas.]

Macros

Como o compilador é escrito em Chicken Scheme, e não PHP (e nem por sonho eu pensaria em escrevê-lo em PHP (embora talvez reescrevê-lo em lows no futuro não seja algo de se descartar)), e não possui um interpretador próprio, ele não é capaz de rodar código lows em tempo de compilação. Isso exclui a possibilidade de macros procedurais nativas em lows. O meu plano, por ora, é permitir que macros em Scheme possam ser escritas, pois essas podem ser executadas pelo compilador. Por um lado é bizarro (em um Lisp) escrever macros em uma linguagem diferente da linguagem de programação, mas por outro isso tem a vantagem de permitir usar todas as funções do Chicken na geração de código. Uma outra possibilidade (não-exclusiva) é ter um sistema de macros baseado em casamento de padrões, como o syntax-rules do Scheme, que não requer a execução de código lows em tempo de compilação.

Além das macros que recebem código lows e produzem código lows, também seria interessante ter uma construção para emitir código PHP diretamente, similar ao asm do GCC e afins. No caso mais simples, a construção seria usada com uma string constante, mas nada impediria que expressões arbitrárias em Scheme pudessem ser usadas para gerar o código PHP.

Enough talk

Acho que era isso por enquanto. Sugestões, opiniões, etc., são bem-vindos.

1 comentário

Lisp meets PHP

2015-09-02 04:25 -0300. Tags: comp, prog, pldesign, php, lisp, lows

Como eu andei comentando por aí, eu comecei a implementar uma linguagem Lisp-like que compila para PHP 5.2, chamada lows. Não sei quando vou mexer nesse projeto de novo, mas deixo aqui algumas notas para o meu eu futuro e para quem tiver interesse.

Prelúdio

Tudo começou no domingo retrasado, quando eu resolvi dar uma mexida no blog system, for a change. Mexer no blog sempre é uma experiência ambivalente, pois por um lado tem uma porção de idéias que eu gostaria de implementar nele, mas por outro lado eu tenho que fazer isso em PHP, porque é a única coisa que roda no inf.ufrgs.br (e eu não pretendo pagar por hospedagem any time soon). Eu comentei com uma pessoa que se eu tivesse a opção, eu já teria reescrito o blog em Scheme há muito tempo. Ela me perguntou por que eu não fazia algo para rodar Scheme em PHP, e eu comentei que já tinha pensado em fazer um compilador de Lisp para PHP, mas que achava que era muita mão só para poder escrever o blog em Lisp. Assim, eu segui meu domingo fuçando no blog em PHP.

Depois de uma porção de gambiarras e mais uma porção de concessões às bizarrices do PHP (e.g., existem os métodos mágicos __call, __callStatic e __get, mas não existe um __getStatic, sabe-se lá por quê), eu consegui reescrever a parte do blog responsável por mensagens multilíngües de uma maneira que me agradasse, e até já não estava mais achando tão horrível escrever o código em PHP.

No final do dia, depois de ter testado o código no meu servidor local, eu resolvi fazer upload da nova versão para o inf.ufrgs.br. Para minha surpresa, o PHP começou a reportar uma porção de erros no código. Turns out que a versão do PHP que roda no inf.ufrgs.br é a "5.2.6-1+lenny16". Para quem não sabe, lenny é o codinome do Debian 5. O Debian 5 foi lançado em 2009 e não recebe mais atualizações desde 2012. Três releases estáveis do Debian saíram desde então (as releases estáveis do Debian saem a cada mais ou menos dois anos). Meanwhile, eu estava rodando PHP 5.6.12 em um Debian testing em casa, e praticamente todo o código que eu tinha escrito usava features introduzidas no PHP 5.3.

Depois de tentar sem muito sucesso mudar um pouco o código para ver se conseguia fazê-lo rodar no PHP 5.2, eu resolvi largar de mão e deixar para mexer no código outro dia. Porém: (1) eu não estava a fim de enfeiar o código só para fazê-lo rodar em um PHP velho; (2) more generally, eu não estava a fim de mexer em PHP de novo; e (3) eu não estava conseguindo dormir aquele dia. Conclusão: comecei a escrever um tradutor Lisp→PHP, primariamente for the lol. Mais uma noite mal-dormida, e eis que eu tinha um tradutor (ou compilador, como preferir) Lisp→PHP que fazia pouca coisa, mas o suficiente para me convencer de que a idéia era pelo menos viável. Nasceu assim o lows, ou Lisp for Old Web Servers.

Idéia

A idéia do projeto é criar uma linguagem Lisp-like que satisfaça os seguintes objetivos:

Compilar para PHP 5.2. A idéia é eu poder rodar o código resultante no inf.ufrgs.br (e idealmente escrever a próxima versão do blog em lows), então eu preciso "targetar" especificamente PHP 5.2. Eu também podia tentar convencer a galera da admrede a atualizar o servidor, mas (1) acho pouco provável que isso aconteça any time soon, e eu não estava a fim de esperar; (2) a essa altura eu já tinha tomado a limitação a PHP 5.2 como um desafio (lembre-se de que eu comecei o projeto para matar tempo enquanto o sono não vinha); (3) já existe um projeto similar, chamado Pharen, que targeta PHP 5.5, e eu queria um diferencial (a.k.a. desculpa) para justificar o meu projeto.

Gerar código PHP relativamente straightforward. Tanto quanto possível, o código PHP resultante da compilação deve ser uma tradução mais ou menos direta do código lows original. A idéia é facilitar a depuração (e em particular a tarefa de encontrar o código lows correspondente a um erro reportado no código PHP), e também a esperança de que quanto mais direto for o código resultante, menor o impacto na performance de escrever o código em lows ao invés de diretamente em PHP.

Integrar facilmente com PHP. Deve ser possível usar funções, classes, etc. do PHP a partir de código lows e vice-versa, sem necessidade de conversões, anotações e afins.

Manter uma essência Lisp-like. A idéia não é simplesmente criar um redressing de PHP em S-expressions, mas sim uma linguagem que permita programar em um estilo semi-funcional e "Lispy" e evite as bizarrices do PHP na medida do possível (ao mesmo tempo em que introduz outras bizarrices).

Esse conjunto de objetivos influencia tanto a implementação (que deve gerar um PHP relativamente limpo/direto) quanto o design da linguagem (que não deve fugir muito do PHP para permitir a tradução relativamente direta e a compatibilidade com código PHP).

Transformando Lisp em PHP

Expressões e statements

Um desafio que eu encontrei logo no começo é o fato de que o PHP faz uma distinção entre expressões e statements, que (mostly) não existe em Lisp. Em particular, coisas como if, let (declaração de variáveis locais) e progn (executa uma seqüência de expressões e retorna o valor da última, mais ou menos análogo a um bloco entre chaves em PHP, mas que produz um valor) são expressões em lows. O if em princípio até poderia ser traduzido para o operador ternário (test? then : else), e o let poderia ser mais-ou-menos contornado já que atribuição é uma expressão em PHP. O problema é que PHP não tem um operador vírgula como em C. Coisas como:

(+ 1 (let ((x 23)
           (y 42))
       (* x y)))

não possuem uma tradução direta para PHP, pois não é possível escrever 1 + ($x=23, $y=42, $x*$y). Uma solução gambiarrenta seria gerar:

1 + ((($x=23) || TRUE) ? ((($y=42) || TRUE) ? ($x*$y)
                                            : whatever)
                       : whatever)

o que simula o operador vírgula usando só um branch do operador ternário, mas: (1) isso não funciona no caso geral (em particular, se uma das expressões é um progn contendo um echo ou alguma outra coisa statementosa); (2) isso vai totalmente contra a idéia de gerar código straightforward. A solução é mover as atribuições para antes da soma, mas, no caso geral, só mover qualquer coisa que não seja uma expressão em PHP para antes da expressão não é suficiente: se os branches de um condicional contêm expressões com efeitos colaterais, não é possível movê-las para fora do condicional. Por exemplo, em algo como:

(defun print-and-return (val)
  (echo "O valor é " val "\n")
  val)

(+ 1 (if (> x y)
         (let ((a (print-and-return (- x y))))
           (* a a))
       0))

não é possível traduzir a soma para:

$a = print_and_return($x-$y);
1 + (($x>$y)? ($a*$a) : 0)

pois print_and_return não pode ser chamada antes que o teste $x>$y seja realizado.

[A essa altura talvez lhe ocorra (como me ocorreu) o pensamento: "Ok, e por que a gente simplesmente não proíbe expressões complexas desse tipo aninhadas em outras expressões? Quando é que eu vou usar isso anyway?" Mas esse é justamente o tipo de limitação tosca de que nós estamos tentando fugir criando uma nova linguagem ao invés de programar em PHP! "Do not tell me “that’s what you get for doing weird things”. If two features exist, someday, someone will find a reason to use them together."]

A solução que eu encontrei foi traduzir o (if ...) para um bloco if em PHP, armazenar o valor do if em uma variável temporária, e usar a variável temporária na soma. O exemplo anterior fica algo como:

if ($x>$y) {
    $a = print_and_return($x-$y);
    $tmp = $a*$a;
} else {
    $tmp = 0;
}

1 + $tmp

Isso significa que para traduzir uma expressão como (+ ...), pode ser necessário emitir blocos de código antes da tradução da soma propriamente dita. Conseqüentemente, a função de tradução não pode ser simplesmente algo como:

translate[(+ lhs rhs)] = translate[lhs] + translate[rhs]

pois tanto a tradução de lhs quanto de rhs podem requerer a inserção de blocos de código antes da soma (e a soma, por sua vez, pode estar aninhada em outra expressão).

A solução que eu encontrei para esse problema foi fazer as funções de tradução retornarem dois valores: a expressão equivalente em PHP, e uma lista de "efeitos", que são basicamente (mas não necessariamente) instruções para emitir código nas redondezas da tradução. Por exemplo, a função de tradução aplicada ao if do exemplo gera a expressão $tmp (que pode ser inserida no meio de outra expressão que usa o valor do if), e o efeito (EmitBefore bloco-if-em-PHP), que indica que o o bloco-if-em-PHP deve ser inserido antes da expressão que contém o if na geração do código PHP. Como a inserção só pode ser realizada fora de uma expressão, o efeito é propagado pelas funções de tradução de expressões, até que ele chega em uma função que emite statements (e.g., o corpo de um bloco if do PHP, ou o corpo de uma função) e pode então ser emitido. Pseudocodiciosamente (oops, hmm):

translate[(+ lhs rhs)] =
   let
       lhs-trans; lhs-effects = translate[lhs]
       rhs-trans; rhs-effects = translate[rhs]
   in
       lhs + rhs; lhs-effects ++ rhs-effects


translate-statement[item] =
   let
       item-trans; item-effects = translate[item]
   in
       (código correspondente aos EmitBefore em item-effects) ++ item-trans ;
       (efeitos em item-effects excluindo os EmitBefore já processados)

O mesmo mecanismo pode ser usado para emitir código em outras situações (e.g., no caso do lambda, como veremos adiante), ou para coletar e propagar informações durante a tradução. Por exemplo, quando uma variável x que não possui declaração visível é usada, é emitido um efeito (Global x). A função que traduz o corpo de uma função coleta esses efeitos para gerar declarações do tipo global $x; no começo da função.

lambda

O próximo desafio foi traduzir o lambda para PHP. PHP >=5.3 possui closures (meio toscas – é necessário declarar explicitamente que variáveis são capturadas pela closure – mas elas existem), mas PHP 5.2 não. A próxima coisa que eu pensei foi usar uma classe "callable" com um método mágico __invoke, mas turns out que classes chamáveis só foram introduzidas em PHP 5.3 também. Porém, as funções que aceitam coisas chamáveis em PHP, como call_user_func e usort, aceitam arrays da forma Array(objeto, nome-de-método) como chamáveis. Pois, aí está algo que o lambda pode retornar.

Capturar as variáveis em uma closure mostrou-se bem mais fácil do que eu antecipava, graças às referências do PHP. Uma closure em lows é representada por uma classe com um membro/slot/propriedade/atributo/whatever para cada variável capturada. Quando a classe é instanciada, as variáveis são passadas por referência para o construtor. Dentro do corpo do lambda, referências a variáveis x são traduzidas para $this->x; como $this->x foi inicializado com uma referência ao $x capturado, o corpo do lambda vê a mesma variável $x através do atributo, inclusive refletindo modificações à mesma.

Como exemplo, algo como:

(defun adder (x)
  (lambda (n)
    (+ x n)))

(defun main ()
  (let ((f (adder 10)))
    (call_user_func f 5)))

vira algo como:

class Closure1 {
    function __construct(&$x) {
        // Captura de variáveis.
        $this->x = &$x;
    }

    function __invoke($n) {
        // Corpo do lambda.
        return $this->x + $n;
    }
}

function adder($x) {
    // Cria a closure, passando a variável a ser capturada para o seu construtor,
    // e retorna um valor que, quando chamado, chama o método "__invoke" da closure.
    return Array(new Closure1($x), "__invoke");
}

function main() {
    $f = adder(10);
    return call_user_func($f, 5);
}

E assim, o PHP e suas referências nos surpreendem positivamente (o que é uma surpresa in itself).

By the way, o mecanismo de efeitos aqui é usado para duas coisas: (1) a geração da classe antes da função que contém o lambda é feita propagando um efeito (EmitBeforeToplevel definição-da-classe); (2) cada referência a uma variável externa ao lambda gera um efeito (CapturedVar x); esses efeitos são coletados pela função que traduz o lambda para saber que atributos devem ser inicializados na classe e que argumentos devem ser passados ao construtor. Quando eu criei a treta dos efeitos eu não tinha pensado em todas essas aplicações, então mui me alegrou descobrir que eu podia reusar o mecanismo para essas coisas.

Name clashes

Em PHP, variáveis locais têm como escopo a função inteira onde se encontram, não apenas o bloco onde foram declaradas. Conseqüentemente, em código como:

(let ((x 23))
  (echo "x aqui vale 23: " x "\n")
  (let ((x 42))
    (echo "x aqui dentro vale 42: " x "\n"))
  (echo "x aqui fora ainda vale 23: " x "\n"))

não se pode usar o mesmo nome para as duas variáveis x na tradução, pois a definição mais interna de x sobrescreveria a mais externa. A solução e renomear uma das (ou ambas as) variáveis. O ideal seria fazer o mínimo de renomeações possível, para facilitar a leitura e depuração do código resultante. Porém, a implementação atual simplesmente renomeia todas as variáveis (adicionando um prefixo _número_), já que testar quando uma variável deve ser renomeada não é muito simples. Essa decisão não é local: mesmo não havendo nenhuma variável x visivelmente declarada no ponto onde ocorre um (let ((x 23)) ...), ainda assim é necessário renomear o x se em um ponto posterior da função uma variável global x for referenciada.

O algoritmo de renomeação / geração de nomes temporários assume que nomes iniciados por _número_ são reservados para o compilador. Acredito que isso não seja um problema na prática. (Para o caso de variáveis locais, uma variável _42_ vai ser renomeada para algo como _1__42_ de qualquer forma.) Um problema mais sério dessa abordagem é no escopo global, em particular nos nomes gerados para as classes que implementam closures (e.g., _1_Closure), pois esses nomes podem conflitar com closures criadas em outros arquivos (e.g., quando os resultados da tradução de múltiplos arquivos são incluídos com include em um programa PHP). Talvez uma solução seja incluir o nome do arquivo no nome da closure, ou gerar um hash a partir do código da closure (mas isso ainda gera conflito se um lambda idêntico aparece em outro arquivo), or something. I don't know. Também seria bom se o nome da classe fosse informativo o suficiente para indicar de onde saiu a definição no código original (e.g., _1_Closure_arquivo_função_linha). [Side remark: namespaces não existem em PHP 5.2.]

Um conflito de variável mais sutil é quando um let é executado múltiplas vezes e um lambda captura uma variável definida pelo let. Por exemplo, supondo a existência de uma construção while:

(let ((array-of-lambdas (array))
      (i 0))
  (while (< i 5)
    (let ((n 0))
      (array_push array-of-lambdas
                  (lambda ()
                    (set! n (+ n 1))
                    (echo n))))
    (set! i (+ i 1))))

Isso seria traduzido para algo como:

Class _1_Closure {
    function __construct(&$n) {
        $this->n = &$n;
    }

    function __invoke() {
        $this->n = $this->n + 1;
        echo $this->n;
    }
}

$array_of_lambdas = Array();
$i = 0;
while ($i < 5) {
    $n = 0;
    array_push($array_of_lambdas,
               Array(new _1_Closure($n), "__invoke"));
    $i = $i + 1;
}

O problema é que todas as iterações do loop usam a mesma variável $n, que é passada por referência ao construtor da closure; o correto seria cada iteração capturar um $n diferente. A solução é lembrar de chamar unset($n) no final do while, de maneira que cada iteração crie uma variável nova, mas eu ainda não implementei isso.

PHP formatado

Um dos objetivos do projeto é gerar PHP legível, e isso involve gerar código com indentação adequada. Depois de alguns false starts (na versão inicial, as funções de tradução geravam strings de código PHP diretamente, e a minha idéia original era usar caracteres especiais do ASCII como indicadores de "increase indentation" e "decrease indentation" quando o código fosse impresso, mas eu me dei conta de que não dava para escolher caracteres para isso porque qualquer caractere pode aparecer em uma string; além disso, misturar geração de código e questões de formatação estava ficando um bocado desagradável), eu resolvi fazer as funções de tradução gerarem estruturas representando árvores de sintaxe abstrata (ASTs) de PHP. Depois da tradução, as árvores são passadas a uma função print-php que trata dos detalhes sórdidos de imprimir o código com quebras de linha, indentações, espaços e parênteses nos lugares apropriados. Separation of concerns FTW.

O futuro

Como o post ficou grande, e eu deveria ir dormir, ficaremos por aqui. Em um post futuro, pretendo falar de algumas features que falta implementar, tais como classes, chamadas de métodos e demais firulas orientadas a objetos, bem como as decisões de design mais tricky (que eram o objetivo inicial do post, mas enfim). Quem tiver interesse, pode dar uma olhada no código no GitHub.

1 comentário

Compiling an LLVM pass against Debian's pre-packaged LLVM

2015-08-21 11:46 -0300. Tags: comp, prog, llvm, in-english

So, I'm writing an LLVM pass for my Master's. Until now, I had been compiling LLVM myself and compiling the pass within LLVM's source tree, following more-or-less the standard instructions. This works fine in my main machine, which has plenty of memory for compiling LLVM, but not in my netbook, which has only 1GB of RAM, which is not nearly enough to compile and link Clang/LLVM.

Fortunately, it is possible (and easy) to compile a pass against the precompiled Debian LLVM packages. For that you'll need the package llvm-dev, which depends on llvm-3.5-dev. (Debian is still using Clang/LLVM 3.5; llvm.org/apt has more recent packages, if you happen to need them.)

Makefile

With the packages installed, all you have to do is adjust your project's Makefile to use the installed headers. (Alternatively, you could probably use CMake to compile your module, just like you can do with an LLVM you compiled yourself, but I got it working with a Makefile and right now I'm more interested in finishing my Master's than figuring out build systems.)

Some additional -D (macro definition) flags also seem to be required for the LLVM headers to work:

CXX := c++ -fPIC -Wall -W -std=c++11 -g \
           -D_DEBUG -D_GNU_SOURCE -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS \
           -I/usr/include/llvm-3.5/ -I/usr/include/llvm-c-3.5/

Then, you have to write the rules to build your pass, which should be compiled to a shared library. My project's rules look like this:

LLVMTyr.so: Tyr.cpp.o
	$(CXX) -shared -o LLVMTyr.so Tyr.cpp.o

Tyr.cpp.o: Tyr.cpp
	$(CXX) Tyr.cpp -c -o Tyr.cpp.o

If compilation succeeds, you can run your pass by running LLVM's opt with the option -load ./YourLibrary.so. For instance:

# Generate an LLVM IR file with Clang.
clang -S -emit-llvm test.c -o test.ll

# Use it as input to your pass.
opt -load ./LLVMTyr.so (your pass' options) test.ll

3 comentários

lash status update

2015-05-14 23:42 -0300. Tags: comp, prog, pldesign, lash, life

Faz quase dois meses desde o primeiro commit do lash. O status do projeto é o seguinte:

No decorrer dessa última semana, o parser original, baseado na biblioteca Comparse de parser combinators, foi substituído por um parser descendente recursivo escrito à mão. Os motivos principais para a reescrita foram que a Comparse não mantém informação de linha e coluna dos elementos parseados, aparentemente não tem suporte nenhum a error reporting (o parser simplesmente "backtracka" quando se depara com um erro, até que o parser inicial backtracka e devolve #f), e o parser estava com uns comportamentos estranhos diante de algumas entradas (o que não é culpa da Comparse, mas não tinha por que eu perder tempo debugando se eu já teria que reescrever o parser uma hora ou outra pelos outros motivos citados). O handling de espaços e newlines no parser antigo também estava o caos, enquanto no atual aparentemente tudo funciona como deveria nesse quesito.

O parser novo reconhece quase toda a linguagem prevista para a "release inicial", lança exceções nos pontos certos do código ao encontrar erros de sintaxe (embora as exceções ainda não sejam muito descritiva, mas já é um começo), e armazena a linha e a coluna de cada construção nos nós da árvore sintática (com pequenos erros, mas nada difícil de resolver). O código ainda está meio crude, e tem muita coisa que ainda dá para refatorar (e.g., repetições que estão codificadas como loops explícitos ao invés de uma construção que abstraia a repetição), mas isso vai ir se resolvendo ao longo do tempo. De repente as partes mais abstratas do parser podem até virar uma biblioteca de parser combinators no futuro (com a diferença de que eu estou usando uma struct mutável e exceções ao invés de uma mônada para manter o estado do parser e indicar erros, o que seria meio unusual para uma biblioteca de parser combinators, mas whatever; ninguém disse que seriam parser combinators funcionais).

O parser novo reconhece mais construções do que o resto do shell é capaz de executar (por exemplo, pipelines, redirects, &, &&, ||, $()... em outras palavras, praticamente todas as funções do shell), o que de certa forma é bom, porque me compele a implementar as coisas que faltam. Nos últimos dias o desenvolvimento anda numa taxa mais ou menos ok (para mim), e acho que é mais ou menos realista prever uma release 0.1* mais ou menos funcional para julho. Pelo menos é (mais ou menos) isso que eu espero. Isso é bom, porque em um momento de otimismo em março eu submeti uma proposta de palestra para o FISL sobre o shell e, na vaga possibilidade de ela ser aceita, até lá seria bom o shell estar num estado usável. (Eu submeti a proposta sob a premissa de que se tudo desse errado com o shell e eu fosse aceito era só pedir para tirarem a palestra, mas a essa altura acho que isso não será mais necessário. Tudo isso assumindo que eu seja aceito, o que seria muito doido, na real.)

Comecei a usar a wiki do projeto no GitHub para fazer anotações. O plano que ela venha a conter:

Para editar a wiki é necessário criar uma conta no GitHub, aparentemente, mas acho que podemos conviver com isso. Contribuições são sempre bem-vindas.

Quanto à linguagem do shell, algumas coisas mudaram:

Durante o desenvolvimento do novo parser eu descobri um "bug" no Chicken que faz com que variáveis criadas com define dentro de um cond sejam declaradas como globais. A galera na mailing list parece ser da opinião de que isso não é um bug e sim uma feature, entretanto. Meanwhile, eu resolvi o problema no lash redefinindo o cond para wrappear as cláusulas em um (let () ...) implícito (o que cria um "scope boundary" que torna as definições locais), e de brinde ainda lançar uma exceção se nenhuma cláusula for verdadeira. Scheme, yay.

Enquanto o lash anda às mil maravilhas, o mestrado vai por água abaixo, mas isso é assunto possivelmente para outro post.

_____

* No momento eu não estou numerando as versões, mas pelo esquema de numeração previsto (<major>.<minor>.<número de commits desde o último update do minor>), estaríamos na versão 0.0.31. Parece bastante, mas é porque eu tenho o hábito de commitar loucamente enquanto estou mexendo no código.

1 comentário

You're doing it completely wrong

2015-04-28 22:03 -0300. Tags: comp, prog, wrong, ramble

Encontrei um e-mail de três anos atrás que explica uma das coisas que eu acho que estão "completely wrong" com os ambientes computacionais modernos. O texto era para ter servido de base para um post que nunca foi escrito. Reproduzo-o semi-intacto abaixo. (Fico feliz que a minha visão ainda seja a mesma, mas por outro lado o "plano para os próximos dez mil anos" parece longe de se realizar...)

* * *

From: Vítor De Araújo
To: Carolina Nogueira
Subject: You're doing it completely wrong
Date: Mon, 12 Mar 2012 00:37:18 -0300

[Core dump follows.]

Acho que eu não vou escrever o post tão cedo, então explico agora. O que há de completely wrong é que os programas em execução são "caixas pretas"; em geral um programa é um brinquedo estático, que tu não pode "olhar dentro" e modificar facilmente. Em um mundo ideal tu deveria poder alterar partes de qualquer programa sem ter que recompilar tudo, e de preferência sem sequer ter que fechar e abrir o programa para as mudanças entrarem em efeito.

Exemplo simples: o Pidgin a cada 60 segundos manda um "XMPP ping" pro servidor de XMPP (Jabber), para ver se a conexão ainda está de pé. Se eu quiser mudar de quanto em quanto tempo ele manda o request, ou quanto tempo ele espera por uma resposta, eu tenho que alterar o fonte. Idealmente:

  1. Isso poderia ser (e talvez já seja) uma variavelzinha feliz do programa, que eu deveria poder alterar durante a execução;
  2. Alterar o fonte não deveria ser nenhum grande mistério; o fonte deveria ser facilmente acessível e modificável a partir do programa, e eu deveria poder recompilar uma função ou módulo individual sem ter que recompilar o universo. (Mais de uma vez eu quis mudar coisinhas simples do Pidgin, e.g., eliminar o atalho Ctrl-Up, que bizonhamente exigem alterar o fonte, e falhei miseravelmente em conseguir fazer o troço compilar.)

Além disso, deveria ser fácil desfazer qualquer modificação. Versioning é uma tecnologia muito supimpa dos anos 60 que, como muitas tecnologias supimpas do passado, se perdeu com a popularização de maquininhas pequenas com recursos insuficientes pra suportar a coisa. Hoje em dia temos recursos sobrando, mas a tecnologia caiu no esquecimento.

Exemplo menos simples: o Claws Mail por padrão ordena as mensagens em ordem ascendente de data (da mais antiga pra mais recente), e ele lembra o critério de ordenação por pasta, o que quer dizer que o usuário tem que alterar pra ordem decrescente individualmente pra cada uma das mais de oito mil pastas que há (Inbox, Sent, Queue, Trash, Drafts pra cada mailbox, mais os feeds de RSS). Provavelmente 99.5% das pessoas esperam ordem decrescente por padrão, e provavelmente teria que alterar pouca coisa do código pra mudar o padrão, mas a barreira pra alterar o software é muito grande (baixar fontes, encontrar o trecho de código relevante, se entender com ./configures e bibliotecas da vida, recompilar o mundo, fechar e abrir o programa). Idealmente, eu deveria poder apontar para um menu relacionado com o que eu quero, teclar alguma combinação mágica, e ir parar na função que aquele menu ativa. Daí em diante eu deveria conseguir navegar pelo código até achar o trecho relevante; os nomes de funções e variáveis deveriam funcionar como links (coisa que acho que é possível com a maior parte das IDEs atuais). Tendo achado o dito cujo, eu deveria poder alterar o código, mandar recompilar aquele trecho, e as mudanças se tornarem imediatamente efetivas.

E aparentemente essa utopia toda já existiu, sob o nome de Lisp Machine. Este cara diz:

Within DW, you can move your mouse over any object or subobject that has been displayed, whether or not it was part of any GUI. You get this virtually for free and have to work hard to suppress it if you don't want it. But it doesn't require any special programming.

One of the most common ways to get a foothold in Genera for debugging something is to (1) find a place where the thing you want to use appears visually, (2) click Super-Left to get the object into your hands for read-eval-print, inspection, etc, (3) use (ed (type-of *)) to find its source reliably and with no foreknowledge of what the type is, who wrote the program, what their file conventions were, who loaded it, or any of myriad other things that other systems make me do.

Quer dizer, tu pode pegar um objeto qualquer presente na tela (e praticamente tudo é um objeto, pelo visto), descobrir o tipo/classe dele e mandar editar o código correspondente [(ed (type-of *))]. Eu queria rodar o simulador de Genera aqui pra ver isso funcionando na prática, mas não consegui fazer ele funcionar total... :-(

Enfim, muita tecnologia gloriosa do passado se perdeu. Um dos meus objetivos para os próximos dez mil anos é avançar o estado da arte a o que ele era nos anos 80... :P

Aí o camarada se pergunta: será que faz sentido ter essa tecnologia em um desktop? Será que o "usuário final" tem algum interesse nisso tudo? A resposta é a mesma que se pode dar para justificar a presença de uma linha de comando em um sistema visando o "usuário final" (e.g., Ubuntu, Mac OS):

  1. Mesmo que o usuário final não tenha interesse, se é de ajuda ao desenvolvedor e não prejudica o usuário de maneira alguma, não há por que não ter a feature.
  2. Alguns usuários têm interesse em explorar o funcionamento do sistema e usar as features mais avançadas. Em particular, se a barreira para editar os programas for pequena e for fácil desfazer quaisquer modificações, é bem possível que muito mais gente tenha interesse em bagunçar com os internals das coisas.
  3. Uma coisa muito legal que se vê com Ubuntus da vida é que mesmo um usuário que não saiba usar a linha de comando pode pedir ajuda num fórum e alguém que sabe pode ajudar o cara a resolver um problema que envolva brincar de linha de comando. Da mesma forma, um usuário poderia resolver problemas que envolvessem alterar um programa com a ajuda de pessoinhas em fóruns e afins.

Além de dar todo o poder para os cidadãos infra-vermelhos, o que lembra um pouco as idéias do camarada Alan Kay [1][2], a tendência é que a facilidade de alterar e testar os programas leve a programas mais bem testados, menos bugados e que tendam à perfeição mais rápido. Todos comemora.

Existem mil dificuldades envolvidas na criação de um sistema assim, especialmente se quisermos abandonar o modelo da Lisp Machine de uma máquina mono-usuário em que todos os processos podem tudo, mas isso fica pra outra discussão.

Escrevi bem mais do que eu pretendia. Parece que eu até tenho material para um post... :P

Comentários

Mind dump

2015-04-10 01:34 -0300. Tags: comp, prog, pldesign, lash, life, mind, ramble, music

Coloquei o lash no GitHub, for what it's worth. Eu me pergunto se foi uma coisa sensata publicar ele agora, mas já faz um tempo que eu vinha anunciando que ia publicar "em breve", então coloquei lá de uma vez. (Além disso, esses dias eu quis mexer nele fora de casa e não tinha o código.) O código está em um estágio bem inicial – vergonhosamente inicial, dado que já faz umas três semanas que comecei a trabalhar nele, e o que eu fiz até agora eu provavelmente poderia ter feito em uns três dias se tivesse tido a disciplina de trabalhar nele semi-diariamente. Por outro lado, o projeto está andando para frente, mesmo que devagar, o que já é melhor do que todos os anos anteriores em que eu disse "puxa, eu queria fazer um shell" e não escrevi uma linha de código. So, that's progress. Além disso, na atual conjuntura eu provavelmente deveria tentar relaxar um pouco a cuca e me preocupar menos com isso; afinal, isso é um projeto pessoal e eu não devo nada para ninguém. No final das contas, megalomanias de dominação mundial à parte, o principal afetado pelo bom sucesso do projeto sou eu mesmo.

Eu cheguei à brilhante e inaudita conclusão de que eu vou ter que reduzir bastante minha atividade twittereira e internética em geral se eu quiser começar a fazer alguma coisa produtiva com a minha vida (onde escrever um shell e estudar línguas obscuras contam como coisas produtivas). Por outro lado, a Internet atualmente é responsável por uns 95% das minhas interações sociais, especialmente agora que eu não tenho mais aulas, coisa que faz uma certa falta, a despeito da minha fama de anti-social. A solução provavelmente é (shudder) sair de casa e falar com pessoas.

Eu também cheguei à conclusão (igualmente brilhante) de que muita coisa nessa vida é questão de criar hábitos. Por exemplo, até algumas semanas atrás eu costumava usar toda a louça da casa até não ter mais louça limpa, momento em que eu aplicava o garbage collector e lavava tudo (ou, dependendo da preguiça, só o que eu precisasse na hora). Eu me dei conta de que isso não estava sendo muito conveniente e resolvi começar a lavar as coisas logo depois que uso, ou antes de ir dormir. No começo era meio ruim ter que me "obrigar" a fazer isso, mas agora eu já me habituei e isso não me incomoda mais tanto (além do que, como a louça não acumula, normalmente o esforço de lavar é pequeno). Talvez seja uma questão de criar o hábito de sentar uma hora do dia para programar/estudar/whatever. O flip side disso é que a gente também se habitua ao longo da vida a uma porção de coisas que a gente deveria questionar e/ou atirar pela janela, não só hábitos acionais como também (e principalmente) hábitos mentais. Estas são minhas (brilhantes e inauditas) palavras de sabedoria do dia. (Tecnicamente todo hábito é mental, mas deu pra entender. Acho.)

Escrever o lash em Chicken Scheme tem sido uma experiência bastante agradável. Eu estou aprendendo (a parte não-R5RS d)a linguagem "as I go", mas até agora a linguagem não me deixou na mão, a implementação é estável e gera executáveis pequenos e razoavelmente rápidos, e o sistema de pacotes funciona. (Rodar chicken-install pacote e consistentemente ver o pacote ser baixado e compilado sem erros era quase chocante no começo. O fato de que as bibliotecas são shared objects (a.k.a. DLLs) de verdade e carregam instantaneamente também muito alegrou o espírito, especialmente dada minha experiência anterior com bibliotecas em Common Lisp.) A única coisa que deixa um pouco a desejar é o error reporting, mas nada "deal-breaking".

Eu me dei conta de que uma das coisas que eu mais gosto em linguagens "dinâmicas" é a habilidade de rodar um programa incompleto. Eu já meio que escrevi sobre isso antes, mas eu já não lembrava mais quão deeply satisfying é poder rodar um programa pela metade e ver a parte que foi escrita até o momento funcionando. Por outro lado, é bastante incômodo errar um nome de função ou os argumentos e só descobrir o erro em tempo de execução. Faz muito tempo que eu acho que o ideal seria uma linguagem com análise estática de tipos, mas em que erros de tipo gerassem warnings ao invés de impedir a compilação, e que permitisse a declaração opcional dos tipos de variáveis e funções. Uma dificuldade que eu via nisso até agora é que enquanto em uma linguagem dinâmica os dados costumam ter uma representação uniforme em memória que carrega consigo alguma tag indicando o tipo do dado, e portanto é possível chamar uma função com argumentos do tipo errado e detectar isso em tempo de execução, em uma linguagem estaticamente tipada convencional os dados costumam ter uma representação untagged/unboxed e de tamanho variável, o que tornaria impossível compilar um programa com um erro de tipo sem violar a segurança da linguagem (e.g., se uma função f recebe um vetor e eu a chamo com um int, ou eu rejeito o programa em tempo de compilação, o que atrapalha minha habilidade de rodar programas incompletos/incorretos, ou eu gero um programa que interpreta o meu int como um vetor sem que isso seja detectado em tempo de execução, o que provavelmente vai causar um segfault ou algo pior). Porém, esses dias eu me dei conta de que ao invés de compilar uma chamada (insegura) a f(some_int), poder-se-ia simplesmente (além de gerar o warning) compilar uma chamada a error(f, some_int), onde error é uma função que avalia os argumentos e lança uma exceção descrevendo o erro de tipo. O resultado prático é que o executável gerado roda até o ponto em que é seguro rodar (inclusive avaliando a função e os argumentos) e interrompe a execução no ponto em que seria necessário chamar a função com um argumento de tipo/representação incompatível. Melhor dos dois mundos, não? Vai para o meu caderninho de idéias para a Linguagem Perfeita™.

Eu ia escrever mais umas notas sobre a vida, mas ultimamente eu ando mui receoso quanto a publicar coisas da minha vida pessoal – o que provavelmente é uma boa idéia. É fácil esquecer que qualquer um, no presente ou no futuro, pode ler o que a gente escreve nessa tal de Internet. Eu também já falei sobre isso mil vezes antes, which makes it all the more surprising que eu ainda tenha que me relembrar disso ocasionalmente. Anyway.

Unrelated com qualquer coisa, conheci recentemente uma bandinha chamada Clannad, na qual eu me encontro totalmente viciado no momento. Também conheci uma coisa totalmente excelente chamada Galandum Galundaina, uma banda mirandesa com certeza.

12 comentários

Blueprints for a shell, parte 4: Ramblings on syntax

2015-03-17 01:10 -0300. Tags: comp, prog, pldesign, shell, lash

Este post é parte de uma série sobre o lash, um shell que eu estou ideando.

Hoje discutiremos algumas questões sintáticas do shell. Depois disso eu provavelmente vou dar uma pausa na série e tentar implementar um protótipo do lash, mesmo com algumas questões ainda em aberto. Em particular, falta falar sobre estruturas de controle (mas o básico (if, while, each) não tem muito o que discutir) e módulos (que vão ficar para o futuro).

O meu objetivo ao escolher a sintaxe do shell é achar um ponto de equilíbrio entre minimalismo sintático total (e.g., S-expressions1) e ter sintaxe especial para tudo (e.g., bash). No geral, o guiding principle é expor a maior parte das funcionalidades do shell por meio de funções, e usar sintaxe especial apenas quando seria inconveniente escrever uma chamada de função, especialmente para features freqüentemente usadas em modo interativo (e.g., redirects e pipelines). Este post é uma survey dos elementos sintáticos do (ba)sh e como eles serão representados em lash.

Comandos simples

A sintaxe básica de um comando em (ba)sh é, em BNF fuleiro:

command ::= {var=value}* {word | redirect}*

A semântica é: se há words no comando, a primeira word é o nome do comando a ser executado, e as demais são os argumentos. O comando é executado em um ambiente acrescido das variáveis de ambiente especificadas, e com os redirects em efeito. Se não há words, as variáveis especificadas (do shell, não de ambiente) recebem os valores atribuídos, e os redirects... bom, aparentemente não fazem nada, mas isso depende da variante de sh, porque o comportamento aparentemente é indefinido no padrão POSIX. A ordem de avaliação das coisas também é um pouco peculiar:

bash# a=$(date >&2) uname $(pwd >&2) 2>/dev/null
/tmp
Mon Mar 16 21:27:09 BRT 2015
Linux

dash# a=$(date >&2) uname $(pwd >&2) 2>/dev/null
/tmp
Linux

Vale notar que os redirects e as words podem aparecer intercalados na linha de comando (inclusive minha BNF está errada, porque redirects podem aparecer intercalados com as atribuições também); a ordem em que eles aparecem relativos aos outros elementos sintáticos parece ser irrelevante.

Em lash, depois de muita hesitação, eu decidi atirar pela janela as atribuições prefixadas; o comando env do Unix já serve para rodar comandos em um ambiente modificado (env FOO=bar comando). Eu pensei em obrigar os redirects a aparecerem no final, mas me dei conta de que pode ser útil escrever um redirect intercalado em comandos que recebem blocos. e.g.:

each_line </etc/passwd {|line|
    echo "bla bla $line"
}

Ainda não sei até que ponto isso pode ser útil, mas por enquanto fica aí. Fica a questão da ordem de avaliação. A remoção das variáveis prefixadas são uma coisa a menos na equação. Quanto ao momento em que os redirects tomam efeito, há algumas possibilidades:

  1. Antes de tudo, afetando inclusive chamadas a comandos com $(...), $[...] e companhia. Tem o detalhe de que o redirect em si também pode envolver avaliação (ls >$[generate-a-file-name]). Nesse caso o redirect evidentemente só pode ter efeito depois do comando.
  2. Depois da avaliação de tudo e imediatamente antes de executar o comando propriamente dito. Aparentemente é isso que o bash faz.
  3. O redirect afeta a avaliação de tudo o que aparece depois dele na linha de comando, i.e., 2>/dev/null foo $(bar) afeta a execução de bar, mas foo $(bar) 2>/dev/null não.

Por ora o plano é fazer como o bash, primariamente porque sim.

Fica ainda a questão da atribuição, já mencionada anteriormente: usar um comando para atribuição (set x = 42), ou tratar o = especialmente no parser? Eu não gosto muito de casos especiais, mas talvez a atribuição mereça tratamento especial. Eu nem sei se atribuição (por oposição a definição de uma nova variável) é particularmente freqüente em um script para justificar um caso especial.

Quoting

O bash possui uma porção de coisas quote-like:

O plano para o lash é:

Outra utilidade de strings com delimitador (semi-)arbitrário é que elas supririam a funcionalidade dos "here-documents" do bash, os quais veremos adiante.

Here-documents

Here-documents permitem embutir um trecho de texto, delimitado por uma string à escolha, a ser enviado para a entrada padrão (ou outro file descriptor) do comando a ser executado:

cat <<FIM >foo.txt
The quick brown fox
jumps over the lazy dog.
FIM

Por padrão, o shell realiza substituições no conteúdo do here-document. Se o delimitador for citado/escapado, o conteúdo é interpretado literalmente. Além disso, se o delimitador é precedido de -, espaços e tabs no começo de cada linha são descartados.

Em alguma versão o bash introduziu também "here-strings", que permitem usar uma string simples ao invés de um documento multi-linha como entrada:

sed 's/foo/bar/' <<<"$content"

Se o lash adotasse um mecanismo para strings com delimitadores (semi-)arbitrários, como a contra-aspa descrita anteriormente, seria possível unificar esses dois casos. Strings com delimitador arbitrário podem ser usadas também para inicializar variáveis, por exemplo, coisa que não é possível com here-documents em bash.

Parameter substitution

O bash possui uma dúzia de coisas da forma ${varsomething}, que permitem fazer alguma transformação sobre o valor de uma variável. Além de a sintaxe ser abstrusa, a string a ser manipulada tem que estar armazanada em uma variável (não pode ser o resultado de outra substituição, por exemplo; para aplicar múltiplas substituições é necessário armazenar os resultados parciais em uma variável). O plano em lash é substituir todas as substituições (heh) por funções.

Existe um pequeno problema envolvido: o bash distingue entre ${var//$match/$replacement} e ${var//"$match"/$replacement}. No primeiro caso, *, ? e similares dentro de $match têm seus significados de globbing, enquanto no segundo eles são interpretados literalmente. Esse problema afeta outras coisas que trabalham com patterns. No comentário linkado (que trata da função glob, que retorna uma lista dos arquivos que casam com um padrão), a solução que eu encontrei foi usar uma format string para separar as partes que devem ser interpretadas como pattern das partes que devem ser interpretadas literalmente (assim como printf em C separa a string de controle de strings incluídas com %s e que são usadas literalmente), mas no caso de substituições não sei se seria muito conveniente – talvez agrupando o pattern e seus argumentos em um array:

# Equivalente a ${string//"$match"*/"$replacement"} em bash.

subst $string ("%s*" $match) $replacement

Kinda weird, mas eu consigo sobreviver. Na verdade, acho que o melhor seria tratar o pattern como literal por padrão, senão certo que alguém vai escrever $[subst $var $match $replacement] sem nem pensar se $match contém asteriscos ou não, e aí vai ser outra daquelas situações em que um script funciona 99% do tempo, até que um dia alguém resolve usar uma string com * e o script tem um comportamento inesperado. A sintaxe de subst poderia ser:

Qual a sua opinião?

Outra situação que usa patterns e sofre do mesmo problema é o case, que a princípio há de ser um comando comum sem sintaxe especial (case STRING (PATTERN-1 BLOCO-1 ... PATTERN-N BLOCO-N)2). Idealmente a sintaxe adotada para as substituições deverá ser utilizada para o case também.

And, or, not

Em (ba)sh, comando1 && comando2 executa comando1 e, se este retornar 0 (i.e., verdadeiro), executa comando2. O exit status do comando como um todo é o exit status do último comando que for executado. Analogamente, comando1 || comando2 executa comando1 e, se este retornar não-zero (i.e., falso), executa comando2. Em ambos os casos, comando é um "comando completo", que pode envolver pipelines. Há dois casos de uso principais desses operadores:

Portanto, eles permanecem.

! nega o exit status do comando (troca de não-zero para 0 e de 0 para 1). Ele também se aplica a um "comando completo", negando uma pipeline inteira (o exit status de uma pipeline é o exit status do último comando), e essa seria a única razão que eu vejo para tratá-lo como sintaxe especial e não apenas um comando chamado !. Não sei se justifica; além de ser uma situação bem rara, nada impede de simplesmente escrever o ! antes do último comando da pipeline. Além disso, talvez fosse o caso de escrever ! {comando1 | comando2} anyway, por clareza. While we are at it, podíamos renomear o comando para not, para deixar mais claro que se trata de um comando comum e não sintaxe especial, mas aí já não sei.

Process substitution

Em bash, <(comando) cria um pipe (um par de file descriptors em que tudo que entra numa ponta sai na outra), executa comando com a saída padrão redirecionada para o lado entrante do pipe, e a expressão é substituída por um nome de arquivo que corresponde ao lado de saída do pipe. Por exemplo, é possível escrever:

diff <(sort file1) <(sort file2)

que executa sort file1 e sort file2 e chama algo como diff /dev/fd/63 /dev/fd/62. Analogamente, >(comando) executa comando com a entrada padrão vinda da ponta de saída do pipe, e a expressão é substituída por um nome de arquivo correspondente à ponta de entrada.

Embora essa sintaxe seja bastante conveniente para usar na linha de comando (e na verdade acho que o exemplo com o diff é o único que eu já usei na linha de comando na vida), não sei se eu quero mantê-la em lash. Não só pelo princípio de evitar sintaxe extra gratuita, mas também porque ela parece um redirecionamento, mas é uma word. Se eu quisesse redirecionar um file descriptor para o resultado do process substitution (o que é útil primariamente para fazer um pipeline com um file descriptor que não seja a stdout, e.g., redirecionar a stderr para um comando), eu teria que escrever algo como (o espaço é necessário):

ls 2> >(comando)

o que não é exatamente óbvio. Talvez uma função desse conta do recado, algo como:

diff $[popen -r {sort file1}] $[popen -r {sort file2}]

Ok, a cara disso é terrível3. Talvez se a popen ganhar outro nome, e o comando aceitar um nome de comando e argumentos diretamente ao invés de obrigatoriamente um bloco:

diff $[readfrom {sort file1}] $[readfrom {sort file2}]
diff $[pipefrom {sort file1}] $[pipefrom {sort file2}]
diff $[pipefrom sort file1] $[pipefrom sort file2]

Não sei.

Outro problema com a sintaxe do bash é que o comando parece um array, e talvez um array fizesse sentido como alvo do redirect (redirecionaria para todos os nomes de arquivo no array). Por outro lado, o caso do array poderia ser representado pelo array "spliced", qualquer que seja a sintaxe escolhida para ele (e.g., >$@(file1 file2)), ou simplesmente permitindo múltiplos redirects do mesmo file descriptor (>file1 >file2; o zsh permite isso, acho). Não sei.

Humanitas precisa dormir

Por hoje ficamos por aqui. Como sempre, tudo o que eu digo que "é" de tal jeito é só o plano atual, tudo está sujeito a discussão, comentários e sugestões são sempre bem-vindos, live free or die, do what you want 'cause a pirate is free, etc. Como esse é, a princípio, o último post da série for a while, sinta-se a vontade para comentar aqui sobre tópicos não abordados até agora na série.

_____

1 Em tempos de outrora eu pensei em usar S-expressions para toda a sintaxe (inclusive redirecionamentos e pipelines), mas permitir omitir os parênteses em torno de comandos que aparecem sozinhos em uma linha. O resultado não me foi exatamente satisfatório. Além disso, turns out que um shell totalmente baseado em S-expressions já foi feito (o qual por sinal provavelmente é uma boa fonte de inspiração).

2 Os patterns e blocos vão em um array primariamente para permitir que eles ocupem múltiplas linhas sem ter que pôr um \ no final de cada linha:

case $file (
    "*.mp3" { ... }
    "*.ogg" { ... }
    "*" { ... }
)

3 Revisando o post, eu olhei para isso e pareceu a sintaxe mais natural do mundo, mas a essa altura minha percepção já está meio alterada pelo sono.

8 comentários

Blueprints for a shell, parte 3: Tipos de dados

2015-03-13 22:47 -0300. Tags: comp, prog, pldesign, shell, lash

Este post é parte de uma série sobre o lash, um shell que eu estou ideando.

A world made of strings

Em (ba)sh só existe um tipo de dado: a string. Em bash, uma variável pode ser declarada como um array (e em versões mais recentes, como um dicionário), mas embora a variável seja um array, o array em si não é um valor de primeira classe: não é possível passar um array como argumento para uma função, ou armazenar um array dentro de outro, por exemplo. Isso limita um bocado o que se pode fazer em bash sem apelar para gambiarras do inferno. (Claro que "dá" para viver sem essas coisas. Também "dá" para programar com máquinas de Turing...)

lash quebra com a tradição, se revolta contra o sistema e introduz arrays, dicionários e blocos de primeira classe (bem como possivelmente outros objetos, como canais de comunicação, mas isso ainda está em aberto). Assim, é possível fazer coisas futurísticas como manter uma coleção de dados estruturados e escrever funções para manipular arrays e produzir outros arrays. Fantástico, não? Welcome to 2015.

Independentemente do shell, variáveis de ambiente e argumentos de processos no Unix também são strings (e strings que não podem conter \0, ainda por cima), o que significa que não temos como passar diretamente nossos valores estruturados para outros processos. Uma abordagem alternativa seria fazer como Tcl: representar tudo como strings, definir certos formatos de string para armazenamento de dados estruturados (e.g., keyed lists, ou XML if you're feeling crazy), e prover funções para interpretar e manipular tais strings. Isso permitiria passar dados "estruturados" para subprocessos, pois eles seriam apenas strings. Mas, seriously, guardar tudo como string e parsear/procurar dentro da string para obter um elemento de uma lista/dicionário? Gerar uma string nova toda vez que se altera um elemento? Tá certo que seria possível mitigar um pouco esses problemas usando alguma representação interna mágica para strings, mas sei lá. Por ora eu prefiro ter dados estruturados normais.5 Além disso, blocos têm que ser dados especiais de qualquer forma, para carregar informação de escopo.

So, tipos de dados.

Strings e números

Uma string em lash é uma seqüência de bytes; internamente, o shell não está preocupado com a interpretação desses bytes (como caracteres codificados em UTF-8, por exemplo). No geral, o ambiente Unix como um todo não está preocupado com o conceito de codificação; nada exige que nomes de arquivo sejam strings UTF-8 válidas, por exemplo, e o resultado de um globbing deveria ser representável por strings do shell sem nenhum mistério. Arquivos/streams também não tem nenhuma codificação inerente, e coisas como echo $str não deveriam ter que fazer nada de mágico para decidir como mandar o conteúdo da string para o arquivo. Interpretar os bytes de uma string como UTF-8 (ou outro encoding) é responsabilidade das funções que o shell provê para manipular strings.

Acho que em um shell não faz muito sentido ter um tipo numérico distinto. Em um shell, quando se escreve algo como my x = 01, espera-se que o 0 permaneça lá; quando se chama xargs -0, espera-se que o - não se perca, etc. Além disso, os argumentos que o script recebe da linha de comando são todos strings, e não me parece interessante ter que convertê-los manualmente para números antes de fazer operações aritméticas com eles. Ao invés disso, a interpretação de uma string como um número cabe aos operadores aritméticos. Por questão de eficiência, o resultado de uma operação aritmética pode ser armazenado internamente como um número (a idéia é evitar ter que converter o resultado para string e reconverter para número caso ele seja usado novamente em uma operação aritmética), mas isso não é observável pelo script.

Diferentemente do (ba)sh, o lash deverá suportar aritmética de ponto flutuante. Isso levanta a questão de como distinguir divisão inteira de divisão em ponto flutuante. Eu sou favorável a adotar / para divisão em ponto flutuante e // para divisão inteira, a la Python 3. Os demais operadores aritméticos produzem resultado em ponto flutuante se um dos argumentos for float, e inteiro caso contrário. A representação em string de um número em ponto flutuante sempre inclui um ponto1 (a idéia é que se alguma coisa estiver produzindo resultados float indevidamente, isso não vai passar silenciosamente durante a execução (ou assim se espera)). Operações aritméticas sobre strings que não são números válidos produzem um erro de execução, i.e., nada de NaN propagation a la JavaScript ou interpretação implícita como 0 a la PHP. Na verdade nem o bash deixa esse tipo de coisa passar em silêncio... com algumas exceções: uma string vazia é tratada como um 0, e espaços em torno de um número são ignorados. Aqui fico na dúvida entre "strictness" e conveniência; talvez em um script seja uma boa aceitar esses dois casos.

Strings não são arrays, e (assim como em bash) não são indexáveis com a sintaxe normal de arrays. Haverá funções para obter substrings, mas ainda não pensei bem nos nomes e na sintaxe, e em como especificar o range de bytes/caracteres desejado (início e tamanho? início e fim? inclusivo ou exclusivo? Todas as opções, dependendo dos parâmetros?). Uma possibilidade seria:

Pode ser meio verboso, mas captura de substring parece ser uma coisa relativamente rara em bash, baseado em um grep na minha amostra extremamente significativa de meia dúzia de scripts que estavam à mão, então acho que a clareza e a flexibilidade compensam a verbosidade.

O tamanho da string pode ser obtido com as funções bytelen e charlen, dependendo do tipo de tamanho desejado. (Há ainda a situação em que se quer a largura impressa da string (combining characters não contam no comprimento, e caracteres chineses-et-al ocupam duas posições), bem como substrings baseadas na posição impressa dos caracteres, mas isso vai ficar para o futuro distante, possivelmente numa biblioteca.)

Funções que trabalham com delimitadores (e.g., split STRING DELIM) têm que aceitar delimitadores de tamanho arbitrário, pelo simples fato de que elas têm que funcionar com delimitadores em UTF-8 e ao mesmo tempo se manterem agnósticas quanto à codificação. (Por outro lado, isso assume que a codificação tem a mesma propriedade do UTF-8, de que é possível identificar o começo de um caractere inambiguamente a partir de um ponto arbitrário na stream, o que basicamente só é verdade no UTF-8 e em encodings em que 1 byte = 1 caractere. Meh.)

Arrays

Arrays são seqüências de valores quaisquer. A sintaxe literal para arrays é (valor1 ... valorN). (Os parênteses são herdados da sintaxe de inicialização de variáveis-array do bash. Além disso, colchetes e chaves já têm outros usos. Isso a princípio conflita com a sintaxe do (ba)sh para rodar um comando em um subprocesso4 (( comandos )), mas eu já não pretendia ter essa sintaxe em lash to begin with. Uma função poderia prover essa funcionalidade (e.g., subproc { comandos }).)

Arrays são indexados com a sintaxe $var[expr]. Assim como em bash, expr é avaliado como uma expressão aritmética, sem necessidade de escrever $var[$((expr))]. Diferentemente de bash, chaves não são exigidas, i.e., não é necessário escrever ${var[expr]}. Por um lado isso é mais limpo, mas por outro pode conflitar com o uso de [] como wildcard, e.g., my prefix = /dev/tty; echo $prefix[1-8]. Acho que isso não chega a ser um grande problema, pois isso gera um erro de execução ($prefix não é um array), e portanto é fácil de detectar e corrigir (para ${prefix}[1-8]; dá até para incluir essa informação na mensagem de erro).

Assim como em bash, o array tem que estar em uma variável para ser indexado ($[função][expr] não seria interpretado como uma indexação do resultado de função, a princípio (ou seria?)), mas nada impede que haja uma função index ARRAY N, com a qual se poderia escrever $[index $[função] N].

A sintaxe de atribuição funciona com arrays também (var[i] = 42). Isso implica que atribuição tem que ter tratamento sintático especial, para que coisas como var[i*i] = 42 não causem globbing.

Como fica o caso de arrays multidimensionais (i.e., arrays que contêm outros arrays)? $var[i][j] é uma sintaxe válida? Se sim, não tem por que não aceitar $[função][expr] também, acho.

É possível atribuir a uma posição que ainda não existe (a la Perl), ou isso é um erro (a la Python)? Se a "label" do índice é importante (e não apenas a ordem), não seria o caso de usar um dicionário anyway? Eu consigo pensar em duas situações em que se poderia querer especificar um índice não-existente explicitamente:

  1. Adicionar um elemento no fim do array. Mas para esse caso poderia haver uma função push (ou append, porque aí também podemos ter uma prepend para adicionar no começo; ou poderia haver uma função mais geral insert, para inserir um elemento entre dois quaisquer, ou no início/fim), ou uma sintaxe a la PHP (var[] = 42).
  2. Inicializar um vetor/matriz com alguma fórmula matemática, e.g.:
    my array = ()
    range 0 -toin 10 {|i|
        array[i] = $(( i * i ))
    }
    

    Parece um caso de uso razoável, mas de qualquer forma ele falha com arrays multidimensionais ($array[i][j] = 42 é um erro porque $array[i] não é um array, a menos que seja inicializado primeiro). Pode-se suprir esse caso com uma função make_matrix que recebe o tamanho das dimensões e retorna um vetor inicializado.

Ou podemos permitir atribuição out-of-bounds (e preencher qualquer elemento entre a última posição preenchida e a posição atribuída com a string vazia) e era isso. Não sei (o plano inicial é não permitir).

Outra função básica de manipulação de arrays é each, que recebe um array e um bloco e chama o bloco com cada elemento do array. Também pode haver uma map, que produz um novo array com cada resultado retornado pelo bloco, e uma versão destrutiva de map (chamada map!, talvez2).

A função len retorna o número de elementos do array. Não sei se há necessidade de uma sintaxe especial para isso (e.g., $#var).

$@var "splices" o array, produzindo um argumento ("word" na terminologia do (ba)sh) para cada elemento do array, i.e.:

my array = (1 2 3)
foo $array         # chama foo com um argumento (o array)
foo $@array        # chama foo com três argumentos (1, 2 e 3)

Dicionários

Um dicionário é um mapeamento de strings para valores. (Por que só strings? Talvez faça sentido permitir valores quaisquer como chave.) A sintaxe literal para dicionários é %(chave1=valor1 chave2=valor2 ...) (o % é para sugerir uma vaga relação com hash-tables em Perl), com espaços opcionais em torno do =, o que fica meio estranho sem delimitadores entre os pares chave = valor, mas pode-se usar quebras de linha se desejado:

my person = %(
    name = Hildur
    age = 18
    country = Iceland
)

[Note to self: Em coisas como %(foo=(1 2 3)), assim como em my foo=(1 2 3), foo=(1 2 3) não é uma "palavra" normal do shell, porque é parte string, parte array, i.e., tanto dicionários literais quanto declaração de variável exigem tratamento especial pelo parser (a menos que haja um tipo de dados "associação" ao qual coisas da forma A=B possam ser mapeadas).]

Elementos de um dicionário são acessados com a sintaxe $var{chave}. Não se usa colchetes como em arrays porque a expressão entre colchetes sofre avaliação aritmética, que não é o que queremos em um dicionário. (Será que foi uma boa idéia fazer avaliação aritmética automática after all?) Isso é outro elemento de sintaxe (além dos blocos) que conflita com a sintaxe de brace expansion do bash (foo{1,2,3}). Não sei se isso é um ponto a favor da mudança da sintaxe de acesso a dicionário ou do brace expansion. Outra possibilidade seria usar colchetes, assim como arrays (e aí eles perdem a propriedade de avaliação aritmética, o que pode tornar o acesso a array meio inconveniente), ou talvez $var<chave>, mas isso conflita com a sintaxe de redirecionamento. (Lembrando que isso poderia ser um redirecionamento se $var contivesse um file descriptor. Nesse caso o > posterior seria um erro de sintaxe, então só a interpretação como acesso a dicionário seria válida, mas eu só descubro isso quando chego no >; além disso a chave não poderia ter um espaço não-escapado. Fora que é uma sintaxe totalmente não-usual para acesso a dicionário (as chaves pelo menos têm precedente em Perl).)

Se my dict = %(a=1 b=2 c=3), qual o resultado de $@dict?

Haveria uma porção de funções para iterar sobre dicionários: each-key; each-value; each-entry, que reberia um dicionário e um bloco de dois argumentos e o chamaria com a chave e o valor de cada entrada no dicionário; ou, havendo o tipo associação, chamaria o bloco com cada associação. Alternativamente, havendo o pipeline de objetos, poderia haver uma função keys que produz todas as chaves, e aí escreveríamos keys $dict |> each {|key| ... } (ou qualquer que seja a sintaxe do pipe de objetos), e da mesma forma para os valores (e associações, em as havendo).

Será que é uma boa ter um tipo dicionário distinto de array, ou o melhor é unificar os dois a la PHP, JavaScript, etc.? Acho que eu prefiro ter dois tipos separados, mas há de se pensar melhor.

Interações entre valores estruturados e strings

Em (ba)sh, diferentemente das linguagens de programação em geral, uma variável pode aparecer como parte de uma "palavra" maior, e.g., foo$bar; o conteúdo da string é concatenado na palavra e era isso. Mas e se $bar não for uma string? Pode-se produzir uma versão serializada do valor (o que provavelmente é mais útil), ou gerar um erro.

Coisas como foo$@bar (onde my bar = (1 2 3)) poderiam expandir para foo1 foo2 foo3, como o brace expansion do bash. O problema é que $@ assume que o array está em uma variável. Daria para expandir arrays literais também3, e,g., foo(1 2 3) geraria foo1 foo2 foo3, e aí seria possível eliminar o uso de chaves para brace expansion. O problema é que by far o meu uso mais freqüente de brace expansion na linha de comando é com a string vazia, e.g., mv file{,~} ao invés de mv file file~, e na nova sintaxe isso seria mv file("" ~) (na verdade o ~ teria que ser escapado para não sofrer tilde expansion...). Talvez dê para sobreviver.

^D

Por hoje ficamos por aqui. Como sempre, tudo o que foi apresentado são só os planos e idéias atuais, tudo pode ser mudado, e comentários e sugestões são muito bem-vindos (mas provavelmente só vou ver/responder comentários depois do fim-de-semana).

_____

1 Ou talvez um e+42 da vida (talvez só como formato de entrada válido, mesmo que as operações do shell sempre produzam resultados em notação decimal).

2 (update) Ou adicionar uma opção -overwrite à função map (que parece uma coisa mais shell-like); ou ainda, adicionar opções -collect e -overwrite à each e nem ter uma map separada.

3 (update) Note' to self: Isso também é uma string misturada com um array, então o my x=(1 2 3) não é mais um caso especial para o parser (ou pelo menos para o "reader", porque ainda teria uma interpretação diferente do caso foo(1 2 3)).

4 (update) Na verdade não conflita, porque um array não faz sentido como primeira coisa na linha de comando (ou faz?).

5 (update) Parafraseando um grande sábio, "If you want Tcl, you know where to find it." (Dito isso, eu vejo mérito na abordagem "everything is a string".)

5 comentários

Blueprints for a shell, parte 2: Variáveis, definições e escopo

2015-03-13 00:11 -0300. Tags: comp, prog, pldesign, shell, lash

Este post é parte de uma série sobre o lash, um shell que eu estou ideando.

Um pouco de contexto

Em (ba)sh todas as variáveis são globais (inclusive as "locais", que são globais com escopo dinâmico). Independentemente das variáveis do shell, todo processo no Unix possui um conjunto de variáveis de ambiente (environment variables). Os shells tendem a unificar variável do shell e de ambiente de alguma forma. A maneira como isso é feito em (ba)sh é tratar todas as variáveis uniformemente como "do shell" e marcar certas variáveis como "exported": essas variáveis são passadas como variáveis de ambiente para os processos chamados pelo shell. Além disso, o bash possui um comando local, que faz com que os valores atribuídos às variáveis passadas ao local a partir desse ponto só durem até a função onde o local foi chamado retornar, i.e., o local permite "shadowar" uma variável durante a execução de uma função. Funções chamadas pela função que declarou a variável "local" também vêem o novo valor, e nada impede "localizar" uma variável de ambiente (que continua sendo uma variável de ambiente).

Nessa situação, determinar a que variável o código está se referindo ao dizer $x é uma questão bastante simples: só existe uma variável x no programa inteiro. Evitar conflitos de nomes é basicamente problema do programador.

Se isso já é um problema em bash, em um shell com lambdas isso seria um disastre, pois um bloco de código pode ser chamado dentro de uma função diferente da que o definiu, e quem escreve o bloco não necessariamente tem como saber (nem deveria ter que saber) os nomes das variáveis usadas nesse outro ponto do programa. Assim, lash adota escopo léxico, como qualquer linguagem sã, o que significa que pode haver múltiplas variáveis com o mesmo nome em um programa. Isso também implica que nós vamos ter que conciliar escopo léxico com variáveis de ambiente.

So, variáveis em lash

O comando my introduz variáveis léxicas, cujo escopo é o bloco onde o my se encontra. A sintaxe básica é:

my nome = valor

Eu estou meio na dúvida quanto ao uso de espaços em torno do =. Em bash, atribuição de variável não permite espaços. Não havendo espaços, seria possível definir múltiplas variáveis no mesmo comando:

my x=1 y=2 z=3

Com espaços, para a coisa continuar legível, acho que seria necessário introduzir um delimitador entre as atribuições, mas isso não é tão simples em um shell, porque em:

 my x=1, y=2, z=3

a vírgula poderia ser parte da string que se está atribuindo. Uma alternativa é permitir declarar uma única variável com espaços, ou múltiplas variáveis sem espaços. A sintaxe não é ambígua, de qualquer forma.

Pergunta: uma definição com my x=1 afeta referências a x no mesmo bloco que apareçam antes do my? Por exemplo, em:

my x = 1
while {true} {
    echo $x
    my x = 2
    echo $x
}

que x é visto pelo primeiro echo quando o while executar pela segunda vez? Ou, de maneira mais convoluta:

my x = 1
my block = {
    my f = { echo $x; }
    my x = 2
    $f
}

imprime o valor de qual x? Se o desejado for o 1, então a implementação de variable lookup tem que tomar o cuidado de não simplesmente pegar o primeiro x subindo na hierarquia de ambientes (a princípio o bloco interno procuraria a variável x primeiro no ambiente do próprio bloco, depois no bloco em que o bloco se encontra, depois fora dos blocos). Por outro lado, essa semântica em que a referência a uma variável nunca muda, independente de declarações posteriores, permitiria resolver tudo estaticamente, o que pode deixar o lookup com uma performance melhor. Outra questão é: esse tipo de coisa acontece na prática? Eu fico seriamente tentado a dizer que é indefinido nesses casos qual das duas variáveis é acessada. Provavelmente alguém vai querer comer meu fígado por introduzir comportamento indefinido em um shell, mas eu não estou propondo nada da natureza de comportamento indefinido em C, em que o programa pode fazer qualquer coisa, incluindo roubar seu dinheiro e fugir do país; certamente uma das duas variáveis é acessada, sem nenhum efeito inesperado. A idéia é apenas manter em aberto a possibilidade de diferentes implementações de lookup de variáveis. Se você acha que isso é uma má idéia, por favor se manifeste.

Atribuição

Estou na dúvida se atribuição vai usar uma keyword do tipo set, ou se só o sinal de igual vai ser suficiente. Parece concebível que alguém invente um comando que recebe = como argumento, então:

foo = 42

poderia ser uma chamada a foo. Esse problema poderia ser evitado exigindo set foo = 42, ou proibindo os espaços em volta do = (que é o que o (ba)sh faz), mas o espaço me parece bem desejável quando o valor atribuido é uma expressão maior com chamadas a funções e what-not, ou quando o lado esquerdo é um array[índice]. Por outro lado, não lembro de nenhum comando que recebe = como primeiro argumento, então talvez tratar um = não escapado/quoted na segunda posição como algo especial e dispensar o set não seja problema. Será?

Também há de se considerar a possibilidade de introduzir outros operadores de atribuição, como +=, e nesse caso, se haverá operadores separados para strings, números e arrays ou se um só basta. (Em bash, += appenda strings e arrays; olhando o lado direito da atribuição dá para saber qual é o caso. Para incrementar variáveis numéricas, é necessário estar em "modo de expressão aritmética", i.e., dentro de ((...)), $((...)), índice de array, etc.)

O que acontece ao se atribuir um valor a uma variável não declarada? Acho que isso seria no mínimo um warning, talvez um erro. Acessar uma variável não-definida também, mas seria bom ter alguma coisa equivalente ao ${var:-default}, i.e., "usa o valor de $var, ou a string default caso var não esteja definida (ou seja vazia, se o : estiver presente)". Eu tinha pensado em ter uma função or valor1 valor2, que devolve valor1 se ele for um valor diferente da string vazia (ou um valor nulo especial? nós teremos um?), ou valor2 caso contrário. O problema é que $[or $var default] vai emitir um warning se $var não estiver definida. Talvez pudesse haver uma sintaxe especial $?var que devolve o valor da variável ou vazio caso ela não exista, sem emitir um warning, e então o equivalente do ${var:-default} seria $[or $?var default]. Meio verboso, mas não parece ruim (eu acho).

Variáveis globais

Nós teremos um sistema de módulos (cujos detalhes eu ainda não pensei direito e que será assunto de um post futuro), e concebivelmente um módulo poderá querer tornar algumas variáveis visíveis a outros módulos. Possibilidades:

Separar variáveis públicas das demais parece uma boa, mas não sei se não é "só uma coisa a mais".

Funções

Funções e variáveis vivem em namespaces separados em (ba)sh, e a princípio isso deve ser mantido em lash. Em (ba)sh, todas as definições de função possuem escopo global (na verdade tudo tem escopo global em (ba)sh). Como já comentado anteriormente, embora possa parecer "óbvio" mudar isso em lash e tornar as definições de função léxicas, assim como as variáveis, código como:

if {some-condition} {
    def foo {
        ...
    }
}

em que se espera que a definição de foo resultante seja global, é comum em arquivos de configuração e afins. Possibilidades:

  1. def define funções globais, i.e., no escopo do módulo em que a definição foi feita. (No escopo léxico, ou no escopo dinâmico? Se um bloco que contém um def é passado como argumento e chamado em uma função definida em outro módulo, em que módulo o def tem efeito? Bom, a julgar pelo if, no módulo em que o def se encontra, i.e., no escopo léxico.) Não há definições locais de função e era isso.
  2. def define funções globais, mas é possível escrever algo como my def foo { ... } para definir uma função local. Pode ser uma boa, só não sei se vale a pena o esforço. Também teria algum efeito no lookup de funções/comandos que precisa ser melhor considerado.
  3. def define funções no escopo léxico local. Bagunça com o caso do def dentro de um if, mas isso poderia ser contornado permitindo algo como public def foo { ... } dentro do if. (Mas quem disse que eu queria exportar do módulo? Também poderia ser usada uma keyword diferente (e.g., global), que torna global mas não exporta do módulo.)

No momento eu estou inclinado à alternativa (1), mas aceito contra-argumentos.

Funções definidas em um módulo são visíveis a partir de outros módulos por default, ou é necessário dizer public def foo { ... } para exportar uma função? (Lembrando que a gente nem decidiu ainda se vai ter uma keyword public ou não na linguagem...)

Variáveis de ambiente

O escopo de uma variável de ambiente a princípio é o processo inteiro. (É possível conceber que cada módulo pudesse ter sua própria idéia de ambiente, mas acho que nunca antes na história desse país uma linguagem tratou variáveis de ambiente assim.) Em um shell, espera-se acessar variáveis de ambiente com a mesma sintaxe das variáveis comuns (acho inventar uma sintaxe nova para dizer $HOME não vai ser uma proposta popular). Outra peculiaridade das variáveis de ambiente é que seus valores só podem ser strings. Seria possível serializar outros valores para permitir passá-los como variáveis de ambiente para subprocessos, mas só o lash reconheceria essas variáveis como valores especiais, e seria necessário indicar de alguma maneira reliable que a variável contém um valor especial, e não uma string que parece muito com um valor especial. Depois do causo do ano passado com o Shellshock, eu estou meio receoso de permitir coisas que não sejam strings em variáveis de ambiente.

Em bash uma conseqüência não muito agradável de o shell misturar as variáveis de ambiente com as comuns é que é possível um script começar a usar uma variável feliz da vida sem saber que havia uma variável de ambiente com o mesmo nome. Isso é agravado pelo fato de que em bash uma variável inexistente pode ser usada sem warning nem erro (a menos que set -u esteja ativo), então um script pode ser escrito assumindo que uma dada variável está vazia e inadvertidamente herdar do ambiente uma variável com conteúdo. Mesmo que esse não seja o caso e o script inicialize suas variáveis antes de usar, ele ainda pode estar inadvertidamente alterando uma variável de ambiente, que será herdada por subprocessos.

Em lash a situação a princípio é menos problemática porque toda variável tem que ser declarada antes de usar, e um my sobrepõe uma variável de ambiente de mesmo nome. Em geral, se eu esquecer de declarar a variável, o shell emitirá um erro, então um script que roda sem erros para mim pelo menos está imune a variáveis de ambiente inesperadas presentes nos sistemas dos outros, mas eu ainda posso acabar esquecendo o my sem gerar erro se der o acaso de eu usar um nome de variável que é uma variável de ambiente presente no meu sistema. Soluções:

  1. Exigir que toda variável de ambiente usada seja explicitamente importada antes do uso. Acho que isso não seria uma opção muito popular. Talvez não fosse tão ruim se algumas variáveis mais tradicionais fossem importadas por default (e.g., HOME, USER), mas isso me parece super-arbitrário.
  2. Permitir o acesso a variáveis de ambiente como qualquer outra variável, mas permitir atribuição apenas com um comando especial (e.g., setenv HOME = /). Acho que isso pega como erro a grande maioria das capturas indevidas de variáveis de ambiente. Fica o caso de se o programador erra o nome da variável de ambiente (uma nova variável seria criada, ao invés de emitir um erro). Evitar esse problema acho que traria mais inconveniente do que vantagem.
  3. Não fazer nada. Na real isso mal é uma opção, já que o setenv tem que existir de qualquer forma para criar variáveis de ambiente novas, e uma vez que ele exista não tem por que não aplicar a solução (2).

So (2) it is, aparentemente.

Escopo dinâmico

E quando eu quero escopo dinâmico, after all? Pode-se argumentar que ninguém em sã consciência quer escopo dinâmico, mas, por exemplo, se formos implementar o tal pipeline de objetos, precisamos de um meio de redirecionar o canal de saída de um comando para o canal de entrada de outro, e uma maneira de fazer isso é ter os canais de entrada e saída como variáveis dinâmicas e shadowá-las para fazer o redirecionamento; é como normalmente se redireciona *standard-output* e companhia em Common Lisp, e (current-output-port) et al. nos Schemes que suportam "fluid variables" (que são variáveis dinâmicas com outro nome).

Se formos ter variáveis dinâmicas, para evitar o caos manifesto, parece uma boa exigir que elas sejam previamente declaradas como tal (i.e., não é possível "localizar" a la bash uma variável previamente declarada com my). Também há o problema de como implementar o escopo dinâmico. Na situação em que só há uma thread, a operação de shadowar uma variável pode ser implementada simplesmente salvando o valor antigo, atribuindo o valor novo, e depois restaurando o valor antigo. Quando há múltiplas threads, entretanto, deseja-se que um shadow dentro de uma thread não afete as outras. E guess what? O nosso pipeline de objetos exige que cada parte do pipeline rode simultaneamente (ou pelo menos cooperativamente), dentro do mesmo processo, e o que cada uma vê como canal de entrada e de saída é diferente, então essa implementação "ingênua" de shadowing não nos serve.

Eu tenho um certo receio de que, a menos que as variáveis dinâmicas sejam identificáveis estaticamente, a presença delas bagunce / afete a performance do lookup de todas as variáveis. Quando a definição da variável dinâmica está lexicamente visível é fácil distingui-las, mas quando elas vêm de outro módulo, isso pode ser complicado. Uma solução é simplesmente usar uma sintaxe diferente para acessar variáveis dinâmicas, e.g., earmuffs: $*output_channel*. Essa sintaxe tem a vantagem de ser imediatamente familiar ao grande contingente de programadores de Common Lisp (right?), e a desvantagem da potencial confusão com o * que faz globbing (e.g.:

dynamic *prefix* = foo
touch foo1 foo2 foo3
echo $*prefix**

), mas outra sintaxe que distinguisse variáveis dinâmicas de variáveis comuns poderia ser escolhida.

Acho que por hoje deu

Reiterando, sempre que eu digo que alguma coisa em lash "é" de tal e tal jeito, eu só quero dizer que esse é o plano atual, mas estou aberto a sugestões. Feedback é sempre bem-vindo.

8 comentários

Blueprints for a shell, parte 1: Funções, blocos e retorno

2015-03-11 23:15 -0300. Tags: comp, prog, pldesign, shell, lash

Este post é parte de uma série sobre o lash, um shell que eu estou ideando.

Hoje vamos discutir a feature que dá nome ao shell, lambdas, ou blocos. (Na verdade eu pensei no nome primeiro e fiquei com ele porque consegui pensar num significado que o justificasse, mas não vamos nos ater a esses detalhes.)

(Em diversos pontos ao longo do texto eu vou dizer que certa feature em lash "é" de tal e tal jeito. Isso só significa que essa é a minha idéia atual sobre a feature, não que eu tenha decidido definitivamente que isso vai ser assim. Comentários e sugestões são sempre bem-vindos.)

Como mencionado anteriormente, a idéia em lash é usar blocos extensivamente ao invés de sintaxe especial para estruturas de controle (if, for, while, etc.). Blocos em lash são valores de primeira classe, i.e., podem ser passados como argumento para funções, por exemplo. Um bloco instanciado é uma closure, i.e., ele lembra do ambiente de variáveis em que foi criado. No geral, variáveis em lash têm escopo léxico, e não escopo dinâmico como em (ba)sh. (A coisa não é tão simples por conta de variáveis de ambiente e outros detalhes, mas discutiremos isso no futuro.)

Blocos são escritos entre chaves ({ comandos }). Blocos podem receber parâmetros, que podem ser declarados com uma sintaxe Ruby-like: {|param1 param2 ... paramN| comandos }. O último parâmetro pode ser precedido de @; nesse caso, ele coleta em um array os argumentos restantes da chamada ao bloco.

Não sei se permitir $1, $2, etc., para acessar os argumentos de um bloco é uma boa idéia; como tudo é bloco em lash, acho que isso ia dar muita confusão ao tentar acessar um argumento de função de dentro de um if e situações similares. Melhor é requerer que os parâmetros sejam declarados. ($1 e companhia talvez possam adquirir outros usos, e.g., em matching de expressões regulares, mas esse é um tópico que eu não vou abordar any time soon.)

Now the thorny questions.

Arity mismatch

O que acontece se o número de parâmetros e de argumentos não casar? No geral o ideal é gerar um erro de execução ou um warning, mas eu me pergunto se não há situações em que pode ser interessante permitir passar um bloco sem parâmetros para uma função que chama o bloco com alguns argumentos, nos quais o bloco não tem interesse. (Por exemplo, o if poderia chamar o bloco do "then" com o resultado retornado pelo teste do if, no qual não temos interesse a maior parte do tempo.) Uma possibilidade seria não permitir mismatch, exceto no caso em que o bloco não tem declaração de parâmetros at all, i.e., {|| true; } 42 é um erro, mas { true; } 42 não é. Mas eu imagino que isso possa fazer funções declaradas sem parâmetros engolirem silenciosamente argumentos passados por engano. Por ora, acho que mismatch vai ser sempre um erro/warning mesmo, enquanto não aparecer um caso de uso que definitivamente sugira que o contrário é desejável.

Retorno

Quando eu digo return 42, quem retorna? O comportamento esperado é retornar da função em que o return se encontra, mas agora o corpo de um if ou foreach tecnicamente também é uma função, que provavelmente não é a função que o usuário tem em mente ao escrever um return.

Se o return retorna da função "esperada", também há o caso em que um bloco que contém um return é passado para uma função definida pelo usuário e chamada de dentro dessa função; nesse caso o return é um non-local exit, i.e., a função que retorna é a função onde o bloco foi definido, não a função que chamou o bloco. (Na verdade o caso do return dentro do if também é um non-local exit, mas é um caso com o qual nós já estamos acostumados.) Outros casos de controle de fluxo não-local são os comandos break e continue dentro de um while. Talvez fosse interessante introduzir uma construção mais geral a partir da qual esses casos mais específicos podem ser implementados, e que também poderia ser usada para implementar exceções. Ao mesmo tempo, eu gostaria que um return fosse uma operação "barata", então é necessário tomar algum cuidado antes de sair over-engineerando controle de fluxo. A construção que naturalmente "suggests itself" para a tarefa é continuations e call/cc, mas esse caminho me dá um certa preocupação, especialmente se continuações que retornam múltiplas vezes forem permitidas. (Incidentalmente, eu pretendo implementar as versões iniciais do shell em Chicken Scheme, o que tornaria tudo isso muito simples, mas eu quero manter aberta a possibilidade de reimplementar em alguma outra linguagem no futuro (e.g., Rust, depois que ele sair de alpha).) Além disso, seria necessário lidar com unwind-protect / dynamic-wind / interação de tratadores de exceção com continuations. Eu não estou gostando muito de toda essa complexidade que surgiu do nada enquanto eu estava tranqüilo aqui inventando meu shell.

Outra dificuldade é como fazer o return, que a princípio seria um comando como qualquer outro, retornar do bloco lexicamente apropriado, já que ele não recebe como argumento nada que lhe sirva para saber de que escopo léxico ele foi chamado. Ele não pode só retornar do contexto mais no topo da pilha de chamadas porque o return pode ser não-local. Por exemplo, em um código como:

def foo {
    bar {|x| return $x}
}

def bar {|block|
    $block 42
}

o return que será executado quando $block for invocado deve retornar de foo, não de bar. Uma solução é fazer todo comando receber implicitamente um argumento escondido que representa o escopo em que o comando foi chamado. That's kinda weird (e me lembra o &environment das macros do Common Lisp e o "dynamic environment argument" em Kernel), mas pode funcionar. Outra solução é fazer def (o comando de definição de função) introduzir uma função local return no escopo do corpo da função, i.e., cada função vê um return diferente, mas a princípio eu não pretendia nem introduzir funções nomeadas locais (more on that later).

Também dá para simplesmente tratar def, return e companhia como special operators e era isso. Eu não queria introduzir nenhum special operator na linguagem, mas talvez isso não seja muito prático. Preciso pensar melhor sobre isso. (No fim das contas, return, break e continue trabalham com escopo léxico, enquanto exceções e unwind-protect trabalham com escopo dinâmico, então a "óbvia" unificação dos conceitos não é tão direta assim.)

Funções locais

A princípio o filosoficamente correto seria que definições de função tivessem escopo léxico, assim como as variáveis. Porém, me parece que coisas do tipo:

if {whatever} {
    def foo {
        ...
    }
}

que define uma função global ou não dependendo de uma condição, são comuns em scripts e bashrcs da vida. Daria para introduzir comandos separados para definir funções locais e globais, mas realmente não vejo muita utilidade para funções locais (além de blocos anônimos) em um shell. Se você discorda, por favor se manifeste.

(Por um lado dá para argumentar que se você realmente precisar de uma função local, pode declarar uma variável local e atribuir um bloco a ela. Por outro lado, há a diferença de que o return dentro de um bloco retorna da função externa, não do bloco. Essa questão do return não vai deixar de me assombrar tão cedo.)

Sintaxe

O uso de chaves para delimitar funções conflita com o uso de chaves em bash, que expande coisas como touch {1,2,3}.txt para touch 1.txt 2.txt 3.txt, bem como coisas como {01..99} para 01 02 ... 99. Uma solução para evitar a ambiguidade é, ao encontrar um {, continuar lendo até o primeiro espaço ou }, e se houver uma , ou .. não-escapado na string lida, considerar como um brace expansion, caso contrário como um bloco. Eu detesto esses look-aheads em parsing, mas talvez seja o caminho a seguir. (O próprio bash já faz alguma distinção contextual com relação às chaves, tratando chaves em comandos como cmd arg1 {arg2 arg3 como caracteres literais, mas em bash o parsing se dá em múltiplos passos, em que primeiro ocorre word splitting e depois brace expansion, o que torna esse tipo de coisa relativamente simples. No caso de blocos, não dá para realizar word splitting primeiro porque o bloco é mais do que só uma seqüência de "words" comuns.) Outra solução é mudar a sintaxe do brace expansion, que sequer é parte do sh to begin with (é uma extensão do bash). Discutiremos alternativas quando falarmos de arrays, em um post futuro.

Returning and replying

Comandos no Unix possuem duas formas primárias de retornar informação para o chamador:

Queremos um mecanismo que permita retornar quaisquer valores, inclusive dados estruturados como listas e blocos. Eu vejo algumas possibilidades:

  1. Estender o conceito de exit status para permitir quaisquer valores, não apenas inteiros entre 0 e 255. O problema com essa abordagem é conciliá-la com o conceito de verdadeiro e falso convencional do (ba)sh: quando meu valor de retorno é um dado arbitrário, eu provavelmente quero que a maioria dos valores sejam tratados como verdadeiro, e coisas como 0, a string vazia, a lista vazia, etc., sejam tratados como falso.
  2. Estender o conceito de stdout para permitir enviar valores arbitrários, não apenas bytes. Isso é uma idéia muito legal, e abre caminho para a implementação de um "pipeline de objetos", mas envolveria uma certa mandinga para tratar a stdout comum do Unix e a stdout de objetos transparentemente. Também tem a vantagem de que se a saída não é capturada, ela é impressa para o terminal, o que faz sentido em modo interativo. Por outro lado, provavelmente muitas vezes queremos rodar um comando apenas pelos side-effects e descartar a saída, e ficar redirecionando para /dev/null every now and then pode ser inconveniente (embora seja possível inventar uma sintaxe abreviada para isso). Além disso, isso impede que uma função cujo valor de retorno esteja sendo capturado possa imprimir normalmente para a stdout.
  3. Criar um novo mecanismo de retorno independente dos dois anteriores. Essa é a solução mais straightforward, e por enquanto é a minha working hypothesis, mas tem a desvantagem de criar um conceito extra. Para diferenciar o retorno de um valor do retorno de um exit status convencional, eu adotei a palavra reply ao invés de return (que continua existindo com seu significado convencional).

A sintaxe para chamar uma função e capturar o valor de retorno por enquanto é $[comando], pelo simples fato de que ela não está sendo usada para mais nada (em bash ela é uma sintaxe deprecated para avaliação aritmética, que hoje em dia se escreve $((expressão))), e, pode-se argumentar, porque lembra a função dos colchetes em Tcl. Eu me pergunto se ${comando} não seria uma escolha melhor, pois tem mais cara de "executa este bloco e pega o valor", mas essa sintaxe é usada em (ba)sh para delimitar nomes de variável (e.g., echo "Eu sabia essa com ${fruta}s), e não sei se é uma boa mudar isso.

Uma questão é se o reply de fato retorna da função, ou só "emite" o valor de retorno. Se o mecanismo de retorno escolhido for o (1) ou o (3), faz mais sentido retornar e sair da função, mas se a escolha for o (2), faz mais sentido emitir o valor, como se fosse um print, e seguir a execução, até porque seria possível imprimir múltiplos valores, no caso do pipeline de objetos (e aí fica a questão de como $[...] se comporta se o comando emite múltiplos valores).

Awey?

Por hoje ficamos por aqui. Como sempre, feedback é muito bem-vindo.

11 comentários

Main menu

Posts recentes

Tags

comp (92) prog (37) life (36) unix (30) random (26) lang (23) mundane (21) about (19) mind (16) pldesign (14) img (13) web (13) rant (12) ramble (11) privacy (8) bash (7) lash (7) esperanto (7) lisp (6) home (6) shell (6) academia (6) misc (5) freedom (5) conlang (5) latex (4) copyright (4) worldly (4) book (4) mestrado (4) editor (4) php (4) music (4) kbd (3) film (3) wrong (3) politics (3) security (3) physics (2) poem (2) android (2) cook (2) network (2) c (2) emacs (2) comic (2) lows (2) treta (2) pointless (1) audio (1) philosophy (1) kindle (1) in-english (1) llvm (1) perl (1)

Elsewhere

Quod vide


Copyright © 2010-2015 Vítor Bujés Ubatuba De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.