I've written a determinstic & non-deterministic finite state machine. I've scrubbed the code quite a bit but I wonder if it could perhaps be scrubbed even more.
Suggestions for code clarity, formatting, API design and test suites (I can't think of any for my NFA) would be welcome.
Also, can the code be modified to support sets without obscuring code clarity? That way would prevent needless iteration of duplicate states.
dfa.hs
:
module DFA (DFA(..), evalDFA) where
import Data.Maybe (Maybe(..))
data DFA s i = DFA {
startState :: s,
delta :: s -> i -> Maybe s,
isFinal :: s -> Bool
}
evalDFA (DFA startState delta isFinal) xs =
case endState of
Nothing -> False
Just s -> isFinal s
where
endState = foldl (\ m i -> m >>= (\ s -> delta s i)) (Just startState) xs
dfa_test.hs
:
import Data.Maybe (Maybe(..))
import Test.QuickCheck (quickCheck, quickCheckWith)
import DFA (DFA(..), evalDFA)
alwaysPass :: DFA Int ()
alwaysPass = DFA 0 moves isFinal
where
moves :: Int -> () -> Maybe Int
moves 0 () = Just 0
moves _ _ = Nothing
isFinal :: Int -> Bool
isFinal _ = True
test_alwaysPass :: IO ()
test_alwaysPass = quickCheck (\ xs -> evalDFA alwaysPass xs)
alwaysFail :: DFA Int ()
alwaysFail = DFA 0 moves isFinal
where
moves :: Int -> () -> Maybe Int
moves 0 () = Just 0
moves _ _ = Nothing
isFinal :: Int -> Bool
isFinal _ = False
test_alwaysFail :: IO ()
test_alwaysFail = quickCheck (\ xs -> not $ evalDFA alwaysFail xs)
onlyTrue :: DFA Int Bool
onlyTrue = DFA 1 moves isFinal
where
moves :: Int -> Bool -> Maybe Int
moves 0 False = Just 0
moves 0 True = Just 0
moves 1 False = Just 0
moves 1 True = Just 1
moves _ _ = Nothing
isFinal :: Int -> Bool
isFinal 1 = True
isFinal _ = False
test_onlyTrue :: IO ()
test_onlyTrue =
quickCheck (\ xs -> isAccept xs $ evalDFA onlyTrue xs)
where
isAccept :: [Bool] -> (Bool -> Bool)
isAccept xs
| all id xs = id
| otherwise = not
endWithFalse :: DFA Int Bool
endWithFalse = DFA 0 moves isFinal
where
moves :: Int -> Bool -> Maybe Int
moves 0 False = Just 1
moves 0 True = Just 0
moves 1 False = Just 1
moves 1 True = Just 0
moves _ _ = Nothing
isFinal :: Int -> Bool
isFinal 1 = True
isFinal _ = False
test_endWithFalse :: IO ()
test_endWithFalse =
quickCheck (\ xs -> isAccept xs $ evalDFA endWithFalse xs)
where
isAccept :: [Bool] -> (Bool -> Bool)
isAccept [] = not
isAccept xs
| not $ last xs = id
| otherwise = not
isBeer :: DFA Int Char
isBeer = DFA 0 moves isFinal
where
moves :: Int -> Char -> Maybe Int
moves 0 'b' = Just 1
moves 1 'e' = Just 2
moves 2 'e' = Just 2
moves 2 'r' = Just 3
moves _ _ = Nothing
isFinal :: Int -> Bool
isFinal 3 = True
isFinal _ = False
test_isBeer :: IO ()
test_isBeer =
do
quickCheck (\ xs -> isAccept xs $ evalDFA isBeer xs)
checkBeer
where
checkBeer :: IO ()
checkBeer
| evalDFA isBeer "beer" = putStrLn "+++ OK, passed \"beer\" test."
| otherwise = putStrLn "*** Failed! Falsifiable:\n\"beer\""
isAccept :: [Char] -> (Bool -> Bool)
isAccept [] = not
isAccept xs
| xs == "beer" = id
| otherwise = not
isBeeeeeer :: DFA Int Char
isBeeeeeer = DFA 0 moves isFinal
where
moves :: Int -> Char -> Maybe Int
moves 0 'b' = Just 1
moves 1 'e' = Just 1
moves 1 'r' = Just 2
moves _ _ = Nothing
isFinal :: Int -> Bool
isFinal 2 = True
isFinal _ = False
test_isBeeeeeer =
do
quickCheck (\ xs -> isAccept xs $ evalDFA isBeeeeeer xs)
quickCheck (\ x -> evalDFA isBeeeeeer ("b" ++ replicate x 'e' ++ "r"))
where
middle :: [a] -> [a]
middle = init . tail
isAccept :: String -> (Bool -> Bool)
isAccept [] = not
isAccept [_] = not
isAccept xs
| head xs == 'b' && (all (== 'e') $ middle xs) && last xs == 'r' = id
| otherwise = not
main :: IO ()
main = do
test_alwaysPass
test_alwaysFail
test_onlyTrue
test_endWithFalse
test_isBeer
test_isBeeeeeer
nfa.hs
:
module NFA (NFA(..), evalNFA) where
import Data.Maybe (Maybe(..))
data NFA s i = NFA {
startState :: s,
delta :: s -> i -> [s],
epsilon :: s -> [s],
isFinal :: s -> Bool
}
step :: NFA s i -> s -> i -> [s]
step (NFA _ delta epsilon _) s i = [s] >>= forward >>= consumeInput >>= forward
where
forward s = case epsilon s of
[] -> [s]
xs -> xs >>= forward
consumeInput s = delta s i
evalNFA :: NFA s i -> [i] -> Bool
evalNFA nfa xs = any (isFinal nfa) endStates
where
endStates = foldl (\ ss i -> ss >>= (\ s -> step nfa s i)) [startState nfa] xs
nfa_test.hs
:
import Data.Maybe (Maybe(..))
import Test.QuickCheck (quickCheck)
import NFA (NFA(..), evalNFA)
alwaysPass :: NFA Int ()
alwaysPass = NFA 0 delta epsilon isFinal
where
delta :: Int -> () -> [Int]
delta 0 () = [0]
delta _ _ = []
epsilon :: Int -> [Int]
epsilon _ = []
isFinal :: Int -> Bool
isFinal 0 = True
isFinal _ = False
test_alwaysPass :: IO ()
test_alwaysPass = quickCheck (\ xs -> evalNFA alwaysPass xs)
alwaysFail :: NFA Int ()
alwaysFail = NFA 0 delta epsilon isFinal
where
delta :: Int -> () -> [Int]
delta 0 () = [0]
delta _ _ = []
epsilon :: Int -> [Int]
epsilon _ = []
isFinal :: Int -> Bool
isFinal _ = False
test_alwaysFail :: IO ()
test_alwaysFail = quickCheck (\ xs -> not $ evalNFA alwaysFail xs)
main = do
test_alwaysPass
test_alwaysFail