Skip to content

Instantly share code, notes, and snippets.

@AndersonTorres
Last active December 31, 2023 09:21
  • Star 14 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save AndersonTorres/219b0cfd522d65630efc5a1e1b3ac98e to your computer and use it in GitHub Desktop.
Zig em 30 Minutos (Tradução)

Zig em 30 Minutos

Este texto é inspirado em A Half Hour to Learn Rust.

O Básico

O comando zig run my_code.zig compila e executa imediatamente seu programa Zig. Cada um destes trechos de código contém um programa Zig que você pode testar. Alguns deles contêm erros de compilação que você pode comentar.

Você precisará declarar uma função main() para começar a executar código.

Este é um programa que não faz nada:

// comments look like this and go to the end of the line
pub fn main() void {}

Você pode importar a biblioteca padrão usando o @import da linguagem e atribuindo o namespace a um identificador. Quase tudo em Zig precisa ser explicitamente atribuído a um identificador. Você também pode importar outros arquivos Zig desta forma, e arquivos C de maneira semelhante mediante @cImport.

const std = @import("std");

pub fn main() void {
    std.debug.print("hello world!\n", .{});
}

Nota: eu explicarei o segundo parâmetro engraçado no comando print mais tarde na seção sobre structs.

var declara uma variável. Na maioria dos casos você deve declarar o seu tipo.

const std = @import("std");

pub fn main() void {
    var x: i32 = 47; // declares "x" of type i32 to be 47.
    std.debug.print("x: {}\n", .{x});
}

const declara que o valor de uma variável é imutável.

pub fn main() void {
    const x: i32 = 47;
    x = 42; // error: cannot assign to constant
}

Zig é bastante rigoroso, e não permitirá que você sombreie identificadores de um escopo mais externo, a fim de evitar que você se confunda:

const x: i32 = 47;

pub fn main() void {
    var x: i32 = 42;  // error: redefinition of 'x'
}

Constantes no escopo global são valores de tempo de compilação (comptime) por padrão, e se você omitir o tipo elas são tipadas em comptime e podem se tornar em tipos de tempo de execução para seus valores de tempo de execução.

const x: i32 = 47;
const y = -47;  // comptime integer.

pub fn main() void {
    var a: i32 = y; // comptime constant coerced into correct type
    var b: i64 = y; // comptime constant coerced into correct type
    var c: u32 = y; // error: cannot cast negative value -47 to unsigned integer
}

Você pode explicitamente escolher deixar a variável indefinida se ela for receber um valor no futuro. Zig irá definir um valor fictício com os bytes 0XAA, a fim de ajudar na detecção de erros, no caso de você provocar um erro ao usar acidentalmente estes valores no momento da depuração.

const std = @import("std");

pub fn main() void {
    var x: i32 = undefined;
    std.debug.print("undefined: {}\n", .{x});
}

Em alguns casos, Zig te permitirá omitir a informação de tipo se ele puder descobrir.

const std = @import("std");

pub fn main() void {
    var x: i32 = 47;
    var y: i32 = 47;
    var z = x + y; // declares z and sets it to 94.
    std.debug.print("z: {}\n", .{z});
}

Mas seja cuidadoso, inteiros literais são tipados em comptime, logo isto aqui não vai funcionar:

pub fn main() void {
    var x = 47; // error: variable of type 'comptime_int' must be const or comptime
}

Funções

Eis uma função (foo) que não retorna nada. A palavra-chave pub significa que a função é exportável a partir do escopo corrente; é por isso que main() precisa ser pub. Você chama funções da mesma forma que faria na maioria das linguagens de programação:

const std = @import("std");

fn foo() void {
    std.debug.print("foo!\n", .{});

    //optional:
    return;
}

pub fn main() void {
    foo();
}

Eis uma função que retorna um valor inteiro:

const std = @import("std");

fn foo() i32 {
    return 47;
}

pub fn main() void {
    var result = foo();
    std.debug.print("foo: {}\n", .{result});
}

Zig não te permite ignorar os valores de retorno das funções:

fn foo() i32 {
    return 47;
}

pub fn main() void {
    foo(); // error: expression value is ignored
}

Mas você pode fazê-lo “explicitamente” se atribuir este retorno à variável de descarte _.

fn foo() i32 {
    return 47;
}

pub fn main() void {
    _ = foo();
}

Você pode fazer uma função que pode receber um parâmetro, declarando seu tipo:

const std = @import("std");

fn foo(x: i32) void {
    std.debug.print("foo param: {}\n", .{x});
}

pub fn main() void {
    foo(47);
}

Structs

Estruturas (structs) são declaradas atribuindo a elas um nome, usando a palavra-chave const. Elas podem receber valores fora de ordem, e podem ser utilizadas mediante de-referência, com a sintaxe usual de ponto ..

const std = @import("std");

const Vec2 = struct{
    x: f64,
    y: f64
};

pub fn main() void {
    var v = Vec2{.y = 1.0, .x = 2.0};
    std.debug.print("v: {}\n", .{v});
}

Structs podem ter valores default. Structs também podem ser anônimas, e podem ser convertidos (coerced) para outra struct desde que todos os valores possam ser inferidos:

const std = @import("std");

const Vec3 = struct{
    x: f64 = 0.0,
    y: f64,
    z: f64
};

pub fn main() void {
    var v: Vec3 = .{.y = 0.1, .z = 0.2};  // ok
    var w: Vec3 = .{.y = 0.1}; // error: missing field: 'z'
    std.debug.print("v: {}\n", .{v});
}

Você pode inserir funções dentro de uma struct a fim de fazer com que ela funcione como um objeto similar à programação orientada a objetos. Tem-se o açúcar sintático onde se você fizer o primeiro parâmetro da função ser um ponteiro para o objeto, ele pode ser chamado à moda da orientação a objetos, semelhante a como Python usa as funções com parâmetro self. A convenção típica é tornar isto óbvio dando à variável o nome self.

const std = @import("std");

const LikeAnObject = struct{
    value: i32,

    fn print(self: *LikeAnObject) void {
        std.debug.print("value: {}\n", .{self.value});
    }
};

pub fn main() void {
    var obj = LikeAnObject{.value = 47};
    obj.print();
}

A propósito, esta coisa que passamos no segundo parâmetro de std.debug.print é uma tupla. Sem entrar em detalhes, é uma struct anônima com campos numerados. Em tempo de compilação, std.debug.print infere os tipos dos parâmetros desta tupla e gera uma versão de si mesma sintonizada para a string de parâmetros que você forneceu - e é assim que Zig sabe como fazer o conteúdo desta impressão ser bem formatado!

const std = @import("std");

pub fn main() void {
    std.debug.print("{}\n", .{1, 2}); #  error: Unused arguments
}

Enums

Enumerações (enums) são declaradas atribuindo o grupo de enums como um tipo usando a palavra-chave const.

Note:

  • Em alguns casos você pode encurtar o nome da enum.
  • Você pode estabelecer um valor de uma Enum para um inteiro, mas ele não é automaticamente convertido, você precisar usar @enumToInt ou @intToEnum para realizar as conversões.
const std = @import("std");

const EnumType = enum{
    EnumOne,
    EnumTwo,
    EnumThree = 3
};

pub fn main() void {
    std.debug.print("One: {}\n", .{EnumType.EnumOne});
    std.debug.print("Two?: {}\n", .{EnumType.EnumTwo == .EnumTwo});
    std.debug.print("Three?: {}\n", .{@enumToInt(EnumType.EnumThree) == 3});
}

Vetores e Fatias

Zig tem vetores (arrays), que são memórias contíguas com comprimento conhecido em tempo de compilação. Você pode inicializá-los declarando seus tipos de antemão e fornecendo uma lista de valores. Você pode acessar o comprimento mediante o campo len do array.

Nota:

  • Arrays em Zig são indexados a partir do zero.
const std = @import("std");

pub fn main() void {
    var array: [3]u32 = [3]u32{47, 47, 47};

    // also valid:
    // var array = [_]u32{47, 47, 47};

    var invalid = array[4]; // error: index 4 outside array of size 3.
    std.debug.print("array[0]: {}\n", .{array[0]});
    std.debug.print("length: {}\n", .{array.len});
}

Zig também tem fatias (slices), que têm comprimento conhecido em tempo de execução. Você pode construir slices a partir de arrays ou de outros slices usando a operação de recorte (slicing). Semelhante a arrays, slices têm um campo len que informa o comprimento.

Nota:

  • O parâmetro de intervalo é aberto (não inclusivo) no extremo superior.

A tentativa de acessar um slice além do limite gera um pânico em tempo de execução (isto significa que teu programa vai quebrar).

const std = @import("std");

pub fn main() void {
    var array: [3]u32 = [_]u32{47, 47, 47};
    var slice: []u32 = array[0..2];

    // also valid:
    // var slice = array[0..2];

    var invalid = slice[3]; // panic: index out of bounds

    std.debug.print("slice[0]: {}\n", .{slice[0]});
    std.debug.print("length: {}\n", .{slice.len});
}

Literais de string são arrays de bytes const u8 terminados em null encodados em UTF-8. Caracteres unicode só são permitidos em strings e comentários.

Nota:

  • O comprimento não inclui o null terminal (oficialmente chamado “sentinela de terminação”).
  • É seguro acessar o terminador null.
  • Índices são por byte, não por glifo Unicode.
const std = @import("std");
const string = "hello 世界";
const world = "world";

pub fn main() void {
    var slice: []const u8 = string[0..5];

    std.debug.print("string {}\n", .{string});
    std.debug.print("length {}\n", .{world.len});
    std.debug.print("null {}\n", .{world[5]});
    std.debug.print("slice {}\n", .{slice});
    std.debug.print("huh? {}\n", .{string[0..7]});
}

Arrays const podem ser convertidos em slices const.

const std = @import("std");

fn foo() []const u8 {  // note function returns a slice
    return "foo";      // but this is a const array.
}

pub fn main() void {
    std.debug.print("foo: {}\n", .{foo()});
}

Estruturas de Controle

Zig te fornece uma estrutura if, que funciona como o esperado.

const std = @import("std");

fn foo(v: i32) []const u8 {
    if (v < 0) {
        return "negative";
    }
    else {
        return "non-negative";
    }
}

pub fn main() void {
    std.debug.print("positive {}\n", .{foo(47)});
    std.debug.print("negative {}\n", .{foo(-47)});
}

Bem como uma estrutura switch:

const std = @import("std");

fn foo(v: i32) []const u8 {
    switch (v) {
        0 => return "zero",
        else => return "nonzero"
    }
}

pub fn main() void {
    std.debug.print("47 {}\n", .{foo(47)});
    std.debug.print("0 {}\n", .{foo(0)});
}

Zig fornece um laço (loop) for, que funciona somente em arrays e slices.

const std = @import("std");

pub fn main() void {
    var array = [_]i32{47, 48, 49};

    for (array) | value | {
        std.debug.print("array {}\n", .{value});
    }
    for (array) | value, index | {
        std.debug.print("array {}:{}\n", .{index, value});
    }

    var slice = array[0..2];

    for (slice) | value | {
        std.debug.print("slice {}\n", .{value});
    }
    for (slice) | value, index | {
        std.debug.print("slice {}:{}\n", .{index, value});
    }
}

Zig fornece um laço while que também funciona como esperado:

const std = @import("std");

pub fn main() void {
    var array = [_]i32{47, 48, 49};
    var index: u32 = 0;

    while (index < 2) {
        std.debug.print("value: {}\n", .{array[index]});
        index += 1;
    }
}

Tratamento de Erros

Erros são tipos especiais de uniões. Você denota que uma função pode errar inserindo ! em frente a ela. Você atira um erro simplesmente retornando-o como um return normal.

const MyError = error{
    GenericError,  // just a list of identifiers, like an enum.
    OtherError
};

pub fn main() !void {
    return MyError.GenericError;
}

Se você escreve uma função que pode errar, você deve decidir o que fazer com este erro quando ele retornar. Duas opções comuns são try, que é bem preguiçosa e simplesmente redireciona o erro para ser o erro para a função. catch explicitamente lida com o erro.

Nota:

  • try é mero açúcar sintático para catch | err | {return err}.
const std = @import("std");
const MyError = error{
    GenericError
};

fn foo(v: i32) !i32 {
    if (v == 42) return MyError.GenericError;
    return v;
}

pub fn main() !void {
    // catch traps and handles errors bubbling up
    _ = foo(42) catch |err| {
        std.debug.print("error: {}\n", .{err});
    };

    // try won't get activated here.
    std.debug.print("foo: {}\n", .{try foo(47)});

    // this will ultimately cause main to print an error trace and return nonzero
    _ = try foo(42);
}

Você também pode usá-lo para conferir erros.

const std = @import("std");
const MyError = error{
    GenericError
};

fn foo(v: i32) !i32 {
    if (v == 42) return MyError.GenericError;
    return v;
}

// note that it is safe for wrap_foo to not have an error ! because
// we handle ALL cases and don't return errors.
fn wrap_foo(v: i32) void {    
    if (foo(v)) | value | {
        std.debug.print("value: {}\n", .{value});
    } else | err | {
        std.debug.print("error: {}\n", .{err});
    }
}

pub fn main() void {
    wrap_foo(42);
    wrap_foo(47);
}

Ponteiros

Tipos ponteiro são declarados colocando * em frente do tipo. Nada de declarações espiraladas como em C! Eles são de-referenciados mediante o campo .*:

const std = @import("std");

pub fn printer(value: *i32) void {
    std.debug.print("pointer: {}\n", .{value});
    std.debug.print("value: {}\n", .{value.*});
}

pub fn main() void {
    var value: i32 = 47;
    printer(&value);
}

Note:

  • Em Zig, ponteiros devem ser corretamente alinhados com o alinhamento do valor que ele está apontando.

Para structs, semelhante a Java, você pode de-referenciar o ponteiro e obter o campo em um único passo com o operador .. Note que isso só funciona com um nível de indireção, de forma que se voc6e tiver um ponteiro para ponteiro, você deve de-referenciar o ponteiro mais exterior primeiro.

const std = @import("std");

const MyStruct = struct {
    value: i32
};

pub fn printer(s: *MyStruct) void {
    std.debug.print("value: {}\n", .{s.value});
}

pub fn main() void {
    var value = MyStruct{.value = 47};
    printer(&value);
}

Zig permite que qualquer tipo (não apenas ponteiros) seja anulável (nullable), mas note que eles são uniões do tipo base com o valor especial null. Para acessar o tipo opcional sem o envelopamento (wrap), use o campo .?:

const std = @import("std");

pub fn main() void {
    var value: i32 = 47;
    var vptr: ?*i32 = &value;
    var throwaway1: ?*i32 = null;
    var throwaway2: *i32 = null; // error: expected type '*i32', found '(null)'

    std.debug.print("value: {}\n", .{vptr.*}); // error: attempt to dereference non-pointer type
    std.debug.print("value: {}\n", .{vptr.?.*});
}

Nota:

  • Quando você usa ponteiros da interface binária (ABI) de C, eles são automaticamente convertidos para ponteiros anuláveis.

Outra forma de obter o valor opcional do ponteiro sem o envelopamento é com a estrutura if:

const std = @import("std");

fn nullChoice(value: ?*i32) void {
    if (value) | v | {
        std.debug.print("value: {}\n", .{v.*});
    } else {
        std.debug.print("null!\n", .{});
    }
}

pub fn main() void {
    var value: i32 = 47;
    var vptr1: ?*i32 = &value;
    var vptr2: ?*i32 = null;

    nullChoice(vptr1);
    nullChoice(vptr2);
}

Um Gostinho de Meta-Programação

A meta-programação em Zig é dirigida por alguns conceitos básicos:

  • Tipos são valores válidos em tempo de compilação
  • Maior parte do código de tempo de execução também funcionará em tempo de compilação
  • Evaluação de campos de struct é duck-typed em tempo de compilação
  • A biblioteca padrão do Zig te dá ferramentas para realizar reflexão em tempo de compilação

Eis um exemplo de despacho múltiplo (você já deve ter visto isso em ação com std.debug.print, agora possivelmente você pode imaginar como isto é implementado):

const std = @import("std");

fn foo(x : anytype) @TypeOf(x) {
    // note that this if statement happens at compile-time, not runtime.
    if (@TypeOf(x) == i64) {
        return x + 2;
    } else {
        return 2 * x;
    }
}

pub fn main() void {
    var x: i64 = 47;
    var y: i32 =  47;

    std.debug.print("i64-foo: {}\n", .{foo(x)});
    std.debug.print("i32-foo: {}\n", .{foo(y)});
}

Eis um exemplo de tipos genéricos:

const std = @import("std");

fn Vec2Of(comptime T: type) type {
    return struct{
        x: T,
        y: T
    };
}

const V2i64 = Vec2Of(i64);
const V2f64 = Vec2Of(f64);

pub fn main() void {
    var vi = V2i64{.x = 47, .y = 47};
    var vf = V2f64{.x = 47.0, .y = 47.0};

    std.debug.print("i64 vector: {}\n", .{vi});
    std.debug.print("f64 vector: {}\n", .{vf});
}

A partir destes conceitos, você pode construir genéricos bastante poderosos!

O HEAP

Zig te fornece muitas formas de interagir com o heap, e usualmente requer que você seja explícito acerca de suas escolhas. Todas elas seguem o mesmo padrão:

  1. Crie uma struct fábrica Allocator.
  2. Recupere a struct std.mem.Allocator struct criada pela fábrica Allocator.
  3. Use as funções alloc/free e create/destroy para manipular o heap.
  4. (opcional) De-inicialize a fábrica Allocator.

Nossa! Isso parece ser coisa para caramba! Mas

  • Isto serve para desencorajar o uso do heap.
  • Isto torna qualquer coisa que chama o heap (o qual é fundamentalmente falível) óbvia.
  • Ao ser desprovido de dogmatismo, você pode cuidadosamente sintonizar seus contrapesos e usar as estruturas de dados padrão sem ter que reescrever a biblioteca padrão.
  • Você pode utilizar um alocador extremamente seguro em seus testes e trocá-lo por um alocador diferente na distribuição/produção.

Ok. Mas você ainda pode ser preguiçoso. Você sente falta de usar jemalloc em todo lugar? Apenas use um alocador global e use-o em todo canto (tomando cuidado que alguns alocadores são seguros-para-threads enquanto outros não)! Por favor não faça isso se estiver escrevendo alguma biblioteca de propósito geral.

Neste exemplo utilizaremos a fábrica std.heap.GeneralPurposeAllocator a fim de criar um alocador com um monte de frufrus (incluindo detecção de vazamentos) e ver como isso tudo surge.

Uma última coisa. Este trecho usa a palavra-chave defer, que é bastante semelhante à mesma palavra em Go! Existe também uma errdefer, mas para aprender sobre ela confira a documentação de Zig (link ao final).

const std = @import("std");

// factory type
const Gpa = std.heap.GeneralPurposeAllocator(.{});

pub fn main() !void {
    // instantiates the factory
    var gpa = Gpa{};

    // retrieves the created allocator.
    var galloc = &gpa.allocator;

    // scopes the lifetime of the allocator to this function and
    // performs cleanup; 
    defer _ = gpa.deinit();

    var slice = try galloc.alloc(i32, 2);
    // uncomment to remove memory leak warning
    // defer galloc.free(slice);

    var single = try galloc.create(i32);
    // defer gallo.destroy(single);

    slice[0] = 47;
    slice[1] = 48;
    single.* = 49;

    std.debug.print("slice: [{}, {}]\n", .{slice[0], slice[1]});
    std.debug.print("single: {}\n", .{single.*});
}

Coda

E é isto! Agora você sabe um montante decente de Zig! Algumas coisas bastante importantes que eu não cobri aqui incluem:

  • Testes! Ah, rapaz, por favor escreva testes! Zig facilita demais escrever testes.
  • A biblioteca padrão.
    • O modelo de memória (de maneira um tanto única, Zig é agressivamente adogmático sobre alocadores).
  • Async
  • Compilação cruzada
  • build.zig

Para maiores detalhes, confira a documentação mais recente: https://ziglang.org/documentation/master/

Ou para um tutorial mais aprofundado, leia: https://ziglearn.org/

META

@kassane
Copy link

kassane commented May 29, 2021

@AndersonTorres, excelente inciativa, parabéns! +1 conteúdo sobre Zig em português, além do site oficial e a wikipédia.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment