Até agora, o plano de estudos não disse muito sobre tipos, mas é evidente que 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 especificámos os tipos, mas a Julia atribuiu-os mesmo assim.
types, que são centrais para o seu design.Type Inference.O compilador JIT percorre (todo) o código, vê como uma variável é usada e infere um tipo predefinido adequado, compatível com essa utilização.
Houve muitos exemplos em conceitos anteriores que obrigam os tipos numéricos a uniformizar-se, começando pela aritmética mais simples.
julia> 2 + 1.3
3.3
Somámos um número inteiro (Int64) e um número de vírgula 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, um tuplo preservou o tipo de cada elemento, mas a conversão para um vetor transformou-os todos num Float64 uniforme.
O compilador de Julia compreende que conversões de tipos são possíveis: de inteiro para vírgula flutuante não há problema, de vírgula flutuante para inteiro perde precisão, pelo que é lançado um InexactError.
A promoção de tipos converte todos os valores da expressão num tipo comum, suficientemente versátil para ser compatível com todos os valores de entrada.
A mesma conversão pode ser feita explicitamente com a função promote():
julia> promote(nums...)
(3.0, 4.1, 0.25)
Confiar na inferência de tipos e na promoção de tipos é suficiente para resolver exercícios simples de tutorial, mas para programas maiores é provável que precises de um controlo mais preciso.
Na maioria dos processadores modernos, um inteiro tem como tipo predefinido Int64.
Vimos no conceito Numbers que um valor pode ser convertido num tipo específico diferente do predefinido.
julia> x = Int16(42)
42
julia> typeof(x)
Int16
No entanto, a variável x pode ainda ser reatribuída a um tipo diferente:
julia> x = "changed"
"changed"
julia> typeof(x)
String
Isto é type instability, que é:
Em vez disso, podemos definir o tipo de x com 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 é do tipo Int16 e sempre o será.
Assim, o compilador sabe quantos bytes deve reservar para ele e pode otimizar o resto do código contando com um tipo estável.
Nisto, a variável é mais ou menos semelhante à de uma linguagem com tipagem estática como C.
A atribuição de tipos, 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 é avaliado como um valor, é geralmente uma asserção de que o valor tem de ser deste tipo; caso contrário, deve ser lançado um erro.
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
Normalmente, isto pode ser usado com o valor devolvido de uma função, como uma última verificação simples de que a função se comportou como esperado.
Repara que existe uma macro @assert para outras formas de asserção.
Int64, Int16, String, Char: de onde é que estes tipos "vêm"?
Em muitas linguagens orientadas a objetos (OO), cada tipo é uma classe, a criação de subclasses organiza-os numa hierarquia de classes e os métodos de classe definem os comportamentos.
Java e Ruby são exemplos óbvios deste padrão, mas até o Python é semelhante internamente.
A Julia não tem classes.
A razão documentada é que as funcionalidades de OO interferem com o compilador JIT e prejudicam o desempenho em tempo de execução.
Quem sabe, talvez também tenham lido esta citação:
"A programação orientada a objetos é uma ideia excecionalmente má, que só poderia ter surgido na Califórnia."
É atribuída a Edsger Dijkstra, um brilhante cientista de computação durante várias décadas a partir da década de 1950 (embora não seja geralmente conhecido pelo seu otimismo radiante nem pela sua diplomacia subtil).
E, no entanto, repara neste código:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
Com mais alguns detalhes:
Int16 é um tipo e podemos criar variáveis deste tipo.Int16 é um subtipo de Signed e a função supertype() mostra-nos isso.Integer, Real e Number até Any, no topo, e subtypes() lista-nos este ramo da hierarquia.Todos os ramos terminam em Any, que é único por ser o seu próprio supertipo.
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
Portanto, a Julia não tem uma hierarquia de class, mas tem uma hierarquia de type.
Tentar mostrar a hierarquia completa dá uma árvore enorme, impossível de ver realmente.
Ver partes dela é algo discutido online.
Mais adiante, tentaremos desvendar como isto funciona, mas há muito mais para explorar primeiro.
Podemos usar typeof() para testar a igualdade da forma habitual.
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
A igualdade de tipos tem de ser exata, pois esta forma de comparação não tem qualquer noção da hierarquia de tipos.
De forma mais flexível, isa diz-nos se um valor tem o mesmo tipo que um comparador ou um subtipo deste.
Pode ser usado tanto na forma infixa como 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
Repara que isa espera um value à esquerda, não um type.
Tentar comparar dois tipos desta 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 isto é relativamente invulgar em Julia idiomática.
Vamos ver no Conceito Multiple Dispatch que muitas vezes é mais eficiente adicionar tipos aos argumentos de uma função e deixar que o mecanismo de dispatch da Julia trate dessa lógica. No entanto, há vários outros conceitos relacionados com tipos de que precisamos de falar antes de chegarmos ao Multiple Dispatch.
Vimos que a hierarquia de tipos forma uma estrutura em árvore (no sentido da ciência de computação, com a raiz no topo).
Cada item da árvore é um node, e estes podem ser divididos em categorias:
julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
Esta é uma distinção importante, porque só os 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
Repara que tentar usar um tipo abstrato não dá mensagem de erro (neste caso), mas o compilador cria um tipo concreto adequado: Int64 em vez de Integer.
Pelo menos, evita que a variável receba um valor que não seja um 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 tipos com um tipo abstrato é, portanto, uma restrição ao tipo da variável, a qualquer subtipo de (no caso acima) Integer.
Isto é algo invulgar no mundo das linguagens de programação: mais fraco do que uma atribuição de tipos em C, mais forte do que uma dica de tipo nas versões recentes do Python.
As funções isabstracttype() e isconcretetype() permitem fazer testes.
Repara que não são apenas negações uma da outra: vamos ver num Conceito mais adiante que alguns tipos podem não 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 este tipo:
julia> eltype([1, 2.3])
Float64