Elmord's Magic Valley

Computers, languages, and computer languages. Às vezes em Português, sometimes in English.

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

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

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.

Comentários / Comments (8)

Marcus Aurelius, 2015-03-13 11:27:38 -0300 #

> 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.

Em casos de ambigüidade como esses eu preferiria que em vez de "certamente uma das duas variáveis é acessada" fosse "nenhuma variável será acessada e o programador terá que deixar sua intenção explícita". Isso possibilita adicionar recursos posteriormente sem estragar scripts existentes, mas a implementação pode ser complicada demais só para emitir a mensagem: "construção ambígua" em vez de simplesmente retornar a primeira variável que encontrar...

Gostei do "my def", até porque dá pra implementar na versão 2.0 sem problemas de compatibilidade (não pensei bem como seria se existisse uma variável chamada def, mas deve dar para parsear quando chegar no igual, abre-parênteses ou abre-chaves).

O $[or $var default] eu escreveria como $[nvl $var default]. Peguei o nvl do SQL. Minha leitura de código sempre fica muito mais lenta quando tem and/or fazendo papel de "if". É perfeitamente **lógico**, mas é meio estranho.


Marcus Aurelius, 2015-03-13 11:52:49 -0300 #

Na maioria dessas questões, a pergunta que sempre fica na minha cabeça é: mas em toda a história desse país, digo, das linguagens de programação nada disso foi *ahem* __resolvido__?

Claro, certas coisas não têm resposta exata e há vários trade-offs, mas já dá para ver que existem certas direções gerais: léxico é preferível a dinâmico, local é melhor que global, explícito é preferível a implícito, o var do JavaScript era tosco e por isso está sendo criado o let, números octais começando com 0 foi uma ideia idiota, etc.

Ninguém em sã consciência criaria uma nova linguagem onde o "else" testa uma variável para ver o resultado do último if: http://stackoverflow.com/a/2015215/427413
Isso que eu quero dizer com "resolvido": o if-else já está "resolvido", só linguagens mais antigas têm essas tosquices. Os problemas de escopo de variáveis, break, continue não chegaram ao mesmo nível de maturidade?

Bem, resolvi testar em Lua só para ver como funciona o escopo:

http://www.lua.org/cgi-bin/demo

local x = 1
for i = 1, 3 do
print(x)
local x = 2
print(x)
end

Output:

1
2
1
2
1
2

----------------------------------

local x = 1
local function fn1()
local function fn2()
print(x)
end
local x = 2
fn2()
end
fn1()

Output:

1


Vítor De Araújo, 2015-03-13 12:58:08 -0300 #

Em tempos de outrora eu já pensei em implementar o if assim, tá. Deixe meu grau de sanidade fora disso. :P (O problema citado é fácil de contornar se o 'if' setar a variável mágica só depois que o bloco then sair. Anyway, eu não pretendo mais fazer isso, às custas de obrigar o usuário a pôr o 'else' na mesma linha do fecha-chave do bloco then.)

O caso do break/continue eu não tenho dúvida da semântica. A questão era só se eles seriam implementados em termos de alguma outra construção mais geral ou se seriam primitivas. O usuário do shell mostly não tem que se preocupar com isso. O caso do return/reply é mais complicado porque a dúvida de "quem retorna" é visível para o usuário.

So, aparentemente a resposta à questão do escopo é "resolve tudo estático", o que é bom do ponto de vista de performance. Acho que o que fica me incomodando é que eu fico pensando no caso de a referência à variável ser só uma string com o nome da variável, que não carrega informação de binding (e.g., o ${!foo} do bash), mas isso não precisa existir na linguagem. (O caso das funções é mais complicado porque a chamada de função *usa* uma string com o nome, mas "tem que ver isso aí".)


Marcus Aurelius, 2015-03-13 14:16:06 -0300 #

A época de implementar o else desse jeito deve ter sido quando tu escreveu este post: http://www.inf.ufrgs.br/~vbuaraujo/blog/?entry=20140706-madman-scholar

Mas bem, se setar a variável mágica depois do bloco "then", então aí tudo bem, o programador nem perceberia.

Com isso lembrei de outra coisa que eu odeio em linguagens/libs: iteradores globais. O cara está lá, bem feliz iterando por uma coleção. Aí no meio do loop precisa verificar uma coisa. Fácil, é só chamar verificarUmaCoisa(), que por sua vez chama diversas outras funções, uma das quais também precisa percorrer toda a coleção. O fato de ser n² nem importa, o que importa que é a coleção tinha implicitamente um "cursor" ou "iterador" que foi completamente avacalhado. Já me aconteceu em Perl e numa libs para acesso ao banco de dados que não vem ao caso.

Acesso a variáveis com nomes em strings? Pra mim a resposta óbvia é: só para hashtables. Se a linguagem expõe as variáveis globais e/ou de ambiente numa hashtable, beleza. Senão, nada de metametametaprogramação.

Scheme é diferente de Lua nos escopos? Bom, o let diz exatamente onde cada coisa termina, então fica mais evidente, né?


Vítor De Araújo, 2015-03-13 14:34:54 -0300 #

Com 'let' no Scheme não tem esse problema (tudo tem referência inambígua e todos comemora). Mas o Scheme também tem o 'define' interno, que tem a semântica de um letrec, i.e., em:

(define x 1)

(define block
(lambda ()
(define f (lambda () (display x)))
(define x 2)
(f)))

(block)

imprime 2, não 1.

*Acho* que em Scheme R5RS os defines têm que aparecer todos no começo de um "bloco", i.e., não dá pra dizer:

(define x 1)

(define (foo)
(display x)
(define x 2)
(display x))

(foo)
(foo)

mas o Chicken aceita e imprime "1212".


Vítor De Araújo, 2015-03-13 14:43:50 -0300 #

Mas esse Chicken é meio malandro:

(define x 1)

(define block
(lambda ()
(define f (lambda () (display x)))
(f)
(define x 2)
(f)))

(block)

imprime "11". Aparentemente defines que aparecem junto contam como um letrec para o código que vem depois, e cada conjunto de defines que aparece no bloco conta como um letrec separado.


Marcus Aurelius, 2015-03-13 16:02:10 -0300 #

define, let, let*, letrec... como é bom ter uma linguagem elegante :-p

"Melhor" que isso, só se tivesse flet, labels, multiple-value-bind, e destructuring-bind. :-PPP


Vítor De Araújo, 2015-03-13 16:24:24 -0300 #

Não se esqueça do prog e prog*. :P

(Bá, tem o progv também, esse eu nem lembrava como funcionava...)


Deixe um comentário / Leave a comment

Main menu

Recent posts

Recent comments

Tags

em-portugues (213) comp (148) prog (71) in-english (62) life (49) unix (38) pldesign (37) lang (32) random (28) about (28) mind (26) lisp (25) fenius (22) mundane (22) web (20) ramble (18) img (13) rant (12) hel (12) scheme (10) privacy (10) freedom (8) esperanto (7) music (7) lash (7) bash (7) academia (7) copyright (7) home (6) mestrado (6) shell (6) android (5) conlang (5) misc (5) emacs (5) latex (4) editor (4) etymology (4) php (4) worldly (4) book (4) politics (4) network (3) c (3) tour-de-scheme (3) security (3) kbd (3) film (3) wrong (3) cook (2) treta (2) poem (2) physics (2) x11 (2) audio (2) comic (2) lows (2) llvm (2) wm (2) philosophy (2) perl (1) wayland (1) ai (1) german (1) en-esperanto (1) golang (1) translation (1) kindle (1) pointless (1) old-chinese (1)

Elsewhere

Quod vide


Copyright © 2010-2024 Vítor 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.