GPT-4
Categorias

Ele usou o GPT-4 para criar uma nova linguagem de programação 

É inegável que os largos modelos de linguagem (LLMs), que são a base dos atuais algoritmos de Inteligência Artificial generativa, chegaram para revolucionar, nem que seja somente o ano de 2023, até a tecnologia criar a próxima tendência. Se houver próxima tendência, a julgar pelos profetas do apocalipse que pregam os riscos das IAs.

Entre aqueles que olham com desconfiança o crescimento desses LLMs estão os desenvolvedores de software. Como se low code e no-code não fossem uma preocupação por si só para o mercado de programação, colocando a possibilidade de gerar aplicativos nas mãos de qualquer pessoa, agora até mesmo uma máquina pode programar. Que audácia!

Entretanto, lutar contra a maré é uma atividade tão desgastante e inútil como… lutar contra a maré. Por isso, o tecnólogo Luke Bechtel resolveu usar todo o potencial escondido do GPT-4 para criar uma nova linguagem de programação. Por quê não? Você está preparado para conhecer a palavra de Tenet?

Aviso: alguns trechos desse artigo foram traduzidos usando o próprio ChatGPT, especificamente nos resultados do ChatGPT que Bechtel obteve. É um exercício de metalinguagem (ou um ensaio de como até mesmo a função de jornalista pode estar com os dias contados).

Uma linguagem para todas dominar

Em uma cena emblemática do filme “Eu, Robô”, o personagem de Will Smith, um policial néo-ludita com sérios traumas com essa tecnologia, pergunta ao robô que se julga inteligente se a máquina seria capaz de compor uma sinfonia. O robô rebate a pergunta: “você conseguiria?”. E isso diz muito sobre a eficiência dos LLMs que temos hoje: eles não são capazes (ainda) de produzir o que os grandes gênios humanos fariam, mas se mostram capazes de fazer tudo que o humano médio consegue. Desta forma, o GPT-4 foi treinado em todas as linguagens de programação conhecidas e é capaz de gerar código em todas elas, como um programador médio. Mas, seria a máquina capaz de criar uma linguagem de programação?

Bechtel sondou o algoritmo inicialmente com uma pergunta bem genérica, à respeito de uma “teoria unificadora de desenvolvimento de software”. Seria possível? A resposta da máquina foi surpreendentemente extensa:

Embora seja difícil fornecer uma única teoria geral unificadora do desenvolvimento de software, aqui estão dez princípios-chave que podem servir como um guia abrangente para o desenvolvimento de software de alta qualidade:

Modularidade: Divida o software em componentes menores, autônomos e reutilizáveis para simplificar o desenvolvimento, manutenção e compreensão.

Abstração: Oculte a complexidade do sistema fornecendo interfaces bem definidas e separando as preocupações para permitir que os desenvolvedores se concentrem em áreas específicas sem se sobrecarregar com detalhes.

Escalabilidade: Projete o software para lidar com uma ampla gama de cargas de trabalho e demandas, com a capacidade de dimensionar de maneira eficiente e eficaz conforme necessário.

Manutenibilidade: Escreva código limpo, compreensível e bem documentado para facilitar a manutenção, atualizações e colaboração.

Testabilidade: Desenvolva uma estratégia abrangente de testes, incluindo testes unitários, de integração e de sistema, para garantir que o software seja robusto e confiável.

Adaptabilidade: Projete o software com a flexibilidade para acomodar futuras mudanças, como atualizações de requisitos, tecnologias ou necessidades do usuário.

Desempenho: Otimize o software para ser executado de maneira eficiente, minimizando o uso de recursos e maximizando os tempos de resposta, sem comprometer a funcionalidade ou a manutenibilidade.

Segurança: Implemente medidas de segurança robustas para proteger o software e os dados do usuário contra acesso não autorizado, uso indevido e vulnerabilidades.

Colaboração: Incentive a comunicação eficaz e o trabalho em equipe entre desenvolvedores, testadores, designers e outros interessados para garantir uma compreensão compartilhada dos objetivos, requisitos e progresso do projeto.

Melhoria contínua: Adote uma cultura de aprendizado contínuo, avaliação e desenvolvimento iterativo para melhorar o software e o processo de desenvolvimento ao longo do tempo, adaptando-se a novas técnicas, tecnologias e melhores práticas.

Para quem não entende como um algoritmo pode gerar uma resposta desse calibre, é importante ter em mente que “LARGOS modelos de linguagem” não tem esse nome por acaso. Por “largo”, queremos dizer que eles são alimentados com uma quantidade absurda de dados, principalmente texto. Em seu banco, o GPT-4 teve acesso a vários, talvez centenas, de artigos relacionados com o mesmo tema. O que a “inteligência” faz é associar nexos, desmembrar as mensagens em fragmentos e reorganizá-los em uma resposta. Não é tão diferente do funcionamento do próprio cérebro humano e tenha a certeza de que todas as suas opiniões a respeito do mesmo assunto foram geradas a partir de vários artigos que você mesmo leu e correlacionou na sua cabeça.

Porém, é aquilo: falar é fácil. A teoria de uma linguagem está na tela. Seria o ChatGPT capaz então de criar uma práxis, uma sintaxe que obedeça aos princípios que ele mesmo estabeleceu?

Mão na massa

Nas palavras do ChatGPT (e que estranho é escrever isso…): “Nesta nova linguagem, chamada ‘TenetLang’, vamos combinar uma sintaxe simples inspirada em Python com algumas características de linguagens funcionais e orientadas a objetos”. Tenet então adotaria algumas regras em seu design (novamente, copiando, colando e traduzindo do ChatGPT, pelo ChatGPT):

  1. Escopo baseado em indentação, semelhante ao Python.
  2. Tipagem forte com inferência de tipos, inspirada em TypeScript e Kotlin.
  3. Funções e fechamentos de primeira classe, semelhantes ao JavaScript.
  4. Estruturas de dados imutáveis por padrão, com contrapartes mutáveis opcionais.
  5. Uma sintaxe lambda concisa para funções anônimas.
  6. Combinação e desestruturação de padrões, inspirados em Haskell e Rust.
  7. Suporte integrado para simultaneidade usando async/await e threads leves.
  8. Interoperabilidade com outras linguagens usando uma Interface de Função Estrangeira (FFI).

Parece uma grande salada de linguagens. E funciona? No mesmo prompt, o algoritmo disponibiliza um exemplo. “Abaixo está um exemplo simples de um programa que demonstra a sintaxe e recursos do TenetLang. Este programa define uma classe Person, cria uma lista de objetos Person, filtra-a com base na idade e a ordena em ordem alfabética pelo nome, tudo isso lidando com concorrência”. Segue o código:

class Person {
    let name: String
    let age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }

    toString() -> String {
        return f"{this.name} ({this.age})"
    }
}

async function fetchPersons() -> List[Person] {
    // Simulate an asynchronous fetch operation using lightweight threads
    await sleep(1000)
    return [
        Person("Alice", 32),
        Person("Bob", 28),
        Person("Carol", 35),
        Person("Dave", 23)
    ]
}

function filterAdults(persons: List[Person]) -> List[Person] {
    return persons.filter(p => p.age >= 18)
}

function sortByName(persons: List[Person]) -> List[Person] {
    return persons.sortBy(p => p.name)
}

async function main() {
    let persons = await fetchPersons()
    let adults = filterAdults(persons)
    let sortedAdults = sortByName(adults)

    for person in sortedAdults {
        print(person)
    }
}

main()

Perceba que o WordPress não usa cores na codificação acima porque não reconhece os princípios de Tenet. Por outro lado, a resposta do ChatGPT usa color coding como se Tenet fosse uma linguagem especificada:

“Este exemplo demonstra como o TenetLang atende aos seguintes critérios:

  1. Simplicidade e legibilidade: O código é fácil de entender, com uma sintaxe semelhante ao Python.
  2. Múltiplos paradigmas de programação: A linguagem suporta tanto a programação orientada a objetos (baseada em classes) quanto a programação funcional (funções de primeira classe, fechamentos e lambdas).
  3. Tipagem forte e inferência de tipos: Os tipos são especificados explicitamente para as propriedades das classes e assinaturas de funções, mas a linguagem infere os tipos para variáveis locais.
  4. Estruturas de dados imutáveis: A palavra-chave let declara variáveis imutáveis, promovendo um código seguro e previsível.
  5. Concorrência: A sintaxe async/await permite lidar facilmente com a execução assíncrona de código e threads leves.
  6. Interoperabilidade: Embora não demonstrado neste exemplo, o TenetLang suportaria chamadas de funções ou uso de bibliotecas de outras linguagens por meio de um mecanismo FFI”.

O GPT-4 não apenas aceitou o desafio, como, a partir de um único prompt, entregou a aplicação prática dos princípios estabelecidos e ainda explicou como o exemplo se encaixa no contexto. O algoritmo ainda finalizou sua resposta com um exercício de humildade que nem todo desenvolvedor de carbono apresenta: “oObserve que criar uma linguagem completa é uma tarefa complexa e este exemplo oferece apenas uma visão geral de como a sintaxe e os recursos do TenetLang poderiam ser”.

Tenet vive!

Luke Bechtel não se deu por satisfeito e retornou ao ChatGPT com uma pergunta criada para desmontar a máquina: e em que Tenet poderia ajudar no processo de computação distribuída? Para quem se preocupa com essa questão, Julia costuma atender muito bem tais necessidades. Tenet estaria à altura de uma tarefa tão específica?

Novamente, o algoritmo devolveu um textão gigante com argumentação, princípios, exemplo e explicação! Sim, Tenet dá conta.

“Para melhorar o suporte do TenetLang à computação distribuída, podemos introduzir recursos que facilitem a comunicação e coordenação entre os componentes distribuídos”, respondeu a máquina antes de apresentar algumas ideias:

  1. Passagem de mensagens e atores leves: Incorporar um mecanismo de passagem de mensagens embutido e atores leves (inspirados em Erlang ou Akka), que permitem a comunicação assíncrona e não bloqueante entre componentes distribuídos.
  2. Chamadas de Procedimento Remoto (RPC): Integrar suporte para chamadas de procedimento remoto para permitir a comunicação entre serviços em execução em máquinas diferentes ou em linguagens diferentes. Isso poderia ser feito fornecendo bibliotecas ou frameworks integrados para protocolos de RPC populares, como gRPC ou Apache Thrift.
  3. Serialização e desserialização: Incluir suporte para serialização e desserialização eficientes de dados, permitindo a transmissão fácil de estruturas de dados complexas entre componentes distribuídos. Considere incorporar suporte a formatos populares como JSON, BSON ou Protocol Buffers.
  4. Estruturas de dados distribuídas: Fornecer suporte integrado para estruturas de dados distribuídas, como tabelas de hash distribuídas (DHTs) ou bancos de dados distribuídos, que podem ser compartilhados e manipulados em vários nós.
  5. Tolerância a falhas: Integrar recursos que promovam a tolerância a falhas, como suporte para árvores de supervisão, repetições automáticas e timeouts, facilitando a construção de sistemas distribuídos resilientes.

Em seguida, foi mostrado um exemplo atualizado que demonstra o suporte do TenetLang à computação distribuída, usando um mecanismo de passagem de mensagens e atores leves:

import actors
import rpc

class Person {
    // Same as before...
}

// RPC service definition for a remote data source
service PersonService {
    rpc fetchPersons() -> List[Person]
}

actor PersonFetcher {
    let service: PersonService

    constructor(service: PersonService) {
        this.service = service
    }

    receive(msg) {
        case FetchPersons => {
            let persons = this.service.fetchPersons()
            reply(persons)
        }
    }
}

function filterAdults(persons: List[Person]) -> List[Person] {
    // Same as before...
}

function sortByName(persons: List[Person]) -> List[Person] {
    // Same as before...
}

async function main() {
    // Connect to a remote PersonService
    let personService = rpc.connect(PersonService, "http://example.com/person-service")

    // Create a PersonFetcher actor
    let fetcher = PersonFetcher(personService)

    // Send a FetchPersons message to the fetcher and await the
        let persons = await fetcher.send(FetchPersons)
    let adults = filterAdults(persons)
    let sortedAdults = sortByName(adults)

    for person in sortedAdults {
        print(person)
    }
}

main()

De acordo com o algoritmo, estamos diante das seguintes funcionalidades para computação distribuída:

  1. Passagem de mensagens e atores leves: O ator PersonFetcher se comunica de forma assíncrona com o PersonService usando o método receive e a função send.
  2. Chamadas de Procedimento Remoto (RPC): Um PersonService remoto é definido, e uma instância é conectada usando a função rpc.connect.
  3. Serialização e desserialização: Embora não seja explicitamente mostrado no exemplo, é implícito que o mecanismo RPC lida com a serialização e desserialização dos dados ao se comunicar com o serviço remoto.

Todos esses recursos teriam como objetivo facilitar o processo de “construir, gerenciar e dimensionar sistemas distribuídos”.

Mais uma vez, Bechtel não se deu por satisfeito. Algo no código estava aguçando sua curiosidade: PersonService. Por que PersonService não poderia ser simplesmente uma classe? O ChatGPT devolveu outro textão justificando suas decisões de design:

No exemplo atualizado, o PersonService representa uma definição de serviço remoto, não uma classe regular. Ele atua como uma interface para uma fonte de dados remota que pode buscar objetos Person de outro local ou serviço, possivelmente em uma máquina diferente ou até mesmo implementado em uma linguagem de programação diferente. Ao definir o PersonService como um serviço RPC, descrevemos explicitamente o contrato de como os clientes podem interagir com este serviço remoto.

Em um contexto de computação distribuída, os serviços geralmente estão distribuídos em vários nós ou processos para obter melhor escalabilidade, tolerância a falhas e desempenho. O PersonService desempenha o papel de um serviço que reside em um nó ou processo diferente, e o aplicativo principal se comunica com ele usando um mecanismo de chamada de procedimento remoto (RPC).

Em contraste, uma classe normalmente representa um objeto dentro do mesmo processo ou aplicativo, sem suporte embutido para comunicação remota. Ao usar um serviço RPC, podemos abstrair as complexidades da comunicação em rede, serialização e desserialização, permitindo que os desenvolvedores se concentrem na funcionalidade central e lógica de sua aplicação.

No exemplo do TenetLang, usamos a função rpc.connect para conectar a uma instância remota do PersonService usando uma URL especificada. Essa conexão retorna uma instância “proxy” que pode ser usada para fazer chamadas remotas como se fossem chamadas de método local. O mecanismo RPC lida de forma transparente com a comunicação em rede, serialização e desserialização de dados.

Para resumir, o PersonService é definido como um serviço RPC em vez de uma classe para enfatizar seu papel como um serviço remoto em um cenário de computação distribuída e facilitar a comunicação entre serviços de maneira mais transparente e simplificada.

Luke Bechtel ainda submeteu o ChatGPT a uma sabatina sobre detalhes bens específicos de Tenet e a máquina conseguiu se sair bem de todas as perguntas.

O futuro de Tenet

Já que estamos falando de uma Inteligência Artificial criando uma linguagem, porque não perguntar para essa mesma IA como ela integraria IAs em sua linguagem?

“Para integrar modelos de linguagem como o GPT-3 ou ChatGPT ao TenetLang, podemos introduzir uma biblioteca integrada que forneça uma interface simples e conveniente para interagir com esses modelos. Essa biblioteca lidaria com as complexidades de gerenciar solicitações de API, autenticação e serialização/desserialização de dados”, respondeu o GPT-4.

Sim, é uma resposta bem padronizada, mas Bechtel provocou os limites da IA e indagou sobre uma integração mais ousada entre Tenet e esse ecossistema. É aí que a metalinguagem começa a ficar estranha:

Para explorar uma integração mais profunda, podemos considerar recursos a nível de linguagem que aproveitem as capacidades desses modelos para auxiliar os desenvolvedores durante o processo de codificação.

Uma ideia é introduzir “sugestões inteligentes” alimentadas por grandes modelos de linguagem dentro da própria linguagem. Essas sugestões podem ajudar os desenvolvedores com preenchimento automático de código, documentação ou até mesmo sugestões de refatoração. Aqui está um esboço de como isso poderia funcionar:

Adicionar uma sintaxe especial para solicitar uma sugestão inteligente do modelo de linguagem dentro do código. Isso poderia ser feito usando uma palavra-chave designada, como suggest, seguida da ação desejada, como complete (completar), document (documentar) ou refactor (refatorar).

Integrar o grande modelo de linguagem no compilador ou interpretador do TenetLang, para que as sugestões inteligentes sejam geradas e incorporadas durante o processo de compilação ou interpretação do código.

Armazenar em cache as sugestões ou integrar com o fluxo de trabalho do desenvolvedor, como editores de código ou IDEs, para fornecer assistência contínua em tempo real.

Tem como essa toca de coelho se tornar ainda mais profunda? Que tal uma linguagem criada por IA que está preparada para ser lida, comentada, alterada por IA, enxagua, repete, sem intervenção humana? Com um IDE “consciente” que se atualiza sozinho? São as sugestões seguintes do GPT-4, que estabeleceu um plano de ação completo em questão de segundos, trazendo funcionalidades que parecem ficção científica:

Auto-completar semântico consciente:

  • Melhore o mecanismo de auto-completar da IDE com um LLM.
  • Treine o LLM em um grande corpus de código e ajuste-o no código específico do projeto.
  • Conforme o desenvolvedor digita, forneça ao LLM o contexto do código circundante e obtenha sugestões de auto-completar.

Refatoração automatizada:

  • Desenvolva uma ferramenta de refatoração que aproveite um LLM para analisar a estrutura do código e sugerir melhorias.
  • Treine o LLM em um conjunto diversificado de código bem-estruturado e exemplos de refatoração.
  • Analise o código do projeto e aplique sugestões de refatoração geradas pelo LLM, possivelmente com a aprovação do desenvolvedor.

Depuração inteligente:

  • Aprimore as capacidades de depuração da IDE ou do depurador integrando um LLM.
  • Treine o LLM em exemplos de código com erros comuns, sessões de depuração e soluções.
  • Analise dados em tempo de execução e forneça insights ou correções possíveis com base em sugestões geradas pelo LLM.

Tradução de código para código:

  • Desenvolva uma ferramenta de tradução de código que utilize um LLM para converter código de uma linguagem para outra.
  • Treine o LLM em exemplos de código paralelo nas linguagens de origem e destino.
  • Traduza o código de entrada alimentando-o para o LLM e gerando o código correspondente na linguagem de destino.

Reconhecimento e aplicação de padrões de design:

  • Desenvolva uma ferramenta que utilize um LLM para analisar o código e reconhecer padrões de design aplicáveis.
  • Treine o LLM em exemplos de padrões de design e seu uso em diversos contextos.
  • Sugira padrões de design apropriados para o contexto atual do código com base em recomendações geradas pelo LLM.

Gerenciamento de projeto e colaboração:

  • Integre LLMs em ferramentas ou plataformas de gerenciamento de projetos.
  • Treine o LLM em dados de gerenciamento de projetos, como atribuições de tarefas, estimativas e perfis de desenvolvedores.
  • Use o LLM para analisar requisitos do projeto e sugerir automaticamente atribuições de tarefas e estimativas de esforço.

Sumarização de código:

  • Desenvolva uma ferramenta de sumarização de código que aproveite um LLM para gerar resumos legíveis por humanos.
  • Treine o LLM em exemplos de código e seus respectivos resumos.
  • Gere resumos de código alimentando-o para o LLM e processando a saída gerada em linguagem natural.

Gerenciamento automático de recursos:

  • Aprimore o compilador ou ambiente de execução com um LLM que analisa o código para o uso de recursos.
  • Treine o LLM em exemplos de gerenciamento eficiente de recursos e técnicas de otimização.
  • Aplique sugestões geradas pelo LLM para otimizar alocação, desalocação ou coleta de lixo de recursos.

Navegação inteligente de código:

  • Integre um LLM à IDE ou ao editor de código para auxiliar na navegação de código.
  • Treine o LLM em exemplos de estrutura de código, relacionamentos de componentes e dependências.
  • Use o LLM para sugerir destinos de navegação relevantes ou exibir um mapa de código inteligente com base no contexto do desenvolvedor.

Recursos de aprendizagem adaptativa:

  • Integre LLMs em plataformas de aprendizagem, tutoriais ou sistemas de documentação.
  • Treine o LLM em conceitos de programação, exemplos e diversos recursos de aprendizagem.
  • Personalize as experiências de aprendizagem com base no conhecimento, nível de habilidade e preferências do desenvolvedor usando recomendações geradas pelo LLM.

Ainda segundo a resposta do algoritmo, “cada um desses recursos requer uma combinação de integração de LLM e modificações na linguagem de programação, compilador, interpretador, IDE ou outras ferramentas de desenvolvimento. A chave para implementar esses recursos de maneira eficaz é treinar o LLM em dados relevantes e diversos”.

Tenet tem promessas, mas teria um futuro, depois que o prompt foi fechado? Luke Bechtel criou um repositório para Tenetlang e está aceitando contribuições (humanas). Se pegar tração, podemos estar diante de um novo paradigma da computação ou apenas uma curiosidade da era dos largos modelos de linguagem.