Swift In The Sky With Types (pt-BR)

Published on Mar 28th 2016

Originally published here. It'll be soon translated into English, I promise ūüėĀ.

Antes de tudo, algumas coisas a se levar em consideração:

  1. Esse post é baseado em experiências - e eu não sou nenhum tipo de dono da verdade: se você tem alguma opinião sobre isso - concordando, discordando, complementando -, sinta-se livre para compartilhá-la.

  2. Nem tudo nesse post é sobre Swift: boa parte dele está relacionada a experiências bem gerais relacionadas a projeto de linguagens de programação.

  3. H√° boatos que a experi√™ncia de ler o post √© enriquecida ouvindo-se o √°lbum Sgt. Pepper's Lonely Hearts Club Band ‚ėļÔłŹ.

Prólogo

Algumas coisas que me levaram a pensar sobre Sistemas de Tipos - e a escrever esse post...

Eu, Matheus, sou originalmente desenvolvedor JavaScript. E, como em outras linguagens, tenho de lidar com alguns probleminhas. Por exemplo, sua tipagem din√Ęmica pode ser problem√°tica: JavaScript n√£o sabe que tipo uma vari√°vel √© at√© que esta seja realmente atribu√≠da em execu√ß√£o - o que significa que pode ser tarde demais, uma vez que voc√™ n√£o sabe se algum erro de tipo estava em seu c√≥digo e quebrou antes de execut√°-lo.

Além disso, nós não podemos contar com caras como o instanceof e o typeof: eles não funcionam de forma consistente - o que acaba nos mostrando que verificação de tipos em JavaScript é um problema - e você provavelmente tem que fazer alguns workarounds para ajudar você a superar tal inconsistência - ou usar caras como o TypeScript ou o Flow.

O que tudo isso tem a ver com Swift? Bem, o contato com algumas inconsistências relacionadas a tipos presenciadas em algumas linguagens - bem como sistemas de tipos exemplares que pude experimentar em linguagens como Haskell, OCaml e F#- me fez ter sempre tal tópico em mente ao começar em uma linguagem - e, há alguns meses, quando comecei a estudar a linguagem da Apple, não foi diferente.

Mas Antes...

... de começarmos a ver coisas específicas de Swift relacionadas a seu sistema de tipos, penso que seja interessante fazermos um apanhado de alguns conceitos mais "gerais".

Dados?

Dados?

Esse é um conceito bem primitivo - e que muitas vezes não é discutido e só aceito. Na Filosofia, temos uma definição parecida com isso:

Coisas conhecidas ou assumidas como fatos, tornando-se a base do raciocínio ou cálculo.

Ao adentrarmos na Computa√ß√£o, come√ßamos a pensar de uma forma ou pouco diferente sobre dados - ou melhor dizendo: um pouco mais espec√≠fica para o contexto em que estamos - e encontramos defini√ß√Ķes como:

Quantidades, caracteres ou s√≠mbolos dos quais opera√ß√Ķes s√£o executadas por um computador, sendo armazenados e transmitidos na forma de sinais el√©ctricos e gravados no suporte de grava√ß√£o magn√©tica, √≥ptica ou mec√Ęnica.

E assim, come√ßamos a pensar mais sobre dados como sendo um determinado peda√ßo de informa√ß√£o - textos, n√ļmeros, listas, figuras etc.: s√£o todos peda√ßos de informa√ß√£o.

E, como podemos ler no famigerado Livro do Mago:

[...] Processos computacionais são seres abstratos que habitam computadores. À medida que evoluem, os processos manipulam outros seres abstratos chamados dados. A evolução de um processo é dirigida por um padrão de regras chamado programa. As pessoas criam programas para direcionar processos. [...]

Dados s√£o uma das partes mais importantes da arte de programar.

E, particularmente, eu diria que a conversa come√ßa a ficar mais legal quando pensamos sobre como essas informa√ß√Ķes s√£o passadas a um computador - afinal, eles n√£o sabem o que um n√ļmero √©, ou o que o infogr√°fico de seu documento √©, ou o que a URL da sua p√°gina √© - e assim por diante.

Então chegamos a - já esperada - conclusão de que, se queremos passar a nosso computador qualquer um desses pedaços de informação, precisamos saber representá-los de alguma maneira que o computador entenda.

Guarda bem essa palavra: representação - pois ela nos leva ao próximo tópico.

Tipos de Dados?

Tipos de Dados?

tl;dr: Uma representação específica de algum(ns) dado(s).

Basicamente, essa galera aqui te diz como interagir com um determinado peda√ßo de informa√ß√£o e ainda pode dizer como essa informa√ß√£o (dado) √© representada por meio de outras informa√ß√Ķes mais primitivas - ou vice-versa: como interpretar dados mais primitivos a partir de um determinado tipo de dados.

Se o ato de programar consiste em gerenciar processos de tal forma que eles possam manipular dados, √© de import√Ęncia crucial que tais processos manipulem dados de forma eficiente e correta.

Seguindo o racioc√≠nio, modelar dados de forma que estes possam ser manipulados de forma eficiente e correta √© uma parte essencial de todas as tarefas de um programador - e os tipos de dados s√£o essenciais para tal modelagem - assim, temos neles a important√≠ssima tarefa de garantir que as intera√ß√Ķes com um determinado peda√ßo de informa√ß√£o estejam sempre corretas.

A propósito, guarde bem, também, essa palavra: correto - pois ela ainda será bem discutida mais a frente.

Sistemas de Tipos?

Sistemas de Tipos?

tl;dr: Sistemas do tipo são, em sua essência, estruturas de análise de programas.

Soa confuso? √Č, realmente pode ser bem confuso.

Que tal pedirmos ajuda aos universit√°rios?

Bem, Benjamin Pierce, em seu livro Types and Programming Languages, nos conta que:

Um sistema de tipos √© um m√©todo sint√°tico trat√°vel para provar o n√£o cumprimento de certos comportamentos do programa atrav√©s da classifica√ß√£o de express√Ķes de acordo com os tipos de valores que estas computam.

Eu diria que uma das chaves para entendermos o tópico é o trecho:

[...] provar o n√£o cumprimento de certos comportamentos do programa [...].

a partir do qual podemos ver que, para todo sistema de tipos específico, haverá uma lista de coisas que este visa a provar - do que exatamente consiste tal prova é deixado em aberto.

Uma forma pr√°tica de tentar enxergar isso √© pensar em simples express√Ķes. Por exemplo, uma vez que o verificador de tipos pega a express√£o 1 + 2, este pode "olhar" para o 1 e inferir que se trata de um inteiro, olhar para o 2 - e tamb√©m inferir que este √© um inteiro - e, em seguida, olhar para o operador +, e saber que, quando + √© aplicada a dois inteiros, o resultado √© um n√ļmero inteiro - e a√≠ temos nossa prova.

Correctness-by-Design

Correctness-by-Design

Esse conceito aqui também é sempre legal de se pensar sobre, garanto.

Um dos principais objetivos de uma linguagem de programação deve ser a capacidade de orientar o programador a uma abordagem de Correctness-by-Design. Isto é, um programa que é válido nesta linguagem, também deve ser um programa que funciona como o esperado - ou seja: o sistema simplesmente não poderá chegar a um estado inválido; um estado que não cumpre os requisitos de tal programa.

Para tanto, a linguagem deve fazer um esforço de rejeitar programas que possam estar errados - ou, ao menos, torná-los mais difíceis de escrever. Assim, você literalmente não pode escrever código incorreto pelo simples fato de o compilador não deixar.

Correctness-by-Design & Sistemas de Tipos

Correctness-by-Design & Sistemas de Tipos

Agora que já temos uma ideia geral em torno de sistemas de tipos e de como linguagens devem ser projetadas de modo a naturalmente evitar que programas feitos nestas atinjam estados inválidos, você deve estar se perguntando: qual é a exata relação entre ambos?

Sinceramente, eu gostaria de ter uma relação própria já estabelecida tão expressiva quanto a feita pelo ilustre pesquisador Luca Cardelli, da Microsoft Research:

O propósito fundamental de um sistema de tipo é evitar a ocorrência de erros durante a execução de um programa.

Ainda subjetivo? Ele vai além:

Sistemas de tipos fornecem ferramentas conceituais para julgar a adequa√ß√£o de aspectos importantes de defini√ß√Ķes da linguagem. Descri√ß√Ķes de linguagem informais muitas vezes n√£o conseguem especificar a estrutura de tipos de uma linguagem com detalhes suficientes de maneira a evitar a aplica√ß√£o equ√≠voca. Muitas vezes, acontece que diferentes compiladores para a mesma linguagem implementam sistemas de tipo ligeiramente diferentes. Al√©m disso, muitas defini√ß√Ķes de linguagens se mostraram ser defeituosas no quesito tipos, permitindo que um programa quebre - mesmo sendo considerado aceit√°vel por um typechecker.

Assim, vemos no nosso sistema de tipos a figura respons√°vel por "julgar a adequa√ß√£o de aspectos importantes das defini√ß√Ķes de uma linguagem", de forma a "evitar a escrita de c√≥digo incorreto pelo simples fato de o compilador n√£o deixar".

O Que Temos Por Aí?

O Que Temos Por Aí?

Esta seção serve mais para contextualizar Swift entre outras linguagens antes de falarmos especificamente desta.

As linguagens se dividem em duas categorias: tipadas e n√£o-tipadas - ou unitipadas.

B√īnus: At√© a forma como pensamos na defini√ß√£o de tipos pode variar dentro desse espectro, mas isso √© tema para outras discuss√Ķes - e vamos manter a nossa defini√ß√£o alcan√ßada nas se√ß√Ķes anteriores.

Na primeira, temos representantes como Haskell, OCaml, F#, entre muitas outras, incluindo nossa querida Swift. Nestas, as express√Ķes t√™m tipos diferentes, e quando se combina uma express√£o particular, os tipos devem estar coerentes - assim, se voc√™ tem uma express√£o Int + Int, n√£o √© poss√≠vel escrever "Swift" + 1, porque "Swift" n√£o tem o tipo Int.

Na segunda, tamb√©m temos representantes de peso, como: Clojure, Erlang e Scheme. Nestas, todas as express√Ķes t√™m o mesmo tipo, portanto, n√£o h√° restri√ß√Ķes em como as express√Ķes podem ser combinadas - assim, a mesma express√£o do exemplo anterior seria um constru√ß√£o v√°lida, pois seria uma express√£o do tipo Object + Object - ou algo assim.

Atualmente - até onde eu sei - exemplos que temos de mais poderosos e expressivos em relação a sistemas de tipos são os de Agda, Idris e Coq, que possuem tipagem dependente e indutiva - programar nestas é realmente uma experiência incrível de interação com seu typechecker.

Um pouco mais abaixo - mas ainda proporcionando lindezas relacionadas a tipos - temos aquelas que se baseiam em varia√ß√Ķes do modelo Hindley‚ÄďMilner - do qual voc√™ pode achar uma explica√ß√£o bem interessante aqui -, como Haskell, ML, OCaml, Rust, Scala e... Swift.

Ainda sobre classifica√ß√Ķes, temos que Swift √©:

  • Estaticamente Tipada: Ou seja, todas as suas vari√°veis, constantes, fun√ß√Ķes etc. devem ter seus tipos declarados - ou inferidos, como veremos mais adiante - antecipadamente. Ent√£o o compilador, ao compilar seu programa, usa essas declara√ß√Ķes de tipo para verificar se n√£o h√° erros. Se houver um erro de tipo, o programa n√£o ser√° compilado.
var x: Int = 5
x = "Swift" // => Aqui temos um erro
  • Fortemente Tipada: O que nos diz que, sempre que voc√™ usar uma vari√°vel, ou passar algo como um argumento de uma fun√ß√£o, Swift ir√° verificar, em tempo de compila√ß√£o, se este √© do tipo correto - assim voc√™ n√£o pode, por exemplo, passar um Float para uma fun√ß√£o que espera um Int.
// Uma simples função que retorna o fatorial de um valor.

// Pela anotação de tipo, temos que fatorial recebe um `Int` e o mapeia para
// um `Int`.

func fatorial(n: Int) -> Int {
  return n == 0 ? 1 : n * fatorial(n‚Ää‚ÄĒ‚Ää1)
}
// Chamando nossa função com um `Int`, teremos um `Int` de retorno.
fatorial(3)   // => 6

// Chamando nossa função com um `Float`, teremos um erro.
fatorial(3.0) // => Erro.

E agora...

Vamos Falar de Swift?

Ufa, finalmente! Um post intitulado "Swift In The Sky With Types" e até agora nada demais sobre Swift?!

Agora, gostaria de levantar algumas coisas que vão além do que vimos na seção anterior.

Type-Safety

Type-Safety

Como podemos encontrar na própria documentação provida pela Apple:

Swift é uma linguagem type-safe, o que significa que a linguagem ajuda você a ser claro sobre os tipos dos valores com os quais seu código pode trabalhar. Se parte do seu código espera uma String, a segurança de tipos impede que você possa, por engano, passar um Int.

Assim, sabemos que todas as vari√°veis t√™m um tipo declarado e todas as fun√ß√Ķes/m√©todos t√™m assinaturas de tipo que declaram os tipos de seus argumentos e retornos. E por fim, o nosso amigo compilador verifica se todos os seus tipos est√£o coerentes e n√£o compila seu programa caso n√£o estejam - erros em tempo de compila√ß√£o s√£o ūüíĖ.

B√īnus: Swift nos permite definir v√°rias "vers√Ķes" de uma mesma fun√ß√£o, s√≥ que com diferentes assinaturas de tipos - e a "vers√£o" que ser√° chamada ser√° aquela cujos argumentos forem compat√≠veis com a assinatura de tipo - inclusive, h√° um monte de coisas legais relacionadas √† Swift e seu suporte a polimorfismo Ad-hoc.

// Uma simples função que retorna o fatorial de um valor.

// Pela anotação de tipo, temos que fatorial recebe um `Int` e o mapeia para
// um `Int`.

func fatorial(n: Int) -> Int {
  return n == 0 ? 1 : n * fatorial(n‚Ää‚ÄĒ‚Ää1)
}

// Agora, pela anotação de tipo, temos que fatorial recebe um `Float` e
// mapeia este para um `Float`.

func fatorial(n: Float) -> Float {
  return n == 0.0 ? 1.0 : n * fatorial(n‚Ää‚ÄĒ‚Ää1.0)
}

// Chamando nossa função com um `Int`, teremos um `Int` de retorno.
fatorial(3)   // => 6

// Chamando nossa função com um `Float`, teremos um `Float` de retorno.
fatorial(3.0) // => 6.0

Type Inference

Type Inference

Se você é daqueles que se assusta com a possibilidade de ter que declarar tipo de cada variável do seu código, relaxe! Swift usa a inferência de tipos para - adivinha? - inferir quais os tipos suas variáveis têm. Caso queira, você pode declarar explicitamente o tipo de suas variáveis, mas, na prática, muitas vezes você não precisa: Swift irá inferir o tipo de uma var se você atribuir a ela um valor inicial.

// Aqui, inicializamos uma vari√°vel `x`, dando a esta o valor `1`. Como
// fornecemos um valor inicial, n√£o precisamos declarar explicitamente o tipo
// de `x`: Swift ir√° inferir que esta se trata de um `Int`.
var x = 1

// Desta vez, declaramos uma vari√°vel, mas sem atribuir valor a esta - assim,
// Swift n√£o pode inferir seu tipo e precisamos definir este explicitamente.
// Logo após a declaração, atribuímos a ela o valor `2` - e caso atribuíssemos
// um valor de tipo não coerente com a declaração, teríamos um erro do compilador.
var y:Int
y = 2

Generics

Generics

Os conhecidos Generics nos permitem declarar uma variável que, na execução, pode ser atribuído a um conjunto de tipos definidos por nós.

Em Swift, um array pode conter dados de qualquer tipo: ao criarmos um array de Ints, Floats ou Strings, por exemplo, o tipo dos valores que este vai carregar é definido quando o mesmo é declarado - e assim, temos neles um bom exemplo do uso de Generics.

O uso destes come√ßa com fun√ß√Ķes gen√©ricas, como uma simples fun√ß√£o para imprimir elementos de um array:

// Fun√ß√Ķes gen√©ricas usam placeholders ao inv√©s de um tipo real, como `String`,
// `Int` ou `Float`. Em nossa função, o placeholder é `T` - mas poderia ser
// qualquer outro: `T` é "apenas" convenção.

// O uso do placeholder não indica que a função aceita um tipo `T` mas sim que
// `T` será substituído por um tipo real que é determinada quando a função é
// chamada.

func imprimeElementos<T>(a: [T]) {
    for elemento in a {
        println(elemento)
    }
}

E a brincadeira com estas pode ir além: poderíamos, por exemplo, ter algo do tipo:

func minhaFuncao<T, U>(a: T, b: U) {}

Onde especificamos mais de um Generic.

Por√©m, a divers√£o n√£o para nas fun√ß√Ķes gen√©ricas: temos os tipos gen√©ricos! Estes s√£o, basicamente, classes, enumerations e structs que trabalham com qualquer tipo - se voc√™ se lembrou de arrays e dictionaries, voc√™ pegou a ideia.

Assim, podemos ter coisas como:

// Um exemplo de necessidade comum, por exemplo, √© obter um valor rand√īmico de
// uma coleção - e podemos implementar isso com Generics!

// Temos uma estrutura que é genérica sobre o tipo `T`.
struct MinhaColecao<T> {

    // Temos uma propriedade, um array do tipo T para armazenar o conjunto de
    // dados passados durante a inicialização.
    let itens: [T]

    init(itens: [T]) {
        self.itens = itens
    }

    // E, por fim, temos uma função genérica que cuida do resto :)
    func geraAleatorio() -> T {
        let indice = Int(arc4random_uniform(UInt32(itens.count)))
        return itens[indice]
    }
}

E, ao testarmos:

let teste = MinhaColecao(itens: ["s", "w", "i", "f", "t"])
teste.geraAleatorio() // => "f"

Concluindo

Falou quase nada de Swift!

√Č... Verdade! Perto do que se tem a ser dito, n√£o foi dito quase nada de nada. Cada um desses t√≥picos sobre Swift - at√© os mais primitivos -, juntamente com alguns que n√£o explorei para evitar text√£o - como Phantom Types, Typeclasses etc. - renderia/mereceria um post ou talk sobre. E com os t√≥picos mais te√≥ricos discutidos no post n√£o √© muito diferente - de fato, √© sim: estes √© que renderiam/mereceriam mais posts e talks para serem discutidos!

Na verdade, o objetivo maior do post é apenas levantar cada um destes tópicos em sua mente - e o fazer pensar e buscar mais sobre eles.

Eu realmente ganho algo?

Voc√™ pode ainda estar se perguntando se todas estas palavras relacionadas √† teoria dos tipos - e outras √°reas de estudo comumente associadas a projeto e implementa√ß√£o de linguagem - que soam algo muito apenas da Academia realmente afetam a forma como voc√™ escreve aplica√ß√Ķes do mundo real; para resolver problemas reais.

Bem, eu confesso que, quando comecei a interessar mais por esse assunto, também tinha muitos pensamentos assim - algo do tipo: "Poxa, até ontem eu escrevia código que funcionava e não entendia nada disso! Não vai ser agora que vou precisar".

O que eu n√£o me ligava muito era que a formo como eu modelo meus dados afeta - e muito! - a forma como eu interajo com estes. E:

  • Poder deduzir o que uma fun√ß√£o faz a partir de sua assinatura de tipo - algo inclusive muito √ļtil para tornar o c√≥digo mais leg√≠vel e compreens√≠vel -;

  • Ter o processo de refatora√ß√£o facilitado, uma vez que conto com um monte de erros de compila√ß√£o para me dizer onde as coisas come√ßaram a dar errado;

  • Ter a garantia que as rela√ß√Ķes entre meus dados est√£o ocorrendo da forma que deveriam.

São etapas que, diria eu, são necessárias para se atingir a coerência requerida para se interagir corretamente com uma informação - e isso, colega, causa grande impacto no seu software final.

Poxa, agora o compilador ser√° meu inimigo?

tl;dr: De forma alguma, galera.

A priori, pode soar muito chato doloroso travar uma "luta" contra um typechecker apenas para ver um programa que você tem certeza de que está correto compilado. Alguns chegam até a ver como uma péssima característica da linguagem se pensarmos em um conceito de bom para uma lang definido através da métrica "ser developer-friendly".

Porém, um ponto que eu penso ser interessante de se discutir é o fato de que a noção técnica de melhor envolve aspectos - estes indo muito além do sistema de tipos, como: seu modelo de execução, o quão segura esta é, maneiras que se usa pra obter melhor expressividade - que nem sempre, à primeira vista, se alinham com a felicidade do desenvolvedor.

Particularmente, o tempo me mostrou que, ao programar em uma linguagem que me faça pensar cuidadosamente sobre tipos, acabo chegando a um código melhor projetado, mais fácil de manter, que falha mais rápido - caso este haja de falhar, claro -, melhor documentado etc. Assim, passei a ver o compilador não como um inimigo, mas como uma ferramenta que me guia de uma bela forma a solução para o meu problema - através de tipos.

E tudo isso faz voc√™ se sentir bem mais confiante sobre seu pr√≥prio c√≥digo - e isso √© t√£o divertido quanto ouvir a faixa Lucy In The Sky With Diamonds ‚ėļÔłŹ.

Referências

Aqui ficam apenas alguns posts, livros, vídeos etc. nos quais me baseei ao longo da escrita deste post - e que eu acho que mereceriam um tempo da atenção de vocês.