Encontros

Encontros

Exercício de aprendizagem

Introdução

A esta altura do percurso, já viste muitas definições de funções. Normalmente, eram apresentadas sem grande comentário sobre o resultado no REPL:

julia> f(x) = x^2
f (generic function with 1 method)

Porque é que aparece "generic function with 1 method"?

Suponhamos que queremos ser mais específicos quanto aos tipos dos argumentos.

julia> f(x::Integer) = x^2
f (generic function with 2 methods)

julia> f(x::Real) = x^2
f (generic function with 3 methods)

Ou podemos alterar o número de argumentos:

julia> f(x, y) = x * y
f (generic function with 4 methods)

Parece que as novas definições da mesma função se chamam «métodos». Os métodos são acrescentados a uma lista, em vez de substituírem a definição antiga.

Podemos listar facilmente os métodos:

julia> methods(f)  # output edited for brevity
# 4 methods for generic function "f" from Main:
 [1] f(x, y)
 [2] f(x::Integer)
 [3] f(x::Real)
 [4] f(x)

Se chamarmos a função f, a Julia sabe de alguma forma qual o método a usar:

julia> f(2.5)  # Float64
6.25

julia> f(2)  # Int64
4

julia> f(2, 3)  # Int64, Int64
6

O que é uma função?

Segundo o manual, uma função é um objeto que mapeia uma tupla de argumentos num valor devolvido.

Normalmente pensamos nos argumentos da função em f(2, 3) como dois elementos separados, mas o compilador vê-os como uma única tupla. Isto é verdade mesmo para um único argumento (embora seja preciso acrescentar uma vírgula para o tornar mais claro):

julia> typeof( (2, 3) )
Tuple{Int64, Int64}

julia> typeof( (2,) )
Tuple{Int64}

Se virmos a função como uma caixa negra, podemos dizer que entra uma tupla e sai um valor devolvido. Mais formalmente, a tupla é mapeada num valor devolvido.

Como é que as funções lidam com números e tipos de argumentos diferentes?

Normalmente, chama-se a isto «polimorfismo».

Este tem sido um tema central da informática há várias décadas, e foram adotadas diferentes abordagens.

A abordagem da Julia é invulgar, embora não seja única.

  • As funções são «genéricas», ou seja, podem ter vários métodos associados.
  • As funções são apenas nomes, até terem métodos associados.
  • Os métodos podem ser acrescentados conforme necessário, potencialmente em grande número (atualmente, o operador + é uma função com 161 métodos).
  • Quando uma função é chamada, a Julia examina a assinatura da chamada: o número de argumentos e o tipo atual de cada um.
  • É selecionado o método que corresponde mais de perto à assinatura da chamada.
    • Em geral, o número de argumentos tem de coincidir exatamente (embora o operador splat/slurp ... possa contornar isto parcialmente).
    • São preferidos os métodos cujos tipos coincidem exatamente com a assinatura da chamada.
    • Se for necessário, a Julia procura os supertipos de cada argumento da assinatura da chamada para encontrar o método mais específico e que corresponde mais de perto.
  • O método escolhido é dispatched com os argumentos dados e devolve um resultado.
  • Se não for encontrado nenhum método adequado, é lançado um MethodError.

Repara que a Julia é dinamicamente tipada, por isso o dispatch de métodos acontece em tempo de execução.

Multiple dispatch: um exemplo

A secção anterior teve muitas palavras, por isso vamos traduzir essas ideias em código. Por uma questão de simplicidade, as funções abaixo têm apenas um argumento, mas não é invulgar encontrar funções com dezenas de argumentos (por exemplo, em pacotes de gráficos ou em aprendizagem automática).

Começa por definir alguns tipos personalizados, para representar formas geométricas.

julia> abstract type Shape end

julia> struct Circle <: Shape
           radius::Float64
       end

julia> struct Square <: Shape
           side::Float64
       end

julia> struct Rectangle <: Shape
           length::Float64
           width::Float64
       end

De seguida, podemos definir métodos para calcular a área de cada forma. Cada um recebe um argumento, de um tipo concreto definido pelo utilizador diferente em cada caso.

julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)

julia> area(s::Square) = s.side^2
area (generic function with 2 methods)

julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)

Agora podemos criar algumas formas e calcular as áreas, contando com o mecanismo de dispatch da Julia para usar a fórmula correta para cada uma.

julia> circle = Circle(2.3)
Circle(2.3)

julia> area(circle)  # call signature Tuple{Circle}
16.619025137490002

julia> square = Square(1.6)
Square(1.6)

julia> area(square)  # call signature Tuple{Square}
2.5600000000000005

julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)

julia> area(rectangle)  # call signature Tuple{Rectangle}
2.94

Métodos paramétricos

As assinaturas de métodos podem receber valores de tipo fictícios, mais ou menos da mesma forma que os tipos paramétricos.

A sintaxe é peculiar e exige uma cláusula where:

julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)

julia> same_type([1, 2], 3)
true

julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)

O exemplo acima exige que ambos os argumentos usem o mesmo tipo T, seja ele qual for.

Podemos ser mais específicos impondo uma restrição a T:

julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)

Com este método, só os tipos numéricos idênticos devolvem true.

É possível ter várias variáveis fictícias, separadas por vírgulas na cláusula where.

julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)

Conclusão

O multiple dispatch é muito flexível e de elevado desempenho na Julia, por isso é amplamente usado.

Como sugere o exemplo dos 161 métodos para +, os programadores de Julia procuram definir métodos com tipos bem especificados. Somar dois inteiros é muito diferente de somar dois números de vírgula flutuante ao nível do bytecode, e somar um inteiro a um número de vírgula flutuante acrescenta uma complicação extra: a promoção de tipos.

Quanto mais específico for o tipo, mais otimizações o compilador consegue fazer: algo de importância marginal num exercício trivial do Exercism, mas vital para o desempenho nas grandes simulações numéricas para que a Julia foi concebida.

Se souberes que valores de entrada um método vai receber, diz ao compilador!

Instruções

Neste exercício vais implementar uma simulação de encontros. Isto vai familiarizar-te com os fundamentos do multiple dispatch, o principal paradigma da Julia.

For this exercise, all function definitions should be a single-line declaration with no if/else logic.

Em geral, um encontro envolve uma entidade a que se cruza com outra entidade b e reage a ela.

encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."

Para começar, vamos simular o que acontece quando gatos e cães se encontram.

1. Define tipos personalizados

Primeiro, adiciona um tipo abstrato Pet.

Depois, define os tipos Cat e Dog como subtipos de Pet. Cada um tem um único campo name.

2. Obtém o nome do animal de estimação.

Implementa uma função name() que devolve o nome de um animal de estimação.

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. Define o que acontece quando gatos e cães se encontram

Implementa os métodos meets() para os seguintes encontros:

  • Quando dois cães se encontram, sniff um ao outro.
  • Quando um gato se encontra com um cão, hisses ao cão.
  • Quando um cão se encontra com um gato, chases o gato.
  • Quando dois gatos se encontram, slink.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. Define um encontro entre duas entidades.

Implementa a função encounter() com um único método, para reportar o que acontece quando duas entidades nomeadas se encontram.

julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."

5. Define uma reação de recurso para encontros entre animais de estimação

O que acontece se encontrarem um animal de estimação diferente no seu passeio, como um cavalo?

  • Se um animal de estimação se encontra com outro animal de estimação que não reconhece, is cautious.
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. Define um recurso para quando um animal de estimação se depara com algo que não conhece

Há muitas outras coisas com que os animais de estimação se podem deparar e que não são animais de estimação: carros, pessoas, plantas, catástrofes naturais, asteroides… O que acontece então?

  • Se um animal de estimação se encontra com algo que nunca viu antes, runs away.
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. Define um recurso genérico

Há muitos outros encontros que podem ocorrer na nossa simulação. Um carro encontra um cão, dois eletrões encontram-se e interagem…

É impossível prever todos os encontros antecipadamente, por isso vamos implementar um recurso genérico.

  • Se duas coisas ou seres desconhecidos se encontrarem, nothing happens.
julia> γ1 = Photon("γ1")
Photon("γ1")

julia> γ2 = Photon("γ2")
Photon("γ2")

julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens." 
# This is true, photons just pass through each other.
Editar via GitHub A ligação abre numa nova janela ou separador
Julia Exercism

Estás pronto para começar Encontros?

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