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
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.
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.
+ é uma função com 161 métodos).... possa contornar isto parcialmente).dispatched com os argumentos dados e devolve um resultado.MethodError.Repara que a Julia é dinamicamente tipada, por isso o dispatch de métodos acontece em tempo de execução.
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
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)
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!
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.
Primeiro, adiciona um tipo abstrato Pet.
Depois, define os tipos Cat e Dog como subtipos de Pet.
Cada um tem um único campo name.
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"
Implementa os métodos meets() para os seguintes encontros:
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
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."
O que acontece se encontrarem um animal de estimação diferente no seu passeio, como um cavalo?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
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?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
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.
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.
Inscreve-te no Exercism para aprenderes e dominares Julia com 35 conceitos128 exercícios, e mentoria humana real, tudo grátis.