Specyfikacja WCCL L0
- Typy danych, składnia zapisu stałych i mechanizm zmiennych
- Składnia i semantyka
Adam Radziszewski i Adam Wardyński
Październik 2010 — kwiecień 2011
W ramach podjęzyka L0 wydzielono najbardziej podstawową część: wyrażenia funkcyjne. L0 odpowiada z grubsza JOSKIPI zaimplementowanemu w C++ z wyjątkiem reguł oraz składni klas niejednoznaczności tagera.
Typy danych, składnia zapisu stałych i mechanizm zmiennych¶
Chociaż wyrażenia języka ewaluowane są na zdaniach i tokenach, żaden z tych bytów nie ma odpowiadającego typu wspieranego przez język. Dla wyrażeń WCCL bieżące zdanie jest „całym uniwersum”, więc nie musimy go nazywać (za odpalenie na konkretnym zdaniu odpowiedzialny jest użytkownik implementacji). Jeśli chodzi o tokeny, odwołujemy się do pozycji, pod którymi widzimy różne cechy, np. orth, wartość przypadka. Fizycznie te cechy są wyłuskiwane z tokenu leżącego na podanej pozycji, lecz na tym poziomie abstrakcji widzimy konkretne cechy, a nie tokeny, leksemy itp.
Język jest statycznie typizowany, typy są wywodzone automatycznie z danych (type-inferred).
Typy danych wspierane przez język:- zbiór symboli z tagsetu, np.
{subst, fin, praet}
,{nom}
- zbiór napisów, np.
["być", "zostać"]
,['nie']
- wartości logiczne: True, False
- pozycje, czyli wskazanie na konkretny token w zdaniu względem bieżącej pozycji lub
begin
/end
(bezwzględne odwołanie do początku/końca zdania)
- liczba całkowita, np. 0, 2
Typy zbiór symboli z tagsetu i zbiór napisów nie mają odpowiadających typów „pojedynczy symbol”, „napis”[1]. Wszystkie operacje na symbolach i napisach zakładają, że przetwarzany jest zbiór. Jeśli mówimy o pojedynczej wartości, jest to zbiór jednoelementowy. To jest celowe. WCCL z założenia operuje na wieloznacznych interpretacjach: tokenowi przypisane może być wiele alternatywnych tagów i wiele alternatywnych lematów. Poniekąd podobnie zachowuje się typ str
i unicode
w Pythonie: nie ma typu char
, 'kocur'[1] == 'o'
.
1 niepełnowartościowy typ „napis” pojawia się na poziomie L1.
Typ liczba jest niepełnowartościowy, ponieważ 1) takich bytów nie można zwrócić, 2) dopuszczamy tylko stałe, 3) wartości te są używane w bardzo wąskich kontekstach. Co więcej, obsługa niepełnowartościowych typów danych na poziomie implementacji może być zamknięta w parserze — wartości tych typów nie można zwrócić, więc i tak nie mogą one opuścić parsera.
Konkretne stałe z tagsetu nie należą do gramatyki, ponieważ tagsety mogą być różne. Gramatyka określa jedynie warunki, które musi spełniać ciąg znaków, by mógł być uznany za symbol z tagsetu. Podczas parsowania stałe z tagsetu zamieniane są na wewnętrzną reprezentację (np. liczby wg wartości zdefiniowanych w tagsecie). Reprezentacja ta jest używana tylko wewnątrz implementacji i użytkownik WCCL może uzyskać jedynie reprezentację tekstową. Stwarza to możliwość optymalizacji.
Symbole z tagsetu¶
Zbiór pusty: {}
Zbiór niepusty: {w1, w2, …, wn}
Zbiór jednoelementowy (cukier składniowy): w1
w1
… wn
musi być zdefiniowany w obecnie używanym przez parser tagsecie jako:
- wartość klasy gramatycznej (np.
subst
) albo - wartość jednego z atrybutów (np.
nom
) albo - nazwa jednego z atrybutów (np.
cas
; klasa gramatyczna to nie atrybut).
Dla każdego symbolu z tagsetu dopuszczalna jest również składnia `w1`, gdzie znakiem jest gravis (U+0060 GRAVE ACCENT, „backquote”; podobną funkcję ma w SQL-u: pozwala odwołać się do tabeli, której nazwa jest zbieżna ze słowem kluczowym).
W ostatnim przypadku każde wystąpienie nazwy atrybutu rozwijane jest do wszystkich jego wartości. Rozwijanie ma miejsce podczas parsowania, różne sposoby określenia tego samego zbioru dają ten sam wynik.
Przykładowo, w tagsecie KIPI mamy {gnd}
= {sg, pl}
{nmb, gnd, cas}
= {nmb, sg, pl, cas}
= … = {m1,m2,m3,n,f,sg,pl,nom,gen,dat,acc,loc,inst,voc}
Uwaga: symbole określające nazwy atrybutów stają się automatycznie nazwami operatorów do pobierania wartości atrybutu. Te oraz pozostałe symbole brane są z pliku definiującego tagset. Autorzy tagsetów KIPI i NKJP nie definiują takich symboli (w poprzednich JOSKIPI określiliśmy własne symbole, np. cas i ppr; dla porównania, Spejd i Pantera korzystają z pełnych nazw atrybutów, np. case, post-prepositionality).
Zbiór napisów¶
Zbiór pusty: []
Zbiór niepusty: [s1, s2, …, sn]
Zbiór jednoelementowy (cukier składniowy): s1
każdy z symboli s1
to napis otoczony cudzysłowami ASCII (U+0022 QUOTATION MARK) bądź apostrofami ASCII (U+0027 APOSTROPHE). Dopuszczalne są sekwencje kontrolne (escape sequences) zdefiniowane przez ICU.
Liczba całkowita¶
Składnia standardowa.
Typ ten jest używany do określenia, ile razy musi być spełniony warunek operatora ustalającego wartość zmiennej.
Zmienne, ich składnia i zasięg¶
W WCCL L0 dostępne są jedynie zmienne po pozycjach, tj. zmienne, których wartość jest typu pozycja.
Składnia. Odwołanie do zmiennej po pozycji to wyrażenie $Var
, gdzie Var
to nazwa zmiennej (musi rozpoczynać się wielką literą lub podkreśleniem).
WCCL stosuje dynamiczny zasięg zmiennych przy założeniu, że zasięgów nie można zagnieżdżać (dążymy do prostoty kosztem uniwersalności). Przez zasięg będziemy tutaj rozumieć przypisanie identyfikatorom zmiennych ich wartości.
Wszystkie wyrażenia funkcyjne z WCCL L0 działają w zasięgu zmiennych. Oznacza to, że do przeparsowania dowolnego wyrażenia funkcyjnego potrzebne jest podanie zasięgu, w którym wyrażenie ma być ewaluowane. Zmienne nie wymagają deklaracji; pierwsze odwołanie do zmiennej o danej nazwie powoduje utworzenie w zasięgu obiektu danego typu (w L0 mamy tylko pozycje) ustawionego na wartość domyślną (w przypadku typu pozycja to nowhere
). Kolejne odwołania mogą użyć tej wartości lub ją zmienić. Nie ma możliwości przesłonięcia zasięgu innym zasięgiem.
Uwaga nt. implementacji: parser powinien zawierać reguły parsowania dodatkowych wyrażeń, które typem odpowiadają wyrażeniom funkcyjnym (po jednym dla każdego ze zwracanych typów), natomiast nie wymagają podania obiektu zasięgu. Reguły takie pozwolą użytkownikowi na przeparsowanie „zewnętrznego wyrażenia WCCL”, tj. zakłada się, że tworzymy nowy obiekt zasięgu i w nim osadzane jest parsowane wyrażenie (fizycznie, obiekt zasięgu przekazywany jest niżej do reguły parsowania wyrażenia funkcyjnego). Takie funkcje powinny stanowić podstawowe API parsera. Podobny mechanizm może zostać zastosowany w implementacji L1 i wyżej dla parsowania pojedynczej reguły, wzorca itp.
Pozycja¶
Pozycja wskazuje na token w przetwarzanym zdaniu. Wartością pozycji może być:nowhere
(nieustawiona),begin
(pierwszy token w zdaniu),end
(ostatni token w zdaniu),- liczba całkowita, np. -3, 0, 1 (wskazuje token względem bieżącej pozycji w zdaniu.
Pozycja to abstrakcyjne określenie tokenu w zdaniu: pozycję określamy z góry w ramach danego wyrażenia, nie wiedząc jeszcze do jakich zdań to wyrażenie zostanie zaaplikowane. W związku z tym nie możemy na tym etapie wymagać, by pozycja mieściła się w granicach zdania. Konkretne zachowanie operatora w sytuacji, gdy jeden z jego argumentów to pozycja, która ewaluuje się do indeksu poza granicami zdania zależy od semantyki danego operatora. Wszystkie jednoargumentowe operatory przyjmujące pozycję i zwracające zbiór symboli zwracają w takiej sytuacji zbiór pusty. Zachowanie innych operatorów może być różne; np. llook
i rlook
wymagają, by obie pozycje znalazły się w granicach zdania, w przeciwnym razie operator od razu kończy.
Składnia i semantyka¶
Uwaga: token rozumiany jest jako para (forma napotkana, zbiór par (lemat, tag)).
Wyrażenie WCCL L0 to wyrażenie funkcyjne WCCL L0 (inne typy wyrażeń WCCL zostaną wprowadzone w ramach L2).
Wyrażenie funkcyjne WCCL L0 to- stała określonego typu lub
- odwołanie do zmiennej określonego typu lub
- operator zwracający wartość, czyli funkcja zwracająca wartość jednego ze zdefiniowanych wcześniej typów.
Wyrażenie funkcyjne nie musi być czysto funkcyjne, tu chodzi o typ zwracanej wartości.
Odwołanie do zmiennej może być zaimplementowane jako operator. Ważne jest to, że wszystkie powyższe przypadki są dla nas wyrażeniami funkcyjnymi, które po aplikacji na kontekście zwracają wartość.
Interpretacja wyrażenia WCCL to abstrakcyjne ujęcie struktury, która powstaje po przeparsowaniu wyrażenia.
Interpretacją wyrażenia funkcyjnego WCCL jest funktor typu kontekst -> zwracany typ. Kontekst to opakowanie na zdanie i bieżącą pozycję. Idea: raz przeparsowane wyrażenie można zaaplikować do różnych zdań względem różnego tokenu centralnego (pozycji bieżącej w zdaniu). Interpretacją stałej jest funktor, który niezaleznie od kontekstu, zwraca tę samą wartość. Dzięki temu samemu typowi interpretacji, możliwe jest używanie stałych i operatorów w tych samych kontekstach.
Kod WCCL może zawierać komentarze:- komentarze do końca linii po znakach
//
- komentarze wieloliniowe
/* … */
(bez zagnieżdżeń)
Odwołania do pozycji¶
Składnia stałej określającej pozycję: patrz Typy danych. Składnia odwołania do zmiennej: patrz Zmienne, ich składnia i zasięg.
Wskazanie zmiennej po pozycji:- składnia odwołania do zmiennej ($Var, gdzie Var to nazwa zmiennej po pozycji)
- stałą określającą pozycję, np.
nowhere
, -2,begin
- wyłuskanie wartości zmiennej: składnia odwołania do zmiennej ($Var, gdzie Var to nazwa zmiennej po pozycji)
Wskazanie zmiennej i odwołanie do zmiennej są zdefiniowane jako osobne konstrukcje składniowe, ponieważ operatory ustawiające wartość zmiennej mogą przyjąć tylko odwołanie do zmiennej (nie powinny akceptować np. stałych).
Interpretacją wskazania zmiennej jest specjalna struktura reprezentująca wskazanie pozycji. Struktura ta może być użyta do zlokalizowania obiektu reprezentującego wartość zmiennej, której nazwę określa parsowane wyrażenie.
Wyrażenia czysto funkcyjne (w tym ograniczenia)¶
Wyrażenia funkcyjne można podzielić ze względu na typ zwracanej wartości: 1) zbiór symboli z tagsetu, 2) wartość logiczną, 3) zbiór napisów 4) pozycja.
Operatory pobierające wartości z tagsetu.¶
Odtąd w definicjach składni operatorów wszystkie argumenty nazwane pos
, pos1
, pos2
to wskazania pozycji.
op[pos]
, op
to
class
(klasa gramatyczna, w starym WCCL zwała sięflex
) lub- nazwa atrybutu z tagsetu,
- nazwa wartości z tagsetu (nowe).
Jeśli podana pozycja leży poza granicami zdania, operatory te zwrócą zbiór pusty.
Operatory zwracają sumę wartości danego atrybutu lub klasy gramatycznej (class
) wszystkich tagów tokenu na podanej pozycji (tu, jak i w całym języku nie patrzymy na znaczniki disamb
). W przypadku podania konkretnej wartości (np. gen[0]
w tagsecie KIPI), operator zwróci albo zbiór jednoelementowy zawierający podaną wartość (wtt. gdy podana wartość wystąpi w którymkolwiek tagu na podanej pozycji) lub zbiór pusty (w przeciwnym razie). Operator odpowiadający atrybutowi zwróci zbiór pusty, jeśli żaden z tagów tokenu na podanej pozycji nie ma określonej wartości podanego atrybutu (np. jeśli spytamy o przypadek, a żaden z tagów nie ma określonego przypadka).
range(symbol, pos1, pos2)
symbol
to nazwa atrybutu z tagsetu lubclass
- jeśli któraś z
pos1
,pos2
ewaluuje się do indeksu poza granicami zdania, zwracamy pusty zbiór symboli - w przeciwny razie zwracamy sumę zbiorów
symbol(pos) dla pos należ. do [pos1,pos2] włącznie
, gdziesymbol
Operatory filtrujące¶
Niech tag|maska
to część wspólna zbioru zwracanego przez operator maska
i zbioru wartości wszystkich atrybutów i klasy gramatycznej tagu tag
. Np. subst:pl:nom:m1|{subst,m1,m2,m3}
= {subst,m1}
, subst:pl:nom:m1|{m1,m2,m3}
= {m1}
.
catflt(pos, which, filter)
zwraca zbiór symboli będący podzbiorem zbioru zwróconego przez filter
which
ifilter
to wyrażenia zwracające zbiory symboli,- niech tags = { (
tag
należące do tokenu na pozycjipos
) :tag|which
!= {} } catflt(pos, which, filter)
= suma zbiorów (tag|filter
dlatag
należących do tags )
Disaster: ops.s_cat_filter
agrflt(pos1, pos2, agr_attrs, filter)
zwraca zbiór symboli będący podzbiorem zbioru zwróconego przez filter
agr_attrs
ifilter
to wyrażenia zwracające zbiory symboli,- sprawdzane jest słabe uzgodnienie na podanym zakresie pozycji
- niech frags =
weak_agr_constrs(pos1, pos2, agr_attrs)
- zwracamy
{}
, jeślifset
jest pusty - w innym razie zwracamy sumę zbiorów ( część wspólna ( filter, fset ) dla fset należącego do frags )
Disaster: ops_agr.s_weak_rng_agr_filter (min_vals = count_categories(s_mask(context))
)
Operatory zwracające zbiór napisów¶
Odtąd w definicjach składni operatorów wszystkie argumenty nazwane strset
, strset1
, strset2
to wyrażenie funkcyjne WCCL typu zbiór napisów. Wszystkie argumenty nazwane tset
, tset1
, … to wyrażenia funkcyjne WCCL typy zbiór symboli z tagsetu.
orth[pos]
zwraca formę napotkaną tokenu na podanej pozycji: zbiór jednoelementowy (lub pusty, jeśli pozycja wychodzi poza zakresem zdania)
base[pos]
zwraca zbiór lematów tokenu na podanej pozycji: zbiór niepusty, jeśli w granicach zdania
lower(strset)
= { lstr : lstr = lowercase ( str ) dla każdego str w strset }
upper(strset)
= { ustr : ustr = uppercase ( str ) dla każdego str w strset }
affix(strset, n)
=
n < 0: { suff : suff = suffix ( str, len
= -n or whole
) dla każdego str w strset }
n > 0: { pref : pref = prefix ( str, len
= n or whole
) dla każdego str w strset }
n = 0: strset
Jeśli długość prefiksu lub sufiksu jest dłuższa niż dany napis, pozostaje cały. Jeśli długość jest równa zero, zwraca niezmieniony zbiór.
Predykaty¶
Predykaty (ograniczenia) to wyrażenia funkcyjne WCCL typu logicznego.
Odtąd w definicjach składni operatorów wszystkie argumenty nazwane pred
, pred1
, pred2
, predn
to predykaty.
and(pred1, …, predn)
ewaluuje predykaty po kolei aż do pierwszej porażki i zwraca False, a w pp. Trueor(pred1, …, predn)
ewaluuje predykaty po kolei aż do pierwszego sukcesu i zwraca True, a w pp. Falsenot(pred1, …, predn)
(operacja NOR == not(or(pred1, …, predn))
) ewaluuje predykaty po kolei aż do pierwszego sukcesu i zwraca False, a w pp. True
TODO czy próbujemy wprowadzić bardziej intuicyjną składnię? Jeśli zostawimy tę składnię, to dla wygody dopuszczamy jednoargumentowe warianty (zeroargumentowych nie).
in(strset1, strset2)
wtt. gdy strset1
!= {} i strset1
jest podzbiorem strset2
TODO nazwa in
jest myląca.inter(strset1, strset2)
wtt. gdy przecięcie_zbiorów ( strset1
, strset2
) != {}equal(strset1, strset2)
wtt. gdy strset1
== strset2
I analogicznie dla zbiorów symboli:in(tset1, tset2)
wtt. gdy tset1
!= {} i tset1
jest podzbiorem tset2
inter
…equal
…
Dla typu pozycja definiujemy operator equal
:equal(pos1, pos2)
wtt. gdy pos1
== pos2
Składnia dla obu typów jest taka sama. Nie stworzy to problemu, bo typ wyrażenia da się wywieść z typów argumentów (o ile zmienimy uporządkujemy składnię zbiorów).
inside(pos)
zwraca True, gdy pozycja leży w granicach zdania.outside(pos
— na odwrót.
regex(strset, re_text)
sprawdza, czy wszystkie elementy zbioru strset
spełniają podane wyrażenie regularne (re_text
element typu nazwa zawierający wyrażenie regularne w formacie ICU)
TODO robimy dwa warianty? Np. anyregex
, allregex
?
TODO hasnum
, isbig
— czy to zostawiamy? regex załatwi wszystko, a te nazwy są nieeleganckie
Osobną grupą predykatów są operatory sprawdzające, czy zachodzi uzgodnienie morfo-syntaktyczne. Interpretacja uzgodnienia różnych typów omówiona jest tu. Poniższe definicje odwołują się do tych typów. agr_attrs
to zbiór wartości atrybutów, których uzgodnienie zachodzi lub nie.
agr(pos1, pos2, agr_attrs)
wtt., gdy zachodzi silne uzgodnienie na podanym zakresie pozycji
Disaster: ops_agr.p_strong_rng_agr
agrpp(pos1, pos2, agr_attrs)
wtt, gdy zachodzi silne uzgodnienie między podanymi dwoma pozycjami
Disaster: ops_agr.p_strong_pp_agr (min_vals = count_categories(s_mask(context))
)
wagr(pos1, pos2, agr_attrs)
wtt, gdy zachodzi słabe uzgodnienie na podanym zakresie pozycji
Disaster: ops_agr.p_weak_rng_agr (min_vals = count_categories(s_mask(context))
)
Operatory warunkowe¶
Konstrukcja warunkowa. Mamy tę samą składnię dla czterech typów. Pierwszy arg. to warunek, drugi to wartość do zwrócenia, gdy warunek spełniony, trzeci to wartość „else”. Typ całego wyrażenia można wywieść z typów zwracanych.if(pred, tset1, tset2)
if(pred, strset1, strset2)
if(pred, pred1, pred2)
if(pred, pos1, pos2)
Operator warunkowy: j.w., ale mamy tylko jedną wartość do zwrócenia; jeśli warunek nie jest spełniony, wartość zwracana zależy od typu:if(pred, tset1)
zwraca wartość wyrażenia tset1
, jeśli pred
, w pp. zwraca pusty zbiór symboliif(pred, strset1)
zwraca wartość wyrażenia strset1
, jeśli pred
, w pp. zwraca pusty zbiór napisówif(pred, pred1)
zwraca wartość wyrażenia pred1
, jeśli pred
, w pp. zwraca False
if(pred, pos1)
zwraca wartość wyrażenia pos1
, jeśli pred
, w pp. zwraca nowhere
Wyrażenia zwracające pozycje¶
Operator +
(pos + int
) zwraca pozycję po zaaplikowaniu przesunięcia (liczby całkowitej).
Wyrażenia zmieniające wartości zmiennych¶
Wyrażenia te zachowują się jak wyrażenia funkcyjne, lecz ich skutkiem ubocznym może być zmiana wartości zmiennych.
Jawne ustawienie wartości¶
setvar(varname, pos)
zawsze zwraca True, powoduje ustawienie zmiennej varname
na określoną wartość (pozycję)
Iteracja za pomocą zmiennej¶
Zakres między pozycjami pos1
i pos2
to ciąg kolejnych indeksów w tablicy tokenów danego zdania (to ma sens dopiero po zastosowaniu przeparsowanego operatora do konkretnego zdania). Jeśli którakolwiek z pozycji jest nieustawiona (nowhere
), otrzymujemy zakres pusty. W przeciwnym wypadku otrzymujemy ciąg kolejnych indeksów począwszy od ewaluowanej pos1
do ewaluowanej pos2
, przycięty do granic zdania.
only(pos1, pos2, posvar, pred)
wtt., gdy predykat jest spełniony na wszystkich pozycjach z zakresuSkutki uboczne:
- w razie sukcesu
posvar
pozostaje ustawiona na ostatniej pozycji z zakresu - w razie otrzymania pustego zakresu, wartość
posvar
pozostaje niezmieniona - w razie niepustego zakresu i porażki,
posvar
zostaje ustawiona nanowhere
Disaster: ops.p_all
atleast(pos1, pos2, posvar, pred, n)
wtt., gdy predykat jest spełniony na co najmniej n
pozycjach z zakresuSkutki uboczne:
- w razie sukcesu
posvar
pozostaje ustawiona na n-tej pozycji, która spełnia predykat - w razie otrzymania pustego zakresu, wartość
posvar
pozostaje niezmieniona - w razie niepustego zakresu i porażki,
posvar
zostaje ustawiona nanowhere
Disaster: ops.p_atleast
UWAGA: wcześniej w specyfikacji był błąd. llook
iteruje w lewo, rlook
iteruje w prawo. A więc llook
znajduje pierwszy dopasowany token z prawej, rlook
— z lewej. Poza tym przy llook
zakres podajemy w odwrotnej kolejności.
llook(pos2, pos1, posvar, pred)
wtt., gdy predykat jest spełniony na którejś pozycji z zakresu oraz pos2
>= pos1
.Skutki uboczne:
- w razie porażki
posvar
jest ustawiana nanowhere
- w razie sukcesu, ustawiana jest na pierwszym od prawej tokenie spełniającym predykat
Disaster: ops.p_look (step=-1)
rlook(pos1, pos2, posvar, pred)
wtt., gdy predykat jest spełniony na którejś pozycji z zakresu oraz pos2
>= pos1
.Skutki uboczne:
- w razie porażki
posvar
jest ustawiana nanowhere
- w razie sukcesu, ustawiana jest na pierwszym od lewej tokenie spełniającym predykat
Disaster: ops.p_look (step=+1)