import qualified Data.Char as C
import qualified Data.List.Split as L
import qualified Test.QuickCheck as Q
import qualified Test.HUnit as T
The qualified import here is good, but your module header is missing. Since there is no documentation you don't want to export some functions, for example the partial
oToBrain :: String -> String
oToBrain = map chunkToBrain . L.chunksOf 3 . filter isAlphabetic
While this might seem correct, the combination of
L.chunksOf doesn't end well. According to esolangs, oOo code may have a non-divisible number of letters. The remaining ones are discarded. More on that later on
brainToO is fine, but
isAlphabetic isn't, as
isAlphabetic = C.isAsciiLower . C.toLower
We will now take a look at your source of duplication,
chunkToBrain. Both suffer from somewhat the same problem: you need to define the map from Brainfuck instructions to oOo instructions and vice-versa. So let's define them:
oOoCode :: [String]
oOoCode = ["ooo", "ooO", "oOo", "oOO", "Ooo", "OoO", "OOo", "OOO"]
bfCode :: [Char]
bfCode = [ '>', '<', '[', ']', '-', '+', '.', ',']
bf2oOo :: [(Char, String)]
bf2oOo = zip bfCode oOoCode
oOo2bf :: [(String, Char)]
oOo2bf = zip oOoCode bfCode
singleCharToO x = fromMaybe "" (lookup x bf2oOo)
fromMaybe is from
Data.Maybe. You can use
maybe "" id instead if you don't want to import another module.
chunkToBrain. We will write a single helper beforehand:
toO :: Char -> Char
toO x = if C.isUpper x then 'O' else 'o'
I think you can already guess what happens next:
chunkToBrain :: String -> Maybe Char
chunkToBrain xs = lookup xs oOo2bf
You can of course use other data structures with better asymptotic complexity, but since there are only \$2^3\$ instructions this might be an overkill and should be benchmarked.
But wait a minute: the return type has changed. That's because the text is allowed to have superflous characters.
"oOo" are the same programs in oOo. This leads to a slight change in
oToBrain = mapMaybe chunkToBrain . L.chunksOf 3 . filter isAlphabetic
mapMaybe is again a function from
Data.Maybe, that discards
Nothing from the list. This version of
oToBrain has the right semantics, it doesn't throw an error.
Last but not least, your tests are sufficient for this code, although you should include tests that have oOo programs with a length that's not divisible by three.
I am also worried that I did not declare any types as Haskell is type-centric.
At this point, we're slowly drifting out of the simple exercise and completely into parsing, pretty printing and (possibly) interpretation. What's a fitting data type for your code? No, let's have a look at the greater picture. What's a fitting data type for a Brainfuck or oOo program?
type Program = [Instruction]
data Instruction = IncVal | DecVal
| IncPos | DecPos
| Put | Get
| Repeat Program
You'll notice that this will only allow syntactic correct programs, e.g. those that have the right pairings of
]. You can use
EndRepeat instead, but that could make evaluation tricky.
You can then define two parsers:
-- or any other type with an erroneous state
oOoParser :: String -> Either String Program
bfParser :: String -> Either String Program
Getting code again can be done similar to your previous code, although you need to apply the codegen recursively for
toBF :: Program -> String
tooOo :: Program -> String
The conversions can be defined in terms of the other four:
bf2oOo, oOo2bf :: String -> Either String String
bf2oOo = fmap toBF . oOoParser
oOo2bf = fmap tooOo . bfParser
This approach also allows you to define your own evaluation function without worrying about the loops:
eval :: Program -> String -> String
eval prog input = ...
But that's out of scope of this answer. Note that
eval gets harder if you don't use the recursive
Repeat variant, but parsing gets easier (and vice-versa).