Pular para o conteúdo

Racional · ensaio 09

Strings

rationale.md · 75 linhas · 3 min de leitura

A redefinição: a string é UTF-8 invariante com unidade explícita (s[i] é proibido) e comparar é método, porque “igual” é ambíguo.

String parece simples até você lembrar que UTF-8 tem três unidades distintas (bytes, codepoints e graphemes, os caracteres visuais) e que “essas duas strings são iguais?” depende de normalização e locale. A maioria das linguagens esconde isso: s[i] te dá algo (byte? char?), e == faz alguma comparação. O conforto é uma mentira de superfície.

A string é sempre UTF-8 válido e expõe três visões: .bytes, .codepoints, .chars (grapheme). s[i] cru é proibido: você qualifica a unidade (s.bytes[i], s.chars[i]), porque indexar sem dizer o quê é ambíguo, e a ambiguidade esconde o custo (byte é O(1); grapheme é O(n)).

Comparação tem um corte deliberado: os operadores ==/!=/</> existem, mas são byte-a-byte (igualdade de bytes, ordem lexicográfica, o caso comum e rápido, que alimenta um sort direto). A semântica Unicode (normalização, collation, case-insensitive) é método (a.equals(b, .semantic)), não operador. E não há ===: um segundo operador de igualdade só criaria a pergunta “qual é a verdadeira?”.

Interpolação é {{ }} via a interface Display, não uma mini-linguagem de format dentro da string.

O critério é o mesmo que governa o resto da linguagem: o que é trivial e mapeável na cabeça é operador; o que é decisão semântica dependente de ambiente é método. Comparar bytes é trivial: você sabe exatamente o que acontece. Comparar “São” com “Sao” respeitando locale não é trivial: depende de normalização, de tabelas Unicode, de uma escolha. Esconder essa escolha atrás de um == seria mágica; expô-la como equals(_, mode) é honestidade, e sinaliza, pelo próprio fato de ser método, que ali mora complexidade (e que as tabelas Unicode pesam, então são opt-in).

Proibir s[i] é o mesmo princípio aplicado a acesso: a string te força a escolher a unidade, porque não existe uma resposta única e barata. É o nome honesto (cap. 12) levado à indexação: s.bytes e s.chars dizem exatamente o que você pega e quanto custa.

  • s[i] permitido. O programador nunca saberia se paga O(1) (byte) ou O(n) (grapheme), e qual unidade recebe. Viola a linearidade-visível; a unidade explícita resolve.
  • Comparação Unicode transparente no operador (Swift et al.). Esconde a decisão de locale/ normalização atrás de um < que parece trivial, e quando a plataforma faz collation onde você queria bytes (ou vice-versa), você está em terreno minado sem aviso. Método torna a decisão visível.
  • === para igualdade bit-exata. Duas igualdades obrigam a perguntar qual é a canônica; é “deixar a resposta errada ser barata”. == já é byte-equality inequívoca; o resto é método.
  • Mini-linguagem de format na string (%d, {:.2}). Uma segunda gramática dentro da string. {{ expr }} via Display reusa a interface e deixa cada tipo controlar a própria renderização.

O “localization hell”: o == que compara certo no seu locale e errado no do usuário; o s[i] que parte um emoji no meio porque indexou byte achando que era caractere; o .length que conta unidades de código UTF-16 e mente sobre quantos caracteres há. Cada uma é a unidade ambígua ou a comparação escondida cobrando o preço, quase sempre em produção, quase sempre num idioma que não é o do desenvolvedor.

Comparar string é método explícito porque “igual” é ambíguo. E indexar exige dizer a unidade, porque não há uma resposta única e barata.

Você digita mais: s.bytes.len em vez de s.len, a.equals(b, .semantic) em vez de a == b quando quer Unicode. O caso comum (bytes) continua curto, mas o semântico custa uma chamada, de propósito, porque é onde a decisão real mora. A string recusa o conforto de superfície em troca de você sempre saber o que está comparando e quanto está pagando.

Próximo: 10 · FFI