Ti

Tipos em Julia

1 exercício

Sobre Tipos

Até agora, o programa de estudos não disse muito sobre tipos, mas é claro que eles existem em Julia:

julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')

julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}

Nunca especificamos os tipos, mas Julia os atribuiu mesmo assim.

  1. Julia tem types, que são centrais para o seu design.
  2. Julia geralmente consegue "adivinhar" o tipo, usando Type Inference.

O compilador JIT vai examinar (todo) o código, ver como uma variável é usada e inferir um tipo padrão adequado e compatível com esse uso.

Promoção de tipos

Houve muitos exemplos em conceitos anteriores que forçam tipos numéricos à uniformidade, começando pela aritmética mais simples.

julia> 2 + 1.3
3.3

Somamos um número inteiro (Int64) e um número de ponto flutuante (Float64) e obtivemos um resultado Float64. Da mesma forma:

julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)

julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}

julia> [nums...]
3-element Vector{Float64}:
 3.0
 4.1
 0.25

No caso acima, uma tupla preservou o tipo de cada elemento, mas a conversão para um vetor mudou todos eles para um Float64 uniforme.

O compilador de Julia entende quais conversões de tipo são possíveis: de inteiro para ponto flutuante não há problema; de ponto flutuante para inteiro há perda de precisão, então um InexactError é lançado. A promoção de tipos converte todos os valores da expressão para um tipo comum, versátil o bastante para ser compatível com todas as entradas.

A mesma conversão pode ser feita explicitamente com a função promote():

julia> promote(nums...)
(3.0, 4.1, 0.25)

Atribuição de tipos

Depender de inferência de tipos e promoção de tipos é suficiente para resolver exercícios simples de tutorial, mas para programas maiores você provavelmente vai precisar de um controle mais preciso.

Na maioria dos processadores modernos, um inteiro assume Int64 como padrão. Vimos no conceito Numbers que um valor pode ser convertido para um tipo específico diferente do padrão.

julia> x = Int16(42)
42

julia> typeof(x)
Int16

No entanto, a variável x ainda pode ser reatribuída a um tipo diferente:

julia> x = "changed"
"changed"

julia> typeof(x)
String

Isso é type instability, que é:

  • Muito conveniente em scripts pequenos.
  • Ruim para o desempenho e a confiabilidade em programas maiores.

Em vez disso, podemos definir o tipo de x usando o operador :::

julia> y::Int16 = 42
42

julia> typeof(y)
Int16

julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.

Agora y é, e sempre será, do tipo Int16. Assim, o compilador sabe quantos bytes reservar para ela e pode otimizar o restante do código contando com um tipo estável. Nesse aspecto, a variável é mais ou menos parecida com as de uma linguagem de tipagem estática como C.

Asserção de tipos

A atribuição de tipo, descrita acima, é usada no lado esquerdo de uma atribuição de variável para restringir o tipo dessa variável.

Usar o operador :: com um valor, ou com algo que resulta em um valor, geralmente é uma asserção de que o valor deve ser desse tipo, caso contrário um erro deve ser lançado.

julia> 42::Number
42

julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String

Com frequência, isso pode ser usado com o valor de retorno de uma função, como uma última verificação simples de que a função se comportou como esperado.

Repare que existe uma macro @assert para outras formas de asserção.

A hierarquia de tipos

Int64, Int16, String, Char: de onde "vêm" esses tipos.

Em muitas linguagens orientadas a objetos (OO), cada tipo é uma classe, a criação de subclasses as organiza numa hierarquia de classes e os métodos de classe definem os comportamentos.

Java e Ruby são exemplos óbvios desse padrão, mas até o Python é semelhante internamente.

Julia não tem classes.

O motivo documentado é que os recursos de OO interferem no compilador JIT e prejudicam o desempenho em tempo de execução.

Quem sabe, talvez eles também tenham lido esta citação:

"Programar orientado a objetos é uma ideia excepcionalmente ruim, que só poderia ter surgido na Califórnia."

Ela é atribuída a Edsger Dijkstra, um cientista da computação brilhante por várias décadas a partir dos anos 1950 (embora não seja geralmente conhecido pelo seu otimismo radiante nem pela sua diplomacia sutil).

E, no entanto, veja este código:

julia> y::Int16 = 42
42

julia> typeof(y)
Int16

julia> supertype(Int16)
Signed

julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)

Preenchendo alguns detalhes:

  • Int16 é um tipo, e podemos criar variáveis desse tipo.
  • Int16 é um subtipo de Signed, e a função supertype() nos mostra isso.
  • Existe uma hierarquia de tipos, subindo por Integer, Real e Number até Any no topo, e subtypes() vai listar esse ramo da hierarquia para nós.

Todos os ramos terminam em Any, que é único por ser seu próprio supertipo.

julia> supertypes(String)
(String, AbstractString, Any)

julia> supertype(Any)
Any

Então, Julia não tem uma hierarquia de class, mas tem uma hierarquia de type. Tentar mostrar a hierarquia inteira resulta em uma árvore enorme, impossível de visualizar de verdade. Olhar partes dela é algo discutido online.

Mais adiante, vamos tentar desvendar como isso funciona, mas há muito mais a explorar antes.

Testando tipos

Podemos usar typeof() para testar igualdade da maneira habitual.

julia> typeof(11)
Int64

julia> typeof(11) == Int64
true

julia> typeof(11) == Number
false

A igualdade de tipos deve ser exata, pois essa forma de comparação não entende nada da hierarquia de tipos.

De forma mais flexível, isa nos diz se um valor tem o mesmo tipo que um comparador ou um subtipo dele. Pode ser usado tanto na forma infixa quanto na forma de função.

julia> 12 isa Int64
true

julia> 12 isa Number
true

julia> isa(12, Number)
true

julia> 12 isa String
false

Repare que isa espera um value à esquerda, não um type.

Tentar comparar dois tipos dessa forma dará resultados inesperados. O operador correto é <:, que vamos ver muito mais em conceitos futuros.

julia> Int64 isa Number  ## Don't do this!
false

julia> Int64 <: Number
true

Testar tipos pode ser usado para controlar o fluxo dentro de uma função, mas isso é relativamente incomum em Julia idiomático.

Vamos ver no conceito de Despacho Múltiplo que muitas vezes é mais eficiente adicionar tipos aos argumentos de uma função e deixar o mecanismo de despacho de Julia cuidar dessa lógica. No entanto, há vários outros conceitos relacionados a tipos que precisamos discutir antes de chegarmos ao Despacho Múltiplo.

Tipos abstratos versus concretos

Vimos que a hierarquia de tipos forma uma estrutura de árvore (no sentido da Ciência da Computação, com a raiz no topo).

Cada item da árvore é um node, e eles podem ser divididos em categorias:

  1. Os nós que têm subtipos são chamados de abstract.
  2. Os nós folha, sem subtipos, são chamados de concrete.
julia> subtypes(Integer)  # an abstract type
3-element Vector{Any}:
 Bool
 Signed
 Unsigned

julia> subtypes(Int64)  # a concrete type
Type[]

Essa é uma distinção importante, porque apenas tipos concretos podem ser instantiated como variáveis.

julia> a::Int16 = 42
42

julia> typeof(a)
Int16

julia> b::Integer = 42
42

julia> typeof(b)
Int64

Repare que tentar usar um tipo abstrato não dá mensagem de erro (neste caso), mas o compilador cria um tipo concreto apropriado: Int64 em vez de Integer. Pelo menos impede que a variável receba um valor não inteiro:

julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.

A atribuição de tipo com um tipo abstrato é, portanto, uma restrição ao tipo da variável, limitando-a a qualquer subtipo de (no caso acima) Integer. Isso é algo incomum no mundo das linguagens de programação: mais fraco que uma atribuição de tipo em C, mais forte que uma dica de tipo nas versões recentes do Python.

As funções isabstracttype() e isconcretetype() permitem fazer esse teste. Repare que essas funções não são apenas a negação uma da outra: vamos ver em um conceito posterior que alguns tipos não podem ser nem abstratos nem concretos.

julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)

julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)

# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)

Embora Vector não seja um tipo concreto, os elementos são concretos. A função eltype() (tipo dos elementos) extrai esse tipo:

julia> eltype([1, 2.3])
Float64
Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Tipos