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
\$\endgroup\$
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 :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.