Saltar para o conteúdo
Ridgun
Português

17 O Livro do Ridgun

A lib csv — Analisador de Estados RFC 4180 para CSV/TSV e Tabelas Indexadas

17.1. Por que existe a lib csv? (O problema real e a filosofia Ridgun)

«Por que preciso de uma lib csv escrita de raiz em Ridgun puro em vez de ligar uma biblioteca C tradicional?»

Essa é a primeira pergunta que qualquer engenheiro de sistemas faz ao chegar à csv. E a resposta resume toda a filosofia do ridgun-libs:

Sejamos sinceros: toda a gente acha que analisar um CSV é fazer str split linha ","... até aparecer um campo entre aspas com uma vírgula lá dentro, aspas duplicadas "" ou uma quebra de linha \r\n a meio de uma célula.

A lib csv implementa uma máquina de estados completa conforme a RFC 4180 para CSV e TSV, serialização automática com aspas inteligentes e o tipo CsvTable para consultar colunas e linhas pelo nome do cabeçalho.

Num ecossistema tradicional, resolver as tarefas do domínio CSV implica arrastar dependências externas em C, lidar com ponteiros brutos, conversões opacas de tipos, exceções não verificadas ou pausas imprevisíveis do coletor de lixo. Em Ridgun, a lib csv (src/main/ridgun/lib/csv.rg) aborda o problema com quatro princípios inegociáveis:

  1. Declare import csv (ou import csv as alias) no início do ficheiro: cada lib define o seu espaço de nomes module :: csv e em modo --strict as funções multi-palavra são sempre invocadas com espaços naturais.
  2. Encadeie os seus pipelines da esquerda para a direita com ->: escreva entrada passo um -> passo dois -> resultado, use máscaras (+ _ _ +) para desestruturar sequências e omita _ = em instruções sequenciais (ou descarte com -> ()).
  3. Recursão de cauda com TCO (Tail-Call Optimization): todas as travessias internas da csv são compiladas para ciclos nativos em espaço de pilha $O(1)$.
  4. Memória determinística (RMM1 / RMM2): cada estrutura intermédia é libertada no instante exato em que termina a sua última leitura, sem Garbage Collector.

17.2. Artefactos nativos, importação e catálogo completo da API de csv

No ridgun-libs, csv é uma lib de primeira classe localizada em src/main/ridgun/lib/csv.rg (domínio CSV). Ao compilar com stable bundle, é empacotada em três formatos nativos:

ArtefactoCaminhoFinalidade
Bundle Ridguntarget/bundles/csv.rglibDistribuição e importação direta com import csv
Ficheiro compiladotarget/csv.rgcArtefacto intermédio verificado pelo ridgunc
Objeto partilhado ELF64target/libcsv.soBiblioteca dinâmica nativa sem dependências de C nem libc

Cada lib declara o seu próprio espaço de nomes com module :: csv: basta escrever import csv (ou import csv as alias) no topo e invocar as suas funções com espaços naturais (minha funcao x) e pipelines -> (x minha funcao -> resultado).

17.2.1. Resumo das famílias funcionais da csv

Assinatura / Função / TipoO que faz e quando usar
csv parse / tsv parse / csv parse withAnalisa texto CSV/TSV (incluindo células multilinha e escapes "") em [[String]]
csv stringify / tsv stringifySerializa [[String]] colocando aspas apenas em células com delimitadores, aspas ou quebras
csv parse table / csv get cell / csv colConsulta sobre CsvTable indexada por cabeçalho (linha, nome_col) ou coluna inteira
csv row to kv / csv table to kv listConverte linhas tabulares em dicionários [String String] indexados pelo cabeçalho

17.2.2. Catálogo exaustivo de tipos e assinaturas em src/main/ridgun/lib/csv.rg

Abaixo encontra-se o inventário completo de tipos algébricos (type), operadores (op ::) e assinaturas de funções (fn ::) definidos em src/main/ridgun/lib/csv.rg (1 tipos, 0 operadores e 23 funções):

Tipos algébricos e registos (type) da csv:

type CsvTable = headers: [String], rows: [[String]], delimiter_char: String, has_headers: Bool
#Função / Assinatura (fn ::)Tipo de Retorno
1fn :: csv delim code StringInt
2fn :: csv append final row [[String]] [String] String[[String]]
3fn :: csv parse quoted String Int Int Int String [String] [[String]][[String]]
4fn :: csv parse unquoted String Int Int Int String [String] [[String]][[String]]
5fn :: csv parse with delim String String[[String]]
6fn :: csv parse String[[String]]
7fn :: tsv parse String[[String]]
8fn :: csv build table [[String]] BoolCsvTable
9fn :: csv parse table String BoolCsvTable
10fn :: csv needs quotes loop String Int Int IntBool
11fn :: csv double quotes loop String Int Int StringString
12fn :: csv format cell String IntString
13fn :: csv join row [String] String IntString
14fn :: csv join rows [[String]] String IntString
15fn :: csv stringify with delim [[String]] StringString
16fn :: csv stringify [[String]]String
17fn :: tsv stringify [[String]]String
18fn :: csv nth str [String] IntString
19fn :: csv nth row [[String]] Int[String]
20fn :: csv col [[String]] Int[String]
21fn :: csv find header idx [String] String IntInt
22fn :: csv get by header CsvTable Int StringString
23fn :: csv row to map [String] [String][String String]

17.3. Explorando a csv a fundo: Exemplos passo a passo de cada funcionalidade

Não basta ver a tabela de assinaturas: vamos explorar cada uma das funcionalidades da lib csv através de exemplos progressivos e executáveis em Ridgun. Começaremos pelas duas receitas tabulares de uso diário e depois percorreremos em detalhe todos os cenários funcionais, transformações e casos de fronteira verificados em src/test/ridgun/test csv.rg.

17.3.1. Exemplo 1: Fluxo principal em estilo tabular com csv

Repare como em Ridgun os sinais = e os passos de transformação com csv ficam alinhados como uma tabela limpa, sem variáveis mutáveis desnecessárias nem efeitos colaterais ocultos:

import csv

fn :: extraer columna precios String => [String] String
      extraer columna precios raw_csv =
        raw_csv csv parse -> tabla
        csv col by name tabla "precio" -> precios
        csv cell by name tabla 0 "producto" -> primero
        (precios, primero)

17.3.2. Exemplo 2: Composição segura e tratamento de casos de fronteira

Todas as funções da lib csv foram desenhadas para se comporem com guardas booleanas | e com as restantes 32 libs do ridgun-libs:

import csv

fn :: exportar informe => String
      exportar informe =
        [["id", "nota"], ["1", "Contiene, coma"], ["2", "Dijo "hola""]] -> filas
        filas csv encode

17.3.3. Exemplo 3: Ida e volta RFC 4180 CSV/TSV com aspas escapadas "", vírgulas internas e quebras de linha embutidas

Neste exemplo prático exploramos de forma exaustiva como a csv se comporta neste bloco funcional, incluindo entradas padrão, combinações de parâmetros e valores limite:

fn :: test csv tsv roundtrip => ()
      test csv tsv roundtrip =
        nl = char from code 10
        tab = char from code 9
        raw_csv = "name,role,note" + nl + "Ada,\"Admin, Lead\",\"Quote \"\"here\"\"\"" + nl + "Bob,User,\"Line 1" + nl + "Line 2\"" + nl
        parsed = csv parse raw_csv
        assert int equals (csv row count parsed) 3 "AC1: 3 rows parsed including multiline quoted cell"
        assert str eq (csv cell at parsed 1 1) "Admin, Lead" "AC1: quoted comma preserved"
        assert str eq (csv cell at parsed 1 2) "Quote \"here\"" "AC1: doubled quote unescaped"
        assert str eq (csv cell at parsed 2 2) ("Line 1" + nl + "Line 2") "AC1: embedded newline in quoted cell preserved"
        re_csv = csv stringify parsed
        assert str eq re_csv raw_csv "AC1/AC2: RFC 4180 CSV quoted comma, escaped quote, and newline round-trip"

        raw_tsv = "col1" + tab + "col2" + nl + "val,1" + tab + "val2" + nl
        parsed_tsv = tsv parse raw_tsv
        assert str eq (csv cell at parsed_tsv 1 0) "val,1" "AC1: TSV unquoted comma preserved"
        re_tsv = tsv stringify parsed_tsv
        assert str eq re_tsv raw_tsv "AC1/AC2: TSV round-trip"
        ()

17.3.4. Exemplo 4: Tratamento de finais de linha CRLF (\r\n), células vazias consecutivas e delimitadores personalizados

Neste exemplo prático exploramos de forma exaustiva como a csv se comporta neste bloco funcional, incluindo entradas padrão, combinações de parâmetros e valores limite:

fn :: test csv crlf and empty cells => ()
      test csv crlf and empty cells =
        cr = char from code 13
        nl = char from code 10
        crlf = cr + nl
        raw_crlf = "a,b,c" + crlf + "1,,3" + crlf + ",2," + crlf
        rows_crlf = csv parse raw_crlf
        assert int equals (csv row count rows_crlf) 3 "AC1: CRLF row count"
        assert str eq (csv cell at rows_crlf 1 0) "1" "AC1: cell (1,0)"
        assert str eq (csv cell at rows_crlf 1 1) "" "AC1: middle empty cell (1,1)"
        assert str eq (csv cell at rows_crlf 1 2) "3" "AC1: cell (1,2)"
        assert str eq (csv cell at rows_crlf 2 0) "" "AC1: leading empty cell (2,0)"
        assert str eq (csv cell at rows_crlf 2 1) "2" "AC1: cell (2,1)"
        assert str eq (csv cell at rows_crlf 2 2) "" "AC1: trailing empty cell (2,2)"

        semi_rows = csv parse with delim ("k1;k2" + nl + "v1;v2" + nl) ";"
        assert int equals (csv row count semi_rows) 2 "AC1: semicolon delimiter row count"
        assert str eq (csv cell at semi_rows 1 1) "v2" "AC1: semicolon delimiter cell (1,1)"
        assert int equals (csv row count (csv parse "")) 0 "AC1: empty input yields 0 rows"
        ()

17.3.5. Exemplo 5: Tabelas com cabeçalho (CsvTable): pesquisa por linha e nome de coluna (csv get cell), extração de colunas e registos chave-valor

Neste exemplo prático exploramos de forma exaustiva como a csv se comporta neste bloco funcional, incluindo entradas padrão, combinações de parâmetros e valores limite:

fn :: test csv table lookup => ()
      test csv table lookup =
        nl = char from code 10
        raw_csv = "id,name,dept" + nl + "10,Ada,Math" + nl + "20,Grace,Compilers" + nl
        tbl = csv parse table raw_csv True
        assert true (has_headers tbl) "AC3: table has_headers True"
        assert int equals (csv len (headers tbl)) 3 "AC3: 3 headers extracted"
        assert str eq (csv get by header tbl 0 "name") "Ada" "AC3: csv get by header row 0"
        assert str eq (csv get by header tbl 1 "dept") "Compilers" "AC3: csv get by header row 1"
        assert str eq (csv get by header tbl 0 "missing") "" "AC3: csv get by header missing column"
        assert str eq (csv get by header tbl 99 "name") "" "AC3: csv get by header out-of-bounds row"

        tbl_no_h = csv parse table raw_csv False
        assert false (has_headers tbl_no_h) "AC3: table has_headers False"
        assert int equals (csv row count (rows tbl_no_h)) 3 "AC3: headerless table keeps all 3 rows"

        names = csv col (rows tbl) 1
        assert int equals (csv len names) 2 "AC3: csv col length"
        assert str eq (csv str at names 0) "Ada" "AC3: csv col[0]"
        assert str eq (csv str at names 1) "Grace" "AC3: csv col[1]"

        rmap = csv row to map ["id", "name", "dept"] ["10", "Ada", "Math"]
        assert str eq (csv pair val rmap "id") "10" "AC3: csv row to map id"
        assert str eq (csv pair val rmap "name") "Ada" "AC3: csv row to map name"
        assert str eq (csv pair val rmap "dept") "Math" "AC3: csv row to map dept"
        ()

17.4. Debaixo do capô: como a csv é construída em Ridgun puro (src/main/ridgun/lib/csv.rg)

Uma das maiores vantagens do ridgun-libs é que não existem caixas-pretas em C. Pode abrir src/main/ridgun/lib/csv.rg e ler exatamente como cada operação é implementada usando pattern matching, árvores de guardas | / |. e recursão de cauda (TCO). Vejamos as peças-chave da sua implementação real:

17.4.1. Implementação interna: RFC 4180 State-Machine Parser

Observe como csv.rg estrutura primeiro as assinaturas fn :: ... => ... e resolve cada caso através de equações alinhadas e chamadas de cauda:

module :: csv

fn :: csv delim code String => Int
      csv delim code "" = 44
      csv delim code d = str char at d 0

fn :: csv append final row [[String]] [String] String => [[String]]
      csv append final row rows [] "" = rows
      csv append final row rows cur_row cell = rows :: [cur_row :: [cell]]

fn :: csv parse quoted String Int Int Int String [String] [[String]] => [[String]]
      csv parse quoted s pos len d_code cell cur_row rows =
        | pos >= len = csv append final row rows cur_row cell
        | (str char at s pos) == 34 and (pos + 1) < len and (str char at s (pos + 1)) == 34 = csv parse quoted s (pos + 2) len d_code (cell + (char from code 34)) cur_row rows
        | (str char at s pos) == 34 = csv parse unquoted s (pos + 1) len d_code cell cur_row rows
        | otherwise = csv parse quoted s (pos + 1) len d_code (cell + (str slice s pos (pos + 1))) cur_row rows

fn :: csv parse unquoted String Int Int Int String [String] [[String]] => [[String]]
      csv parse unquoted s pos len d_code cell cur_row rows =
        | pos >= len = csv append final row rows cur_row cell
        | (str char at s pos) == 34 and cell == "" = csv parse quoted s (pos + 1) len d_code "" cur_row rows
        | (str char at s pos) == d_code = csv parse unquoted s (pos + 1) len d_code "" (cur_row :: [cell]) rows
        | (str char at s pos) == 13 and (pos + 1) < len and (str char at s (pos + 1)) == 10 = csv parse unquoted s (pos + 2) len d_code "" [] (rows :: [cur_row :: [cell]])
        | (str char at s pos) == 10 or (str char at s pos) == 13 = csv parse unquoted s (pos + 1) len d_code "" [] (rows :: [cur_row :: [cell]])
        | otherwise = csv parse unquoted s (pos + 1) len d_code (cell + (str slice s pos (pos + 1))) cur_row rows

fn :: csv parse with delim String String => [[String]]
      csv parse with delim s delim =
        csv parse unquoted s 0 (str len s) (csv delim code delim) "" [] []

fn :: csv parse String => [[String]]
      csv parse s = csv parse with delim s ","

fn :: tsv parse String => [[String]]
      tsv parse s = csv parse with delim s (char from code 9)

fn :: csv build table [[String]] Bool => CsvTable
      csv build table [] has_h = CsvTable [] [] "," has_h
      csv build table (h:rest) True = CsvTable h rest "," True
      csv build table all_rows False = CsvTable [] all_rows "," False

fn :: csv parse table String Bool => CsvTable
      csv parse table s has_h = csv build table (csv parse s) has_h

17.4.2. Implementação interna: RFC 4180 Serialization & Escaping

Observe como csv.rg estrutura primeiro as assinaturas fn :: ... => ... e resolve cada caso através de equações alinhadas e chamadas de cauda:

fn :: csv needs quotes loop String Int Int Int => Bool
      csv needs quotes loop s pos len d_code =
        | pos >= len = False
        | (str char at s pos) == d_code or (str char at s pos) == 34 or (str char at s pos) == 10 or (str char at s pos) == 13 = True
        | otherwise = csv needs quotes loop s (pos + 1) len d_code

fn :: csv double quotes loop String Int Int String => String
      csv double quotes loop s pos len acc =
        | pos >= len = acc
        | (str char at s pos) == 34 = csv double quotes loop s (pos + 1) len (acc + (char from code 34) + (char from code 34))
        | otherwise = csv double quotes loop s (pos + 1) len (acc + (str slice s pos (pos + 1)))

fn :: csv format cell String Int => String
      csv format cell cell d_code =
        | csv needs quotes loop cell 0 (str len cell) d_code = (char from code 34) + (csv double quotes loop cell 0 (str len cell) "") + (char from code 34)
        | otherwise = cell

fn :: csv join row [String] String Int => String
      csv join row [] _ _ = ""
      csv join row (c:[]) _ d_code = csv format cell c d_code
      csv join row (c:cs) delim d_code = (csv format cell c d_code) + delim + (csv join row cs delim d_code)

fn :: csv join rows [[String]] String Int => String
      csv join rows [] _ _ = ""
      csv join rows (r:rs) delim d_code = (csv join row r delim d_code) + (char from code 10) + (csv join rows rs delim d_code)

fn :: csv stringify with delim [[String]] String => String
      csv stringify with delim rows delim =
        csv join rows rows delim (csv delim code delim)

fn :: csv stringify [[String]] => String
      csv stringify rows = csv stringify with delim rows ","

fn :: tsv stringify [[String]] => String
      tsv stringify rows = csv stringify with delim rows (char from code 9)

17.4.3. Implementação interna: Column Extraction & Header-Mapped Lookup

Observe como csv.rg estrutura primeiro as assinaturas fn :: ... => ... e resolve cada caso através de equações alinhadas e chamadas de cauda:

fn :: csv nth str [String] Int => String
      csv nth str [] _ = ""
      csv nth str (x:xs) idx =
        | idx < 0 = ""
        | idx == 0 = x
        | otherwise = csv nth str xs (idx - 1)

fn :: csv nth row [[String]] Int => [String]
      csv nth row [] _ = []
      csv nth row (r:rs) idx =
        | idx < 0 = []
        | idx == 0 = r
        | otherwise = csv nth row rs (idx - 1)

fn :: csv col [[String]] Int => [String]
      csv col [] _ = []
      csv col (r:rs) col_idx = [csv nth str r col_idx] :: (csv col rs col_idx)

fn :: csv find header idx [String] String Int => Int
      csv find header idx [] _ _ = (0 - 1)
      csv find header idx (h:hs) target idx =
        | h == target = idx
        | otherwise = csv find header idx hs target (idx + 1)

fn :: csv get by header CsvTable Int String => String
      csv get by header tbl row_idx col_name =
        headers tbl -> csv find header idx col_name 0 -> c_idx
        rows tbl -> csv nth row row_idx -> csv nth str c_idx

fn :: csv row to map [String] [String] => [String String]
      csv row to map [] _ = []
      csv row to map _ [] = []
      csv row to map (h:hs) (v:vs) = [(h, v)] :: (csv row to map hs vs)

17.5. Garantias algorítmicas, memória RMM1/RMM2 e as 5 Regras de Ouro da csv

  • Garantia algorítmica e de segurança: Suporte exato de quebras \r\n e \n dentro e fora de aspas, e zero perda de dados em viagens de ida e volta csv parse (csv stringify linhas).
  • Complexidade de pilha $O(1)$ com TCO: Todos os ciclos recursivos em src/main/ridgun/lib/csv.rg estão em posição de cauda, pelo que o ridgunc os compila para saltos nativos sem crescimento de pilha.
  • Memória determinística (RMM1/RMM2): Zero pausas de coletor de lixo; todas as estruturas intermédias são libertadas de forma determinística à saída do escopo.
  • Verificação contínua: Testada exaustivamente com stable check e pela suite automatizada src/test/ridgun/test csv.rg.

As 5 Regras de Ouro ao programar com a csv

  1. Declare import csv no início do ficheiro e aproveite a equivalência entre espaços e underscores para que o seu código se leia como prosa.
  2. Alinhe os seus pipelines de dados com =: encadeie as chamadas à csv em passos nomeados e deixe o stable format alinhar as colunas.
  3. Aproveite as funções totais da csv nas suas guardas |: verifique os valores de retorno ou desempacote os ADTs com pattern matching em vez de assumir que a entrada externa é sempre válida.
  4. Evite copiar grandes estruturas se só precisa de consultar um campo: lembre-se de que no RMM2 os argumentos de entrada não incrementam nem decrementam contadores (+0 / -0).
  5. Combine a csv com test (import test): escreva sempre asserções com assert equals ou assert true para fixar os invariantes do seu domínio.

No Capítulo 18 passaremos a explorar em profundidade a lib ini (A lib ini — Ficheiros .ini por Secções, .env e Interpolação de Variáveis).