Executando verificação de segurança...
22

Linguagens Formais e Autômatos

EDIT 1: Não consegui deixar as imagens disponíveis no post por muito tempo, então, para que vocês não fiquem sem referências, deixei o link contendo as imagens.

Introdução

Todos nós sabemos que a área de TI é muito ampla e variada, quando se trata de programação muito se fala de qual a melhor stack, qual melhor curso, estrutura de dados, como criar apps revolucionários se vale a pena ou não fazer faculdade pra ser programador e etc...

Mas poucos falam de tópicos que são fundamentais, que um dia mudaram o mundo e ainda estão mudando, um deles é Linguagens Formais e Autômatos, que irei tentar abordar aqui hoje.

Por que estudar autômatos e para que serve?

Há várias razões pelas quais o estudo de autômatos e complexidade é uma parte importante do núcleo de Ciência da Computação, Autômatos finitos constituem um modelo útil para muitos elementos importantes de hardware e software, vou listar a baixo alguns dos elementos mais importantes:

  1. Software para projetar e verificar comportamento de circuitos digitais;

  2. O "Analisador Léxico" de um compilador típico, isto é, o componente do compilador que divide o texto de entrada em unidades lógicas como identificadores, palavras-chave e pontuações.

  3. Software para examinar grandes corpos de texto, como coleções de páginas Web, a fim de encontrar ocorrências de palavras, frases ou outros padrões.

  4. Software para verificar sistemas de todos os tipos que têm u número finito de estados destintos, como protocolos de comunicação ou protocolos para troca segura de informações.

Linguagens formais e Autômatos existem para serem reconhecedores de cadeias, no nosso dia a dia estamos mais próximos dos reconhecedores de cadeias do que pensamos, como por exemplo o famoso corretor ortográfico, que é uma aplicação que foi criada para reconhecer cadeias de caracteres, ou falando de algo mais moderno, o tão falado ChatGPT, ele também é um reconhecedor de cadeias.

Caractere

Mas a final o que é um caractere? Nada mais é do que um símbolo qualquer, cada letra do alfabeto romano, cada número é um caractere.
Existe uma quantidade infinita de caractere, e com o poder computacional que temos, é impossível trabalhar com números infinitos de dados, então reduzimos a quantidade de caractere para 256, criando a tabela ASCII.

Alfabeto

O alfabeto é um subconjunto de caracteres da tabela ASCII.
Nós utilizamos um símbolo para representar o alfabeto, o símbolo de somatória.

  • Σ ← Significa alfabeto
  • Σ = { a, b, c } Aqui nosso alfabeto está sendo representado pelos caracteres a, b, c.
  • Σ = { 1, 2, 3 } Aqui estamos representando nosso alfabeto com os caracteres 1, 2 e 3.
  • Σ = { 0, 1 } Aqui representamos com 0 e 1.
  • Σ ={@,3, , ,/, A, º, } Aqui temos vários símbolos diferentes que juntos representam nosso alfabeto.

Vale ressaltar que o alfabeto é um conjunto e não uma sequência, na realidade a sequência li nem importa, geralmente colocamos em ordem para facilitar a leitura.

Cadeias

Cadeias são uma sequência de caractere, todos eles pertencentes ao alfabeto, se pegarmos nosso caractere e sequencial-los temos uma cadeia de caractere, como muitos conhecem uma "String".
Strings nada mais são do que uma cadeia de caracteres, vamos chamar nossas cadeias de C e seu índice.

  • C¹ = aba
  • C² = bbaa
  • C³ = b

É possível notar que é possível criar uma infinidade de cadeias com nosso alfabeto.

Propriedades das cadeias

Aqui nós temos algumas propriedades para podermos fazer algumas operações, algumas manipulações.

Tamanho | |

O tamanho é a quantidade de caracteres da cadeia representado por | |

  • C¹ = aba | C¹ | = 3

  • C³ = b | C³ | = 1

Concatenação //

É a fusão de duas ou mais cadeias, ou seja é a concatenação de duas ou mais cadeias de caractere, representado por: //

C¹ = aba

C² = bbaa

A propriedade não é comutativa, se eu alterar a ordem ela altera o resultado.

C¹ // C² = aba // bbaa = ababbaa

C² // C¹ = bbaa // aba = bbaaaba

Cadeia reversa __

É a cadeia invertida, ou seja, ela é escrita de trás pra frente

  • C¹ = aba C¹= aba

  • C² = bbaa C² = aabb

É colocado um travessão mas não consegui colocar aqui com esse editor de texto.

Elemento Neutro λ

Aqui entra uma das principais propriedades, o elemento neutro ele é uma cadeia sem caractere, onde vamos chamá-lo de "vazio", representado pelo símbolo de lambda, λ.

Não confundir com o caractere de espaço em branco da tabela ASCII na posição 32. O λ é ausência de caractere.
Se eu concatenar ele com qualquer cadeia ele não influencia, por isso chamamos de elemento neutro.

C¹ = λ

C² = aba

C¹ // C² // C¹ = C² = aba

| C¹ | = 0

Fecho

Fecho forma um conjunto gerado a partir de concatenações sucessivas de cadeias com ela mesma até o infinito, ou seja, nós temos uma cadeia, e quando nós criamos um conjunto a partir dessa cadeia, nós chamamos de fecho, o mesmo pode ser fecho positivo, quanto fecho estrela.

  • Fecho positivo é representado pelo sinal de mais +
  • O fecho estrela é representado pelo sinal de asterisco *

Fecho positivo

Imagine que temos o Σ = { a, b }

Temos a como uma cadeia válida, pois ela pertence ao alfabeto, ao adicionarmos nela o símbolo de mais, ou seja a+ isso vai nos gerar um conjunto formado pelo próprio a e a concatenação de a com ele mesmo.

a+ = {a, aa, aaa, aaaa, aaaaa, … } Isso é possível fazer com qualquer cadeia, podemos colocar o fecho positivo nela para gerar um conjunto infinito.

(ab)+ = {ab, abab, ababab, abababab, …} Como podemos ver nesse caso, o ab está junto dentro dos parênteses, então se repete o que está dentro infinitamente.

a+ b = {ab, aab, aaab, aaaab, aaaaab} Aqui podemos ver que só incrementamos o a pois b é uma constante, ele não possuí um fecho.

Fecho estrela

O fecho estrela é igual ao fecho positivo, exceto por um detalhe, o elemento neutro vai fazer parte do fecho. Onde vamos incluir mais um elemento que é o vazio λ

a* = {λ, a, aa, aaa, aaaa, … }

Estamos incluindo ele no começo só para facilitar a visualização, isso não é uma sequência, é um conjunto, ou seja não precisa está em rodem.

a* b = a* // b = {λ, a, aa, aaa, …} // b

Linguagem

Linguagem que é representada por L, é uma ou mais regras que definem um conjunto de cadeias.

Linguagem é uma regra e as expressões regulares são os reconhecedores de cadeias.

Eu posso criar uma regra qualquer, e isso se chamara de linguagem, como por exemplo: Digamos que nós temos o Σ = { a, b } então todas as cadeias que começam com a, serão válidas no meu sistema.

Ou seja eu criei uma regra, que diz que as cadeias só serão válidas no meu sistema, as cadeias que iniciam com o caractere a. **L¹ = {a, ab, aa, aaa, abab, aabaabbb, … }

Vamos criar uma regra que diz, só serão válidas no nosso sistema, as cadeias que terminam em b

L² = {ab, b, bbb, aaab, abab, abbabbb, ...} Então é isso, linguagem são regras, e cadeias válidas são aquelas que seguem a regra.

Poder

Poder quantidade de cadeia que uma linguagem reconhece

O poder de uma linguagem é o conjunto de todos os elemento válidos de uma linguagem. Geralmente é infinito, como por exemplo, as cadeias que começam com a, podemos perceber que são infinitas as possiblidades. Para entender melhor esse conceito de poder, vamos supor que nós temos 3 linguagens:

  1. Todas as cadeias que começam com a, L¹ = {a, ab, aab, abba, aaba, … }
  2. Todas as cadeias que terminam em b, L² = { b, ab, aab, babbb, aaabab, … }
  3. Todas as cadeias que começam em a e terminam em b: L³ = {ab, aab, abab, abbb, ... }

Como podemos perceber o e o possuem mais cadeias válidas que o pois todas as cadeias que estão no estão dentro do e do 2º, mas não o contrário.

Então podemos sim dizer que uma linguagem tem mais poder que a outra, mesmo ambas sendo infinitas. Pois a quantidade de cadeias válidas de uma linguagem é maior que a outra, mesmo que matematicamente elas sejam equivalentes, pois são infinitas.

Propriedade das linguagens

Tomando como base as linguagens anteriores, vamos vez as propriedades das linguagens

L¹ = { a, ab, aab, aa, aaa, abbaa, aababa, … } ,todas que começam com a.

L² = { b, ab, bbb, bb, abb, abbab, babab, … } ,todas que terminam em b.

1. União ∪ OU

Então nesse conjunto união nós teremos tudo que está nos 2 conjuntos de L¹ e L²

Em linguagens de programação é o operador “OR” ou “OU

Exemplo: L¹ U L² = {a, ab, aab, aa, aaa, abbaa, aababa,b, bbb, bb, abb, abbab, babab, … }

2. Intersecção ∩ E

É tudo que está em ambos os conjuntos, ou seja, só cadeias que começam com a, e ao mesmo tempo terminam em b. Nas linguagens de programação é operador “AND” ou “E”.

Exemplo: L¹ ∩ L² = {ab, abab, aabb, ababab, … }

3. Não ~

Ou seja ele é o operador que inverte, em linguagens de programação ele é o “NOT” ou “Não”.

Exemplo: ~L¹ = { λ, b, ba, bab, bb, bbb, baa, … } todos que não começam em a.

4. Diferença —

Essa propriedade quer dizer que é um conjunto menos o outro, vamos imaginar L¹ - L² ou seja nós vamos tirar de L¹ o que tiver em L².

Se L¹ é todos que começam com a, e L² é todos que terminam em b, então eu vou remover de L¹ todos que terminam em b

L¹ - L² = { a, aba, aa, aabaaba, aabbbbbaba, … }

Expressões regulares

Expressões regulares são nosso primeiro reconhecedor de cadeias, como mencionado anteriormente, linguagens são regras, e expressões regulares vão obedecer essas regras para que seja possível validar cadeias de caracteres.

Expressão regular, é escrever a linguagem por meio de alguns elementos específicos, então quando eu for escrever uma linguagem, não vamos escrever “começa com a”, nós iremos escrever uma expressão que represente isso, que represente tudo que comece com a.

Expressões regulares são representação de uma linguagem por meio de:

  • Elementos do alfabeto;
  • Parênteses;
  • Propriedades da cadeia ( Elemento Neutro e Fecho );
  • Propriedades da linguagem ( União e Não)

Vejamos exemplos de expressões considerando Σ = { a, b }

  • L = { a, aa, aaa, aaaa, … }
    • L = a+
  • L = { ab, aab, aaab, aaaab, … }
    • L = a+b
  • L = {a, ba, bba, bbba, … }
    • L = b* a
  • L = { a, aa, ab, aab, abb, aabb, aaab, abbb, aabbb, … }
    • L = a+b*
  • L = { a, aab, aabab, aababab, aabababab, … }
    • L = a (ab)*
    • L = (a+b*)+

Exemplos de expressões regulares

L = Todas as cadeias que começam em “a“

  • L = (a+b*)+
    L = Todas que terminam em “b”
  • L = (a*b+)+
    L = Todas as cadeias válidas no alfabeto { a, b }
  • L = (a* b*)*

Nós chamamos essa expressão L = (a* b*)* de “qualquer coisa” pois dado o alfabeto {a, b} ele pode assumir qualquer valor, ou seja pode ser qualquer cadeia desse alfabeto.

Reescrevendo expressões regulares anteriores

L = Todas que começam em “a”

  • L = a(ab)*

L = Todas que terminam em “b”

  • L = (a* b*)* b

L = Todas que possuam um par de “a” juntos

Exemplos de cadeias válidas: { **aa**, b**aa**, **aa**b, ababb**aa**bbab, abab**aa**b }

  • (ab)* aa (ab)*

Como podemos perceber no exemplo anterior não colocamos fecho positivo pois na questão pede explicitamente um par de a juntos, e como é constante então não colocamos o fecho positivo pois se colocássemos estaríamos invalidando a cadeia, pois com o fechoele poderia ser um valor impar.

Usando a propriedade da linguagem

Σ = { a, b }

L = Todas que tenham pelo menos 2 elementos.

  • L = (ab)* aa U (ab)* bb U (ab)* ab U (ab)* ba

L = Todas que não terminam em “b”

  • L = (ab)*a U λ

L = Todas que possuam exatamente 3 “b”

  • L = a* b a* b a* b a*

L = Todas que tenham uma quantidade Par de “a”

  • L = (b* a b* a b*)*

L = Todas que tenham uma quantidade Impar de “a”

  • ~(b* a b* a b*)*

Autômatos Finitos

O que temos no autômato finito

Nós temos dois elementos importantes.

Estado

Estado é a descrição do sistema em um determinado instante, estado também poderia ser uma “tela”.

Transição

Transição é uma mudança de estado, as transições são as setas, que no caso do nosso dígrafo, são as arestas.

Podemos ter 3 tipos de transições:

  1. De usuário: provocada por uma ação do usuário;
  2. Espontâneas: Provocada por uma ação mecânica, do sistema.
  3. Instantânea: Provocada apenas uma vez, e acontece na inicialização do sistema

O que é um Autômato Finito?

É uma quíntupla, formada por:

  • Q: Conjunto finito de estados;
  • Σ: Alfabeto;
  • S⁰: Estado Inicial. S⁰ ∈ Q e é único em todo o autômato;
  • F: Conjunto de estado finais. F ⊆ Q. F são os estados de aceitação de uma cadeia;
  • δ: Função de transição Q x Σ → Q.

Principal objetivo

O principal objetivo dos autômatos é a validação de cadeias. Para uma cadeia ser válida, ela precisa fazer transições que iniciem no estado inicial do autômato e terminem em algum estado final do mesmo. Caso a cadeia termine em um estado dito não final, a cadeia é considerada invalida.

Ele vai testar as cadeias, pegando como exemplo a questão que a gente fez, “Contenham um par de ao autômato finito, vai receber essa cadeia e vai testar se ela contem ou não, assim definindo se é uma cadeia válida ou inválida.

Uma grande utilidade do autômato é fazer um compilador, pois compilador é isso, nós temos uma cadeia de caracteres, que é o algoritmo, e ele vai verificar se aqueles caracteres são cadeias válidas.

Convenções

As convenções usadas aqui são usadas por muitos autores, mas não por todos, portanto, em algumas literaturas, símbolos deferentes podem ser encontrados, sem que isso altere o conceito do mesmo.

  • Nome dos estados, será sempre a letra q, e serão diferenciados por um índice.
    • q⁰, q¹, q², q³ … qn
  • q será sempre o estado inicial.
  • Os estados serão representados por um círculo, com seu nome no interior.
    Estado
  • O estado inicial será demarcado com uma seta que vem do “nada” e e apontando para o estado inicial.
    Estado Incial
  • Os estados finais serão demarcados com um círculo em seu interior.
    Estado Final
  • Duas transições diferentes, partindo de uma mesma origem e chegando a um mesmo destino, poderão ser representados, com uma seta única, e seus nomes serão separados por vírgula.
    Duas Trasições

Determinismo

O autômato finito é dito Determinístico se, e somente se, para cada elemento do alfabeto exista uma, e apenas uma transição de saída, de cada estado pertencente ao autômato, ou seja, um mesmo elemento do alfabeto não pode ter duas ou mais transições de saída de um mesmo estado, obrigatoriamente, deve ter uma saída do mesmo.

Imagine que estamos na nossa tela inicial do programa e temos 2 saídas ao digitar 1, para onde o programa deveria seguir?

Um autômato finito determinístico não pode ter transições espontâneas. Ou seja não pode ter nenhuma transição vazia, ou dos elementos que não seja do alfabeto.

Construção de autômatos finitos

Para construir autômatos finitos, nós precisamos obedecer 2 regras.

  1. A regra da cadeia mínima.
  2. A regra da transição.

Vale ressaltar que se, somente se ao perceber que não teria como gerar a cadeia mínima, ai nós pegamos o resultado errado, e utilizamos a regra da transição, para transforma-los em um estado certo.

Cadeia mínima

Passo 1 - Verificar se os elementos que compõe a linguagem fazem parte do alfabeto;

Passo 2 - Isolar a cadeia mínima.

Para ver a cadeia mínima é fácil, só precisamos remover o fecho estrela, e o que sobrar é a cadeia mínima.

Passo 3 - Escrever uma sequência do estado inicial a um estado final, com a cadeia mínima (esqueleto).

Aqui para a construção do esqueleto, nós precisamos da cadeia mínima, supondo que a cadeia mínima tem 5 caracteres, então nosso esqueleto tem 6 estados. q0, q1, q2, q3, q4, q5

Passo 4 - Tornar o autômato determinístico, usando os lemas e o estado morto quando necessários.

Temos que ter para cada caractere da nossa linguagem, 1 saída válida.

Exemplo

Σ = {0,1} L = 01+

Passo 1 - Todos os elementos que compõe a linguagem fazem parte do alfabeto.

Passo 2 - A cadeia mínima é 01

Passo 3 - montar o esqueleto

Esqueleto do Autômato
Passo 4 - aplicar o determinismo, onde temos que ter uma saída em cada estado, para cada caractere do alfabeto, incluindo o morto.

Autômato Completo

Considerações finais

Espero ter te estigado pelo menos um pouco ao ponto de fazer você ir pesquisar mais sobre o tema, deu muito trabalho escrever esse post, espero de coração que ajude alguém, qualquer dúvida só deixar seu comentário a baixo. ❤️

Carregando publicação patrocinada...
2
2
2

Não se apegue muito a isso por agora, nas instituições de nível superior, Linguagens Formais e Autômatos, só são visto do 5º pra o 7º período dependendo da grade, ou seja, só depois de 2 anos de estudo.

Tenta se apegar ao básico, a base, não sei qual teu foco e objetivo, mas se for pra ser programador, recomendo: algoritmo, programação imperativa, estrutura de dados, programação orientada a objetos, teoria dos grafos, sistemas operacionais, arquitetura de computadores, autômatos, compiladores.

Creio que essa seja uma quantidade enorme de assunto, mas isso é se você quiser aprender a base da base, nível graduação superior. Caso contrário tu pode aprender alguma linguagem de programação, estudar bastante ela, sua sintaxe, e começar a fazer projetos, que geralmente é o caminho que a maioria segue, e com um tempo aprende um framework.

O importante é entender, que mais cedo ou mais tarde talvez você precise, de algum conteúdo que falei a cima.

2
1

A turma de autômatos, é a turma mais cheia do meu curso, ganhando às vezes até das turmas gigantescas de direito. É uma matéria que tem que ter uma base legal de grafos, entender bem o conceito de fecho, e o porquê das coisas. No tempo que estava tendo aulas remotas, meu professor gravou as aulas e deixou disponível no YouTube. Playlist de Autômatos

1
2

O cara simplesmente resumiu a minha matéria da faculdade KKKKKKKK MUITO BOM IRMAO
quando fiz ela, meu professor acabou usando o livro do Hopcroft, muito bom também, se quiserem dar uma conferida (em inglês).

2
2

Velho!! Por que você não criou esse post há 05 anos atrás quando eu estava na faculdade?? ahhahaha.

Muito bom!! Parabéns!

P.S.: Assistam ao filme "O jogo da imitação", que conta a história de Alan Turing

2

Achei massa o conteúdo! Só queria fazer um adendo:

Existe uma quantidade infinita de caractere, e com o poder computacional que temos, é impossível trabalhar com números infinitos de dados, então reduzimos a quantidade de caractere para 256, criando a tabela ASCII.

Se fosse nos anos 70 essa afirmação seria válida, mas hoje em dia não estamos limitados a tabela ASCII. Existem caracteres multibyte1 de até 32 bits, o que dá 2³² caracteres possíveis. Linguagens de programação como JavaScript ou C, por exemplo, suportam caracteres multibyte no seu código.

1

Mas quando se trata de validação de cadêias, imagine que temos que validar um valor infinito de caracteres, princiapalmente na hora de compilar o código, levaria anos, decadas, seculos, milenios, infinito tempo. Não se trata de aceitação de caractere, mas da validação, 2³² é um número muito grande, mas não se compara ao infinito.

Existe alguns Np Problems teoria da complexidade computacional, NP é o acrônimo em inglês para Tempo polinomial não determinístico que denota o conjunto de problemas que são decidíveis em tempo polinomial por uma máquina de Turing não-determinística.

1
1

Então não entendi sua afirmação:

Se fosse noas anos 70 essa afirmação seria válida.

Hoje em dia a afirmação ainda é válida, a tabela ASCII foi uma forma de reduzir a quantidade infinita de caracteres, como você falou, não ficamos limitados a tabela ASCII, a pesar de 2³² ser um número muito grande, quando se trata de poder computacional, isso não é 50% da quantidade máxima de coisas que podemos processar hoje em dia.

Um processador operando a 2,5 GHz (Gigahertz) completa 2,5 bilhões de ciclos por segundo, por exemplo.

2

Eu não estou dizendo que computadores não conseguem processar 2³², não entendi porque você entendeu isso.

A questão é que «então reduzimos a quantidade de caractere para 256, criando a tabela ASCII.»: faria sentido dizer isso nos anos 70 mas hoje em dia qual compilador/corretor/tradutor/<insira-um-tipo-de-software-que-faz-processamento-de-texto-aqui> vai se limitar ao uso de ASCII?

O meu ponto é que hoje em dia não "reduzimos a quantidade de caracteres para 256", por que alguém faria isso?

2

Aeeeeee! Post bonito, post formoso! Tome minhas tabcoins!
Dificil ver um post assim tecnico raiz aqui, é um assunto que todo dev deveria saber, eu fiz um pequeno interpretador de C com instruções reduzidas e foi muito gratificante entender como tudo fucniona ali no baixo nivel. Novamente parabéns pelo post!

1

Meus pensamentos também seguem na mesma linha que os teus, acho que certos conteúdos deveriam ser estudados por todo os devs, ao menos 1 vez na vida, tem muita coisa base para tudo que temos hoje em dia, não é pq já tem pronto que nós não precisamos ao menos entender como funciona. Se possível gostaria de dar uma olhada no teu interpretador se tiver o código, se tiver no github melhor ainda, raramente pessoas fazem coisas do tipo, geralmente estão mais ligadas as coisas mais "modernas" que no fundo, só é uma aplicação de uma coisa mais antiga.

2

Claro que pode ver, segue o link do github:
https://github.com/CristianoSword/Mini-C

Eu gosto muito de programação em baixo nivel, acredito que estamos numa época de hardware melhor e códigos piores. Certa vez vi uma palestra da Grace Hopper (criadora do Cobol) falando sobre a importancia dos nanosegundos! Fiquei fascinado de como os devs antigamente faziam tanto com muito pouco. Se quiser ver a palestra vou deixar o link tbm:
https://www.youtube.com/watch?v=9eyFDBPk4Yw&t=1s

2
2

Excelente! Deu até saudades das aulas de automatos agora!

Na minha época de faculdade, a maior parte dos meus colegas não se importavam com automatos, achavam chato e inútil. Mas isso foi a solução para um grande problema que enfrentei uma vez no trabalho.
O sistema que estavamos desenvolvendo gerava fluxo de caixa com base nas compras e vendas do comércio, e tinha uma funcionalidade que chamávamos de 'campos lógicos', a função desses campos era permitir que o usuário criasse uma fórmula personalizada para uma linha do fluxo de caixa dele, podendo fazer calculos usando linhas anteriores ao fluxo ou buscando dados de alguma tabela do banco. Nossa primeira implementação, foi antes de eu ter aulas de automatos, e naturalmente, fiz a validação cheia de IFs, FORs e afins, horrível de se ver, e com um desempenho terrível, levava quase 3h para executar um fluxo de caixa com 1 milhão de linhas...Depois das aulas de automatos, reescrevi o código fazendo um automato finito. O código ficou muito mais legível e eficiente, o mesmo fluxo de caixa passou a ser executado em menos de dois minutos. E isso me fez aprender outros termos ainda, muito interessantes, como Dynamic Branch Prediction, que é muito implementado em compiladores e tem um impacto gritante no desempenho das aplicações...

1

Sensacional sua solução, muitas pessoas da minha turma simplesmente negligenciam muitas matérias, "Não vou precisar disso", "faculdade com grade desatualizada", já eu sempre tentei enxergar de um modo diferente, tentar aprender o que não se acha com facilidade na internet, hoje muitas pessoas se perguntam sobre o chatGPT, mas não sabem ao menos o que é um reconhecedor de cadeias, e eu não estou nem me referindo a pessoas que estão começando, mas pessoas que já passaram por um nível superior, e às vezes até já tiveram aulas de autômatos.

2

Ótimo POST! Gostaria de ter visto isso no ano passado, pois não iria ter dificuldade com essa disciplina e posteriormente compiladores.

A pessoa pode não entender a fundo os detalhes, mas os conceitos de como tudo isso funciona constrói um conhecimento fora da caixa para um programador.

Interessante é que na minha cabeça, linguagens formais estava limitada ao uso de regex e construção de compiladores, ver outros exemplos acabou ampliando a minha visão.