Elmord's Magic Valley

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

Blueprints for a shell, parte 3: Tipos de dados

2015-03-13 22:47 -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.

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

Comentários / Comments (5)

Marcus Aurelius, 2015-03-16 10:37:37 -0300 #

Bah, ajeitar sintaxe de shell é negócio complicado!

O que é claro e fácil não é compatível nem conveniente, o que é compatível e conveniente de digitar não é claro...

Sugiro trocar o -toin por -tojn, para ficar mais internacional. :-p
(sqn)


Vítor De Araújo, 2015-03-16 11:29:41 -0300 #

Boa, também vou adicionar "-na -toj" como sintaxe alternativa. :P


Marcus Aurelius, 2015-03-16 11:55:54 -0300 #

-na -toj, é uma boa idéia!

Seria legal também o -na no lugar do --, significando "daqui em diante, só nomes de arquivos".

rm -f -na -f # usando a opção "-f", remove um arquivo chamado "-f", hehe
# claro que aí precisaria de colaboração dos utilitários do sistema...

Além disso, que tal $@(), como em:

file$@(1 2 3)

Ou o superexplícito

$[regexp file(1|2|3)]

O comando mv ficaria:

mv $[regexp file~?]

ou também poderia ser:

mv $[glob file{, ~}]

Fica longo, mas fica tããããão bom de ler e lembrar! Quem sabe deixar uma única exceção para o asterisco (file* em vez de $[glob file*]) porque é muito usado.


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

"-na" é muito propenso a conflitar com opções de outros programas, melhor usar -את, que tem menos risco. (cf. https://en.wikipedia.org/wiki/Modern_Hebrew_grammar#Direct_objects )

rm -f -את -f

I like it already! :P

---

Zoeira à parte, o causo é: file{1,2,3} não é globbing, é expansion, i.e., diferente de *, ? e [], os arquivos não precisam existir para que as palavras sejam geradas. Evidentemente, daria para ter um comando que gera todas as strings que casam com um dado padrão (pelo menos nos casos finitos), mas de qualquer forma se perde a informação da ordem em que as strings devem ser geradas, e.g., "mv file{,~}" e "mv file{~,}" fazem coisas diferentes, mas não dá para especificar isso com um "file~?" da vida.

$[glob file{,~}] não teria esse problema (mas ainda tem o problema de ser super-verboso para uma coisa cujo caso de uso principal é na linha de comando, or so I think. Além disso isso não é globbing[2], mas podia ter um comando 'expand'). O comando 'glob' tem que existir anyway porque lash não vai fazer globbing em casos como 'my file = "a*"; echo $file', a princípio, então tem que haver uma maneira programática de fazer globbing.

Isso nos leva a outra questão, que é como distinguir um * literal de um * de globbing em um comando como $[glob "$file*"]. Daria para escrever $[glob "\"$file\"*"], para inibir o globbing em $file, a menos que $file contenha aspas também. Bom, daria para usar uma format string da vida, a la printf: $[glob "%s*" $file]. Funciona, mas definitivamente não é algo que seria usado em modo interativo. :P


Anchorman, 2015-04-04 23:09:08 -0300 #

That doesn't make sense.


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.