Usprawnienie programowania z użyciem typów w Glasgow Haskell

Transkrypt

Usprawnienie programowania z użyciem typów w Glasgow Haskell
Politechnika Łódzka
Wydział Fizyki Technicznej, Informatyki
i Matematyki Stosowanej
Instytut Informatyki
Michał Sośnicki
nr albumu 180692
Usprawnienie programowania z użyciem
typów w Glasgow Haskell Compiler
Praca inżynierska
napisana pod kierunkiem
dr inż. Jana Stolarka
Łódź 2016
ii
Spis treści
Spis treści
iii
1 Wstęp
1.1 Cele pracy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Przegląd literatury . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3 Układ pracy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Budowa kompilatorów i systemy typów
2.1 Budowa kompilatora . . . . . . . . . .
2.1.1 Parser . . . . . . . . . . . . . .
2.1.2 Renamer . . . . . . . . . . . . .
2.1.3 Type checker . . . . . . . . . .
2.2 Rozszerzenia Glasgow Haskell Compiler
2.2.1 Rodziny typów . . . . . . . . .
2.2.2 Częściowe sygnatury typów . .
3 Użyte narzędzia i technologie
3.1 Język programowania . . . .
3.2 Narzędzia . . . . . . . . . .
3.2.1 Git . . . . . . . . . .
3.2.2 Trac . . . . . . . . .
3.2.3 Phabricator . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4 Usprawnienia Glasgow Haskell Compiler
4.1 Zgłoszenie nr 10839 . . . . . . . . . . . .
4.1.1 Rozwiązanie . . . . . . . . . . . .
4.1.2 Testy . . . . . . . . . . . . . . . .
4.2 Zgłoszenie nr 10982 . . . . . . . . . . . .
4.2.1 Rozwiązanie . . . . . . . . . . . .
4.2.2 Testy . . . . . . . . . . . . . . . .
4.3 Zgłoszenie nr 11098 . . . . . . . . . . . .
4.3.1 Rozwiązanie . . . . . . . . . . . .
4.3.2 Testy . . . . . . . . . . . . . . . .
iii
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
2
3
3
.
.
.
.
.
.
.
5
5
7
7
10
13
13
15
.
.
.
.
.
19
19
19
19
20
21
.
.
.
.
.
.
.
.
.
23
23
23
26
27
28
29
30
31
32
SPIS TREŚCI
SPIS TREŚCI
5 Podsumowanie i wnioski
33
5.1 Dyskusja wyników . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5.2 Perspektywy dalszych badań . . . . . . . . . . . . . . . . . . . . . . 33
Bibliografia
35
Spis rysunków
39
Spis fragmentów programów
41
A Płyta CD
43
iv
Rozdział 1
Wstęp
Zakresem niniejszej pracy inżynierskiej jest tworzenie kompilatorów dla języków
funkcyjnych ze statycznymi systemami typów. Przedmiotem pracy jest usprawnienie programowania z użyciem typów w Glasgow Haskell Compiler. Temat został
wybrany ze względu na chęć zapoznania się z budową kompilatora oraz udziału
w rozwoju powszechnie używanego programu.
W popularnych, statycznie typowanych językach programowania, gramatyka
przewiduje kilka rodzajów wyrażeń. Są to termy, służące do wyrażania obliczeń,
które mają zostać wykonane po uruchomianiu programu oraz typy, które przypisuje
się termom, by rozróżnić termy poprawne oraz niepoprawne. Niektóre języki, w tym
Haskell, zawierają jeszcze rodzaje (kinds), które pełnią wobec typów taką rolę,
jak typy wobec termów. Zawarte w temacie pracy mechanizmy programowania
z użyciem typów dodają do typów, obok ich pierwotnej roli, możliwości wyrażania
obliczeń, jakie zwykle zarezerwowana są dla termów. Obliczenia te wykonywane są
na etapie sprawdzania typów w czasie kompilacji.
Praca została poświęcona językowi Haskell i kompilatorowi Glasgow Haskell
Compiler. Wybór ten został podyktowany tym, iż jest to projekt open source i dołączenie do niego ułatwione jest dzięki zasobom opisującym jak się wdrożyć oraz
pomocnej społeczności. Drugim kryterium była dostępność rozszerzeń umożliwiających programowanie z użyciem typów. GHC oferuje między innymi uogólnione algebraiczne typy danych (GADTs[8][19]), promocję typów danych (data type
promotion[27]), zależności funkcyjne między parametrami w klasach typów (functional dependencies in multi-parameter type classes[14]) i rodziny typów (type
families[6][7][9]) Szczególnie istotnym rozszerzeniem są rodziny typów, gdyż umożliwiają definiowanie na poziomie typów funkcji wykorzystujących dopasowywanie
wzorców oraz zależności funkcyjne w klasach typów, które dają równoważne możliwości, lecz wymagają programowania w stylu relacyjnym zamiast funkcyjnego.
Biblioteka standardowa GHC dostarcza również pomocne moduły, na przykład
Data.Type, Data.Proxy i GHC.TypeLits, z przydatnymi typami i funkcjami na
poziomie typów, jak operatory logiczne.
Programowanie z użyciem typów możliwe jest również w innych językach. Na
przykład, Scala oferuje do tego path dependent types, a w C++ możliwe jest
metaprogramowanie z użyciem szablonów. Istnieją też języki z typami zależnymi
1
ROZDZIAŁ 1. WSTĘP
jak Agda lub Idris. Wiele z tych języków ma implementacje o otwartych źródłach
i mogłoby służyć do realizacji pracy o tym samym zakresie zamiast Haskella.
Istnieje kilka implementacji języka Haskell. Spośród nich tylko dwie są zgodne
z aktualną specyfikacją języka Haskell 2010, Glasgow Haskell Compiler i Utrecht
Haskell Compiler[12]. Porównanie repozytoriów pozwala stwierdzić, że spośród nich
GHC jest bardziej rozwinięty. Różnica ta objawia się w tym, iż rozszerzenia kompilatora pozwalające na programowanie na poziomie typów jak rodziny typów istnieją
wyłącznie w GHC[25]. Z tego powodu w pracy został wybrany ten kompilator.
Rodziny typów zostały wprowadzone do Haskella w wersji 6.10.1, czyli w roku 2008[10]. Do tej pory są przedmiotem prac naukowych, a ich implementacja
jest nieustannie rozwijana. Na przykład, w roku 2015 zostały wprowadzone do
GHC różnowartościowe rodziny typów[13]. Aktywny rozwój świadczy o tym, iż
jest to temat bardzo aktualny. Czynnik ten sprawia też, iż wiele usprawnień do
nowo dodanych funkcji, ułatwiających korzystanie z nich, wciąż czeka na realizację. Istotny jest też fakt, iż type checker to największy komponent GHC[16], więc
podczas modyfikacji z dużym prawdopodobieństwem powstają nowe błędy. Podjęcie się realizacji tych usprawnień i naprawienia błędów są zatem dobrym tematem
na pracę.
1.1
Cele pracy
Celem pracy jest dokonanie jak największej liczby usprawnień w kompilatorze
GHC. Przez usprawnienia rozumiane są zmiany w GHC lub w powiązanych z tym
projektem bibliotekach. Zmiany w GHC zgłaszane są w systemie Trac, który używany jest w projekcie do organizacji pracy programistów. Jako cele pracy wybranych zostało kilka zgłoszeń spośród tych znajdujących się w systemie Trac.
Podjęte zostały wyłącznie usprawnienia polegające na zmianach w kodzie, w częściach związanych z rozszerzeniami opisanymi wcześniej. Wybrane usprawnienia to:
• Ujednolicenie wyświetlania rodzin typów w błędach i ostrzeżeniach kompilatora.
• Dodanie ostrzeżeń o nieużywanych zmiennych w rodzinach typów.
• Naprawienie błędu z niewłaściwym parsowaniem zmiennych zaczynających
się od podkreślnika w przypadku kompilacji z rozszerzeniem NamedWildCards.
Poprawne wprowadzenie zmiany w kodzie wymaga przejścia przez procedurę
ustaloną przez osoby z prawem zapisu do repozytorium[26]. Konieczne jest zatem
pisanie kodu akceptowalnej jakości, zgodnego z ustalonymi konwencjami. Zgodnie
z procedurą, wykonanie usprawnienia wymaga:
• Informowania o stanie pracy w systemie Trac. Oznaczenie swojego konta jako
właściciela zadania, uzupełnienie informacji na przykład o testach i powiązanych zadaniach, a na koniec oznaczenie zgłoszenia jako zrealizowanego.
2
ROZDZIAŁ 1. WSTĘP
• Przygotowania testów pokrywających wprowadzone zmiany. Dodanie ich do
zestawu testów wykonywanych w czasie walidacji.
• Dokonanie zmian w kodzie w zgodzie z obowiązującymi konwencjami wraz
z dokumentacją. Sporządzenie z nich commitów w systemie kontroli wersji
git.
• Wysłanie łatki do systemu Phabricator, gdzie przejdzie ona inspekcję. Jeżeli nie będzie ku temu przeszkód, zmiany zostaną przeniesione do głównego
repozytorium.
1.2
Przegląd literatury
Podstaw teorii budowy kompilatorów dostarcza [1]. Autorzy opisują budowę kompilatora, a następnie każdą fazę kompilacji. Ma to zastosowanie także w przypadku
kompilatora GHC. Krótki opis poświęcony konkretnie GHC dostępny jest w [16].
Podstawy teorii typów zawarte są w [21]. Omówiony jest w tej pozycji nietypowany
i typowany rachunek lambda i kilka jego wzbogaconych wersji. To bardzo istotne,
gdyż pojęcia z rachunku lambda występują powszechnie w dokumentacji, w kodzie
GHC i są powszechnie używane w dyskusjach przez programistów. Z drugiej strony
część poświęcona podtypowaniu nie ma bezpośredniego zastosowania do GHC oraz
opis nie obejmuje typów zależnych. Szczególnie warte uwagi w ramach wprowadzenia teoretycznego są również dwa kursy, dostępne za darmo na platformie Coursera:
Automata prowadzony przez Jeffa Ullmana oraz Programming Languages prowadzony przez Dana Grossmana. Są to zaadoptowane do formy kursu internetowego
kursy akademickie.
Praca nad GHC wymaga znajomości Haskella, w którym jest tworzony kompilator. [15] i [23] stanowią dobre wprowadzenie do tego języka, obie pozycje są
dostępne za darmo w Internecie. Z rozszerzeniami zmienionymi w tej pracy, TypeFamilies i PartialTypeSignatures można zapoznać się w poradniku użytkownika GHC, w zasobach [24] i [18].
Strona systemu Trac projektu GHC zawiera wiele zasobów pozwalających zainteresowanym programistom wdrożyć się w projekt. Jest tam między innymi opis
jak pobrać i skompilować źródła GHC[17], opis procedury wprowadzania zmian
w kodzie[26] oraz instrukcja konfiguracji i wykorzystania narzędzia Phabricator[20].
1.3
Układ pracy
Rozdział 1 zawiera wprowadzenie i określenie tematu oraz celu pracy. Rozdział 2
zawiera opis teorii budowy kompilatorów i systemów typów związanej z tematem.
Rozdział 3 opisuje technologie i narzędzia wykorzystane w pracy. W rozdziale 4
przedstawiono opis dokonanych zmian w GHC. Rozdział 5 zawiera podsumowanie
czy założone cele zostały osiągnięte. W dodatku A znajduje się płyta CD z ko3
ROZDZIAŁ 1. WSTĘP
dem aplikacji, wersją elektroniczną tej pracy oraz kopią wykorzystanych źródeł
internetowych.
4
Rozdział 2
Budowa kompilatorów i systemy
typów
2.1
Budowa kompilatora
Kompilator to program, który przyjmuje na wejściu reprezentację programu w pewnym języku źródłowym i zwraca na wyjściu równoważny program w innym języku.
Językiem docelowym często bywa kod wykonywalny, bajtkod, ale może to być też
na przykład C lub JavaScript. Pod względem architektury w kompilatorach można wyróżnić frontend, dokonujący przetworzenia programu wejściowego i backend,
generujący kod docelowy. Podział ten umożliwia utrzymywanie wielu frontendów
i backendów, na przykład w GHC dostępne są trzy backendy, generujące kod maszynowy, kod w języku pośrednim LLVM lub kod w C[16]. GHC może być uruchomiony w trybie interaktywnym, jako interpreter GHCi.
Frontend odpowiada za przeanalizowanie tekstu otrzymanego na wejściu zgodnie z gramatyką języka, zbudowanie z niego drzewa składniowego i przeprowadzenie
statycznej kontroli typów. W razie błędów konieczne jest wyświetlenie użytkownikowi wiadomości z opisem wystarczającym do jego zrozumienia. Następnie następuje faza generowania kodu pośredniego. Język, w którym program pisany jest
przez użytkownika, ma zwykle rozbudowaną składnię i jest zaprojektowany z myślą
o łatwym użytkowaniu przez ludzi. Języki pośrednie są z kolei możliwie uproszczone pod względem składni i zaprojektowane z myślą o dalszym przetwarzaniu przez
kompilator. Kod ten przekazywany jest do backendu, gdzie wykonywane są na nim
optymalizacje. Możliwe jest, że ostatnie dwie fazy powtórzą się wielokrotnie, czyli
program przejdzie przez kilka języków pośrednich i faz optymalizacji. Na koniec
następuje wygenerowanie kodu w języku docelowym[1].
Glasgow Haskell Compiler ma budowę wpisującą się w ten krótki, ogólny opis.
Na schemacie 2.1 można odnaleźć wymienione wcześniej fazy. W tej pracy dokonano modyfikacji we frontendzie, dlatego poświęcono mu więcej uwagi.
5
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Rysunek 2.1: Schemat Glasgow Haskell Compiler[16]
6
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
2.1.1
Parser
Parsowanie w GHC dzieli się na etapy analizy leksykalnej i składniowej. Jest to
proces tak dobrze opracowany od strony algorytmicznej, że istnieją narzędzia, które
pozwalają na generowanie gotowych lekserów i parserów z opisu gramatyki.
W czasie analizy leksykalnej plik na wejściu jest skanowany, dzielony na ciągi
znaków, tzw. leksemy i generowany jest z niego strumień wartości zwanych tokenami. W GHC tokeny to wartości typu Token, zdefiniowanego w pliku Lexer.x.
Gramatyka leksykalna składa się z szeregu wyrażeń regularnych i odpowiadających im tokenów. Leksemy wyodrębniane są, gdy dojdzie do dopasowania ciągu
znaków na wejściu do jednego z wyrażeń. Stosowana jest metoda najdłuższego dopasowania, to znaczy lekser wczytuje kolejne znaki tak długo, dopóki przynajmniej
jedno wyrażenie akceptuje wczytany podciąg. Po dopasowaniu lekser zwraca token
odpowiadający temu wyrażeniu. Proces ten jest kontynuowany do przetworzenia
całego wejścia.
W GHC lekser jest generowany z wykorzystaniem narzędzia Alex. Definicja
gramatyki leksykalnej zawarta jest w pliku Lexer.x. W przykładzie 2.1 pokazane
są fragmenty tego pliku. Na początku widoczne są makra, za którymi stoją wyrażenia regularne. Na końcu fragmentu pokazane jest kilka reguł z tymi makrami
po lewej stronie i wyrażeniami, które mają zostać zwrócone po dopasowaniu do
danego wyrażenia regularnego. Znajdujące się tam idtoken, varid itd. to zwykłe
funkcje w Haskellu, zdefiniowane w innym miejscu, które zwracają tokeny[2].
W czasie analizy składniowej strumień tokenów zamieniany jest w bardziej złożoną strukturę, drzewo składniowe. W GHC parser, tak jak lekser, został wygenerowany automatycznie. Wykorzystano do tego narzędzie Happy. Definicja gramatyki
w formacie akceptowanym przez Happy znajduje się w pliku Parser.y. Jest podana w notacji Backusa-Naura wzbogaconej, podobnie jak w Alex, o fragmenty
kodu w Haskellu wywoływane w celu stworzenia struktury danych reprezentującej
to drzewo[11]. Przykład 2.2 pokazuje wybrane produkcje z tego pliku, wykorzystujące przy tym token ITvarid zwracany przez funkcję varid z przykładu 2.1.
2.1.2
Renamer
W Haskellu, jak w wielu innych językach, może istnieć kilka funkcji, zmiennych,
typów danych, klas, itd. o tej samej nazwie. Na przykład, mogą one występować
w różnych plikach albo w lokalnym zakresie w konstrukcjach let lub where. Z tego
powodu później ten sam identyfikator może odnosić się do różnych rzeczy, w zależności od tego, gdzie występuje i co w tym miejscu jest widoczne w zakresie.
Kompilator, w części zwanej renamerem, decyduje, do której definicji odnosi się
każde wystąpienie danej nazwy.
Każda osobna definicja otrzymuje na tym etapie unikalny, wewnętrzny identyfikator. Następnie nazwy w programie są zamieniane na odniesienia do jednej z tych
zmiennych. Utrzymywany jest w tym celu słownik nazw znajdujących się w danej chwili w zakresie. Jeżeli w danym miejscu nie da się na podstawie zawartości
słownika jednoznacznie określić, do czego odnosi się pewne wystąpienie nazwy, to
7
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.1: Wycinki z pliku Lexer.x składające się na reguły opisujące co jest
wyodrębniane jako zmienna i konstruktor.
$unilarge
$asclarge
$large
= \ x01 -- Trick Alex into h a n d l i n g Unicode . See a l e x G e t B y t e .
= [A - Z ]
= [ $asclarge $unilarge ]
$unismall
$ascsmall
$small
= \ x02 -- Trick Alex into h a n d l i n g Unicode . See a l e x G e t B y t e .
= [a - z ]
= [ $ascsmall $unismall \ _ ]
$ascdigit
$unidigit
$digit
= 0 -9
= \ x03 -- Trick Alex into h a n d l i n g Unicode . See a l e x G e t B y t e .
= [ $ascdigit $unidigit ]
$digit
$suffix
$idchar
= [ $ascdigit $unidigit ]
= \ x07 -- Trick Alex into h a n d l i n g Unicode . See a l e x G e t B y t e .
= [ $small $large $digit $suffix \ ’]
@varid
@conid
= $small $idchar *
= $large $idchar *
-- v a r i a b l e i d e n t i f i e r s
-- c o n s t r u c t o r i d e n t i f i e r s
@qual = ( @conid \.) +
@qvarid = @qual @varid
@qconid = @qual @conid
haskell : <0 , option_prags > {
@qvarid
@qconid
@varid
@conid
}
{
{
{
{
idtoken qvarid }
idtoken qconid }
varid }
idtoken conid }
Fragment 2.2: Wycinki z pliku Parser.y z produkcjami odpowiadającymi za zmienne typów, wykorzystujące tokeny, których dotyczył przykład 2.1.
% token
VARID
CONID
tyvar
tyvar
{ L _ ( ITvarid
{ L _ ( ITconid
_) }
_) }
:: { Located RdrName }
: tyvarid
{ $1 }
tyvarid :: { Located RdrName
: VARID
{
| special_id
{
| ’ unsafe ’
{
| ’ safe ’
{
| ’ interruptible ’ {
}
sL1
sL1
sL1
sL1
sL1
$1
$1
$1
$1
$1
$!
$!
$!
$!
$!
-- i d e n t i f i e r s
mkUnqual
mkUnqual
mkUnqual
mkUnqual
mkUnqual
8
tvName
tvName
tvName
tvName
tvName
( getVARID $1 ) }
( unLoc $1 ) }
( fsLit " unsafe " ) }
( fsLit " safe " ) }
( fsLit " interruptible " ) }
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.3: Przykład działania renamera - niejednoznaczne odwołanie.
import Data . Maybe ( isJust )
isJust :: Maybe a -> Bool
isJust Nothing = False
isJust _ = True
main :: IO ()
main = print $ isJust ( Just 0)
{ - Wywo ł uje b ł ą d :
renamer . hs :8:16:
A m b i g u o u s o c c u r r e n c e ‘ isJust ’
It could refer to either ‘ Main . isJust ’ , defined at renamer . hs :4:1
or ‘ Data . Maybe . isJust ’ ,
i m p o r t e d from ‘ Data . Maybe ’ at renamer . hs :1:20 -25
-}
Fragment 2.4: Przykład działania renamera - shadowing.
foo = 10
main :: IO ()
main = let foo = 20 in print foo
-- Po u r u c h o m i e n i u wypisze 20
wywoływany jest błąd, jak w przykładzie 2.3. W algorytmie brane jest przy tym
pod uwagę, czy nazwa jest kwalifikowana i w jaki sposób importowany jest inny
moduł, co czasami pozwala usunąć wieloznaczność. Możliwe jest również zjawisko
zwane shadowingiem, polegające na zastąpieniu umieszczonej w zakresie definicji
przez definicję lokalną. Zachodzi ono na fragmencie 2.4. Wreszcie, jeżeli w danym
miejscu w zakresie nie będzie nic o danej nazwie, to renamer zakończy działanie
z błędem, jak w przykładzie 2.5.
Na tym etapie kompilacji wykonywane jest również wiele pobocznych zadań, na
przykład wyświetlenie ostrzeżeń, gdy pewna zmienna zostanie zdefiniowana, lecz
nie będzie później użyta w wyrażeniu, gdy kompilacja została rozpoczęta z flagą
-fwarn-unused-matches. To zachowanie jest jednym z usprawnień poruszonych
w tej pracy.
Fragment 2.5: Przykład działania renamera - nieodnalezienie w zakresie.
main :: IO ()
main = print bar
-- Wywo ł uje b ł ą d :
-- r e n a m e r 3 . hs :2:14: Not in scope : ‘ bar ’
9
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.6: Przykład bezsensownego wyrażenia w programie, z błędem GHC,
który wywołuje.
module Nonsense where
nonsense = 10 + " a "
{ - B ł ą d k o m p i l a c j i w GHC :
[1 of 1] C o m p i l i n g N o n s e n s e
( n o n s e n s e . hs , n o n s e n s e . o )
n o n s e n s e . hs :3:15:
No i n s ta n c e for ( Num [ Char ]) arising from a use of ‘ + ’
In the e x p r e s s i o n : 10 + " a "
In an e q u a t i o n for ‘ n o n s e n s e ’ : n o n s e n s e = 10 + " a "
-}
2.1.3
Type checker
Warto w tym miejscu wspomnieć o podstawowych pojęciach związanych z systemami typów. Częścią definicji języka jest jego gramatyka, która określa, jakie
ciągi znaków można uznać za program w tym języku. W skład gramatyki wchodzą
wyrażenia, które w wielu językach można podzielić na termy i typy.
Termy pozwalają wyrażać obliczenia, dzięki zdefiniowanej dla nich semantyce.
Jednym z rodzajów semantyki jest semantyka operacyjna, w której wykonywanie obliczeń modeluje się za pomocą maszyny stanowej. Term jest lub, w bardziej
złożonych semantykach, zawiera się w stanie maszyny. Ponadto istnieje relacja
przejścia, określającej stany do jakich przechodzi maszyna w wyniku obliczeń. Semantykę operacyjną określa się przez przez podanie reguł ewaluacji, z których
wynika ta relacja.
Wykonywanie programu może zatrzymać się w stanie uważanym za końcowy.
Term w postaci, do której został znormalizowany w stanie końcowym, nazywamy
wartością. Obliczenia mogą również nigdy nie zatrzymać lub utknąć w stanie, który
nie jest końcowy, lecz dla którego relacja przejścia nie określa dalszych kroków.
Termy, dla których ewaluacja daje taki rezultat, nazywamy niepoprawnymi lub
bezsensownymi[21]. Przykład 2.6 pokazuje niepoprawny term.
Typy są przyporządkowywane termom w relacji typowania, wynikającej z pewnych reguł typowania. Zbiór tych reguł składa się na system typów. System typów służy do wykrywania bezsensownych termów bez ich ewaluowania, często już
na etapie kompilacji. System typów jest poprawny (sound) lub bezpieczny, jeżeli
każdy term, który ma w nim przyporządkowany typ, jest sensowny. Z kolei jest
zupełny (complete), jeżeli każdy sensowny term jest w nim otypowany. Systemy
typów w popularnych językach są przeważnie poprawne, lecz niezupełne. Rozszerzenia systemu typów dążą do zmniejszenia liczby poprawnych programów, które są
w systemie typów niedopuszczalne, co można określić jako zwiększenie jego ekspresywności. Rozszerzenia mogą doprowadzić do tego, że problem sprawdzenia, czy
dany program jest poprawnie otypowany, stanie się nierozstrzygalny. Nie będzie
wtedy możliwe stworzenie odpowiedniego algorytmu i zagwarantowanie, że proces
kompilacji się zakończy. Może to być niedopuszczalne ze względu na oczekiwania
10
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.7: Przykład programu, dla którego statyczne sprawdzanie typów w
GHC się nie zakończy.
{ - # L A N G U A G E TypeFamilies , U n d e c i d a b l e I n s t a n c e s # -}
module Diverge where
type family Diverge a where
Diverge a = Diverge a
evil :: Diverge Int
evil = 0
Fragment 2.8: Przykład użycia polimorfizmu parametrycznego w Haskellu.
{ - # L A N G U A G E E x p l i c i t F o r A l l # -}
twice :: ( a -> a ) -> a -> a
twice f = f . f
twice ’ :: forall a . ( a -> a ) -> a -> a
twice ’ f = f . f
main :: IO ()
main = do
print $ twice not True
print $ twice ( ’ - ’:) " Hello "
-- True
-- " - - Hello "
użytkowników - programistów[21].
Polimorfizm to możliwość posiadania przez term więcej niż jednego przypisanego mu typu. Może on wtedy występować w miejscach, gdzie oczekiwany jest term
jednego z tych typów.
W polimorfizmie parametrycznym uzyskuje się to przez wprowadzenie do typu
zmiennych, za które mogą być podstawiane inne typy. Termy otypowane w ten
sposób muszą mieć sens niezależnie od tego, jakie typy zostaną przyporządkowane zmiennym, choć np. w Haskellu istnieje możliwość wprowadzenia ograniczeń
na zmienne. Polimorfizm parametryczny odpowiada kwantyfikacji ogólnej z logiki.
Typy takie mogą mieć składnię pozwalającą na zapisanie jawnego kwantyfikatora,
wiążącego zmienne w typie, zamiast pozostawiania tych zmiennych jako niezwiązanych. W przykładzie 2.8 funkcja twice jest polimorficzna, w jednym miejscu
zmienna a służy za Bool, a w drugim za String. Funkcja twice’ pokazuje wspomnianą jawną kwantyfikację, lecz poza tym jest równoważna poprzedniej funkcji.
W języku Java polimorfizm parametryczny dostępny jest pod postacią typów generycznych.
W polimorfizmie ad hoc term może mieć przypisane różne typy dzięki temu, iż
ma wiele definicji. W zależności od typu wykorzystywana jest inna z nich. W Haskellu ten polimorfizm jest dostępny w postaci klas typów[21]. W przykładzie 2.9
funkcja foo występuje raz jako funkcja typu Bool -> Int i raz jako String ->
Int. W każdym przypadku ma inną definicję, sensowną dla wybranego wtedy typu. W Javie polimorfizm ten występuje pod postacią przeładowania metod, jak
w przykładzie 2.10 z metodą bar.
11
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.9: Przykład użycia polimorfizmu ad hoc w Haskellu.
class C a where
foo :: a -> Int
i n s t a n c e C Bool where
foo True = 1
foo _
= 0
i n s t a n c e C [ a ] where
foo = length
main :: IO ()
main = do
print $ foo True
print $ foo " Hello "
-- 1
-- 5
Fragment 2.10: Przykład użycia polimorfizmu ad hoc w Javie.
public class Parametric {
public static void main ( String ... args ) {
bar (10) ;
// bar int 10
bar ( " str " ) ;
// bar String str
}
static void bar ( int arg ) {
System . out . println ( " bar int " + arg ) ;
}
static void bar ( String arg ) {
System . out . println ( " bar String " + arg ) ;
}
}
12
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
W kompilatorze sprawdzenie, czy program jest poprawnie otypowany, zachodzi
w części zwanej type checkerem. Jednym z zadań type checkera jest wyświetlenie
w razie błędu takich komunikatów, by pozwolić użytkownikowi zrozumieć problem
i go naprawić. Z tego powodu aż do tego etapu kompilacji zachowywane są takie informacje jak lokalizacja w pliku wejściowym i identyfikatory wybrane przez
użytkownika.
2.2
Rozszerzenia Glasgow Haskell Compiler
Przez rozszerzenia GHC rozumiane są zaimplementowane w nim funkcje, które nie
są częścią specyfikacji języka przedstawioną w Haskell 2010 Language Report. Zliczenie linii w rezultacie wywołania ghc --supported-extensions pozwala stwierdzić, że jest ich obecnie około stu. Rozszerzenia są opcjonalne i domyślnie wyłączone. Uaktywnia się je dyrektywą języka umieszczoną na początku pliku modułu
lub przez podanie odpowiednich flag wśród argumentów wywołania kompilatora.
Wszystkie zmiany wykonane w tej pracy wiążą się z pewnymi rozszerzeniami, dlatego warto je omówić.
2.2.1
Rodziny typów
Rozszerzenie TypeFamilies
Rodziny typów to funkcje operujące na typach w trakcie kompilacji. Stworzenie
rodziny typów wymaga podania deklaracji rodziny z liczbą i rodzajami parametrów oraz pewnej liczby definicji lub równań. W przypadku rodzin związanych
z klasą, deklaracja znajduje się w deklaracji klasy i musi wykorzystywać parametry klasy. Przykładem takiej rodziny jest Box z klasy T na fragmencie 2.11. Może
dodatkowo posiadać równanie domyślne, o takim samym znaczeniu co domyślne
definicje funkcji w klasie. Równania podaje się przy definiowaniu instancji klasy.
W przypadku rodzin niezależnych od klasy, jak Cont z przykładu 2.11, deklarację
tworzy się z użyciem słowa kluczowego family, a równania tworzy się ze słowem
kluczowym instance.
Rodziny typów dzielą się na rodziny synonimów typów, deklarowane ze słowem
kluczowym type i rodziny typów danych, deklarowane ze słowem kluczowym data.
Równania należące do rodzin synonimów tworzą synonimy typów, podobne do tych
z nierozszerzonego Haskella. W przykładzie 2.12 Equal Int Int jest synonimem
Int i oba równie dobrze mogą znaleźć się w deklaracji typu dla foo. Analogicznie, rodziny typów danych grupują typy danych data lub newtype. Na przykład
Cont Int z przykładu 2.11 to nazwa typu danych, którego wartości konstruuje się
z użyciem ContI0 lub ContI1.
Rodziny synonimów mogą mieć postać otwartą i zamkniętą. W formie zamkniętej, wszystkie równania rodziny wymienione są w miejscu jej deklaracji. Mają tę
zaletę wobec rodzin otwartych, iż poszczególne równania mogą na siebie nachodzić
i w takim wypadku stosowany jest synonim, którego równanie jest pierwsze od
13
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.11: Przykład pozwiązanej z klasą i niezależnej rodziny typów.
{ - # L A N G U A G E T y p e F a m i l i e s # -}
module TypeFamilies where
class T a where
data Box a
store :: Box a -> a -> Box a
i n s t a n c e T Int where
data Box Int = BoxI0 | BoxI1 Int
store BoxI0 n = BoxI1 n
store ( BoxI1 acc ) n = BoxI1 $ acc + n
i n s t a n c e T Bool where
data Box Bool = BoxB0 Int Int
store ( BoxB0 ts fs ) True = BoxB0 ( ts + 1) fs
store ( BoxB0 ts fs ) _
= BoxB0 ts ( fs + 1)
data family Cont a
data i n s t a nc e Cont Int = ContI0 | ContI1 Int
data i n s t a nc e Cont Bool = ContB0 Int Int
class S a where
put :: Cont a -> a -> Cont a
i n s t a n c e S Int where
put ContI0 n = ContI1 n
put ( ContI1 acc ) n = ContI1 $ acc + n
i n s t a n c e S Bool where
put ( ContB0 ts fs ) True = ContB0 ( ts + 1) fs
put ( ContB0 ts fs ) _
= ContB0 ts ( fs + 1)
14
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.12: Przykład otwartej i zamkniętej funkcji na typach z nachodzącymi
na siebie dziedzinami.
{ - # L A N G U A G E T y p e F a m i l i e s # -}
module TypeFamilies where
type family Equal a b where
Equal a a = a
Equal a b = (a , b )
foo :: Equal Int Int
foo = 10
type family Open a b
type i n s t a n c e Open a a = a
type i n s t a n c e Open a b = (a , b )
{families - o v e r l a p p i n g . hs :13:15:
C o n f l i c t i n g family i n s t a n c e d e c l a r a t i o n s :
Open a a -- Defined at families - o v e r l a p p i n g . hs :13:15
Open a b -- Defined at families - o v e r l a p p i n g . hs :14:15
-}
góry. Przypomina to mechanizm dopasowania wzorców dostępny na poziomie termów. W przypadku rodzin otwartych, kolejność równań nie jest określona i mogą
one być definiowane w wielu plikach, więc nachodzenie na siebie dziedzin równań
powoduje błąd. Różnicę tę widać w przykładzie 2.12[24].
2.2.2
Częściowe sygnatury typów
Rozszerzenie PartialTypeSignatures
W Haskellu, podczas definiowania wyrażenia, możliwe, i zgodne z dobrymi praktykami, jest podanie sygnatury typu. Jest to jednak opcjonalne, gdyż przy braku
sygnatury w czasie sprawdzania typów dojdzie do próby rekonstrukcji. Istnieje jednak również inna, pośrednia opcja: podanie niepełnej sygnatury, zawierającej tak
zwane symbole wieloznaczne, zapisywane w postaci podkreślnika.
Domyślnie obecność wieloznacznika wywoła błąd, w którego treści znajdzie się
zrekonstruowany typ, którym można uzupełnić sygnaturę, a kompilacja zostanie
przerwana. Rozszerzenie PartialTypeSignatures zmieni jednak to zachowanie,
sprawiając, iż zamiast błędu wygenerowane zostaną jedynie ostrzeżenia i obecność
wieloznacznika nie będzie przyczyną zakończenia działania kompilatora. Dodatkowo ostrzeżenia te można ukryć uruchamiając GHC z flagą -fno-warn-partialtype-signatures. Przykład 2.13 demonstruje przebieg kompilacji z tym rozszerzeniem.
Symbol wieloznaczny może także znaleźć się wśród ograniczeń klasowych jak
w przykładzie 2.14. Wtedy w błędzie lub ostrzeżeniu, generowanym na takich samych zasadach, co w poprzednim przypadku, znajdą się wywnioskowane przez
kompilator zero lub więcej ograniczeń. Oprócz wieloznacznika w sygnaturze mogą
znaleźć się jawnie podane ograniczenia.
15
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.13: Przykład użycia anonimowego symbolu wieloznacznego w sygna
turze typu.
foo :: _ -> Bool
foo = not
{ - Wywo ł a b ł ą d lub ostrze ż enie :
partial . hs :3:8:
Found hole ‘ _ ’ with type : Bool
To use the i n f e r r e d type , enable P a r t i a l T y p e S i g n a t u r e s
In the type s i g n a t u r e for ‘ foo ’ : _ -> Bool
-}
Fragment 2.14: Przykład użycia anonimowego symbolu wieloznacznego w ograniczeniach typu.
bar :: _ = > [ a ] -> Bool
bar ( x : y : _ ) = x == y
bar _ = False
{ - Wywo ł a b ł ą d lub ostrze ż enie :
partial - constr . hs :3:8:
Found hole ‘ _ ’ with i n f e r r e d c o n s t r a i n t s : ( Eq a )
To use the i n f e r r e d type , enable P a r t i a l T y p e S i g n a t u r e s
In the type s i g n a t u r e for ‘ bar ’ : _ = > [ a ] -> Bool
-}
Drugim rozszerzeniem związanym z częściowymi sygnaturami jest NamedWildCards. Sprawia ono, iż identyfikatory zaczynające się od podkreślnika są również
traktowane jako wieloznaczniki i są traktowane w podobny sposób do tego zaprezentowanego w 2.13 i 2.14. Bez niego wyłącznie same podkreślniki są traktowane
jako wieloznaczniki, a identyfikatory zaczynające się od podkreślników jako zwyczajne zmienne typów. Te opcjonalne aktywowane wieloznaczniki określane są jako
wieloznaczniki nazwane, a te opisane poprzednio jako wieloznaczniki anonimowe.
Warte uwagi jest, iż nazwane wieloznaczniki to nie zmienne typów. Pokazuje
to przykład 2.15, gdzie po aktywowaniu rozszerzenia NamedWildCards podana sygnatura jest poprawna, z wywnioskowanym typem Bool w miejscu _x. Bez tego
rozszerzenia z kolei jest ona niepoprawna, gdyż _x jest wtedy traktowana jako zwykła zmienna, a zatem foo jest wtedy funkcją polimorficzną, która powinna mieć
sens dla każdego _x, co wyraźnie nie jest prawdą dla wyrażenia not _x[18].
16
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
Fragment 2.15: Demonstracja różnicy między zmienną z podkreślnikiem i nazwanym wieloznacznikiem.
omodule Partial where
foo :: _x -> ( Bool , _x )
foo a = ( True , not a )
foo :: _ -> Bool
foo = not
{ - Wywo ł a b ł ą d lub ostrze ż enie :
partial - named . hs :4:16:
Couldn ’ t match e x p e c t e d type ‘ _x ’ with actual type ‘ Bool ’
‘ _x ’ is a rigid type v a r i a b l e bound by
the type s i g n a t u r e for foo :: _x -> ( Bool , _x )
at partial - named . hs :3:8
R e l e v a n t b i n d i n g s include
a :: _x ( bound at partial - named . hs :4:5)
foo :: _x -> ( Bool , _x ) ( bound at partial - named . hs :4:1)
In the e x p r e s s i o n : not a
In the e x p r e s s i o n : ( True , not a )
-}
{- Z rozszerzeniem NamedWildCards :
partial - named . hs :3:8:
Found hole ‘ _x ’ with type : Bool
To use the i n f e r r e d type , enable P a r t i a l T y p e S i g n a t u r e s
In the type s i g n a t u r e for ‘ foo ’ : _x -> ( Bool , _x )
-}
17
ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW
18
Rozdział 3
Użyte narzędzia i technologie
3.1
Język programowania
Glasgow Haskell Compiler został napisany w języku Haskell. Wykorzystano została
w projekcie technika bootstraping, polegająca na tym, iż kompilator rozwijany jest
w języku, który kompiluje i jedną z zależności potrzebną do zbudowania GHC jest
on sam. Środowiskiem wykorzystanym do edycji kodu było IntelliJ IDEA z wtyczką zapewniającą wsparcie dla języka Haskell. Wtyczka ta niestety w pełni działa
tylko z projektami systemu Cabal i nie jest dostosowana do prac nad GHC. Z tego powodu środowisko nie zapewniało na przykład ciągłego raportowania błędów
w kodzie lub wyszukiwania deklaracji określonego typu, choć z tych udogodnień
można korzystać w innych projektach.
Testowanie i walidacja kompilatora są realizowane przez skrypty w języku Python. Jest on wymagany do zbudowania GHC również z tego powodu, że do stworzenia dokumentacji wykorzystane zostało narzędzie Sphinx, wymagające interpretera.
W projekcie użyty został również język C do napisania środowiska uruchomieniowego, linkowanego z każdym programem skompilowanym przez GHC. W pozostałych częściach GHC można znaleźć dyrektywy preprocesora C, który GHC może
wykorzystać zanim przejdzie do kompilacji. W tej pracy jednak kontakt z nim był
znikomy.
3.2
3.2.1
Narzędzia
Git
Kod GHC znajduje się w repozytorium Git i jest publicznie dostępny do pobrania pod adresem http://git.haskell.org/ghc.git. Biblioteki wykorzystywane
w kompilatorze również są przechowywane w repozytoriach Gita, lecz są osobnymi
projektami, dołączonymi do GHC jako submoduły. Klon oficjalnego repozytorium
znajduje się również w popularnym serwisie GitHub. Prawa zapisu do repozytorium są ograniczone[3].
19
ROZDZIAŁ 3. UŻYTE NARZĘDZIA I TECHNOLOGIE
Krokiem wymaganym do rozpoczęcia pracy było sklonowanie repozytorium i lokalne zbudowanie kompilatora ze źródeł. Po lokalnej edycji plików zmiany musiały
zostać zapisane w repozytorium i opatrzone komentarzem stosującym się do konwencji opisanej na wiki GHC, zanim mogły zostać wysłane z użyciem systemu
Phabricator.
3.2.2
Trac
Trac to system do organizacji pracy programistów. Przechowuje listę zgłoszeń, czyli
spójnych propozycji zmian w projekcie. Zapisywane są w nim zadania do wykonania oraz błędy wykryte w GHC, także przez zwykłych użytkowników. Chroni
to je przed zapomnieniem w razie, gdyby nie było możliwe zajęcie się nimi od razu. Znajdują się w tym systemie również propozycje dodania nowych funkcji. Do
każdego zgłoszenia może być dołączony opis, w przypadku błędu sposób reprodukcji, zapewnione jest miejsce na dyskusję i mechanizm śledzenia stanu wykonania
zadania. Aby ułatwić organizację pracy, możliwe jest przypisanie do zgłoszeń właściciela, by zapobiec sytuacji, w której tę samą rzecz wykonuje wiele osób. Możliwe
jest określenie priorytetu i planowanej wersji, w której zadanie powinno już być
wykonane i późniejsze sprawdzanie, czy ten cel został osiągnięty. Ułatwione jest
również umieszczanie odnośników do innych, powiązanych zgłoszeń czy do systemu
Phabricator. Wszystkie te funkcje wykorzystywane są w GHC.
Rysunek 3.1: Opis zgłoszenia w systemie Trac
Przykład zgłoszenia widoczny jest na rysunku 3.1. Dotyczy ono jednego z wykonanych w pracy usprawnień, pozostałe również mają swoje opisy w systemie.
Aktualizacja informacji na stronie Trac jest wymagana przez procedurę wprowadzania zmian w GHC[26].
20
ROZDZIAŁ 3. UŻYTE NARZĘDZIA I TECHNOLOGIE
3.2.3
Phabricator
[20]
Phabricator wykorzystywany jest do inspekcji kodu w czasie zgłaszania ulepszenia oraz do automatycznej walidacji z wykorzystaniem narzędzia Harbormaster.
Inspekcja jest ułatwiona, gdyż program prezentuje obok siebie wersję sprzed i po
dokonaniu zmian, z wyróżnieniem zmodyfikowanych linii kodu. Daje też możliwość
komentowania wybranych fragmentów.
Rysunek 3.2: Informacje o proponowanej zmianie w kodzie na stronie Phabricator
Na rysunku 3.2 widać opis jednego z ulepszeń dokonanego w tej pracy. Zgodnie
z procedurą [26], aby zmiany zapisane w lokalnym repozytorium Git znalazły się na
serwerze, łatka musi przejść wpierw przez proces rewizji w systemie Phabricator.
Polega on na tym, iż inni programiści, szczególnie ci, którzy posiadają uprawnienia
do zapisu do repozytorium, zapoznają się z przesłanym kodem. Zgłaszają oni swoje
uwagi i decydują, czy zmiana może zostać naniesiona na repozytorium na serwerze,
czy wymaga poprawek.
Z chwilą wysłania proponowanych zmian w kodzie do Phabricatora, wykonane zostaje również automatyczne zbudowanie i przetestowanie nowej wersji GHC
przez moduł Harbormaster na przeznaczonym do tego serwerze. Jeżeli walidacja
wykryje błędy, jest to znak, iż konieczne są poprawki i oszczędza to czas poświęcony
na inspekcję kodu przez programistów. Na rysunku 3.3 widać raport z Harbormastera do jednego z wprowadzonych w tej pracy usprawnień, w którym nie wykryto
problemów.
21
ROZDZIAŁ 3. UŻYTE NARZĘDZIA I TECHNOLOGIE
Rysunek 3.3: Wynik walidacji programu Harbormaster widoczny na stronie Phabricator
Jak wspomniane zostało w sekcji 3.1, GHC jest budowany z użyciem innej
wersji GHC. Aby zapewnić stabilność, istnieje wymaganie, że nowa wersja kompilatora musi być możliwa do skompilowania z wykorzystaniem dowolnej z dwóch
poprzednich stabilnych wersji[26]. Sam proces budowania przebiega wieloetapowo.
Najpierw, z użyciem starszej wersji kompilatora, budowane są biblioteki, zależności
GHC. Następnie kompilowany w ten sposób jest sam GHC, czego rezultatem jest
kompilator w nowszej wersji. Następnie te dwa kroki, budowania bibliotek i kompilacji GHC, są powtarzane, lecz z wykorzystaniem nowego kompilatora i to on jest
uznawany za wynik całego procesu[5]. Dzięki temu sposobowi działania, usprawnienia w kompilacji są od razu zastosowane do kompilatora, przy jednym etapie
wykorzystywane byłyby optymalizacje i sposób generowania kodu ze starszej wersji. Stanowi to również dobry test nowej wersji, gdyż jeżeli pojawiły się w niej nowe
błędy, to jest duża szansa, iż ujawnią się one w czasie budowania kompilatora na
drugim etapie.
Uzyskany kompilator jest poddawany kilku tysiącom testów. W większości dzielą się one na trzy kategorie: testów, w których kompilacja pewnego programu musi
zakończyć się powodzeniem; testów, których kompilacja musi zakończyć się błędem;
testów, w których kompilacja musi się udać, a następnie sprawdzane jest zachowanie programu po uruchomieniu. Wiele testów polega na sprawdzeniu komunikatów
kompilatora i porównaniu ze wzorcem.
22
Rozdział 4
Usprawnienia Glasgow Haskell
Compiler
4.1
Zgłoszenie nr 10839
Consistent pretty-printing of type families
Pierwszym z wykonanych w pracy usprawnień było ujednolicenie wyświetlania rodzin typów. Problem polegał na tym, iż w zależności od miejsca i rodzaju błędu,
wyświetlane były one w różny sposób. Co gorsza, różniły się one nie tylko formatem,
ale też tym, które informacje zawierają. Widać to na poniższych wycinkach z komunikatów generowanych przez kompilator. We fragmencie 4.2, z błędem w otwartej
rodzinie typów, widoczna jest lewa strona równań i wskazania, w którym miejscu
w pliku się one znajdują.
We fragmencie 4.4 z zamkniętą rodziną typów widoczna jest zarówno lewa jak
i prawa strona równania. Nie jest jednak podane miejsce wystąpienia.
W 4.6 wyświetlany jest kwantyfikator wiążący zmienne równania i obie strony
równania, ale brak jest wcięcia, przez co równanie jest wyrównanie w pionie z resztą
komunikatu i cierpi czytelność komunikatu. Brak jest również lokalizacji.
4.1.1
Rozwiązanie
Po zapoznaniu się z kodem GHC, udało się odnaleźć miejsca odpowiadające za
każdy z tych komunikatów. Była to funkcja inaccessibleCoAxBranch z modułu
TcValidity w przypadku 4.4 oraz funkcje pprCoAxBranch i pprCoAxBranchHdr
z modułu Coercion w przypadkach odpowiednio 4.6 i 4.2. Znaleziono jeszcze inFragment 4.1: Fragment testu T7524 z dwoma równaniami otwartej rodziny typów
będącymi w konflikcie.
4
5
6
type family F ( a :: k1 ) ( b :: k2 )
type i n s t a n c e F a a = Int
type i n s t a n c e F a b = Bool
23
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
Fragment 4.2: Błąd generowany przez kompilator w przypadku 4.1 przed wprowadzeniem zmian.
T7524 . hs :5:15:
Conflicting family instance declarations :
F a a -- Defined at T7524 . hs :5:15
F a b -- Defined at T7524 . hs :6:15
Fragment 4.3: Fragment testu T6018 z zamkniętą rodziną typów z równaniami o
nachodzących na siebie dziedzinach.
70
71
72
73
74
75
-- This is similar , except that the last e q u a t i o n c o n t a i n s c o n c r e t e type .
-- it is o v e r l a p p e d it should be dropped with a warning
type family Foo a = r | r -> a where
Foo Int = Bool
Foo Bool = Int
Foo Bool = Bool
Since
Fragment 4.4: Ostrzeżenie generowane przez kompilator w przypadku 4.3 przed
wprowadzeniem zmian.
T6018 . hs :75:5: Warning :
Type family instance equation is overlapped :
Foo Bool = Bool
Fragment 4.5: Fragment testu T6018fail z równaniem rodziny typów nie spełnia
jącej warunku różnowartościowości.
64
65
66
-- This should fail because there is no way to d e t e r m i n e k from the RHS
type family Fc ( a :: k ) ( b :: k ) = r | r -> k
type i n s t a n c e Fc a b = Int
Fragment 4.6: Błąd generowany przez kompilator w przypadku 4.5 przed wprowadzeniem zmian.
T6018fail . hs :66:15: error : Type family equation
violates injectivity annotation . Kind variable ‘ k ’ cannot be inferred from
the right - hand side . ( enabling - fprint - explicit - kinds might help ) In the type
family equation : forall ( k :: BOX ) ( a :: k ) ( b :: k ) . Fc a b = Int
24
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
Fragment 4.7: Fragment testu T9371 z rodziną typów danych powiązaną z klasą C
i
równaniami o nachodzących na siebie dziedzinach.
13
14
15
16
17
18
19
i n s t a n c e { - # O V E R L A P P A B L E # -} Monoid x = > C x where
data D x = D1 ( Either x () )
makeD = D1 ( Left mempty )
i n s t a n c e ( Monoid x , Monoid y ) = > C (x , y ) where
data D (x , y ) = D2 (x , y )
makeD = D2 ( mempty , mempty )
ną funkcję, pprCoAxiom, używaną do generowania komunikatów przy kompilacji
z flagą -ddump-types. Wszystkie one operowały na typie danych CoAxBranch, odpowiadającym równaniu rodziny typów oraz CoAxiom, który odpowiada deklaracji
rodziny typów i odrobinę różni się w zależności od tego, czy rodzina jest otwarta
czy zamknięta. Główną różnicą jest to, czy posiada więcej niż jedną CoAxBranch.
Mimo tego podobieństwa, które uczyniło ich ujednolicenie w tej pracy prostym
zadaniem, zostały one zaimplementowane osobno.
Funkcja inaccessibleCoAxBranch generuje ostrzeżenie w sytuacji, gdy równanie w zamkniętej rodzinie typów nigdy nie może zostać zastosowane z powodu
ogólniejszego równania wymienionego wcześniej. pprCoAxBranchHdr używana jest
do zgłaszania błędów w otwartych rodzinach typów, gdy dziedziny dwóch równań
nachodzą na siebie. Z kolei pprCoAxBranch wykorzystywany jest do generowania błędów związanych z naruszeniem zadeklarowanej różnowartościowości rodziny typów. Różne warunki wystąpienia sprawiły, że funkcje znalazły się w różnych
miejscach programu, w kilku modułach i w komunikacie skupiały się na różnych
informacjach. Stąd nietrudno było o rozbieżności.
Dokonano usprawnienia, które polegało na zrefaktorowaniu kodu i wydzieleniu części wspólnej do osobnej funkcji ppr_co_ax_branch. Została ona napisana
tak, by wyświetlać wszystkie informacje, które wydobywały zastąpione funkcje.
W komunikacie znajduje się zatem jawny kwantyfikator, wyświetlany tylko przy
kompilacji z odpowiednią flagą lub, jeżeli rodzina typów wykorzystuje polimorfizm na poziomie rodzajów jak w przykładzie 4.6, obie strony równania i miejsce
wystąpienia w kodzie kompilowanego programu.
W trakcie pracy okazało się, że rodziny typów danych wymagają innego sposobu wyświetlania prawej strony równania. Jest tak, gdyż ich reprezentacja na
etapie sprawdzania typów zawiera wygenerowany przez kompilator konstruktor
typu, o którym użytkownik może nie wiedzieć. Z tego powodu pprCoAxBranch
używa funkcji pprDataCons, która wydobywa z tego wewnętrznego konstruktora
typu konstruktory wartości i odtwarza prawą stronę równania taką, jaką podał
użytkownik. Przykład działania widoczny jest we fragmencie 4.9. Wcześniej prawa
strona nie była w tym przypadku wcale wyświetlana, co widać na wycinku 4.8.
Inaczej wygląda to w przypadku funkcji pprCoAxiom, która jest używana przy
generowaniu zrzutu z type checkera przy kompilacji z odpowiednią flagą. Tutaj
pożądane jest wyświetlenie wewnętrznej reprezentacji i pprDataCons nie jest używana. Przykładowy efekt widać na przykładzie 4.10.
25
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
Fragment 4.8: Błąd generowany przez kompilator w przypadku 4.8 przed wprowa
dzeniem zmian.
T9371 . hs :14:10:
Conflicting family instance declarations :
D -- Defined at T9371 . hs :14:10
D (x , y ) -- Defined at T9371 . hs :18:10
Fragment 4.9: Błąd generowany przez kompilator w przypadku 4.9 przed wprowadzeniem zmian.
T9371 . hs :14:10:
Conflicting family instance declarations :
D = D1 ( Either x () ) -- Defined at T9371 . hs :14:10
D (x , y ) = D2 (x , y ) -- Defined at T9371 . hs :18:10
Przy okazji tego zadania wykonany został również drobny refaktoring, polegający na dodaniu funkcji vbar, reprezentującej komunikat "|". Definiowane wcześniej
lokalnie wystąpienia tego komunikatu zostały zastąpione.
4.1.2
Testy
Do tego zadania nie zostały utworzone nowe testy. Jako że wprowadzona zmiana
dotyczyła komunikatów o błędach, istniejące już testy pokrywały ją w wystarczającym stopniu. Dwadzieścia dziewięć testów wykazało wprowadzone przy tym
zgłoszeniu zmiany. Zostały one uaktualnione, by oczekiwać nowych komunikatów.
Fragmenty testów 4.1, 4.5 i 4.3 zaprezentowane wcześniej również wywołały inne
komunikaty, które przedstawiono na wycinkach 4.1, 4.5 i 4.3.
Wszystkie trzy komunikaty mają ten sam format. Zawierają obie strony równania i lokalizację oraz takie same wcięcia, wyróżniające je od reszty błędu. Dwa
fragmenty, w których użyta została zmienna k, są wyświetlane z kwantyfikatorem,
choć wcześniej w przypadku 4.1 informacja o polimorfizmie na poziomie rodzajów
typów była niewidoczna.
Fragment 4.10: Fragment oczekiwanego zrzutu z type checkera testu DataFamily
InstanceLHS z wyświetlonym równaniem rodziny typów danych.
COERCION AXIOMS
axiom D a t a F a m i l y I n s t a n c e L H S . TFCo : R : SingMyKind_ ::
Sing = D a t a F a m i l y I n s t a n c e L H S . R : SingMyKind_
-- Defined at D a t a F a m i l y I n s t a n c e L H S . hs :8:15
26
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
Fragment 4.11: Błąd generowany przez kompilator w przypadku 4.1 po wprowa
dzeniu zmian.
T7524 . hs :5:15:
Conflicting
forall ( k
forall ( k
F a b =
family instance declarations :
:: BOX ) ( a :: k ) . F a a = Int -- Defined at T7524 . hs :5:15
:: BOX ) ( k1 :: BOX ) ( a :: k ) ( b :: k1 ) .
Bool -- Defined at T7524 . hs :6:15
Fragment 4.12: Ostrzeżenie generowane przez kompilator w przypadku 4.3 po wpro
wadzeniu zmian.
T6018 . hs :75:5: Warning :
Type family instance equation is overlapped :
Foo Bool = Bool -- Defined at T6018 . hs :75:5
4.2
Zgłoszenie nr 10982
Warn about unused pattern variables in type families
Zgłoszenie to polegało na dodaniu ostrzeżeń o nieużywanych zmiennych w funkcjach na poziomie typu, działających analogicznie do obecnych już w GHC ostrzeżeń w funkcjach na poziomie termów. Są one uaktywniane tą samą flagą -fwarnunused-matches, domyślnie nie są wyświetlane.
Ważnym zagadnieniem okazało się zdefiniowanie, kiedy zmienna typu uważana jest za używaną. W zaimplementowanej wersji, by ostrzeżenie nie było generowane, zmienna musi występować po prawej stronie równania lub występować
przynajmniej dwa razy po lewej stronie równania. Wartym uwagi jest, iż ten drugi
przypadek nie występuje w przypadku funkcji na poziomie termów. Jest tak, gdyż
wielokrotne użycie tej samej zmiennej we wzorcach zwykłej funkcji jest uważane za próbę wprowadzenia konfliktowych definicji i zawsze generuje błąd. Z kolei
w rodzinach typów jest to zupełnie dopuszczalne i oznacza, że takie równanie ma
zastosowanie wyłącznie wtedy, gdy za zmienną podstawiony jest ten sam typ we
wszystkich miejscach jej wystąpienia. Zatem definicja funkcji equal a a = True
jest niepoprawna i w szczególności nie pozwala wyrazić tego, iż oba argumenty
muszą mieć równe wartości. Natomiast równanie rodziny typów Equal a a = ()
jest poprawne i zostanie wykorzystane przy aplikacji Equal do dwóch równych ty-
Fragment 4.13: Błąd generowany przez kompilator w przypadku 4.5 po wprowa
dzeniu zmian.
T6018fail . hs :66:15: error :
Type family equation violates injectivity annotation .
Kind variable ‘ k ’ cannot be inferred from the right - hand side .
( enabling - fprint - explicit - kinds might help )
In the type family equation :
forall ( k :: BOX ) ( a :: k ) ( b :: k ) .
Fc a b = Int -- Defined at T6018fail . hs :66:15
27
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
Fragment 4.14: Przykład funkcji na poziomie typów ze zmiennymi uznawanymi za
wykorzystywane.
type family X a where
X a = a
type family Y a b where
Y a a = Int
Fragment 4.15: Przykład funkcji na poziomie typów ze zmiennymi zastąpionymi
lub poprzedzonymi podkreślnikami.
type family D a b where
D a _b = a
type family E a b where
E a _ = a
pów. Ze względu na to zachowanie, zmienne typów występujące wielokrotnie we
wzorcach są uznawane za używane. We fragmencie 4.14 zmienna a jest uznawana
za używaną w rodzinie X ze względu na pierwszy warunek, a w Y ze względu na
drugi.
Aby ostrzeżenie nie wystąpiło, gdy zmienna jest nieużywana, należy zastąpić
ją lub poprzedzić podkreślnikiem. Pokazane to zostało we fragmencie 4.15.
4.2.1
Rozwiązanie
Wyświetlania ostrzeżeń dokonano przez zmiany w renamerze. Po analizie kodu
udało się stwierdzić, że funkcja rnFamInstDecl z modułu RnSource odpowiada za
wszystkie rodziny typów w kodzie, otwarte, zamknięte i związane z klasą. Ponadto
jest już w niej używana funkcja extractHsTysRdrTyVars z modułu RnTypes, która
przechodzi po wzorcach po lewej stronie równania, kolekcjonując nazwy zmiennych,
by utworzyć z nich zmienne typów. Zbiór zmiennych używanych to z kolei to samo,
co zbiór zmiennych wolnych po prawej stronie równania, zwracany przez funkcję
rnPayload. Większość informacji potrzebnych do realizacji założeń zgłoszenia było
zatem dostępnych od początku, należało pozyskać informacje o duplikatach.
W tym celu funkcja extractHsTysRdrTyVars zastąpiona została bardzo podobną extractHsTysRdrTyVarsDups, która zwraca wszystkie wystąpienia zmiennych
z lewej strony równania rodziny typów, nie usuwając wielokrotnych wystąpień tej
samej nazwy. To umożliwiło sprawdzanie drugiego z warunków, kiedy zmienna jest
uznawana za używaną.
Do wygenerowania ostrzeżeń użyta została funkcja warnUnusedMatches, ta sama, która odpowiada za ostrzeżenia w funkcjach na poziomie termów. Gwarantuje
to, że komunikaty są w tym samym formacie. Następuje w niej również odczytanie
ze środowiska globalnego w monadzie renamera, czy opcja generowania ostrzeżeń
została wybrana przez programistę, na przykład przez ustawienie flagi -fwarnunused-matches lub -Wall.
28
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
Fragment 4.16: Fragment testu UnusedTyVarWarnings z równaniami wywołującymi
ostrzeżenia lub zawierającymi zmienne poprzedzone podkreślnikiem.
7
8
9
10
11
12
13
14
15
16
17
type family C a b where
C a b = a
-- should warn
type family C2 a b
type i n s t a n c e C2 a b = a
-- should warn
type family D a b where
D a _b = a
-- should not warn
type family D2 a b
type i n s t a n c e D2 a _b = a
-- should not warn
Zmienne o identyfikatorach zaczynających się od podkreślnika nie powinny wywoływać ostrzeżeń niezależnie od tego czy są używane. Ich ignorowanie zachodzi w funkcji warnUnusedMatches z RnEnv przez zwyczajne sprawdzenie jaki jest
pierwszy znak w nazwie. Z kolei anonimowe i nazwane symbole wieloznaczne nie
wywołują ostrzeżeń, gdyż nie są kolekcjonowane w funkcji extractHsTysRdrTyVarsDups, więc nigdy nie znajdują się na zwróconej przez nią liście nazw.
Zmiana wymagała przejrzenia rodzin typów w GHC oraz powiązanych bibliotekach i uaktualnienia ich tak, by nie generowały ostrzeżeń. W przeciwnym razie
proces walidacji kończyłby się błędem, gdyż został on skonfigurowany, by nie dopuszczać ostrzeżeń w pewnych częściach projektu. Z tego powodu konieczne było
stworzenie niewielkiej łatki do biblioteki Hoopl, w której poprzedzono problematyczne zmienne podkreślnikami. Bez żadnych przeszkód znalazła się ona w repozytorium tego projektu.
4.2.2
Testy
Testami sprawdzającymi działanie usprawnienia są UnusedTyVarWarnings i UnusedTyVarWarningsNamedWCs. Oba działają przez próbę kompilacji pewnego programu i sprawdzenie, czy na wyjściu kompilatora pojawiają się oczekiwane ostrzeżenia. Program ten zawiera jedenaście przykładów rodzin typów otwartych, zamkniętych lub rodzin typów danych. Wśród ich równań są przypadki zmiennych
nieużywanych, używanych ze względu na wystąpienie po prawej stronie i używanych ze względu na wielokrotne wystąpienie po lewej stronie, z nazwą poprzedzoną
podkreślnikiem oraz symboli wieloznacznych. Testy te różnią się od siebie tym,
iż w przypadku UnusedTyVarWarningsNamedWCs kompilacja zachodzi z dodatkowo uaktywnionym rozszerzeniem NamedWildCards. Ma to związek ze zgłoszeniem
opisanym w sekcji 4.3.
Fragment 4.16 to część jednego z opisanych testów. Z kolei fragment 4.17 to
ostrzeżenia, które są dla niego oczekiwane.
Dodanie nowej funkcji wymagało jeszcze uaktualnienia dokumentacji. Informacje o dodanych ostrzeżeniach zostały dodane do przewodnika użytkownika w części
z opisem opcjonalnych ostrzeżeń, w części z opisem rodzin typów i do dziennika
zmian.
29
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
Fragment 4.17: Ostrzeżenia generowany przez kompilator dla fragmentu 4.16 po
wprowadzeniu zmian.
U n u s e d T y V a r W a r n i n g s . hs :8:5: warning :
Defined but not used : type variable ‘ b ’
U n u s e d T y V a r W a r n i n g s . hs :11:18: warning :
Defined but not used : type variable ‘ b ’
Fragment 4.18: Fragment testu NamedWildcardExplicitForall sprawdzającego,
czy zmienne związane kwantyfikatorem nie zostają zamienione w wieloznaczniki. 13
14
15
16
17
baz :: forall _a . _a -> _b -> ( _a , _b )
baz x y = ( not x , not y )
-- _a is a variable , _b is a w i l d c a r d
qux :: _a -> ( forall _a . _a -> _a ) -> _a
qux x f = let _ = f 7 in not x
-- the _a bound by forall is a tyvar
-- the other _a are w i l d c a r d s
4.3
Zgłoszenie nr 11098
PartialTypeSignatures mishandles type variables that begin with
an underscore
W tym zgłoszeniu konieczne było naprawienie błędu związanego z rozszerzeniem
NamedWildCards. Problem ten napotkany został w czasie pracy nad zgłoszeniem
nr 10982 opisanym w sekcji 4.2, gdy zbadano jak uaktywnienie tego rozszerzenia wpływa na nazwane symbole wieloznaczne w rodzinach typów. Dlatego został
wybrany po tamtym zgłoszeniu.
Opis błędu początkowo skupiał się na przypadku, w którym zmienna typu
z nazwą zaczynającą się od podkreślnika, była traktowana jako nazwany symbol
wieloznaczny, mimo tego, iż była jawnie związana kwantyfikatorem. Przypadek
ten został pokazany na wycinku 4.18. Problem okazał się jednak wpływać na więcej programów. Uaktywnienie NamedWildCards powodowało błędy kompilacji, gdy
zmienna typu zaczynająca się od podkreślnika znalazła się poza sygnaturą typu, na
przykład w deklaracji synonimu typu, typu danych, klasy czy rodziny typów. Znaczenie symboli wieloznacznych w sygnaturach jest dobrze określone w specyfikacji
PartialTypeSignatures, lecz nie są one oczekiwane poza nimi. Przykładowy kod
demonstrujący błędy w różnych kontekstach znajduje się we fragmencie 4.19.
Jak widać z uaktywnioną opcją NamedWildCards kompilacja wielu poprawnych programów kończyła się błędem. Zmienne zaczynające się od podkreślnika
są w Haskellu od dawna i nie ma powodu, by w tej sytuacji były niedopuszczalne. Zgłoszenie polegało na dokonaniu takich zmian w GHC, by przestały one być
źródłem błędów.
30
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
Fragment 4.19: Deklaracje, w których kompilacja kończyła się błędem, przy ak
tywnym rozszerzeniu NamedWildCards.
type Synonym _a = _a -> _a
-- " U n e x p e c t e d type ’_a ’ In the type d e c l a r a t i o n for ’ Synonym ’"
data A a _b = ACon a a
-- " U n e x p e c t e d type ’_b ’ In the data d e c l a r a t i o n for ’A ’"
data B _a b = BCon _a _a
-- " U n e x p e c t e d type ’_a ’ In the data d e c l a r a t i o n for ’B ’"
type family C a b where
C a _b = a -> a
-- " W i l d c a r d ’_b ’ not allowed in a type pattern of family i n s t a n c e for ’C ’"
type family D a b where
D _a b = _a -> _a
-- " W i l d c a r d ’_a ’ not allowed in a type pattern of family i n s t a n c e for ’D ’"
-- " W i l d c a r d ’_a ’ not allowed in the d e c l a r a t i o n for type synonym ’D ’" ( twice )
data family E a b
data i n s t a n c e E a _b = ECon a a
-- " W i l d c a r d ’_b ’ not allowed in a type pattern of family i n s t a n c e for ’E ’"
data family F a b
data i n s t a n c e F _a b = FCon _a _a
-- " W i l d c a r d ’_a ’ not allowed in a type pattern of family i n s t a n c e for ’F ’"
-- " W i l d c a r d ’_a ’ not allowed in the d e f i n i t i o n of data c o n s t r u c t o r ’ FCon ’" ( twice
)
4.3.1
Rozwiązanie
Źródłem problemów było rozwinięcie tyvar w definicji gramatyki programu Happy.
Początkowo generowało ono taki parser, który w sytuacji, gdy rozszerzenie NamedWildCards jest aktywne, parsował wszystkie zmienne zaczynające się od podkreślników jako nazwane wieloznaczniki.
Wykorzystany pomysł na rozwiązanie problemu był taki, by parsować wszystkie
identyfikatory jako zwykłe zmienne typów, a następnie zamieniać część z nich
w renamerze. Rzeczywiście, w ramach pracy dokonano zmian w Parser.y, by
bezwarunkowo konstruowane były HsTyVar, reprezentujące zmienne typu.
Do rekordu LocalRdrEnv przekazywanego w monadzie renamera RnM dodane
zostało pole lre_nwcs, będące zbiorem nazw zmiennych, które powinny zostać
zastąpione przez symbole wieloznaczne. Rozwiązanie problemu z fragmentu 4.19
uzyskano poprzez kontrolę, kiedy ten zbiór ma być wypełniany. Wolne zmienne,
zaczynające się od podkreślnika, umieszczane są w nim bowiem wyłącznie w funkcjach, które operują na typie LHsSigWcType. Tym bowiem cechują się funkcje, którymi dokonywane jest zastępowanie nazw w kontekstach dopuszczających nazwane
symbole wieloznaczne. Do tego zbiór pozostaje pusty, gdy opcja NamedWildCards
jest nieaktywna.
Zbiór ten jest modyfikowany w funkcjach rnWcSigTy i rnHsTyKi, w wariantach
oczekujących argumentów o wartości HsForAllTy, czyli reprezentujących jawne
kwantyfikatory. Podczas zastępowania nazw w ciele kwantyfikatora, nazwy zwią31
ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER
zane nim są usunięte ze zbioru lre_nwcs. Nie są one zamieniane w wieloznaczniki,
co rozwiązuje problem z fragmentu 4.18.
lre_nwcs jest wykorzystywany w wariancie funkcji rnHsTyKi przyjmującym
argument HsTyVar. Do zamiany zmiennej typu na wieloznacznik dochodzi, gdy
nazwa zmiennej zostanie w nim odnaleziona. W przeciwnym razie argument traktowany jest jako zwykła zmienna.
4.3.2
Testy
Poprawność rozwiązania weryfikują trzy testy. T11098 to skrypt w GHCi, który
znalazł się w opisie błędu. NamedWildcardsAsTyVars sprawdza, czy uaktywnienie
rozszerzenia NamedWildCards nie powoduje błędów kompilacji w poprawnych programach, lecz zawierających zmienną zaczynająca się od podkreślnika w kontekście
innym niż sygnatura typu, podobnie jak w przykładzie 4.19. NamedWildcardExplicitForall testuje, czy zmienne związane przez jawnie podane kwantyfikatory są traktowane jak zwykłe zmienne, a nie zamieniane w symbole wieloznaczne.
Przypomina on fragment 4.18.
32
Rozdział 5
Podsumowanie i wnioski
5.1
Dyskusja wyników
Dzięki zrealizowaniu pracy, sposób wyświetlania rodzin typów w błędach i ostrzeżeniach kompilatora jest ujednolicony. Dodane zostało wyświetlanie prawych stron
równań rodzin typów danych w sposób naśladujący składnię tych konstrukcji. Stary sposób wyświetlania, oddający wewnętrzną reprezentację rodzin typów danych,
został zachowany w zrzucie z etapu sprawdzania typów.
Dodane zostało także opcjonalne wyświetlanie ostrzeżeń o nieużywanych zmiennych w rodzinach typów. Zmienna uważana jest za nieużywaną jeżeli występuje we
wzorcach po lewej stronie równania tylko raz i nie występuje w typie po prawej
stronie.
Poprawiony został błąd z zamianą zmiennych typów zaczynających się od podkreślnika w symbole wieloznaczne w kontekstach, gdzie są one niedozwolone. Algorytm renamera został również zmieniony tak, by nie dokonywał zamiany zmiennych jawnie związanych kwantyfikatorem. Dzięki temu uaktywnienie rozszerzenia
NamedWildCards nie powoduje już odrzucania poprawnych programów.
Wszystkie te trzy modyfikacje przeszły proces rewizji kodu w systemie Phabricator i znalazły się w repozytorium. Od tamtej pory wprowadzony kod podlegał
dodatkowym modyfikacjom i refaktoryzacji dokonanej przez innych programistów.
W szczególności część dotycząca NamedWildCards została w dużej części zastąpiona
przez alternatywne rozwiązanie Simona Peytona Jonesa. Z kolei ostrzeżenia o nieużywanych zmiennych typów w wyniku zgłoszenia nr 11451 są uaktywniane nową
flagą -Wunused-type-variables zamiast -Wunused-matches. Jednak wszystkie
wprowadzone w tej pracy usprawnienia dalej są dostępne w kompilatorze, dlatego
cele należy uznać za zrealizowane.
5.2
Perspektywy dalszych badań
System Trac na stronie GHC zawiera obecnie ponad 1600 otwartych zgłoszeń[22].
W innych pracach można podjąć się dokonania następnych usprawnień związanych
z programowaniem z użyciem typów lub z inną częścią kompilatora. Złożone pro33
ROZDZIAŁ 5. PODSUMOWANIE I WNIOSKI
pozycje mają poświęcone sobie podstrony na wiki GHC, gdzie można znaleźć ich
planowane funkcje, projekty, opisy przebiegu implementacji i odnośniki do prac
badawczych, na których bazują. Są to na przykład propozycja dodania typów
zależnych do Haskella lub wprowadzenia definiowanych przez użytkownika błędów typów. Praca mogłaby polegać również na sformułowaniu nowej propozycji
i zaimplementowaniu jej. Możliwości są szerokie i wiele osób zdecydowało się już
poświęcić swój czas GHC.
34
Bibliografia
[1] Alfred Aho. Compilers : principles, techniques, and tools. Pearson, Harlow,
Essex, 2014.
[2] Alex Documentation.
https://www.haskell.org/alex/doc/alex.pdf.
(Stan na dzień: 2015-01-31).
[3] Building/GettingTheSources - GHC. https://ghc.haskell.org/trac/ghc/
wiki/Building/GettingTheSources. (Stan na dzień: 2015-01-31).
[4] Building/Preparation - GHC. https://ghc.haskell.org/trac/ghc/wiki/
Building/Preparation. (Stan na dzień: 2015-01-31).
[5] Building/Using - GHC.
https://ghc.haskell.org/trac/ghc/wiki/
Building/Using. (Stan na dzień: 2015-01-31).
[6] Manuel M. T. Chakravarty, Gabriele Keller, Simon Peyton Jones. Associated
type synonyms. Proceedings of the Tenth ACM SIGPLAN International Conference on Functional Programming, ICFP ’05, strony 241–253, New York,
NY, USA, 2005. ACM.
[7] Manuel M. T. Chakravarty, Gabriele Keller, Simon Peyton Jones, Simon Marlow. Associated types with class. Proceedings of the 32Nd ACM SIGPLANSIGACT Symposium on Principles of Programming Languages, POPL ’05,
strony 1–13, New York, NY, USA, 2005. ACM.
[8] James Cheney, Ralf Hinze. First-class phantom types. Raport instytutowy,
Cornell University, 2003.
[9] Richard A. Eisenberg, Dimitrios Vytiniotis, Simon Peyton Jones, Stephanie
Weirich. Closed type families with overlapping equations. Proceedings of
the 41st ACM SIGPLAN-SIGACT Symposium on Principles of Programming
Languages, POPL ’14, strony 671–683, New York, NY, USA, 2014. ACM.
[10] GHC/Type families - HaskellWiki.
https://wiki.haskell.org/GHC/
Indexed_types. (Stan na dzień: 2015-01-31).
[11] Happy Documentation. https://www.haskell.org/happy/doc/happy.pdf.
(Stan na dzień: 2015-01-31).
35
BIBLIOGRAFIA
[12] Implementations
HaskellWiki.
https://wiki.haskell.org/
Implementations. (Stan na dzień: 2015-01-31).
[13] InjectiveTypeFamilies - GHC. https://ghc.haskell.org/trac/ghc/wiki/
InjectiveTypeFamilies. (Stan na dzień: 2015-01-31).
[14] Mark P. Jones. Type classes with functional dependencies. Gert Smolka,
redaktor, Programming Languages and Systems, wolumen 1782 serii Lecture
Notes in Computer Science, strony 230–244. Springer Berlin Heidelberg, 2000.
[15] Miran Lipovača. Learn you a Haskell for great good! a beginner’s guide. No
Starch Press, San Francisco, Calif, 2011.
[16] S. Marlow, S. Peyton Jones. The Glasgow Haskell Compiler. A. Brown,
G. Wilson, redaktorzy, The Architecture of Open Source Applications, wolumen II, rozdzia/l 5. 2012.
[17] Newcomers - GHC. https://ghc.haskell.org/trac/ghc/wiki/Newcomers.
(Stan na dzień: 2015-01-31).
[18] Partial Type Signatures. https://downloads.haskell.org/˜ghc/latest/
docs/html/users_guide/partial-type-signatures.html. (Stan na dzień:
2015-01-31).
[19] Simon Peyton Jones, Dimitrios Vytiniotis, Stephanie Weirich, Geoffrey Washburn. Simple unification-based type inference for gadts. Proceedings of the
Eleventh ACM SIGPLAN International Conference on Functional Programming, ICFP ’06, strony 50–61, New York, NY, USA, 2006. ACM.
[20] Phabricator - GHC.
https://ghc.haskell.org/trac/ghc/wiki/
Phabricator. (Stan na dzień: 2015-01-31).
[21] Benjamin Pierce. Types and programming languages. MIT Press, Cambridge,
Mass, 2002.
[22] Status/Tickets - GHC.
https://ghc.haskell.org/trac/ghc/wiki/
Status/Tickets. (Stan na dzień: 2015-01-31).
[23] Bryan Sullivan. Real world Haskell. O’Reilly, Sebastopol, CA, 2009.
[24] Type families.
https://downloads.haskell.org/˜ghc/latest/docs/
html/users_guide/type-families.html. (Stan na dzień: 2015-01-31).
[25] UHCUserDocumentation < Ehc < UCCS. http://foswiki.cs.uu.nl/
foswiki/Ehc/UhcUserDocumentation. (Stan na dzień: 2015-01-31).
[26] WorkingConventions/FixingBugs - GHC. https://ghc.haskell.org/trac/
ghc/wiki/WorkingConventions/FixingBugs. (Stan na dzień: 2015-01-31).
36
BIBLIOGRAFIA
[27] Brent A. Yorgey, Stephanie Weirich, Julien Cretin, Simon Peyton Jones, Dimitrios Vytiniotis, José Pedro Magalhães. Giving haskell a promotion. Proceedings of the 8th ACM SIGPLAN Workshop on Types in Language Design and
Implementation, TLDI ’12, strony 53–66, New York, NY, USA, 2012. ACM.
37
BIBLIOGRAFIA
38
Spis rysunków
2.1
Schemat Glasgow Haskell Compiler . . . . . . . . . . . . . . . . . .
3.1
3.2
3.3
Opis zgłoszenia w systemie Trac . . . . . . . . . . . . . . . . . . . . 20
Informacje o proponowanej zmianie w kodzie na stronie Phabricator 21
Wynik walidacji programu Harbormaster widoczny na stronie Phabricator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
39
6
SPIS RYSUNKÓW
40
Spis fragmentów programów
2.1
2.2
2.3
2.4
2.5
2.6
2.7
2.8
2.9
2.10
2.11
2.12
2.13
2.14
2.15
4.1
4.2
4.3
4.4
4.5
4.6
Wycinki z pliku Lexer.x składające się na reguły opisujące co jest
wyodrębniane jako zmienna i konstruktor. . . . . . . . . . . . . . .
Wycinki z pliku Parser.y z produkcjami odpowiadającymi za zmienne typów, wykorzystujące tokeny, których dotyczył przykład 2.1. . .
Przykład działania renamera - niejednoznaczne odwołanie. . . . . .
Przykład działania renamera - shadowing. . . . . . . . . . . . . . .
Przykład działania renamera - nieodnalezienie w zakresie. . . . . . .
Przykład bezsensownego wyrażenia w programie, z błędem GHC,
który wywołuje. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Przykład programu, dla którego statyczne sprawdzanie typów w
GHC się nie zakończy. . . . . . . . . . . . . . . . . . . . . . . . . .
Przykład użycia polimorfizmu parametrycznego w Haskellu. . . . .
Przykład użycia polimorfizmu ad hoc w Haskellu. . . . . . . . . . .
Przykład użycia polimorfizmu ad hoc w Javie. . . . . . . . . . . . .
Przykład pozwiązanej z klasą i niezależnej rodziny typów. . . . . .
Przykład otwartej i zamkniętej funkcji na typach z nachodzącymi
na siebie dziedzinami. . . . . . . . . . . . . . . . . . . . . . . . . . .
Przykład użycia anonimowego symbolu wieloznacznego w sygnaturze typu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Przykład użycia anonimowego symbolu wieloznacznego w ograniczeniach typu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Demonstracja różnicy między zmienną z podkreślnikiem i nazwanym wieloznacznikiem. . . . . . . . . . . . . . . . . . . . . . . . . .
Fragment testu T7524 z dwoma równaniami otwartej rodziny typów
będącymi w konflikcie. . . . . . . . . . . . . . . . . . . . . . . . . .
Błąd generowany przez kompilator w przypadku 4.1 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Fragment testu T6018 z zamkniętą rodziną typów z równaniami o
nachodzących na siebie dziedzinach. . . . . . . . . . . . . . . . . . .
Ostrzeżenie generowane przez kompilator w przypadku 4.3 przed
wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . .
Fragment testu T6018fail z równaniem rodziny typów nie spełniającej warunku różnowartościowości. . . . . . . . . . . . . . . . . . .
Błąd generowany przez kompilator w przypadku 4.5 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
8
8
9
9
9
10
11
11
12
12
14
15
16
16
17
23
24
24
24
24
24
SPIS FRAGMENTÓW PROGRAMÓW
4.7
4.8
4.9
4.10
4.11
4.12
4.13
4.14
4.15
4.16
4.17
4.18
4.19
Fragment testu T9371 z rodziną typów danych powiązaną z klasą C
i równaniami o nachodzących na siebie dziedzinach. . . . . . . . . .
Błąd generowany przez kompilator w przypadku 4.8 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Błąd generowany przez kompilator w przypadku 4.9 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Fragment oczekiwanego zrzutu z type checkera testu DataFamilyInstanceLHS z wyświetlonym równaniem rodziny typów danych. . .
Błąd generowany przez kompilator w przypadku 4.1 po wprowadzeniu zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ostrzeżenie generowane przez kompilator w przypadku 4.3 po wprowadzeniu zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Błąd generowany przez kompilator w przypadku 4.5 po wprowadzeniu zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Przykład funkcji na poziomie typów ze zmiennymi uznawanymi za
wykorzystywane. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Przykład funkcji na poziomie typów ze zmiennymi zastąpionymi lub
poprzedzonymi podkreślnikami. . . . . . . . . . . . . . . . . . . . .
Fragment testu UnusedTyVarWarnings z równaniami wywołującymi
ostrzeżenia lub zawierającymi zmienne poprzedzone podkreślnikiem.
Ostrzeżenia generowany przez kompilator dla fragmentu 4.16 po
wprowadzeniu zmian. . . . . . . . . . . . . . . . . . . . . . . . . . .
Fragment testu NamedWildcardExplicitForall sprawdzającego, czy
zmienne związane kwantyfikatorem nie zostają zamienione w wieloznaczniki. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Deklaracje, w których kompilacja kończyła się błędem, przy aktywnym rozszerzeniu NamedWildCards. . . . . . . . . . . . . . . . . . .
42
25
26
26
26
27
27
27
28
28
29
30
30
31
Dodatek A
Płyta CD
43
DODATEK A.
Zawartość katalogów na płycie:
doc : elektroniczna wersja pracy dyplomowej oraz dwie prezentacje wygłoszone
podczas seminarium dyplomowego
src : repozytorium z kodem źródłowym aplikacji, z gałęzią master z dnia 2016-0131 i dwiema gałęziami trac-10839-consistent-ppr i trac-10982-unusedvars-in-tyfams, na których wykonywane były prace
web : kopie źródeł elektronicznych umieszczonych w bibliografii
Przełączenie się między gałęziami w repozytorium wymaga dodatkowego uaktualnienia submodułów używanych w projekcie komendą git submodule update
--init. Szczegóły zawarte są w zasobie [3], którego kopię można znaleźć w katalogu web.
Zbudowanie ghc wymaga przygotowania środowiska. Opis jak tego dokonać na
poszczególnych platformach jest dostępny w [4]. Do katalogu web zostały skopiowane podstrony z instrukcjami dla systemów Windows, Linux i MacOS X. Sam
proces budowania jest opisany w zasobie [17].
44