Percursos
/
Julia
Julia
/
Exercícios
/
Guru das notas
Guru das notas

Guru das notas

Exercício de aprendizagem

Introdução

Até agora, o currículo não disse muito sobre tipos, mas claramente eles existem na 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 na mesma.

  1. A Julia tem types, que são centrais no seu design.
  2. A Julia consegue normalmente "adivinhar" o tipo, recorrendo a Type Inference.

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

Atribuição de tipos

Confiar na inferência de tipos é suficiente para resolver exercícios simples de tutorial, mas em programas maiores é provável que precises de um controlo mais preciso.

Na maioria dos processadores modernos, um número inteiro assume por defeito o tipo Int64. Vimos no conceito Numbers que um valor pode ser convertido para um 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:

  • É muito conveniente em pequenos scripts.
  • É má para o desempenho e a fiabilidade em programas maiores.

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 é, e será sempre, do tipo Int16. Assim, o compilador sabe quantos bytes deve reservar para ela e pode otimizar o resto do código contando com um tipo estável. Neste aspeto, a variável é mais ou menos semelhante às de uma linguagem de tipos estáticos como C.

A hierarquia de tipos

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-as numa hierarquia de classes e os métodos de classe definem os comportamentos.

O Java e o Ruby são exemplos óbvios deste padrão, mas até o Python é semelhante a nível interno.

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.

E, no entanto, olha para 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)

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.
  • Há uma hierarquia de tipos, que sobe através de 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.

Mais tarde, tentaremos perceber como isto funciona, mas ainda há muito mais para explorar primeiro.

Testar tipos

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, porque esta forma de comparação não tem em conta a hierarquia de tipos.

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

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 dá resultados inesperados. O operador correto é <:, que vamos ver muito mais vezes em conceitos futuros.

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

Tipos abstratos e tipos concretos

Vimos que a hierarquia de tipos forma uma estrutura em árvore (no sentido da informática, com a raiz no topo).

Cada item da árvore é um node, e estes podem dividir-se em categorias:

  1. Os nós com subtipos chamam-se abstract.
  2. Os nós folha, sem subtipos, chamam-se concrete.
julia> subtypes(Integer)  # an abstract type
3-element Vector{Any}:
 Bool
 Signed
 Unsigned
julia> subtypes(Int64)  # a concrete type
Type[]

Esta 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.

Instruções

Neste exercício, vais fazer um pouco de engenharia de dados, ao pré-processar dados de notas. Uma escola com muitos, muitos, muitos (demasiados?) alunos está a tentar fazer uma análise de dados às notas recebidas pelos alunos e quer fazê-lo de forma eficiente, por isso a ideia é minimizar e organizar os dados.

A escala de notas costuma ser de 0 a 10, por isso o mais eficiente é usar um tipo de dados com apenas oito bits por nota.

  • Alguns professores usam números reais (porque são obcecados com a precisão), e vais ter de os converter para o tipo de dados UInt8.
  • Outros professores usam inteiros com números negativos (para castigar alunos especialmente indisciplinados), e nestas situações vais ter de os converter para o tipo de dados Int8.

Depois de implementares a função que converte as notas, vais ter de escrever outra para tratar as coleções que as contêm, rebaixando as notas e devolvendo-as por ordem decrescente.

  • Alguns professores usam Vectors ordenados por ordem crescente para guardar as notas, porque são meticulosos com os seus dados.
  • Outros professores usam Sets, que não estão ordenados, para guardar as notas, porque são um pouco mais preguiçosos.

Em ambas as funções, vais ter de tratar entradas inválidas lançando um MethodError.

Note

O tratamento de exceções será abordado num Conceito mais adiante, por isso, para efeitos deste exercício, podes usar a seguinte sintaxe:

throw(MethodError(f, args))

Em que f é a função e args é um tuplo com os argumentos passados à função. Na função demote(n), f é demote e args é (n,):

throw(MethodError(demote, (n,)))

1. Rebaixa as notas

Implementa o método demote(n).

  • Com uma entrada do tipo Float64, deve arredondar para cima até ao número inteiro mais próximo e devolver o tipo de dados UInt8.
  • Com um Integer arbitrário, deve devolver o mesmo inteiro com o tipo de dados Int8.
  • Todas as outras entradas devem lançar um MethodError.
julia> demote(4.2)::UInt8
5

julia> demote(4)::Int8
4

julia> demote("hi")
MethodError: no method matching demote(::String)     #output truncated

2. Pré-processa os dados

Implementa o método preprocess(coll).

  • Com uma entrada do tipo Vector, deve rebaixar todos os números e inverter o vetor.
  • Com uma entrada do tipo Set, deve rebaixar todos os números e devolver um vetor ordenado por ordem decrescente.
  • Todas as outras entradas devem lançar um MethodError.
julia> preprocess([1, 2, 3])
3-element Vector{Int8}:
 3
 2
 1

julia> preprocess(Set([2.2, 5.8, 3.4]))
3-element Vector{UInt8}:
 6
 4
 3

julia> preprocess(42)
MethodError: no method matching preprocess(::Int64)     #output truncated
Editar via GitHub A ligação abre numa nova janela ou separador
Julia Exercism

Estás pronto para começar Guru das notas?

Inscreve-te no Exercism para aprenderes e dominares Julia com 35 conceitos128 exercícios, e mentoria humana real, tudo grátis.