mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 13:02:11 +03:00
152 lines
4.9 KiB
Haskell
152 lines
4.9 KiB
Haskell
|
{-# LANGUAGE QuantifiedConstraints #-}
|
||
|
{-# LANGUAGE TemplateHaskellQuotes #-}
|
||
|
{-# LANGUAGE NoOverloadedStrings #-}
|
||
|
|
||
|
-- | A suite of test cases for memoization monads.
|
||
|
-- Designed to be reusable.
|
||
|
module Control.Monad.MemoizationSpecDefinition (Memoizer (..), memoizationSpec) where
|
||
|
|
||
|
import Control.Monad.TimeLimit
|
||
|
import Data.HashMap.Strict qualified as Map
|
||
|
import Data.Kind (Type)
|
||
|
import Data.Typeable (Typeable)
|
||
|
import Hasura.Prelude
|
||
|
import Language.Haskell.TH (Name)
|
||
|
import Test.Hspec
|
||
|
|
||
|
class
|
||
|
( forall k v. MonadTrans (m k v),
|
||
|
forall k v n. Monad n => Functor (m k v n),
|
||
|
forall k v n. Monad n => Applicative (m k v n),
|
||
|
forall k v n. Monad n => Monad (m k v n),
|
||
|
forall k v n s. MonadState s n => MonadState s (m k v n)
|
||
|
) =>
|
||
|
Memoizer (m :: Type -> Type -> (Type -> Type) -> Type -> Type)
|
||
|
where
|
||
|
runMemoizer ::
|
||
|
forall k v n a.
|
||
|
(MonadFix n, MonadIO n, Eq k, Ord k, Hashable k) =>
|
||
|
m k v n a ->
|
||
|
n a
|
||
|
memoize ::
|
||
|
forall k v n.
|
||
|
(MonadFix n, MonadIO n, Eq k, Ord k, Hashable k, Typeable k, Typeable v) =>
|
||
|
Name ->
|
||
|
k ->
|
||
|
m k v n v ->
|
||
|
m k v n v
|
||
|
|
||
|
memoizationSpec :: forall m. Memoizer m => Spec
|
||
|
memoizationSpec = do
|
||
|
describe "circular graphs" $ checkCircularGraphs @m
|
||
|
describe "infinite lists" $ checkInfiniteLists @m
|
||
|
describe "memoization" $ checkMemoization @m
|
||
|
describe "does not protect against bad code" $ checkFailure @m
|
||
|
|
||
|
--------------------------------------------------------------------------------
|
||
|
-- Graphs
|
||
|
|
||
|
data Node = Node String [Node]
|
||
|
|
||
|
nodeName :: Node -> String
|
||
|
nodeName (Node n _) = n
|
||
|
|
||
|
instance Show Node where
|
||
|
show (Node name succs) = "Node " ++ show name ++ " " ++ show (nodeName <$> succs)
|
||
|
|
||
|
instance Eq Node where
|
||
|
Node n1 s1 == Node n2 s2 = n1 == n2 && map nodeName s1 == map nodeName s2
|
||
|
|
||
|
checkCircularGraphs :: forall m. Memoizer m => Spec
|
||
|
checkCircularGraphs = do
|
||
|
it "builds A -> B -> C -> A" do
|
||
|
(a, b, c) <- succeedsWithinTimeLimit $ runMemoizer @m do
|
||
|
let buildA = memoize 'checkCircularGraphs "a" do
|
||
|
b <- buildB
|
||
|
pure $ Node "a" [b]
|
||
|
buildB = memoize 'checkCircularGraphs "b" do
|
||
|
c <- buildC
|
||
|
pure $ Node "b" [c]
|
||
|
buildC = memoize 'checkCircularGraphs "c" do
|
||
|
a <- buildA
|
||
|
pure $ Node "c" [a]
|
||
|
(,,)
|
||
|
<$> buildA
|
||
|
<*> buildB
|
||
|
<*> buildC
|
||
|
a `shouldBe` Node "a" [b]
|
||
|
b `shouldBe` Node "b" [c]
|
||
|
c `shouldBe` Node "c" [a]
|
||
|
it "builds A -> A" do
|
||
|
a <- succeedsWithinTimeLimit $ runMemoizer @m do
|
||
|
let buildA = memoize 'checkCircularGraphs "a" do
|
||
|
a <- buildA
|
||
|
pure $ Node "a" [a]
|
||
|
buildA
|
||
|
a `shouldBe` Node "a" [a]
|
||
|
|
||
|
--------------------------------------------------------------------------------
|
||
|
-- Infinite lists
|
||
|
|
||
|
checkInfiniteLists :: forall m. Memoizer m => Spec
|
||
|
checkInfiniteLists = do
|
||
|
it "builds `x = 1 : x`" do
|
||
|
l <- succeedsWithinTimeLimit $ runMemoizer @m do
|
||
|
let x = memoize 'checkInfiniteLists () do
|
||
|
y <- x
|
||
|
pure $ (1 :: Int) : y
|
||
|
x
|
||
|
take 5 l `shouldBe` [1, 1, 1, 1, 1]
|
||
|
it "builds `[0,1,2..]`" do
|
||
|
l <- succeedsWithinTimeLimit $ runMemoizer @m do
|
||
|
let x = memoize 'checkInfiniteLists () do
|
||
|
y <- x
|
||
|
pure $ (0 :: Int) : map succ y
|
||
|
x
|
||
|
take 5 l `shouldBe` [0, 1, 2, 3, 4]
|
||
|
|
||
|
--------------------------------------------------------------------------------
|
||
|
-- Memoization
|
||
|
|
||
|
checkMemoization :: forall m. Memoizer m => Spec
|
||
|
checkMemoization = do
|
||
|
it "memoizes fibo" do
|
||
|
(fibos, count) <- succeedsWithinTimeLimit $
|
||
|
flip runStateT (mempty :: HashMap Int Int) $ runMemoizer @m do
|
||
|
let fibo n = memoize 'checkMemoization n do
|
||
|
modify $ Map.insertWith (+) n (1 :: Int)
|
||
|
case n of
|
||
|
0 -> pure 0
|
||
|
1 -> pure 1
|
||
|
_ -> (+) <$> fibo (n - 2) <*> fibo (n - 1)
|
||
|
traverse fibo [0 .. 20]
|
||
|
fibos !! 20 `shouldBe` (6765 :: Int)
|
||
|
count `shouldBe` Map.fromList (zip [0 .. 20] (repeat 1))
|
||
|
|
||
|
--------------------------------------------------------------------------------
|
||
|
-- Failure
|
||
|
|
||
|
checkFailure :: forall m. Memoizer m => Spec
|
||
|
checkFailure = do
|
||
|
it "unsuccessfully attempts to memoize Maybe" do
|
||
|
result <- runWithTimeLimit $ runMemoizer @m do
|
||
|
let buildA :: m String (Maybe Node) IO (Maybe Node)
|
||
|
buildA = memoize 'checkFailure "a" do
|
||
|
mb <- buildB
|
||
|
-- a can only exist if b exists
|
||
|
pure $ mb <&> \b -> Node "a" [b]
|
||
|
buildB :: m String (Maybe Node) IO (Maybe Node)
|
||
|
buildB = memoize 'checkFailure "b" do
|
||
|
ma <- buildA
|
||
|
-- b can only exist if a exists
|
||
|
pure $ ma <&> \a -> Node "b" [a]
|
||
|
buildA
|
||
|
result `shouldBe` Nothing
|
||
|
it "unsuccessfully attempts to build a self-referential int" do
|
||
|
result <- runWithTimeLimit $ runMemoizer @m do
|
||
|
let go = memoize 'checkFailure () do
|
||
|
x <- go
|
||
|
pure $ if odd x then 1 else 0 :: Int
|
||
|
go
|
||
|
result `shouldBe` Nothing
|