Uploaded avatar of PercyGrunwald

Unicode-egyeztetés Elixirben

@PercyGrunwald
Több, mint 7 éve ezelőtt

Az Exercism feladatai kicsik, mesterségesek, és gyakran látszólag triviálisak. Könnyű azt képzelni, hogy egy tapasztalt fejlesztőnek semmi tanulnivalója sincs belőlük. Ha azonban megoldod ezeket a mesterséges feladatokat, olyan részeibe is bepillanthatsz a nyelvnek, amelyeket korábban esetleg nem fedeztél fel, és ezt a tudást alkalmazhatod is. Ez az új tudás ahhoz vezethet, hogy a valós problémákat hatékonyabban vagy kifejezőbben oldod meg.

A Párhuzamos betűgyakoriság egy közepes nehézségű feladat az Exercism Elixir-kurzusán, amely meglepően sok érdekes tanulságot rejt. A feladat megoldásának egyik központi kihívása a különböző nyelvekből származó betűk kezelése, mivel az egyik teszteset német nyelvű, és az angol ábécén kívüli karaktereket is tartalmaz. Ha az időd nagy részét angol anyanyelvűeknek szánt alkalmazások fejlesztésével töltöd, lehet, hogy most először találkozol ilyen követelménnyel. Az ebből a feladatból szerzett tudás egyértelműen hasznos mindenkinek, aki többnyelvű vagy nem angol nyelvű alkalmazást ír, de sok más területen is segíthet, például a felhasználónevek és jelszavak robusztusabb ellenőrzésében.

A feladat sikeres megoldásához implementálnod kell egy Frequency.frequency/2 függvényt, amely meghatározza a betűk gyakoriságát egy tetszőleges nyelvű stringeket tartalmazó listában:

iex> Frequency.frequency(["Freude", "schöner", "Götterfunken"], workers)
%{
  "c" => 1, 
  "d" => 1, 
  "e" => 5, 
  ...
  "ö" => 2
}

Kezdjük azzal az alapvető problémával, amelyet ennek a függvénynek meg kell oldania, majd haladjunk lépésről lépésre a teljes implementációig.

Annak eldöntése, hogy egy karakter betű-e Elixirben

Hogyan használnád az Elixirt annak eldöntésére, hogy az "a" betű-e?

Azt hiszem, a legtöbben egy /[a-z]/-hez hasonló reguláris kifejezést alkalmaznának:

iex> String.match?("a", ~r/^[a-z]$/)
true

És mi a helyzet az "A"-val?

Valószínűleg az i (kis- és nagybetűre érzéketlen) módosító hozzáadása lenne a legegyszerűbb:

iex> String.match?("A", ~r/^[a-z]$/i)
true

Rendben, és mi a helyzet az "ö"-vel?

Amikor először nekiálltam a problémának, nem voltam biztos benne, mi lenne a legjobb megoldás, de a /[a-z]/i biztosan nem működik:

iex> String.match?("ö", ~r/^[a-z]$/i)
false

Az Exercism-feladat megoldásának központi része annak eldöntése, hogy az "ö" betű-e, mivel a tesztekben szereplő egyik szöveg német:

# parallel_letter_frequency_test.exs
...
# Poem by Friedrich Schiller. The corresponding music is the European Anthem.
@ode_an_die_freude """
Freude schöner Götterfunken
...
"""

Talán ellenőrizhetnéd reguláris kifejezéssel is, hogy egy karakter nem speciális karakter-e, de az valószínűleg hosszú, nem elegáns és törékeny lenne. Mennyire lehetsz biztos benne, hogy minden lehetséges speciális karaktert lefedtél, amelyet bemenetként a függvényed kaphat? Szerintem van jobb megközelítés.

Unicode reguláris kifejezések Elixirben

Erre a problémára jobb megközelítés, ha az Elixir Regex moduljának u módosítóját használod:

unicode (u): lehetővé teszi a Unicode-specifikus mintákat, például a \p-t, és átalakítja a \w, \W, \s és hasonló módosítókat, hogy azok is illeszkedjenek Unicode karakterekre.

Kiderül, hogy az u módosító, és konkrétan a \p minta, valóban elegáns megoldás. A \p mintával egy grafémát (egyetlen Unicode-karakter másik elnevezését) illeszthetsz a Unicode-karakterkategóriák bármelyikében. Ez nemcsak az olyan konkrét kategóriákat foglalja magában, mint az Ll (betű, kisbetű) és az Sc (szimbólum, valuta), hanem a szülőkategóriákat is, például az L (betű) és az S (szimbólum).

A \p{L} mintával bármelyik betűre illeszkedhetsz, bármilyen kis- és nagybetűs írásmódban, bármelyik Unicode által lefedett emberi nyelvben. Ez meglehetősen erőteljes illesztést tesz lehetővé.

Az angol alapvető latin karakterei a szokásos módon működnek:

iex> String.match?("a", ~r/^\p{L}$/u)
true
iex> String.match?("A", ~r/^\p{L}$/u)
true

A latin karakterek umlautos és éles ékezetes változatai sem jelentenek problémát:

iex> String.match?("ö", ~r/^\p{L}$/u)
true
iex> String.match?("Á", ~r/^\p{L}$/u)
true

Győződjünk meg róla, hogy nem egyszerűen bármely karakterre ad találatot. És mi a helyzet azokkal a karakterekkel, amelyek betűnek látszanak, de mégsem azok:

iex> String.match?("$", ~r/^\p{L}$/u)
false
iex> String.match?("@", ~r/^\p{L}$/u)
false

Nagyon szép, de emlékszel, amikor azt mondtam, bármelyik nyelv? Semmi gond:

# Chinese character for "you"
iex> String.match?("你", ~r/^\p{L}$/u)
true

# Cyrillic capital letter "zhe"
iex> String.match?("Ж", ~r/^\p{L}$/u)
true

A Unicode-illesztés alkalmazása a konkrét problémára

Most, hogy van egy eszközünk, amely segít eldönteni, hogy egy graféma betű-e, alkalmazhatjuk a probléma megoldására. A Frequency.frequency/2 függvény egy első implementációja így nézhet ki:

def frequency(texts, _workers) do
  texts
  |> get_all_graphemes()
  |> count_letters()
end

defp get_all_graphemes(texts) do
  texts
  |> Enum.join()
  |> String.graphemes()
end

A count_letters/1 függvénynek nem kell mást tennie, mint alkalmazni a fent meghatározott String.match?(grapheme, ~r/^\p{L}$/u) mintát, hogy növelje az egyes betűk számát a graphemes listában. Íme egy példaimplementáció, amely az erre az Exercism-feladatra adott megoldásomból származik:

defp count_letters(graphemes) do
  Enum.reduce(graphemes, %{}, fn grapheme, acc ->
    if String.match?(grapheme, ~r/^\p{L}$/u) do
      downcased_letter = String.downcase(grapheme)
      Map.update(acc, downcased_letter, 1, fn count -> count + 1 end)
    else
      acc
    end
  end)
end

Ez a függvény grafémák listáját fogadja, például ["a", "A", "ö", "$"], és olyan map-et ad vissza, amely csak a betűket számolja, a kis- és nagybetűt figyelmen kívül hagyva, például %{"a" => 2, "ö" => 1}. Tekintve, hogy ez a függvény bármilyen nyelvű bemenetet képes kezelni, azt mondanám, ez a 9 sor kód meglehetősen erőteljes.

Összegzés

Kiderül, hogy a nem angol betűk illesztése meglehetősen egyszerűvé válik, ha ismered a Unicode-illesztést, és szerencsénkre ez az Elixir Regex moduljának egyik alapvető funkciója. Mielőtt megoldottam ezt az Exercism-feladatot, alig tudtam erről a funkcióról, ma viszont az Elixir-eszköztáram nélkülözhetetlen részének tartom.

Ezt az új eszközt sokféleképpen felhasználhatod; néhány, ami eszembe jut: a jelszavak és felhasználónevek robusztusabb ellenőrzése, vagy akár annak megállapítása, hogy egy bemeneti string érvényes pénznemértéket ír-e le, anélkül hogy kézzel kellene felsorolnod az összes lehetséges pénznemszimbólumot:

iex> currency_string_regex = ~r/\p{Sc}\d+\.\d{2}/u
~r/\p{Sc}\d+\.\d{2}/u

iex> ["$1.00", "£1.00", "¥1.00", "€1.00", "&1.00"] \
...> |> Enum.filter(&String.match?(&1, currency_string_regex))
["$1.00", "£1.00", "¥1.00", "€1.00"]
3. Mar 2019 · Hasznosnak találtad?