poniedziałek, 6 kwietnia 2015

Tam gdzie statyczne typowanie nie wystarcza...

tam pojawia się... No właśnie, co? Zanim o tym, najpierw napiszę trochę o problemach z typowaniem. Statyczne typowanie daje nam pewność, że nigdy nie przekażemy innego typu do funkcji niż tego oczekuje. W językach dynamicznych często musimy pisać dodatkowe testy jednostkowe aby mieć jakąś dozę pewności, że dana funkcja zachowa się poprawnie dla niepoprawnego typu. Dla przykładu co ma zrobić funkcja zliczająca elementy w tablicy jeśli zamiast tablicy przekażemy jej liczbę? Mamy kilka możliwości:

  • zwracamy 0 - zazwyczaj nie wywoła to od razu większych problemów, gdyż kod wykorzystujący taką funkcję jest raczej przygotowany na obsługę pustej tablicy. Niestety w ten sposób ukrywamy błąd, którym jest oczywiście przekazanie liczby zamiast tablicy. Znalezienie go przy takim rozwiązaniu może być kłopotliwe.
  • zwracamy ujemną wartość - ujemna liczba elementów tablicy nie ma sensu więc może to sygnalizować jakiś błąd. Problem w tym, że wszędzie, gdzie używamy takiej funkcji musielibyśmy się ubezpieczyć na taką ewentualność. Tutaj znów potrzebowalibyśmy sporej liczby testów jednostkowych by jakoś to wymusić. Jeżeli tego nie zrobimy to istnieje ryzyko, że ktoś np. będzie coś przemnażał przez ujemną liczbę co znów może być trudne do wykrycia (choć łatwiejsze niż w przypadku zwracania 0).
  • rzucamy wyjątek - błąd ukaże się nam od razu więc będzie łatwiejszy w lokalizacji. Tyle, że znów musielibyśmy się przed tym zabezpieczyć w miejscach, gdzie wykorzystujemy taką funkcję. Jeżeli tego nie zrobimy to cała aplikacja się po prostu wywali. Zatem i w tym wypadku potrzebujemy sporej liczby testów.

Wszystkie te rozwiązania mają jedną zasadniczą wadę w stosunku do statycznego typowania. Musimy dopisać sporą liczbę testów by móc wykryć błędy przed uruchomieniem aplikacji. W językach ze statycznym typowaniem, błąd zostanie wychwycony już przez kompilator/interpreter więc testy dla takich problemów są zbędne. No ale czy zawsze?


Typ a wartość

Czasami mimo używania statycznych typów i tak będziemy mieli problem. Dla przykładu weźmy sobie funkcję liczącą średnią z listy liczb. Czyli przyjmujemy sobie taką listę liczb, sumujemy jej elementy i dzielimy przez ich liczbę. A co jeśli przekazana lista jest pusta? Mielibyśmy dzielenie przez 0. Statyczne typowanie nas przed tym nie ustrzeże bo pusta lista to też lista (jest tego samego typu, tak jak 0 i 1). Mamy więc dwa wyjścia. Albo korzystamy z tych samych rozwiązań, które opisałem wyżej albo tworzymy dwa typy danych: jeden to lista pusta, a drugi to lista niepusta. Problem z dwoma typami danych jest taki, że pisanie w ten sposób byłoby raczej karkołomne. W Haskellu wszędzie, gdzie zwracaliśmy np. [Int] teraz mielibyśmy coś w rodzaju Either EmptyList (NotEmptyList Int). Wychodzi więc na to, że nie pozostaje nam nic innego jak pisanie testów i modlenie się przed wrzuceniem tego na produkcję, że wszystko zadziała. Na szczęście mamy jeszcze jedno wyjście (przynajmniej w Haskellu).


LiquidHaskell

LiquidHaskell to w skrócie dodatkowy walidator kodu Haskella, który umożliwia nam pisanie w komentarzach pewnych dodatkowych logicznych warunków co do wartości danego typu. Np. w powyższym przykładzie moglibyśmy dla funkcji liczącej średnią zapisać warunek, że nie przyjmuje ona listy pustej. Warunek ten będzie sprawdzony przed uruchomieniem aplikacji, a oto właśnie nam chodzi. Tylko, że jak to działa?

Najlepiej będzie pokazać to na przykładzie. Weźmy sobie przykładową implementację gry FizzBuzz. Gra polega na wymawianiu kolejnych liczb ale jeśli są one podzielne przez 3 to mówimy "Fizz", jeśli przez 5 to "Buzz", a jeśli przez 3 i 5 to "FizzBuzz". Oto przykładowa implementacja w Haskellu:

Przykładowy wynik działania programu:

Implementacja jest bardzo prosta. Jest też bardzo podatna na błędy. Np. weźmy sobie funkcję numberType i zamieńmy Fizz, Buzz, FizzBuzz na Normal i. Program skompiluje się bez problemu, a aplikacja będzie działać błędnie.

Teraz przyjrzyjmy się implementacji z wykorzystaniem LiquidHaskell:

Różni się ona tylko komentarzami, które są interpretowane przez program o nazwie liquid, który jest dostępny po zainstalowaniu LiquidHaskell. Co możemy z nich wyczytać? Dla przykładu weźmy tę linijkę: Normal (i :: {v:Int | v mod 3 /= 0 && v mod 5 /= 0}). Mówi ona o tym, że konstruktor Normal może przyjąć liczbę i, która jest typu Int ale tylko taką, która nie jest podzielna przez 3 i przez 5. No to wypróbujmy to. Spróbujmy zrobić tak jak poprzednio czyli w funkcji numberType we wszystkich warunkach wpisać Normal i. Po tej zmianie uruchamiamy liquid fizzbuzz.hs i otrzymujemy:

liquid pokazuje nam, że w liniach 31, 32, 33 (czyli tam gdzie zmienialiśmy) nie zgadzają się typy. Dla przykładu w linii 31 oczekiwano typu Int, który nie jest podzielny przez 3 ani przez 5 ale nic na to nie wskazuje. Zauważmy, że wszystkie dane do programu są wczytywane w trakcie jego działania. Zatem liquid musi interpretować wszystkie ify w naszym kodzie by móc czegokolwiek dowodzić. Nie wymaga on więc od nas innego pisania kodu niż zwykle.


Podsumowanie

Jak widać LiquidHaskell to potężne narzędzie. Oczywiście wydłuża to sprawdzanie (i pisanie) kodu ale jak się okazuje w praktyce nie potrzeba wcale dokładać wielu dodatkowych warunków by pozbyć się sporej liczby testów jednostkowych. Taka koncepcja jest szerzej znana jako dependent types i istnieje nawet kilka języków, które takie typy posiadają. Problemem jest oczywiście czas kompilacji takich programów i jak się wydaje LiquidHaskell stara się ten problem rozwiązać. Więcej można poczytać w samouczku.

środa, 1 kwietnia 2015

BŁĄD: znak sekwencją bajtów 0xe2 0x80 0x9e kodowany w "UTF8" nie ma równoważnego w kodowaniu "LATIN2"

Jeżeli ktoś kiedyś natknął się na taki błąd to oznacza, że jego aplikacja powstawała co najmniej 20 lat temu. Komunikat oznacza mniej więcej tyle, że łączy się ona z bazą używając kodowania jednobajtowego latin-2 czy też iso-8859-2, a baza danych używa kodowania utf-8 oraz zawiera tekst, którego nie da się zakodować w latin-2 (np. tekst zawierający długi myślnik). Być może rozwiązanie, które za chwilę przedstawię nikomu się już do niczego nie przyda ale przynajmniej zostanie na pamiątkę dla przyszłych pokoleń.

Nie da się tego problemu tak po prostu obejść. Tzn. jeżeli tylko wykonywane zapytanie na bazie danych przy połączeniu w kodowaniu latin-2 zwraca dane, których nie da się w tym kodowaniu zakodować to każde jedno się po prostu wywali. Zatem jedynym rozwiązaniem tego problemu jest poprawienie tych znaków. Jednakże wyszukiwanie ich ręcznie może być bardzo czasochłonne.

Pierwsza rzecz, którą możemy zrobić to połączenie się psqlem i ustawienie odpowiedniego kodowania dla tego połączenia:

set client_encoding = latin2;

Teraz możemy próbować szukać w jakiej tabeli występuje dany znak. Wystarczy po prostu wykonać:

select * from tabela;

i jeżeli mamy szczęście to otrzymamy od razu komunikat:

BŁĄD: znak sekwencją bajtów 0xe2 0x80 0x9e kodowany w "UTF8" nie ma równoważnego w kodowaniu "LATIN2"

Pozostaje nam teraz zlokalizować wiersz i kolumnę, gdzie interesujący nas znak występuje. Mam na to dwie metody.


Pierwszy sposób

Pierwsza wykorzystuje fakt, że w komunikacie o błędzie występuje zapis szesnastkowy danego znaku. Możemy spróbować zatem go zlokalizować w taki oto sposób:

select id from tabela where encode(kolumna::bytea, 'hex') ilike '%e2809e%';

Zapytanie wykorzystuje postgresową funkcję encode, która potrafi zamienić tekst na reprezentację szesnastkową i w tej reprezentacji poszukujemy ciągu znaków zwróconego nam przez komunikat błędu. Pomijamy tylko 0x występujące w komunikacie. Metoda ta ma dwie wady:

  1. musimy znać kolumnę, w której dany znak może wystąpić (albo podać warunki na każdą kolumnę z użyciem or: ... where encode(kolumna1::bytea, 'hex') ilike '%..%' or encode(kolumna2::bytea, 'hex') ilike '%..%' or ..).
  2. komunikat o błędzie kodowania znaku zwraca nam informację tylko o pierwszym takim przypadku. Jeżeli jest ich więcej to będziemy poprawiać po jednym i czekać aż postgres zwróci nam informację o kolejnym.

Drugi sposób

Druga metoda znajduje wszystkie rekordy, które zawierają "błędne" znaki w tabeli. Niestety wykonanie go trwa znacznie dłużej.

select id
from tabela
where regexp_replace(encode(translate(kolumna, 'ęóąśłżźćńĘÓĄŚŁŻŹĆŃ', '')::bytea, 'hex'), '(.{2})', '\1 ', 'g') ~ '^[8-9a-f]| [8-9a-f]';

Jak już wspomniałem zapytanie wyłapie wszystkie rekordy w tabeli z "niepoprawnymi" znakami w podanej kolumnie. Zapytanie jest dość skomplikowane więc omówię je krok po kroku:

  1. zacznijmy od najbardziej zagnieżdżonego wywołania: translate(kolumna, 'ęóąśłżźćńĘÓĄŚŁŻŹĆŃ', '') Funkcja translate zamienia znaki występujące w jej drugim argumencie na znaki występujące w jej trzecim argumencie. Czyli w tym przypadku po prostu je usuwa. Robimy tak dlatego, że polskie znaczki są jedynymi dwubajtowymi znakami w kodowaniu utf-8, które posiadają swój odpowiednik w kodowaniu latin-2. Takie nas nie interesują. Chcemy wyłapać wszystkie wielobajtowe znaki, których nie ma w latin-2.
  2. encode(..::bytea, 'hex') - zamieniamy znaki na ich szesnastkową reprezentację. Każdy bajt jest teraz zapisany jako dwa znaki.
  3. regexp_replace(.., '.{2}', '\1 ', 'g') - zamieniamy każde dwa znaki jako x i spację. Po prostu rozdzielamy każdy bajt spacją.
  4. ~ '^[8-9a-f]| [8-9a-f]' - operator ~ sprawdza czy podany string odpowiada wyrażeniu regularnemu. Z kolei to wyrażenie sprawdza czy w ciągu znaków występuje 8, 9, a, b, c, d, e lub f, które jest albo na początku stringa albo jest poprzedzone spacją. Chodzi o to, że chcemy wyłapać czy istnieje bajt większy od 127 a taki będzie się zaczynał od tych znaków w reprezentacji szesnastkowej. Jeżeli istnieje to mamy nasz rekord.

Przy tym rozwiązaniu znów musimy testować sobie wszystkie tekstowe kolumny ale przynajmniej wyłapiemy od razu wszystkie rekordy. Możemy oczywiście napisać sobie zapytanie, które wszystkie niedozwolone znaki usunie lub zamieni na inne. Pozostawiam to już w kwestii czytelnika.