Pular para o conteúdo

Polimorfismo

O mesmo nome, comportamentos diferentes — e por que isso elimina o if gigante.

Polimorfismo é um nome, várias implementações, escolhidas pelo tipo do objeto. O que ele resolve, na prática: o if gigante que cresce a cada tipo novo.

O problema#

dataforge
// Sem polimorfismo: cada forma nova mexe nesta função
action area(forma):
    match forma["tipo"]:
        point "circulo":
            yield 3.14159 * forma["raio"] ** 2
        point "quadrado":
            yield forma["lado"] ** 2
        default:
            trigger "forma desconhecida"

assert area({"tipo": "quadrado", "lado": 3}) is 9

Acrescentar um triângulo obriga a mexer aqui — e em toda outra função que faça esse match. Perímetro, desenhar, serializar: cada uma tem o seu.

A solução#

dataforge
trait Forma:
    action area()
    action perimetro()

blueprint Circulo(raio) with Forma:
    action area():
        yield 3.14159 * self.raio ** 2
    action perimetro():
        yield 2 * 3.14159 * self.raio

blueprint Quadrado(lado) with Forma:
    action area():
        yield self.lado ** 2
    action perimetro():
        yield 4 * self.lado

// esta função nunca mais muda
action area_total(formas):
    total := 0.0
    cycle f in formas:
        total += f.area()
    yield total

assert area_total([spawn Quadrado(3), spawn Quadrado(4)]) is 25.0

Um triângulo novo é um arquivo novo. Nada do que já existe muda — é o princípio aberto/fechado em ação.

Sobrescrita (override)#

O herdeiro redefine um método da mãe. root chama a versão dela:

dataforge
blueprint Animal:
    action setup(nome):
        self.nome := nome

    action falar():
        yield $"{self.nome} faz algum som"

blueprint Cachorro extends Animal:
    action falar():
        yield $"{self.nome} late"

blueprint Filhote extends Cachorro:
    action falar():
        yield root.falar() + " baixinho"

assert (spawn Animal("bicho")).falar() is "bicho faz algum som"
assert (spawn Cachorro("Rex")).falar() is "Rex late"
assert (spawn Filhote("Pip")).falar() is "Pip late baixinho"

Sobrecarga (overload)#

DataForge não tem sobrecarga por assinatura — dois métodos com o mesmo nome e parâmetros diferentes. O motivo: numa linguagem dinâmica não há tipo em tempo de compilação para escolher qual chamar.

O que existe no lugar, e resolve os mesmos casos:

dataforge
blueprint Registro:
    // valores padrão cobrem o caso de "menos argumentos"
    action registrar(mensagem, nivel := "info", quando := void):
        marca := quando ?? "agora"
        yield $"[{nivel}] {marca}: {mensagem}"

r := spawn Registro()
assert r.registrar("oi") is "[info] agora: oi"
assert r.registrar("erro", "grave") is "[grave] agora: erro"
dataforge
// e 'match' cobre o caso de "tipos diferentes"
action descrever(x):
    match x:
        point Integer as n:
            yield $"inteiro {n}"
        point String as s:
            yield $"texto de {len(s)} letras"
        point [a, b]:
            yield "par"
        default:
            yield "outro"

assert descrever(42) is "inteiro 42"
assert descrever("oi") is "texto de 2 letras"
assert descrever([1, 2]) is "par"

Upcasting e downcasting#

Tratar um Cachorro como Animal é upcasting — sempre seguro, e é o que o polimorfismo faz o tempo todo. O caminho de volta precisa de verificação:

dataforge
blueprint Animal:
    action setup(nome):
        self.nome := nome

blueprint Cachorro extends Animal:
    action buscar():
        yield $"{self.nome} busca a bolinha"

action interagir(a):
    // 'linhagem' diz de que blueprints o objeto descende
    given "Cachorro" in linhagem(a):
        yield a.buscar()
    yield $"{a.nome} não busca nada"

assert interagir(spawn Cachorro("Rex")) is "Rex busca a bolinha"
assert interagir(spawn Animal("bicho")) is "bicho não busca nada"

Tempo de compilação e tempo de execução#

Em linguagens estáticas, sobrecarga é resolvida na compilação e sobrescrita na execução. Em DataForge tudo é na execução — o método vem do objeto real, sempre:

dataforge
blueprint A:
    action quem():
        yield "A"

blueprint B extends A:
    action quem():
        yield "B"

action perguntar(x):
    yield x.quem()

// a função não sabe o tipo; o objeto decide
assert perguntar(spawn A()) is "A"
assert perguntar(spawn B()) is "B"

É mais flexível e mais lento — a busca do método acontece a cada chamada. Ver análise estática.