Co oznacza wykrzyknik w deklaracji Haskell?

261

Natrafiłem na następującą definicję, próbując nauczyć się Haskell, używając prawdziwego projektu do jej prowadzenia. Nie rozumiem, co oznacza wykrzyknik przed każdym argumentem, a moje książki chyba o tym nie wspominały.

data MidiMessage = MidiMessage !Int !MidiMessage
David
źródło
13
Podejrzewam, że może to być bardzo częste pytanie; Wyraźnie pamiętam, jak zastanawiałem się nad dokładnie tą samą rzeczą, w przeszłości.
cjs

Odpowiedzi:

313

To deklaracja ścisłości. Zasadniczo oznacza to, że należy go ocenić do tak zwanej „postaci normalnej słabej głowy” podczas tworzenia wartości struktury danych. Spójrzmy na przykład, abyśmy mogli zobaczyć, co to znaczy:

data Foo = Foo Int Int !Int !(Maybe Int)

f = Foo (2+2) (3+3) (4+4) (Just (5+5))

Powyższa funkcja fpo ocenie zwróci „thunk”: kod, który należy wykonać, aby obliczyć jego wartość. W tym momencie Foo jeszcze nie istnieje, tylko kod.

Ale w pewnym momencie ktoś może spróbować zajrzeć do środka, prawdopodobnie poprzez dopasowanie wzoru:

case f of
     Foo 0 _ _ _ -> "first arg is zero"
     _           -> "first arge is something else"

Spowoduje to wykonanie kodu wystarczającego do zrobienia tego, czego potrzebuje, i nic więcej. Stworzy to Foo z czterema parametrami (ponieważ nie możesz zajrzeć do niego bez jego istnienia). Po pierwsze, odkąd go testujemy, musimy ocenić aż do miejsca 4, w którym zdajemy sobie sprawę, że nie pasuje.

Drugiego nie trzeba oceniać, ponieważ go nie testujemy. Tak więc, zamiast 6być przechowywane w tej lokalizacji pamięci, będziemy po prostu zapisać kod do ewentualnej późniejszej oceny (3+3). To zmieni się w 6 tylko wtedy, gdy ktoś na to spojrzy.

Trzeci parametr ma jednak !przed sobą, więc jest ściśle oceniany: (4+4)jest wykonywany i 8jest przechowywany w tym miejscu pamięci.

Czwarty parametr jest również ściśle oceniany. Ale tutaj jest nieco trudniej: oceniamy nie w pełni, ale tylko do słabej normalnej postaci głowy. Oznacza to, że ustalamy, czy to, Nothingczy Justcoś, i przechowujemy to, ale nie idziemy dalej. Oznacza to, że przechowujemy nie, Just 10ale faktycznie Just (5+5), pozostawiając thunk w środku nieocenionym. To ważne, aby wiedzieć, choć uważam, że wszystkie implikacje tego wykraczają poza zakres tego pytania.

Możesz opisać argumenty funkcji w ten sam sposób, jeśli włączysz BangPatternsrozszerzenie języka:

f x !y = x*y

f (1+1) (2+2)zwróci thunk (1+1)*4.

cjs
źródło
16
To jest bardzo pomocne. Nie mogę się jednak zastanawiać, czy terminologia Haskell komplikuje zrozumienie przez ludzi takich pojęć, jak „słaba, normalna głowa”, „surowy” i tak dalej. Jeśli dobrze cię rozumiem, to brzmi jak! operator oznacza po prostu przechowywanie oszacowanej wartości wyrażenia zamiast przechowywania anonimowego bloku w celu późniejszego oszacowania. Czy to rozsądna interpretacja, czy może jest coś więcej?
David
71
@David Pytanie brzmi, jak daleko można ocenić wartość. Normalna postać słabej głowy oznacza: oceniaj ją, aż dojdziesz do najbardziej zewnętrznego konstruktora. Normalna forma oznacza oszacowanie całej wartości, dopóki nie pozostaną żadne nieocenione składniki. Ponieważ Haskell pozwala na różne poziomy głębokości oceny, ma bogatą terminologię do opisania tego. Nie wyróżniasz tych różnic w językach, które obsługują tylko semantykę call-by-value.
Don Stewart
8
@David: Napisałem tu bardziej szczegółowe wyjaśnienie: Haskell: Co to jest normalna postać słabej głowy? . Chociaż nie wspominam o wzorach huku lub adnotacjach dotyczących ścisłości, są one równoważne z użyciem seq.
hammar
1
Dla pewności zrozumiałem: czy to lenistwo odnosi się tylko do nieznanych argumentów w czasie kompilacji? To znaczy, jeśli kod źródłowy naprawdę zawiera instrukcję (2 + 2) , czy nadal byłby zoptymalizowany do 4 zamiast kodu, aby dodać znane liczby?
Cześć Anioł,
1
Cześć Angel, to naprawdę zależy od tego, jak daleko kompilator chce zoptymalizować. Chociaż używamy grzywki, aby powiedzieć, że chcielibyśmy wymusić pewne rzeczy na słabej, normalnej formie głowy, nie mamy (i nie potrzebujemy ani nie chcemy) sposobu na powiedzenie „upewnij się, że jeszcze nie ocenisz tego grubiańskiego krok dalej." Z semantycznego punktu widzenia, biorąc pod uwagę thunk „n = 2 + 2”, nie można nawet stwierdzić, czy jakieś konkretne odniesienie do n jest obecnie oceniane, czy było wcześniej oceniane.
cjs
92

Prostym sposobem na dostrzeżenie różnicy między ścisłymi i niesurymi argumentami konstruktora jest to, jak zachowują się, gdy są niezdefiniowane. Dany

data Foo = Foo Int !Int

first (Foo x _) = x
second (Foo _ y) = y

Ponieważ nieprecyzyjny argument nie jest oceniany przez second, przekazanie undefinednie powoduje problemu:

> second (Foo undefined 1)
1

Ale ścisłym argumentem nie może być undefined, nawet jeśli nie użyjemy wartości:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined
Chris Conway
źródło
2
Jest to lepsze niż odpowiedź Curt'a Sampsona, ponieważ faktycznie wyjaśnia możliwe do zaobserwowania przez użytkownika efekty, jakie !ma symbol, zamiast zagłębiać się w wewnętrzne szczegóły implementacji.
David Grayson
26

Uważam, że jest to adnotacja o ścisłości.

Haskell jest czystym i leniwym językiem funkcjonalnym, ale czasem lenistwo może być zbyt duże lub zbyteczne. Aby sobie z tym poradzić, możesz poprosić kompilatora o pełną ocenę argumentów funkcji zamiast analizowania.

Na tej stronie znajduje się więcej informacji: Wydajność / Surowość .

Chris Vest
źródło
Hmm, przykład na tej stronie, do której się odwołujesz, zdaje się mówić o użyciu! podczas wywoływania funkcji. Ale wydaje się to inne niż umieszczenie go w deklaracji typu. czego mi brakuje?
David
Utworzenie instancji typu jest również wyrażeniem. Można myśleć o konstruktorach typów jako funkcjach, które zwracają nowe instancje określonego typu, biorąc pod uwagę dostarczone argumenty.
Chris Vest
4
W rzeczywistości, dla wszystkich celów i konstruktorów typów funkcje; możesz je częściowo zastosować, przekazać do innych funkcji (np. map Just [1,2,3]aby uzyskać [Tylko 1, Tylko 2, Tylko 3]) i tak dalej. Uważam, że warto pomyśleć o możliwości dopasowania do nich wzoru, a także o zupełnie niezwiązanym obiekcie.
cjs