snippetMinor
Convert oOo to Brainfuck and vice-versa
Viewed 0 times
viceoooconvertversabrainfuckand
Problem
This program was inspired by a Code Golf challenge that I wanted to do properly. It simply converts oOo to Brainfuck and vice-versa.
The oOo language basically uses UPPER / lower case information in groups of 3 to encode a Brainfuck program. For further info see the Esolang wiki page of oOo.
The main problem with this code is that I feel like there is repetition in the encode / decode functions because they seem to contain similar type of information. I am also worried that I did not declare any types as Haskell is type-centric.
The oOo language basically uses UPPER / lower case information in groups of 3 to encode a Brainfuck program. For further info see the Esolang wiki page of oOo.
The main problem with this code is that I feel like there is repetition in the encode / decode functions because they seem to contain similar type of information. I am also worried that I did not declare any types as Haskell is type-centric.
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
oToBrain :: String -> String
oToBrain = map chunkToBrain . L.chunksOf 3 . filter isAlphabetic
brainToO :: String -> String
brainToO = concatMap singleCharToO
singleCharToO :: Char -> String
singleCharToO x = case x of
'>' -> "ooo"
' "ooO"
'[' -> "oOo"
']' -> "oOO"
'-' -> "Ooo"
'+' -> "OoO"
'.' -> "OOo"
',' -> "OOO"
_ -> ""
isAlphabetic :: Char -> Bool
isAlphabetic = (`elem` ['a'..'z']) . C.toLower
chunkToBrain :: String -> Char
chunkToBrain [a, b, c] = case (C.isUpper a, C.isUpper b, C.isUpper c) of
(False, False, False) -> '>'
(False, False, True ) -> ' '['
(False, True, True ) -> ']'
(True, False, False) -> '-'
(True, False, True ) -> '+'
(True, True, False) -> '.'
(True, True, True ) -> ','
chunkToBrain _ = error "`oOo` chunks can only be 3 chars long"
brainChars :: Q.Gen String
brainChars = Q.listOf (Q.elements "<>[]+-.,")
doubleConversionIsId :: Q.Property
doubleConversionIsId = Q.forAll brainChars $ \x -> x == (oToBrain . brainToO) x
main = do
T.assertEqual "Wiki example" ",[>,]<[.<]+" (oToBrain "PROgRam reVERsES giVeN iNPut sEqUENcE")
Q.quickCheck doubleConversionIsIdSolution
import qualified Data.Char as C
import qualified Data.List.Split as L
import qualified Test.QuickCheck as Q
import qualified Test.HUnit as TThe 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
chunkToBrain.oToBrain :: String -> String
oToBrain = map chunkToBrain . L.chunksOf 3 . filter isAlphabeticWhile this might seem correct, the combination of
chunkToBrain and 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 chunkToBrain.brainToO is fine, but isAlphabetic isn't, as elem ['a'..'z'] is C.isAsciiLower:isAlphabetic = C.isAsciiLower . C.toLowerWe will now take a look at your source of duplication,
singleCharToO and 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 bfCodeNow
singleCharToO is singleCharToO x = fromMaybe "" (lookup x bf2oOo)where
fromMaybe is from Data.Maybe. You can use maybe "" id instead if you don't want to import another module.Now to
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 oOo2bfYou 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.
"oOoOO" and "oOo" are the same programs in oOo. This leads to a slight change in oToBrain:oToBrain = mapMaybe chunkToBrain . L.chunksOf 3 . filter isAlphabeticWhere
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 ProgramYou'll notice that this will only allow syntactic correct programs, e.g. those that have the right pairings of
[ and ]. You can use StartRepeat and 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 ProgramGetting code again can be done similar to your previous code, although you need to apply the codegen recursively for
Repeat.toBF :: Program -> String
tooOo :: Program -> StringThe conversions can be defined in terms of the other four:
bf2oOo, oOo2bf :: String -> Either String String
bf2oOo = fmap toBF . oOoParser
oOo2bf = fmap tooOo . bfParserThis 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).Code Snippets
import qualified Data.Char as C
import qualified Data.List.Split as L
import qualified Test.QuickCheck as Q
import qualified Test.HUnit as ToToBrain :: String -> String
oToBrain = map chunkToBrain . L.chunksOf 3 . filter isAlphabeticisAlphabetic = C.isAsciiLower . C.toLoweroOoCode :: [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 bfCodesingleCharToO x = fromMaybe "" (lookup x bf2oOo)Context
StackExchange Code Review Q#121393, answer score: 2
Revisions (0)
No revisions yet.