6
\$\begingroup\$

Haskell newbie here. Wanted to improve my skills by re-implementing an esolang I designed back in 2011 called TinCan. If there are things that are perhaps badly coded or unclear things due to my inexperience with the language then I would love to know about them so I can move forward in my Haskell usage.

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as T
import Text.Read
import Data.Char
import Data.Map (Map)
import qualified Data.Map as Map
data Token = Dollar | At | Ampersand | Variable Char | Value Int deriving (Eq, Show)
data Program = Program { program :: [(Token, Token, Token)]
 , len :: Int
 , currentLine :: Int
 , pc :: Int
 , stack :: [Int]
 , executionFinished :: Bool
 , variables :: Map Char Int
 } deriving (Show)
stripLines :: [String] -> [String]
stripLines = filter (\x -> ((==40) . length) x && (head x == '#') && (last x == '#'))
removeHash :: String -> String
removeHash = filter (/='#')
toToken :: String -> Token
toToken t = case readMaybe t :: Maybe Int of
 Just i -> Value i
 Nothing -> if (length t == 1 && (t !! 0) `elem` ['A'..'Z'])
 then Variable (t !! 0)
 else (case t of
 "$" -> Dollar
 "@" -> At
 "&" -> Ampersand
 _ -> error ("Could not parse '" ++ t ++ "'"))
clean :: T.Text -> Token
clean = toToken . T.unpack . T.strip
tokenize [x,y,z] = (clean x, clean y, clean z)
tokenize _ = error "Could not tokenize"
tokenizeLine :: String -> (Token, Token, Token)
tokenizeLine line = if (size == 3) then (tokenize split) else (error line)
 where size = length split
 split = T.splitOn "," (T.pack line)
getVariable :: Program -> Char -> Int
getVariable p = ((variables p) Map.!)
tokenToInt :: Program -> Token -> Int
tokenToInt p t = case t of
 Dollar -> currentLine p
 At -> pc p
 Ampersand -> currentLine p + 1
 Variable c -> getVariable p c
 Value i -> i 
updateVariable :: Program -> Char -> Int -> Program
updateVariable p c i = p { variables = Map.insert c i (variables p) }
jump :: Int -> Program -> Program
jump i p
 | i >= len p || i < 0 = p { currentLine = i, executionFinished = True }
 | otherwise = p { currentLine = i }
push :: Int -> Program -> Program
push i p = p { stack = i : stack p }
executeLine :: Program -> (Token, Token, Token) -> Program
executeLine p (a,(Variable b),c) = 
 if pushToStack
 then jump (currentLine p + 1) $ push diff $ updateVariable p b diff
 else if diff <= 0
 then jump address $ updateVariable p b diff
 else jump (currentLine p + 1) $ updateVariable p b diff
 where diff = value - differential
 differential = tokenToInt p a
 value = getVariable p b
 pushToStack = address == -1
 address = tokenToInt p c
executeLine _ _ = error "Could not execute line."
executeProgram :: Program -> Program
executeProgram p = executeLine p line 
 where line = program p !! currentLine p
generateProgram :: [String] -> Program
generateProgram xxs = Program (map tokenizeLine xxs) (length xxs) 0 0 [] False (Map.fromList $ zip ['A'..'Z'] (repeat 0))
main = readFile "countdown.txt" >>= putStrLn . reverse . map chr . stack . until executionFinished executeProgram . generateProgram . map removeHash . stripLines . lines
Vogel612
25.5k7 gold badges59 silver badges141 bronze badges
asked May 19, 2018 at 15:46
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I'd rewrite toToken a slight bit:

toToken :: String -> Token
toToken [t]
 | isAsciiUpper t = Variable t
 | t == '$' = Dollar
 | t == '@' = At
 | t == '&' = Ampersand
toToken str = Value $ read str

Notice how I'm handling the single character string cases first and that this is much less indented. I've also omitted the explicit error message you have and eliminated the explicit type-signature.

This saves on vertical indentation and just reads a bit cleaner to me.

let's have a look at executeLine

executeLine :: Program -> (Token, Token, Token) -> Program
executeLine p (a,(Variable b), c)
 | pushToStack = jump (currentLine p + 1) $ push diff $ updateVariable p b diff
 | (diff <= 0) = jump address $ updateVariable p b diff
 | otherwise = jump (currentLine p + 1) $ updateVariable p b diff
 where
 diff = value - differential
 differential = tokenToInt p a
 value = getVariable p b
 pushToStack = address == -1
 address = tokenToInt p c
executeLine _ _ = error "Could not execute line."

This again removes indentation reads a bit cleaner to me.

Let's also add some intermediate variables to main, just to clarify what happens:

main = do
 code <- readFile "countdown.txt"
 let program = (generateProgram . map removeHash . stripLines . lines) code
 let result = (reverse . map chr . stack . until executionFinished executeProgram) program
 putStrLn result

While we're at adding variables, let's also look at names. The functions are generally named really well, that's a little less the case for their arguments. Especially for executeLine we can improve the names from a, b, and c. Let's instead use the terminology from the language itself:

executeLine program (differential, (Variable storage), adress)

Names can really help with understanding a program and choosing good names is really hard. On that note pc feels somewhat misleading. I think something like executedInstructions could be more helpful :)

answered May 27, 2018 at 12:17
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.