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

Guru das notas

Exercício de aprendizagem

Introdução

Até agora, o syllabus 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 de qualquer forma.

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

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

Atribuição de tipos

Contar com a inferência de tipos dá conta de exercícios simples de tutorial, mas, em programas maiores, você provavelmente vai precisar de um controle mais preciso.

Na maioria dos processadores modernos, um inteiro tem Int64 como tipo 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 é do tipo Int16, e sempre será. 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.

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 em uma 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 é parecido internamente.

Julia não tem classes.

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

E, mesmo assim, 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)

Detalhando um pouco:

  • Int16 é um tipo, e podemos criar variáveis desse tipo.
  • Int16 é um subtipo de Signed, e a função supertype() nos mostra isso.
  • Há uma hierarquia de tipos, que sobe 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.

Mais adiante, vamos tentar destrinchar como isso funciona, mas há muito mais para 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 precisa 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 do que está sendo comparado ou um subtipo dele. Ele pode ser usado tanto na forma de função quanto na forma infixa.

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 vai 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

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. Nós com subtipos são chamados de abstract.
  2. 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 só 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 gera mensagem de erro (neste caso), mas o compilador cria um tipo concreto adequado: Int64 em vez de Integer.

Instruções

Neste exercício, você vai fazer um pouco de engenharia de dados ao pré-processar alguns dados de notas. Uma escola com muitos, muitos, muitos (alunos demais?) alunos está tentando fazer uma análise de dados das notas recebidas pelos alunos e quer fazer isso de forma eficiente, então a ideia é minimizar e organizar os dados.

A escala de notas geralmente vai de 0 a 10, então é mais eficiente usar um tipo de dados com apenas oito bits por nota.

  • Alguns professores usam números reais (já que são exigentes com precisão), e você vai precisar convertê-los para o tipo de dados UInt8.
  • Outros professores usam inteiros com números negativos (para punir alunos especialmente indisciplinados), e nesses casos você vai precisar convertê-los para o tipo de dados Int8.

Depois de implementar a função para converter as notas, você vai precisar escrever outra para lidar com as coleções em que elas são guardadas, rebaixando as notas e retornando-as em ordem decrescente.

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

Nas duas funções, você vai precisar tratar entradas inválidas lançando um MethodError.

Note

O tratamento de exceções será abordado em um Conceito posterior, então, para os propósitos deste exercício, você pode usar a seguinte sintaxe:

throw(MethodError(f, args))

Onde f é a função e args é uma tupla com os argumentos passados para a função. Na função demote(n), f é demote e args é (n,):

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

1. Rebaixe as notas

Implemente o método demote(n).

  • Com uma entrada Float64, ele deve arredondar para cima até o número inteiro mais próximo e retornar um tipo de dados UInt8.
  • Com um Integer arbitrário, ele deve retornar o mesmo inteiro em um 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é-processe os dados

Implemente o método preprocess(coll).

  • Com uma entrada Vector, ele deve rebaixar todos os números e inverter o vetor.
  • Com uma entrada Set, ele deve rebaixar todos os números e retornar um vetor ordenado em 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 O link abre em uma nova janela ou aba
Julia Exercism

Tudo pronto para começar Guru das notas?

Crie sua conta no Exercism para aprender e dominar Julia com 35 conceitos128 exercícios e mentoria humana de verdade, tudo de graça.