ideas-1.0: src/Common/Exercise.hs
{-# LANGUAGE Rank2Types #-}
-----------------------------------------------------------------------------
-- Copyright 2011, Open Universiteit Nederland. This file is distributed
-- under the terms of the GNU General Public License. For more information,
-- see the file "LICENSE.txt", which is included in the distribution.
-----------------------------------------------------------------------------
-- |
-- Maintainer : bastiaan.heeren@ou.nl
-- Stability : provisional
-- Portability : portable (depends on ghc)
--
-- This module defines the concept of an exercise
--
-----------------------------------------------------------------------------
module Common.Exercise
( -- * Exercises
Exercise, makeExercise, emptyExercise
, exerciseId, status, parser, prettyPrinter
, equivalence, similarity, ready, suitable, isReady, isSuitable
, hasTermView
, strategy, navigation, canBeRestarted, extraRules, ruleOrdering
, difference, differenceEqual
, testGenerator, randomExercise, examples, getRule
, simpleGenerator, useGenerator
, randomTerm, randomTermWith, ruleset
, makeContext, inContext, recognizeRule
, ruleOrderingWith, ruleOrderingWithId
, Examples, mapExamples, Difficulty(..), readDifficulty, level
, hasTypeable, useTypeable, castFrom, castTo
-- * Exercise status
, Status(..), isPublic, isPrivate
-- * Miscellaneous
, withoutContext, simpleSimilarity, simpleEquivalence
, prettyPrinterContext, restrictGenerator
, showDerivation, printDerivation
, ExerciseDerivation, defaultDerivation
, derivationDiffEnv, derivationPrevious
, checkExercise, checkParserPretty
, checkExamples, exerciseTestSuite
) where
import Common.Classes
import Common.Context
import Common.Derivation
import Common.DerivationTree
import Common.Id
import Common.Navigator
import Common.Predicate
import Common.Rewriting
import Common.Strategy hiding (not, fail, repeat, replicate)
import Common.Transformation
import Common.Utils (ShowString(..), commaList)
import Common.Utils.TestSuite
import Common.View
import Control.Monad.Error
import Data.Char
import Data.Function
import Data.List
import Data.Maybe
import Data.Ord
import Data.Typeable
import System.Random
import Test.QuickCheck hiding (label)
import Test.QuickCheck.Gen
import qualified Common.Rewriting.Difference as Diff
import qualified Common.Strategy as S
data Exercise a = Exercise
{ -- identification and meta-information
exerciseId :: Id -- identifier that uniquely determines the exercise
, status :: Status
-- parsing and pretty-printing
, parser :: String -> Either String a
, prettyPrinter :: a -> String
-- syntactic and semantic checks
, equivalence :: Context a -> Context a -> Bool
, similarity :: Context a -> Context a -> Bool -- possibly more liberal than syntactic equality
, ready :: Predicate a
, suitable :: Predicate a
, hasTermView :: Maybe (View Term a)
, hasTypeable :: Maybe (IsTypeable a)
-- strategies and rules
, strategy :: LabeledStrategy (Context a)
, navigation :: a -> Navigator a
, canBeRestarted :: Bool -- By default, assumed to be the case
, extraRules :: [Rule (Context a)] -- Extra rules (possibly buggy) not appearing in strategy
, ruleOrdering :: Rule (Context a) -> Rule (Context a) -> Ordering -- Ordering on rules (for onefirst)
-- testing and exercise generation
, testGenerator :: Maybe (Gen a)
, randomExercise :: Maybe (StdGen -> Difficulty -> a)
, examples :: [(Difficulty, a)]
}
instance Eq (Exercise a) where
e1 == e2 = getId e1 == getId e2
instance Ord (Exercise a) where
compare = comparing getId
instance Apply Exercise where
applyAll ex = concatMap fromContext . applyAll (strategy ex) . inContext ex
instance HasId (Exercise a) where
getId = exerciseId
changeId f ex = ex { exerciseId = f (exerciseId ex) }
makeExercise :: (Show a, Eq a, IsTerm a) => Exercise a
makeExercise = emptyExercise
{ prettyPrinter = show
, similarity = (==)
, hasTermView = Just termView
}
emptyExercise :: Exercise a
emptyExercise = Exercise
{ -- identification and meta-information
exerciseId = error "no exercise code"
, status = Experimental
-- parsing and pretty-printing
, parser = const (Left "<<no parser>>")
, prettyPrinter = const "<<no pretty-printer>>"
-- syntactic and semantic checks
, equivalence = \_ _ -> True
, similarity = \_ _ -> True
, ready = true
, suitable = true
, hasTermView = Nothing
, hasTypeable = Nothing
-- strategies and rules
, strategy = label "Fail" S.fail
, navigation = noNavigator
, canBeRestarted = True
, extraRules = []
, ruleOrdering = compareId
-- testing and exercise generation
, testGenerator = Nothing
, randomExercise = Nothing
, examples = []
}
makeContext :: Exercise a -> Environment -> a -> Context a
makeContext ex env = newContext env . navigation ex
-- | Put a value into an empty environment
inContext :: Exercise a -> a -> Context a
inContext = flip makeContext emptyEnv
---------------------------------------------------------------
-- Difficulty levels
type Examples a = [(Difficulty, a)]
mapExamples :: (a -> b) -> Examples a -> Examples b
mapExamples f = map (second f)
data Difficulty = VeryEasy | Easy | Medium | Difficult | VeryDifficult
deriving (Eq, Ord, Enum)
instance Show Difficulty where
show = (xs !!) . fromEnum
where
xs = ["very_easy", "easy", "medium", "difficult", "very_difficult"]
readDifficulty :: String -> Maybe Difficulty
readDifficulty s =
case filter p [VeryEasy .. VeryDifficult] of
[a] -> Just a
_ -> Nothing
where
normal = filter isAlpha . map toLower
p = (== normal s) . normal . show
level :: Difficulty -> [a] -> Examples a
level = zip . repeat
---------------------------------------------------------------
-- Exercise generators
-- returns a sorted list of rules (no duplicates)
ruleset :: Exercise a -> [Rule (Context a)]
ruleset ex = nub (sortBy compareId list)
where
list = extraRules ex ++ rulesInStrategy (strategy ex)
simpleGenerator :: Gen a -> Maybe (StdGen -> Difficulty -> a)
simpleGenerator = useGenerator (const True) . const
useGenerator :: (a -> Bool) -> (Difficulty -> Gen a) -> Maybe (StdGen -> Difficulty -> a)
useGenerator p makeGen = Just (\rng -> rec rng . makeGen)
where
rec rng gen@(MkGen f)
| p a = a
| otherwise = rec (snd (next rng)) gen
where
(size, r) = randomR (0, 100) rng
a = f r size
restrictGenerator :: (a -> Bool) -> Gen a -> Gen a
restrictGenerator p g = do
a <- g
if p a then return a
else restrictGenerator p g
randomTerm :: Difficulty -> Exercise a -> IO a
randomTerm dif ex = do
rng <- newStdGen
return (randomTermWith rng dif ex)
randomTermWith :: StdGen -> Difficulty -> Exercise a -> a
randomTermWith rng dif ex =
case randomExercise ex of
Just f -> f rng dif
Nothing
| null xs -> error "randomTermWith: no generator"
| otherwise ->
snd (xs !! fst (randomR (0, length xs - 1) rng))
where xs = examples ex
difference :: Exercise a -> a -> a -> Maybe (a, a)
difference ex a b = do
v <- hasTermView ex
Diff.differenceWith v a b
differenceEqual :: Exercise a -> a -> a -> Maybe (a, a)
differenceEqual ex a b = do
v <- hasTermView ex
Diff.differenceEqualWith v (simpleEquivalence ex) a b
-- Recognize a rule at (possibly multiple) locations
recognizeRule :: Exercise a -> Rule (Context a) -> Context a -> Context a -> [(Location, ArgValues)]
recognizeRule ex r ca cb = rec (fromMaybe ca (top ca))
where
rec x =
let here = case ruleRecognizer (similarity ex) r x cb of
Just as -> [(location x, as)]
Nothing -> []
in here ++ concatMap rec (allDowns x)
ruleOrderingWith :: [Rule a] -> Rule a -> Rule a -> Ordering
ruleOrderingWith = ruleOrderingWithId . map getId
ruleOrderingWithId :: HasId b => [b] -> Rule a -> Rule a -> Ordering
ruleOrderingWithId bs r1 r2 =
let xs = map getId bs in
case (findIndex (==getId r1) xs, findIndex (==getId r2) xs) of
(Just i, Just j ) -> i `compare` j
(Just _, Nothing) -> LT
(Nothing, Just _ ) -> GT
(Nothing, Nothing) -> compareId r1 r2
---------------------------------------------------------------
-- Using type representations for casts
data IsTypeable a = IT (forall b . Typeable b => a -> Maybe b)
(forall b . Typeable b => b -> Maybe a)
useTypeable :: Typeable a => Maybe (IsTypeable a)
useTypeable = Just (IT cast cast)
castFrom :: Typeable b => Exercise a -> a -> Maybe b
castFrom ex a = do
IT f _ <- hasTypeable ex
f a
castTo :: Typeable b => Exercise a -> b -> Maybe a
castTo ex a = do
IT _ g <- hasTypeable ex
g a
---------------------------------------------------------------
-- Exercise status
data Status
= Stable -- ^ A released exercise that has undergone some thorough testing
| Provisional -- ^ A released exercise, possibly with some deficiencies
| Alpha -- ^ An exercise that is under development
| Experimental -- ^ An exercise for experimentation purposes only
deriving (Show, Eq)
-- | An exercise with the status @Stable@ or @Provisional@
isPublic :: Exercise a -> Bool
isPublic ex = status ex `elem` [Stable, Provisional]
-- | An exercise that is not public
isPrivate :: Exercise a -> Bool
isPrivate = not . isPublic
---------------------------------------------------------------
-- Rest
-- | Function for defining equivalence or similarity without taking
-- the context into account.
withoutContext :: (a -> a -> Bool) -> Context a -> Context a -> Bool
withoutContext f a b = fromMaybe False (fromContextWith2 f a b)
isReady :: Exercise a -> a -> Bool
isReady = evalPredicate . ready
isSuitable :: Exercise a -> a -> Bool
isSuitable = evalPredicate . suitable
-- | Similarity on terms without a context
simpleSimilarity :: Exercise a -> a -> a -> Bool
simpleSimilarity ex = similarity ex `on` inContext ex
-- | Equivalence on terms without a context
simpleEquivalence :: Exercise a -> a -> a -> Bool
simpleEquivalence ex = equivalence ex `on` inContext ex
prettyPrinterContext :: Exercise a -> Context a -> String
prettyPrinterContext ex =
maybe "<<invalid term>>" (prettyPrinter ex) . fromContext
getRule :: Monad m => Exercise a -> Id -> m (Rule (Context a))
getRule ex a =
case filter ((a ==) . getId) (ruleset ex) of
[hd] -> return hd
[] -> fail $ "Could not find ruleid " ++ showId a
_ -> fail $ "Ambiguous ruleid " ++ showId a
-- |Shows a derivation for a given start term. The specified rule ordering
-- is used for selection.
showDerivation :: Exercise a -> a -> String
showDerivation ex a = show (present der) ++ extra
where
der = derivationPrevious (derivationDiffEnv (defaultDerivation ex a))
extra =
case fromContext (lastTerm der) of
Nothing -> "<<invalid term>>"
Just b | isReady ex b -> ""
| otherwise -> "<<not ready>>"
present = biMap (ShowString . f) (ShowString . prettyPrinterContext ex)
f ((b, env), old) = showId b ++ part1 ++ part2
where
newl = "\n "
g (ArgValue descr x) = labelArgument descr ++ "=" ++ showArgument descr x
part1 = case expectedArguments b old of
Just xs -> newl ++ commaList (map g xs)
Nothing -> ""
part2 | nullEnv env = ""
| otherwise = newl ++ show env
type ExerciseDerivation a = Derivation (Rule (Context a)) (Context a)
defaultDerivation :: Exercise a -> a -> ExerciseDerivation a
defaultDerivation ex a =
let ca = inContext ex a
tree = sortTree (ruleOrdering ex) (derivationTree (strategy ex) ca)
single = emptyDerivation ca
in fromMaybe single (derivation tree)
derivationDiffEnv :: Derivation s (Context a) -> Derivation (s, Environment) (Context a)
derivationDiffEnv = updateSteps $ \y b x ->
let env = diffEnv (getEnvironment x) (getEnvironment y)
in (b, deleteEnv "location" env)
-- helper, needed for showing arguments
derivationPrevious :: Derivation s a -> Derivation (s, a) a
derivationPrevious = updateSteps $ \a s _ -> (s, a)
printDerivation :: Exercise a -> a -> IO ()
printDerivation ex = putStrLn . showDerivation ex
---------------------------------------------------------------
-- Checks for an exercise
checkExercise :: Exercise a -> IO ()
checkExercise = runTestSuite . exerciseTestSuite
exerciseTestSuite :: Exercise a -> TestSuite
exerciseTestSuite ex = suite ("Exercise " ++ show (exerciseId ex)) $ do
-- get some exercises
xs <- if isJust (randomExercise ex)
then liftIO $ replicateM 10 (randomTerm Medium ex)
else return (map snd (examples ex))
-- do tests
assertTrue "Exercise terms defined" (not (null xs))
assertTrue "Equivalence implemented" $
let eq a b = equivalence ex (inContext ex a) (inContext ex b)
in length (nubBy eq xs) > 1
assertTrue "Similarity implemented" $
let sim a b = similarity ex (inContext ex a) (inContext ex b)
in length (nubBy sim xs) > 1
checkExamples ex
case testGenerator ex of
Nothing -> return ()
Just gen -> do
let showAsGen = showAs (prettyPrinter ex) gen
addProperty "parser/pretty printer" $ forAll showAsGen $
checkParserPrettyEx ex . inContext ex . fromS
suite "Soundness non-buggy rules" $
forM_ (filter (not . isBuggyRule) $ ruleset ex) $ \r ->
let eq a b = equivalence ex (fromS a) (fromS b)
myGen = showAs (prettyPrinterContext ex) (liftM (inContext ex) gen)
myView = makeView (return . fromS) (S (prettyPrinterContext ex))
args = stdArgs {maxSize = 10, maxSuccess = 10, maxDiscard = 100}
in addPropertyWith (showId r) args $
propRuleSmart eq (liftRule myView r) myGen
addProperty "soundness strategy/generator" $
forAll showAsGen $
maybe False (isReady ex) . fromContext
. applyD (strategy ex) . inContext ex . fromS
data ShowAs a = S {showS :: a -> String, fromS :: a}
instance Show (ShowAs a) where
show a = showS a (fromS a)
showAs :: (a -> String) -> Gen a -> Gen (ShowAs a)
showAs f = liftM (S f)
-- check combination of parser and pretty-printer
checkParserPretty :: (a -> a -> Bool) -> (String -> Either String a) -> (a -> String) -> a -> Bool
checkParserPretty eq p pretty a =
either (const False) (eq a) (p (pretty a))
checkParserPrettyEx :: Exercise a -> Context a -> Bool
checkParserPrettyEx ex ca =
let f = mapSecond make . parser ex
make = newContext (getEnvironment ca) . navigation ex
in checkParserPretty (similarity ex) f (prettyPrinterContext ex) ca
checkExamples :: Exercise a -> TestSuite
checkExamples ex = do
let xs = map snd (examples ex)
unless (null xs) $ suite "Examples" $
mapM_ (checksForTerm True ex) xs
checksForTerm :: Bool -> Exercise a -> a -> TestSuite
checksForTerm leftMost ex a = do
let tree = derivationTree (strategy ex) (inContext ex a)
-- Left-most derivation
when leftMost $
case derivation tree of
Just d -> checksForDerivation ex d
Nothing ->
fail $ "no derivation for " ++ prettyPrinter ex a
-- Random derivation
g <- liftIO getStdGen
case randomDerivation g tree of
Just d -> checksForDerivation ex d
Nothing -> return ()
checksForDerivation :: Exercise a -> Derivation (Rule (Context a)) (Context a) -> TestSuite
checksForDerivation ex d = do
-- Conditions on starting term
let start = firstTerm d
assertTrue
("start term not suitable: " ++ prettyPrinterContext ex start) $
maybe False (isSuitable ex) (fromContext start)
{-
b2 <- do let b = False -- maybe True (isReady ex) (fromContext start)
when b $ report $
"start term is ready: " ++ prettyPrinterContext ex start
return b-}
-- Conditions on final term
let final = lastTerm d
{-
b3 <- do let b = False -- maybe True (isSuitable ex) (fromContext final)
when b $ report $
"final term is suitable: " ++ prettyPrinterContext ex start
++ " => " ++ prettyPrinterContext ex final
return b -}
assertTrue
("final term not ready: " ++ prettyPrinterContext ex start
++ " => " ++ prettyPrinterContext ex final) $
maybe False (isReady ex) (fromContext final)
-- Parser/pretty printer on terms
let ts = terms d
p1 = not . checkParserPrettyEx ex
assertNull "parser/pretty-printer" $ take 1 $ flip map (filter p1 ts) $ \hd ->
let s = prettyPrinterContext ex hd
in "parse error for " ++ s ++ ": parsed as "
++ either show (prettyPrinter ex) (parser ex s)
-- Equivalences between terms
let pairs = [ (x, y) | x <- ts, y <- ts ]
p2 (x, y) = not (equivalence ex x y)
assertNull "equivalences" $ take 1 $ flip map (filter p2 pairs) $ \(x, y) ->
"not equivalent: " ++ prettyPrinterContext ex x
++ " with " ++ prettyPrinterContext ex y
-- Similarity of terms
let p3 (x, r, y) = not (isFinalRule r) && similarity ex x y
assertNull "similars" $ take 1 $ flip map (filter p3 (triples d)) $ \(x, r, y) ->
"similar subsequent terms: " ++ prettyPrinterContext ex x
++ " with " ++ prettyPrinterContext ex y
++ " using " ++ show r
let xs = [ x | x <- terms d, not (similarity ex x x) ]
assertNull "self similarity" $ take 1 $ flip map xs $ \hd ->
"term not similar to itself: " ++ prettyPrinterContext ex hd