Czy wszystkie pojemniki o stałym rozmiarze są silnymi funktorami monoidalnymi i / lub odwrotnie?

9

Ta Applicativeklisza reprezentuje luźne funktory monoidalne, które zachowują kartezjańską monoidalną strukturę w kategorii typowanych funkcji.

Innymi słowy, biorąc pod uwagę obserwowane kanoniczne izomorfizmy, które (,)tworzą strukturę monoidalną:

-- Implementations left to the motivated reader
assoc_fwd :: ((a, b), c) -> (a, (b, c))
assoc_bwd :: (a, (b, c)) -> ((a, b), c)

lunit_fwd :: ((), a) -> a
lunit_bwd :: a -> ((), a)

runit_fwd :: (a, ()) -> a
runit_bwd :: a -> (a, ())

Typekę i jej prawa można zapisać w następujący sposób:

class Functor f => Applicative f
  where
  zip :: (f a, f b) -> f (a, b)
  husk :: () -> f ()

-- Laws:

-- assoc_fwd >>> bimap id zip >>> zip
-- =
-- bimap zip id >>> zip >>> fmap assoc_fwd

-- lunit_fwd
-- =
-- bimap husk id >>> zip >>> fmap lunit_fwd

-- runit_fwd
-- =
-- bimap id husk >>> zip >>> fmap runit_fwd

Można się zastanawiać, jak mógłby wyglądać funktor, który jest opoidem monoidalnym w odniesieniu do tej samej struktury:

class Functor f => OpApplicative f
  where
  unzip :: f (a, b) -> (f a, f b)
  unhusk :: f () -> ()

-- Laws:

-- assoc_bwd <<< bimap id unzip <<< unzip
-- =
-- bimap unzip id <<< unzip <<< fmap assoc_bwd

-- lunit_bwd
-- =
-- bimap unhusk id <<< unzip <<< fmap lunit_bwd

-- runit_bwd
-- =
-- bimap id unhusk <<< unzip <<< fmap runit_bwd

Jeśli pomyślimy o typach zawartych w definicjach i prawach, rozczarowująca prawda zostanie ujawniona; OpApplicativenie jest bardziej szczegółowym ograniczeniem niż Functor:

instance Functor f => OpApplicative f
  where
  unzip fab = (fst <$> fab, snd <$> fab)
  unhusk = const ()

Jednak chociaż każdy Applicativefunktor (naprawdę każdy Functor) jest trywialny OpApplicative, niekoniecznie istnieje przyjemny związek między Applicativewiotkościami i OpApplicativezmętnieniami. Możemy więc poszukać silnych funktorów monoidalnych w kartezjańskiej strukturze monoidalnej:

class (Applicative f, OpApplicative f) => StrongApplicative f

-- Laws:
-- unhusk . husk = id
-- husk . unhusk = id
-- zip . unzip = id
-- unzip . zip = id

Pierwsze powyższe prawo jest trywialne, ponieważ jedynym mieszkańcem tego typu () -> ()jest funkcja tożsamości ().

Jednak pozostałe trzy prawa, a zatem sama podklasa, nie są trywialne. W szczególności nie każdy Applicativejest zgodną z prawem instancją tej klasy.

Oto niektóre Applicativefunktory, dla których możemy zadeklarować legalne przypadki StrongApplicative:

  • Identity
  • VoidF
  • (->) r
  • Monoid m => (,) m (Zobacz odpowiedzi)
  • Vec (n :: Nat)
  • Stream (nieskończony)

A oto kilka Applicatives, dla których nie możemy:

  • []
  • Either e
  • Maybe
  • NonEmptyList

Wzór tutaj sugeruje, że StrongApplicativeklasa jest w pewnym sensie FixedSizeklasą, gdzie „ustalony rozmiar” * oznacza, że liczebność ** mieszkańców aw mieszkance f ajest ustalona.

Można to określić jako dwie domysły:

  • Każda Applicativereprezentująca kontener „o stałym rozmiarze” elementów argumentu typu jest instancją klasyStrongApplicative
  • Nie StrongApplicativeistnieje żaden przypadek , w którym liczba wystąpień amoże się różnić

Czy ktoś może pomyśleć o kontrprzykładach, które obalają te przypuszczenia, lub o przekonującym uzasadnieniu, które pokazuje, dlaczego są one prawdziwe lub fałszywe?


* Zdaję sobie sprawę, że nie zdefiniowałem właściwie przymiotnika „fixed size”. Niestety zadanie jest trochę okrągłe. Nie znam żadnego formalnego opisu kontenera o „stałym rozmiarze” i próbuję go wymyślić. StrongApplicativeto moja najlepsza jak dotąd próba.

Aby jednak ocenić, czy jest to dobra definicja, potrzebuję czegoś do porównania. Biorąc pod uwagę formalną / nieformalną definicję tego, co to znaczy dla funktora mieć określoną wielkość lub krotność w stosunku do mieszkańców tego rodzaju argumentu, pytanie brzmi, czy istnienie StrongApplicativeinstancji dokładnie rozróżnia funktory o stałej i zmiennej wielkości.

Nie zdając sobie sprawy z istniejącej definicji formalnej, odwołuję się do intuicji, posługując się terminem „ustalony rozmiar”. Jeśli jednak ktoś już wie o istniejącym formalizmie dotyczącym wielkości funktora i może go porównać StrongApplicative, tym lepiej.

** Przez „wielokrotność” w luźnym znaczeniu odnoszę się do „ilu” dowolnych elementów typu parametru funktora występuje u mieszkańca typu kodomenu funktora. Jest to bez względu na rodzaj specyficznego funktor jest nakładana, a więc bez uwzględniania jakichkolwiek mieszkańców konkretnych typu parametru.

Nie precyzowanie tego spowodowało pewne zamieszanie w komentarzach, więc oto kilka przykładów tego, co uważam za rozmiar / mnogość różnych funktorów:

  • VoidF: naprawiono, 0
  • Identity: naprawiono, 1
  • Maybe: zmienna, minimum 0, maksimum 1
  • []: zmienna, minimum 0, maksymalna nieskończoność
  • NonEmptyList: zmienna, minimum 1, maksymalna nieskończoność
  • Stream: naprawiony, nieskończony
  • Monoid m => (,) m: naprawiono, 1
  • data Pair a = Pair a a: naprawiono, 2
  • Either x: zmienna, minimum 0, maksimum 1
  • data Strange a = L a | R a: naprawiono, 1
Asad Saeeduddin
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Samuel Liew
Jedną z możliwych definicji „stałego rozmiaru” byłoby „reprezentowalne”. Wszystkie reprezentowalne funktory są silnymi aplikacjami w opisanym tutaj znaczeniu, ponieważ (->) rsą i są izomorficzne we właściwy sposób.
Daniel Wagner
@DanielWagner Myślę, że potrzebujesz czegoś więcej niż tylko izomorfizmu, aby odziedziczyć silną aplikację (->) r; potrzebujesz składników izomorfizmu, aby zachować silną strukturę aplikacyjną. Z jakiegoś powodu Representabletyp w Haskell ma tajemnicze tabulate . return = returnprawo (które tak naprawdę nie ma sensu nawet w przypadku funktorów niemonadycznych), ale daje nam 1/4 warunków, które musimy powiedzieć, tabulatei zipsą morfizmami odpowiedniej kategorii monoidów . Pozostałe 3 to dodatkowe prawa, których musisz wymagać.
Asad Saeeduddin
Przepraszam, to powinno być „ tabulatei indexsą morfizmy odpowiedniej kategorii ...”
Asad Saeeduddin
@AsadSaeeduddin Sposób, w jaki prawo jest określone w dokumentach, może jest dziwnie specyficzny, ale okazuje się, że wymaganie returnnie jest poważnym problemem. cotraverse getConst . Constjest domyślną implementacją dla return/ purepod względem Distributive, a ponieważ dystrybutorzy / reprezentanci mają ustalony kształt, ta implementacja jest unikalna.
duplode

Odpowiedzi:

4
  • Każda Applicativereprezentująca kontener „o stałym rozmiarze” elementów argumentu typu jest instancją klasyStrongApplicative
  • Nie StrongApplicativeistnieje żaden przypadek , w którym liczba wystąpień amoże się różnić

Czy ktoś może pomyśleć o kontrprzykładach, które obalają te przypuszczenia, lub o przekonującym uzasadnieniu, które pokazuje, dlaczego są one prawdziwe lub fałszywe?

Nie jestem pewien tej pierwszej przypuszczenia i na podstawie dyskusji z @AsadSaeeduddin prawdopodobnie trudno będzie ją udowodnić, ale druga hipoteza jest prawdziwa. Aby zobaczyć dlaczego, rozważ StrongApplicativeprawo husk . unhusk == id; to jest dla wszystkich x :: f (), husk (unhusk x) == x. Ale w Haskell, unhusk == const ()tak, że prawo jest równoznaczne ze stwierdzeniem, dla wszystkich x :: f (), husk () == x. Ale to z kolei oznacza, że ​​może istnieć tylko jedna odrębna wartość typu f (): gdyby były dwie wartości x, y :: f (), wtedy x == husk ()i husk () == ytak x == y. Ale jeśli istnieje tylko jedna możliwa f ()wartość, fmusi mieć ustalony kształt. (np. dla data Pair a = Pair a aistnieje tylko jedna wartość typu Pair (), to jest Pair () (), ale istnieje wiele wartości typu Maybe ()lub[()] .) Zatemhusk . unhusk == idoznacza, że fmusi mieć ustalony kształt.

bradrn
źródło
Hm Czy naprawdę jest jasne, że „może istnieć tylko jedna odrębna wartość typu f ()„ implikuje ”, że liczba wystąpień anie może się różnić” w obecności fantazyjnych GADT i innych rzeczy?
Daniel Wagner
@DanielWagner Okazało się, że „liczba wystąpień anie może się różnić” nie jest wystarczającym warunkiem dla StrongApplicativewystąpienia; na przykład data Writer w a = Writer (w,a)ma niezmienną wielokrotność a, ale nie jest StrongApplicative. W rzeczywistości potrzebujesz kształtu funktora, aby był niezmienny, co, jak sądzę, jest konsekwencją f ()bycia singletonem.
bradrn
Nie jestem pewien, czy rozumiem, jak to jest istotne. W odpowiedzi, potwierdzając drugą hipotezę, argumentujesz, że „silna aplikacja” oznacza „jedna wyraźna f ()” implikuje „liczba wystąpień anie może się różnić”. Nie zgadzam się z tym, że ostatni krok tego argumentu nie jest wyraźnie prawdziwy; np data Weird a where One :: a -> Weird a; None :: Weird Bool. rozważ . Istnieje wyraźna wartość typu Weird (), ale różni konstruktorzy mają różną liczbę as wewnątrz. (Nie jest to pełny kontrprzykład, ponieważ Functorjest trudny, ale skąd wiemy, że nie można tego naprawić?)
Daniel Wagner
@DanielWagner Dobry punkt, który Weird ()jest singletonem, ale nie ma ustalonego kształtu. Ale Weirdnie jest Functor, więc i tak nie może być StrongApplicative. Przypuszczam, że właściwym przypuszczeniem byłoby: jeśli fjest Functor, to czy f ()bycie singletonem implikuje, że fma ustalony kształt ? Podejrzewam, że to prawda, ale jak zauważyłeś, nie mam jeszcze żadnego dowodu.
bradrn
5

Możemy odpowiedzieć przynajmniej na jedno z poniższych pytań przecząco:

Każdy aplikant reprezentujący kontener „o stałym rozmiarze” elementów argumentu typu jest instancją StrongApplicative

W rzeczywistości jeden z przykładów zgodnych z prawem StrongApplicativew pierwotnym pytaniu jest błędny. Aplikacja pisarzaMonoid => (,) m nie jest StrongApplicative, ponieważ na przykład husk $ unhusk $ ("foo", ()) == ("", ()) /= ("foo", ()).

Podobnie przykład kontenera o stałym rozmiarze:

data Strange a = L a | R a

o stałej krotności 1, nie jest silną aplikacją, ponieważ jeśli zdefiniujemy husk = Leftto,husk $ unhusk $ Right () /= Right () , i vice versa. Równoważnym sposobem na zobaczenie tego jest to, że jest to po prostu aplikacja pisarska do wyboru monoidu Bool.

Istnieją więc aplikacje o ustalonym rozmiarze, które nie są StrongApplicative. Nie wiadomo, czy wszystkie StrongApplicativemają stały rozmiar.

Asad Saeeduddin
źródło
5

Weźmy reprezentatywne funktory jako naszą definicję „kontenera o stałym rozmiarze”:

class Representable f where
    type Rep f
    tabulate :: (Rep f -> a) -> f a
    index :: f a -> Rep f -> a

Rzeczywistość Representablema kilka praw i nadklas, ale do celów tej odpowiedzi potrzebujemy tylko dwóch właściwości:

tabulate . index = id
index . tabulate = id

(Okej, potrzebujemy również prawnie przestrzeganych przepisów instance StrongApplicative ((->) r). Spokojny, już zgadzasz się, że istnieje).

Jeśli weźmiemy tę definicję, mogę potwierdzić tę hipotezę 1:

Każdy Applicativeelement reprezentujący „stały rozmiar” kontenera elementów argumentu typu jest zgodną z prawem instancjąStrongApplicative

jest prawdziwy. Oto jak:

instance Representable f => Applicative f where
    zip (fa, fb) = tabulate (zip (index fa, index fb))
    husk = tabulate . husk

instance Representable f => OpApplicative f where
    unzip fab = let (fa, fb) = unzip (index fab) in (tabulate fa, tabulate fb)
    unhusk = unhusk . index

instance Representable f => StrongApplicative f

Jest wiele praw do udowodnienia, ale skupię się tylko na Wielkiej Czwórce, która StrongApplicativedodaje - prawdopodobnie już wierzysz w wprowadzenie Applicativei OpApplicative, ale jeśli nie, ich dowody wyglądają tak jak te poniżej ( które z kolei wyglądają bardzo podobnie do siebie). Dla jasności, użyję zipf, huskfitp na przykład funkcji, a zipr, huskritp dla przedstawialnym instancji, dzięki czemu można śledzić, który jest który. (I aby łatwo było zweryfikować, że nie bierzemy tego, co próbujemy udowodnić, jako założenie! Można to wykorzystać unhuskf . huskf = idpodczas dowodzenia unhuskr . huskr = id, ale błędne byłoby założenie, żeunhuskr . huskr = id tego samego dowodu.)

Potwierdzenie każdego prawa przebiega w zasadzie w ten sam sposób: rozwinąć definicje, porzucić izomorfizm, który Representabledaje, a następnie zastosować analogiczne prawo dla funkcji.

unhuskr . huskr
= { def. of unhuskr and huskr }
(unhuskf . index) . (tabulate . huskf)
= { index . tabulate = id }
unhuskf . huskf
= { unhuskf . huskf = id }
id

huskr . unhuskr
= { def. of huskr and unhuskr }
(tabulate . huskf) . (unhuskf . index)
= { huskf . unhuskf = id }
tabulate . index
= { tabulate . index = id }
id

zipr (unzipr fab)
= { def. of unzipr }
zipr (let (fa, fb) = unzipf (index fab) in (tabulate fa, tabulate fb))
= { def. of zipr }
let (fa, fb) = unzipf (index fab) in tabulate (zipf (index (tabulate fa), index (tabulate fb)))
= { index . tabulate = id }
let (fa, fb) = unzipf (index fab) in tabulate (zipf (fa, fb))
= { def. of (fa, fb) }
tabulate (zipf (unzipf (index fab)))
= { zipf . unzipf = id }
tabulate (index fab)
= { tabulate . index = id }
fab

unzipr (zipr (fa, fb))
= { def. of zipr }
unzipr (tabulate (zipf (index fa, index fb)))
= { def. of unzipr }
let (fa', fb') = unzipf (index (tabulate (zipf (index fa, index fb))))
in (tabulate fa', tabulate fb')
= { index . tabulate = id }
let (fa', fb') = unzipf (zipf (index fa, index fb))
in (tabulate fa', tabulate fb')
= { unzipf . zipf = id }
let (fa', fb') = (index fa, index fb)
in (tabulate fa', tabulate fb')
= { def. of fa' and fb' }
(tabulate (index fa), tabulate (index fb))
= { tabulate . index = id }
(fa, fb)
Daniel Wagner
źródło
Obecnie zastanawiać: instance StrongApplicative f => Representable f where type Rep f = forall x. f x -> x. indexjest proste. Do tej pory nie wypracowałem tej sztuczki tabulate, ale wydaje się kusząco blisko.
Daniel Wagner,
W dyskusji z @AsadSaeeduddin udało mi się również znaleźć tę samą StrongApplicativeinstancję, ale nie mogłem udowodnić praw. Gratulacje za zrozumienie! Próbowałem zrobić to Representablesamo, co podane StrongApplicative, ale nie mogłem wymyślić dobrego Reptypu - byłbym ciekawy, jak to forall x. f x -> xosiągnąć?
bradrn
@bradrn Przypomnijmy, że hipoteza jest taka, że ​​te rzeczy mają ustalony zestaw „dziur”, w które wchodzą elementy. Zatem funkcjami typu forall x. f x -> xsą dokładnie te funkcje, które wybierają otwór i zwracają wartość w tym otworze. (I zastanawiając się, jak wdrożyć tabulate, wpadłem na sprzeciw wobec tego typu unhusk; zobacz szczegóły na temat samego pytania).
Daniel Wagner
Dzięki @DanielWagner! To naprawdę sprytne podejście - nie pomyślałbym o tym.
bradrn
Po próbie zaimplementowania go, nie sądzę, że jestem przekonany, że forall x. f x -> xzadziała jak Rep. Moje rozumowanie jest takie, że używając tego Repmożesz pisać indexdla dowolnego typu, nie tylko dla StrongApplicativetych - więc podejrzewam, że forall x. f x -> xmoże to być zbyt ogólne.
bradrn