Encontros

Encontros

Exercício de aprendizagem

Introdução

Neste ponto do curso, você já viu muitas definições de função. Normalmente, elas eram apresentadas sem muitos comentários sobre a saída no REPL:

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

Por que "generic function with 1 method"?

Suponha que você queira ser mais específico 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 poderíamos mudar o número de argumentos:

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

Parece que novas definições de uma mesma função são chamadas de "métodos". Os métodos são adicionados a uma lista, em vez de sobrescrever a definição antiga.

Podemos listar os métodos facilmente:

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, Julia de alguma forma sabe qual método 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?

De acordo com o manual, uma função é um objeto que mapeia uma tupla de argumentos para um valor de retorno.

Normalmente pensamos nos argumentos da função em f(2, 3) como dois itens separados, mas o compilador os vê como uma única tupla. Isso vale até para um único argumento (embora seja preciso adicionar uma vírgula para deixar isso mais claro):

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

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

Vendo a função como uma caixa-preta, podemos dizer que entra uma tupla e sai um valor de retorno. De forma mais formal, a tupla é mapeada para um valor de retorno.

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

Isso costuma ser chamado de "polimorfismo".

Esse é um tema importante na ciência da computação há várias décadas, e diferentes abordagens foram adotadas.

A abordagem de Julia é incomum, embora não seja única.

  • As funções são "genéricas", ou seja, podem ter vários métodos associados a elas.
  • As funções são apenas nomes, até que tenham métodos associados.
  • Métodos podem ser adicionados conforme a necessidade, potencialmente em grande número (o operador + é uma função com 161 métodos, atualmente).
  • Quando uma função é chamada, Julia examina a assinatura da chamada: o número de argumentos e o tipo atual de cada um.
  • O método que corresponde mais de perto à assinatura da chamada é selecionado.
    • Em geral, o número de argumentos precisa corresponder exatamente (embora o operador splat/slurp ... possa contornar isso parcialmente).
    • Métodos cujos tipos correspondem exatamente à assinatura da chamada são preferidos.
    • Se necessário, Julia vai olhar os supertipos de cada argumento na assinatura da chamada para encontrar o método que corresponde mais de perto, ou seja, o mais específico.
  • O método escolhido é dispatched com os argumentos dados e retorna um resultado.
  • Se nenhum método adequado for encontrado, um MethodError é lançado.

Repare que Julia tem tipagem dinâmica, então o despacho de métodos acontece em tempo de execução.

Despacho múltiplo: um exemplo

A seção anterior teve muitas palavras, então vamos traduzir essas ideias para código. Por simplicidade, as funções abaixo têm apenas um argumento, mas não é incomum encontrar funções com dezenas de argumentos (como em pacotes de plotagem ou de aprendizado de máquina).

Comece definindo 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

Em seguida, podemos definir métodos para calcular a área de cada forma. Cada um recebe um argumento, de um tipo concreto e definido pelo usuário 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 despacho de Julia para usar a fórmula correta em cada caso.

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 como os tipos paramétricos.

A sintaxe é peculiar, exigindo 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 os dois 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 esse método, apenas tipos numéricos idênticos retornam true.

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

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

Conclusão

O despacho múltiplo é muito flexível e tem alto desempenho em Julia, por isso é usado amplamente.

Como sugere o exemplo dos 161 métodos para +, quem programa em Julia busca definir métodos com tipos bem específicos. Somar dois inteiros é bem diferente de somar dois números de ponto flutuante no nível do bytecode, e somar um inteiro a um número de ponto flutuante traz a complicação extra da promoção de tipos.

Quanto mais específico o tipo, mais otimização o compilador pode fazer: algo de importância marginal em um exercício simples no Exercism, mas vital para o desempenho nas grandes simulações numéricas para as quais Julia foi projetada.

Se você sabe quais entradas um método vai receber, conte para o compilador!

Instruções

Neste exercício você vai implementar uma simulação de encontros. Isso vai te familiarizar com o básico de multiple dispatch, o principal paradigma de Julia.

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

De modo geral, um encontro envolve uma entidade a que encontra outra entidade b e reage a ela.

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

A princípio, vamos simular o que acontece quando gatos e cachorros se encontram.

1. Defina tipos personalizados

Primeiro, adicione um tipo abstrato Pet.

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

2. Obtenha o nome do animal de estimação.

Implemente uma função name() que retorna o nome de um animal de estimação.

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

julia> name(fido)
"Fido"

3. Defina o que acontece quando gatos e cachorros se encontram

Implemente os métodos de meets() para os seguintes encontros:

  • Quando dois cachorros se encontram, eles sniff um ao outro.
  • Quando um gato encontra um cachorro, ele hisses para o cachorro.
  • Quando um cachorro encontra um gato, ele chases o gato.
  • Quando dois gatos se encontram, eles slink.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. Defina um encontro entre duas entidades.

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

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

5. Defina uma reação de fallback para encontros entre animais de estimação

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

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

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

6. Defina um fallback caso um animal de estimação encontre algo que não conhece

Existem muitas outras coisas que os animais de estimação podem encontrar e que não são animais de estimação: carros, humanos, plantas, desastres naturais, asteroides… E o que acontece então?

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

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

7. Defina um fallback genérico

Existem muitos outros encontros que podem acontecer na nossa simulação. Um carro encontra um cachorro, dois elétrons se encontram e interagem…

É impossível prever todos os encontros de antemão, então vamos implementar um fallback genérico.

  • Se duas coisas ou seres desconhecidos se encontram, 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 O link abre em uma nova janela ou aba
Julia Exercism

Tudo pronto para começar Encontros?

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