Zoey de Souza Pessanha
Posted on September 30, 2021
AVISO INICIAL
Este artigo é uma tradução (não direta e com adaptações) de um artigo publicado no blog do exercism.io pelo Percy Grunwald. O artigo original pode ser encontrado por esse link.
Uma das maiores vantagens de usar uma linguagem expressiva como Elixir é a possibilidade de solucionar problemas de forma clara, explícita e com um toque de elegância devido a sua sintaxe!
Hoje vou mostrar como podemos identificar ou "casar" (it's a match!) caracteres pertencentes ao padrão Unicode. Para quem não conhece ou não sabe em detalhes, Unicode é uma solução criada para evitar o conflito entre diferentes codificadores de caracteres.
Isto é, cada um dos codificadores de caracteres implementava números diferentes para representar cada símbolo de uma ou mais línguas.
Porém, nenhum deles realmente conseguia abranger as extensas possibilidades de combinações, letras, símbolos e pontuações que centenas de línguas possuem. Por isso nasce o Unicode ou, código único (unique code), onde cada caractere recebe um identificador único, não sendo restingido por nenhuma plataforma, programa ou linguagem!
Ok, mas vamos falar de Elixir agora! Já de ínicio vou propor um desafio:
"Implemente uma função
frequencia/1
que dada uma lista de strings devolva a frequência de cada caractere. Lembre que as strings podem estar descritas em qualquer linguagem!"
Exemplo:
iex> Frequencia.frequencia(["ciências", "naturais"])
%{
"a" => 3,
"i" => 3,
"ê" => 1,
"c" => 2,
"n" => 2,
"r" => 1,
"s" => 2,
"t" => 1,
"u" => 1
}
Aqui estão alguns casos de teste:
["Eorum" "argumentum" "audiri" "posse" "per" "raedam" "multum"]
["When" "transplanting" "seedlings" "candied" "teapots" "will" "make" "the" "task" "easier"]
["El" "zorro" "del" "sombrero" "de" "copa" "le" "susurró" "al" "oído" "del" "conejo"]
["Estava" "escurecendo" "e" "ainda" "não" "estávamos" "lá"]
["E" "ʻike" "ʻoe" "i" "ke" "alahaka" "ānuenue" "ma" "hope" "o" "ka" "ua" "ʻana" "o" "nā" "pōpoki" "a" "me" "nā" "ʻīlio"]
["Σήμερα" "είναι" "η" "μέρα" "που" "θα" "ξέρω" "επιτέλους" "τι" "γεύση" "έχει" "το" "τούβλο"]
Vai lá, tenta implementar! Precisa de tempo? Tudo bem, eu espero...
Identificação de letras em Elixir
5 minutos depois... 10 anos depois... Podemos fazer em conjunto! Vamos tentar com testes mais simples!
Partindo do básico, como podemos determinar que "z"
é uma letra?
Obs: Existe uma funcionalidade na linguagem Elixir que torna a resolução desse exercício algo, digamos... único e intrigante! Nada prático, mas interessante! Deixarei uma demonstração no final deste artigo!
O caminho mais "comum" seria escolher o uso de expressões regulares. Uma dica: o site regexr.com será seu guia para entender cada modificador e sintaxes específicas.
iex> String.match?("z", ~r|[a-z]|)
true
Realmente bem simples, mas e a letra "Z"
?
Ah, fácil também:
iex> String.match?("Z", ~r|[A-Za-z]|)
true
iex> String.match?("Z", ~r|[a-z]|i)
true
No segundo exemplo, é adicionado o nosso primeiro modificador: i
modificador caseless que permite a não diferenciação entre letras maiúsculas ou minúsculas.
Tudo bem, por enquanto está bem simples, que tal testarmos com um caractere com acento, como "ê"
. Bem, esse caractere também é uma letra, então...
iex> String.match?("ê", ~r|[a-z]|i)
false
Opa, estranho, mas esperado, uma vez que "ê"
é um caractere especial. Uma solução poderia ser usar uma expressão regular que falhe caso encontre um caractere especial. Algo como (de forma simplificada):
iex> String.match? "É", ~r|[a-z0-9]|i
false
É uma solução válida, mas muito frágil. Como poderíamos cobrir todos os casos possíveis? Fora que seria necessário testar se cada caractere é especial ou não, identificá-lo manualmente e contabilizar sua frequência. Definitivamente tá longe de ser o caminho feliz e muito menos elegante.
Expressões regulares com suporte a Unicode
Existe um modificador para expressões regulares que inclui e permite a identificação de caracteres Unicode numa expressão regular. Esse modificador é o u
unicode (
u
) - habilita a identificação de padrões Unicode específicos como\p
e modifica os já conhecidos operadores\w
,\W
e\s
e similares para que possam detectar caracteres Unicode.
De fato o modificador u
e em especial o operador \p
, torna nossa solução bem elegante. O operador \p
permite a identificação de um grafema.
Grafema é a unidade fundamental de um sistema de escrita que pode representar um fonema em escritas alfabéticas, uma ideia numa escrita ou até uma sílaba nas escritas silábicas. Pode ser entendido como um conjunto de caracteres.
Com este operador é possível detectar um grafema dentro de qualquer categoria de caracteres Unicode. Não incluindo apenas as subcategorias Ll
(letra, caixa baixa) ou Sc
(símbolo, moeda), mas também as categorias mães L
(letra) e S
(símbolo).
Em outras palavras, você consegue identificar qualquer caractere, em qualquer tipografia em qualquer língua humana suportada pelo Unicode com o padrão \p{L}
!
Vamos aos testes então!
Letras latinas comuns do português são facilmente detectadas:
iex> String.match?("o", ~r|\p{L}|u)
true
iex> String.match?("O", ~r|\p{L}|u)
true
Caracteres latinos com acentos agudos ou trema também não causam nenhum problema:
iex> String.match?("ü", ~r|\p{L}|u)
true
iex> String.match?("í", ~r|\p{L}|u)
true
Mas então esse padrão não acusaria verdadeiro para qualquer caractere? Veja a seguir:
iex> String.match?("$", ~r|\p{L}|u)
false
iex> String.match?("&", ~r|\p{L}|u)
false
Entendido. Esses grafemas parecem letras, mas não são! Mas e quanto ao nosso problema com internacionalizações? Bem...
# Grafema que representa "casa" ou "construção" em japônes
iex> String.match?("舎", ~r|\p{L}|u)
true
# Grafema que significa "eu" em cingalês
iex> String.match?("මට", ~r|\p{L}|u)
true
Aplicando a identificação de caracteres Unicode ao nosso desafio
Agora temos todas as ferramentas necessárias para nos ajudar a decidir se um determinado grafema é ou não uma letra. A seguir, temos uma implementação inicial:
defmodule Frequencia do
def frequencia(sentencas) when is_list(sentencas) do
sentencas
|> separar_grafemas()
|> contar_letras()
end
defp separar_grafemas(sentencas) do
sentencas
|> Enum.join()
|> String.graphemes() # sim, esta função é nativa :P
end
end
A função principal é a contar_letras/1
, que aplicará a expressão regular ~r|\p{L}|u
em cada grafema, decidindo se é ou não uma letra! Aqui está uma implementação de exemplo:
defmodule Frequencia do
defp contar_letras(grafemas) do
for grafema <- grafemas, letra?(grafema), reduce: %{} do
acc -> Map.update(acc, grafema, 1, & &1 + 1)
end
end
defp letra?(grafema), do: String.match?(grafema, ~r|\p{L}|u)
end
Nossa função aceita uma lista de grafemas e por meio de compreensões de lista, iteramos sobre cada elemento da lista, apenas mantendo os elementos que passarem no teste (a chamada da função auxiliar letra?/1
) e reduzimos a lista em um mapa, onde cada chave é uma letra e seu valor é a frequência em que tal caractere aparece!
Bônus: Sequências!
No começo deste artigo eu mencionei que Elixir possibilita uma forma bem mais "diferentona" de se resolver esse problema. Isso se deve ao fato de que a própria linguagem em si, suporta caracteres Unicode.
Uma das funcionalidades mais interessantes da linguagem são as sequências! Dado um valor inicial, um valor final e um "passo", é gerado um conjunto que representa aquela sequência (de forma inclusiva).
Exemplos!
iex> Enum.to_list(1..10)
[1, 2, 3, 4, 5]
iex> Enum.to_list(1..5//-1)
[5, 4, 3, 2, 1]
Mas isso é muito comum, vamos incrementar essa funcionaldiade! Que tal criarmos sequências para letras? A expressão ?
antes dos caracteres permite que recupermos seus códigos em decimal:
iex> Enum.to_list(?a..?c)
'abc'
iex> Enum.to_list(?c..?a)
'cba'
Uhumm... Mas onde quero chegar com isso? Lembra que Elixir suporta nativamente caracteres Unicode (infelizmente não para criação de variáveis haha)? Vamos usar isso então:
iex> ?é
233
iex> ?ﬦ
64294
iex> ?😅
128517
Possível solução para o problema
Com um algumas composições de funções, podemos solucionar nosso problema, não de uma maneira eficiente, não de uma maneira totalmente legível, mas certamente com toda a elegância possível!
defmodule Frequencia do
# [...]
defp contar_letras(grafemas) do
Enum.reduce(grafemas, %{}, fn grafema, acc ->
codigo =
grafema
|> String.to_charlist() # charlists são literalmente listas de caracteres
|> hd() # logo, precisamos apenas do cabeçalho da mesma, ou seja, um "codepoint"
cond do
caixa_alta?(codigo) -> Map.update(acc, grafema, 1, & &1 + 1)
caixa_baixa?(codigo) -> Map.update(acc, grafema, 1, & &1 + 1)
latin_A_e_B?(codigo) -> Map.update(acc, grafema, 1, & &1 + 1)
true -> acc
end
end)
end
defp caixa_alta?(ch), do: ch in ?A..?Z # 65..90
defp caixa_baixa?(ch), do: ch in ?a..?z # 97..122
defp latin_A_e_B?(ch), do: ch in ?À..?ɏ # 192..591
end
Conclusão
Descobrimos duas maneiras incríveis de detectar grafemas de múltiplas nacionalidades! Ok, talvez uma delas seja um tanto quanto.. exótica eu diria, mas ainda sim divertida de se implementar!
Essas funcuionalidades permitam que um leque de outras funcionalidades ou problemas sejam contruídos e solucionados com uma expressividade clara, sucinta e simples.
Vale lembrar que no exemplo usando sequências eu limitei os códigos Unicode levando emconta apenas os caracteres latinos básicos e as extensões A e B, além do suplemento 1 latino para fins de exemplificação, mas qualquer caractere Unicode é válido em Elixir!
Posted on September 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.