Convert oOo to Brainfuck and vice-versa 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.

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 -&gt; String
oToBrain = map chunkToBrain . L.chunksOf 3 . filter isAlphabetic

brainToO :: String -&gt; String
brainToO = concatMap singleCharToO

singleCharToO :: Char -&gt; String
singleCharToO x = case x of
  '&gt;' -&gt; "ooo"
  '&lt;' -&gt; "ooO"
  '[' -&gt; "oOo"
  ']' -&gt; "oOO"
  '-' -&gt; "Ooo"
  '+' -&gt; "OoO"
  '.' -&gt; "OOo"
  ',' -&gt; "OOO"
  _ -&gt; ""

isAlphabetic :: Char -&gt; Bool
isAlphabetic = (`elem` ['a'..'z']) . C.toLower

chunkToBrain :: String -&gt; Char
chunkToBrain [a, b, c] = case  (C.isUpper a, C.isUpper b, C.isUpper c) of
  (False, False, False) -&gt; '&gt;'
  (False, False, True ) -&gt; '&lt;'
  (False, True,  False) -&gt; '['
  (False, True,  True ) -&gt; ']'
  (True,  False, False) -&gt; '-'
  (True,  False, True ) -&gt; '+'
  (True,  True,  False) -&gt; '.'
  (True,  True,  True ) -&gt; ','
chunkToBrain _ = error "`oOo` chunks can only be 3 chars long" 

brainChars :: Q.Gen String
brainChars = Q.listOf (Q.elements "&lt;&gt;[]+-.,")

doubleConversionIsId :: Q.Property
doubleConversionIsId = Q.forAll brainChars $ \x -&gt; x == (oToBrain . brainToO) x

main = do
  T.assertEqual "Wiki example" ",[&gt;,]&lt;[.&lt;]+" (oToBrain "PROgRam reVERsES giVeN iNPut sEqUENcE")
  Q.quickCheck doubleConversionIsId
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 chunkToBrain.


oToBrain :: String -&gt; String
oToBrain = map chunkToBrain . L.chunksOf 3 . filter isAlphabetic



While 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.toLower


We 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  = [  '&gt;',   '&lt;',   '[',   ']',   '-',   '+',   '.',   ',']

bf2oOo :: [(Char, String)]
bf2oOo = zip bfCode oOoCode

oOo2bf :: [(String, Char)]
oOo2bf = zip oOoCode bfCode 


Now 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 -&gt; Char
toO x = if C.isUpper x then 'O' else 'o'


I think you can already guess what happens next:

chunkToBrain :: String -&gt; 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. "oOoOO" and "oOo" are the same programs in oOo. This leads to a slight change in oToBrain:

oToBrain = mapMaybe chunkToBrain . L.chunksOf 3 . filter isAlphabetic


Where 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 [ 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 -&gt; Either String Program 
bfParser  :: String -&gt; Either String Program 


Getting code again can be done similar to your previous code, although you need to apply the codegen recursively for Repeat.

toBF  :: Program -&gt; String
tooOo :: Program -&gt; String


The conversions can be defined in terms of the other four:

bf2oOo, oOo2bf :: String -&gt; 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 -&gt; String -&gt; 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).