komentarz DarkOtter wspomina QuickCheck za Arbitrary
i CoArbitrary
klas, które są z pewnością pierwszą rzeczą, jaką należy spróbować. QuickCheck ma tę instancję:
instance (CoArbitrary a, Arbitrary b) => Arbitrary (a -> b) where ...
Jak to się dzieje, właśnie wczoraj odczytaniu kodu QuickCheck zrozumieć, jak to działa, więc mogę po prostu dzielić się dowiedziałem podczas gdy to świeże w moim umyśle. QuickCheck zbudowana jest wokół rodzaju, który wygląda tak (i to nie będzie dokładnie taka sama):
type Size = Int
-- | A generator for random values of type @[email protected]
newtype Gen a =
MkGen { -- | Generate a random @[email protected] using the given randomness source and
-- size.
unGen :: StdGen -> Size -> a
}
class Arbitrary a where
arbitrary :: a -> Gen a
Pierwszy trik jest QuickCheck posiada funkcję, która działa w ten sposób (i nie wyszło dokładnie jak to jest realizowane):
-- | Use the given 'Int' to \"perturb\" the generator, i.e., to make a new
-- generator that produces different pseudorandom results than the original.
variant :: Int -> Gen a -> Gen a
Wtedy to wykorzystać, aby realizować różne instancje tej CoArbitrary
Klasa:
class CoArbitrary a where
-- | Use the given `a` to perturb some generator.
coarbitrary :: a -> Gen b -> Gen b
-- Example instance: we just treat each 'Bool' value as an 'Int' to perturb with.
instance CoArbitrary Bool where
coarbitrary False = variant 0
coarbitrary True = variant 1
teraz z tych elementów w miejscu, chcemy w ten sposób:
instance (Coarbitrary a, Arbitrary b) => Arbitrary (a -> b) where
arbitrary = ...
nie będę pisać realizacji, ale idea jest taka:
- Korzystanie instancję
CoArbitrary
z a
i wystąpienie b
Arbitrary
możemy funkcję \a -> coarbitrary a arbitrary
, która ma typ a -> Gen b
.
- Należy pamiętać, że
Gen b
jest nowym typem dla StdGen -> Size -> b
, więc typ a -> Gen b
jest izomorficzny z a -> StdGen -> Size -> b
.
- Możemy trywialnie napisać funkcję, która przyjmuje dowolną funkcję tego ostatniego typu i przełącza kolejność argumentów wokół, aby zwrócić funkcję typu
StdGen -> Size -> a -> b
.
- Ten przestawiony typ jest izomorficzny z
Gen (a -> b)
, więc voilà, pakujemy zmienioną funkcję do Gen
, a otrzymaliśmy generator losowych funkcji!
Zalecam przeczytanie źródła QuickCheck, aby samemu to zobaczyć. Kiedy to rozwiążesz, natrafisz tylko na dwa dodatkowe szczegóły, które mogą cię spowolnić. Po pierwsze, klasa Haskell RandomGen
ma tę metodę:
-- | The split operation allows one to obtain two distinct random generators.
split :: RandomGen g => g -> (g, g)
Operacja ta jest wykorzystywana w przypadku Monad
dla Gen
i jest dość ważne. Jedną z tych sztuczek jest to, że StdGen
to czysty generator liczb pseudolosowych; tak działa Gen (a -> b)
dla każdej możliwej wartości a
zaburzamy generator b
, wykorzystujemy ten zakłócony generator do wygenerowania wyniku b
, ale wtedy nigdy nie przesunie stanu zniekształconego generatora; w zasadzie wygenerowana funkcja a -> b
jest zamknięciem nad pseudolosowym ziarnem i za każdym razem, gdy nazywamy je z pewnym a
, używamy tego specyficznego a
do deterministycznego tworzenia nowego nasienia, a następnie używamy go do deterministycznego generowania b
, który zależy od a
i ukryte nasienie.
skrócona rodzaj Seed -> a -> b
mniej więcej podsumowuje to, co się dzieje, to funkcja pseudolosowych jest regułą generowania b
z nasion pseudolosowych oraz a
. To nie zadziała w przypadku imperatywnych generatorów liczb losowych.
Po drugie: zamiast bezpośrednio funkcji (a -> StdGen -> Size -> b) -> StdGen -> Size -> a -> b
, jak opisałem powyżej, kod QuickCheck ma promote :: Monad m => m (Gen a) -> Gen (m a)
, który jest uogólnieniem tego do dowolnego Monad
. Kiedy m
jest instancją funkcji Monad
, promote
jest zbieżna z (a -> Gen b) -> Gen (a -> b)
, więc jest naprawdę taka sama jak szkicuję powyżej.
Czy po pełnym przejrzeniu wszystkie funkcje będą miały ten sam typ wyniku? – mhwombat
@mhwombat Tak, będą. – Eyal
Jakie wyniki i typy argumentów powinny mieć funkcje?Warto przyjrzeć się na przykład klasie Arbitra i CoArbitrary w QuickCheck, które są używane do losowego generowania funkcji do celów testowych. Ponadto, jeśli naprawdę trzeba ominąć typechecker, można to zrobić za pomocą niebezpiecznego samochodu. Jest to faktycznie używane wewnętrznie w funkcji rzutowania Typeable. – DarkOtter