23
\$\begingroup\$

In the game Factorio there are string-encoded representations of blueprints that I wish to decode.

The implementation according to their wiki is the following:

This is a technical description of the blueprint string format, used to share blueprints with other users.

A blueprint string is a json representation of the blueprint, encoded using base 64 with the version byte in front (0 in vanilla 0.15) and then compressed with zlib deflate. So to get the json representation of a blueprint from a blueprint string, skip the first byte, base64 decode the string, and finally decompress using zlib deflate.

Please keep in mind that I am very new to Haskell, having only played around with it maybe once earlier in my life, though I do have some functional programming experience.

The list of dependencies is the following:

  • base >= 4.7 && < 5
  • bytestring
  • base64-bytestring
  • zlib

The Main.hs file:

module Main where

import Lib
import System.Environment

main :: IO ()
main = do
    args <- getArgs
    if null args
        then putStrLn "Usage: factorio-exe <blueprint-string>"
        else putStrLn (blueprintJson (head args))

The Lib.hs file:

module Lib
    ( blueprintJson
    ) where

import Codec.Compression.Zlib
import qualified Data.ByteString.Char8 as C
import qualified Data.ByteString.Lazy.Char8 as L
import Data.ByteString.Base64

blueprintJson :: [Char] -> [Char]
blueprintJson str = bpDeflate (bpDecode str)

bpDecode :: [Char] -> C.ByteString
bpDecode str = let (Right decoded) = decode (C.pack (tail str)) in decoded

bpDeflate :: C.ByteString -> [Char]
bpDeflate bstr = L.unpack (decompress (L.fromStrict bstr))

An example blueprint string to test with is the following:

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

Do note that when decoding that there may appear characters with which the Windows Command Prompt may have issues displaying.

\$\endgroup\$

1 Answer 1

17
\$\begingroup\$

There's not much to say, to be honest. The type signatures are all there, which is a plus.

Communicate possible errors and make functions total

However, bpDecode has two possible runtime error sources: tail (which is partial) and Right _ = decode, since the latter can return a Left. Therefore bpDecode is partial too. In Haskell, we try to keep the partial functions to a minimum. bpDecode can fail, but it's type does not communicate that to us.

Instead, bpDecode should return Either String C.ByteString as decode. We can use String to communicate another possible error, namely an empty String:

bpDecode :: [Char] -> Either String C.ByteString
bpDecode "" = Left "bpDecode: Empty String"
bpDecode s  = decode . C.pack . tail $ s

Alternatively, if you don't care about empty string case, we can use drop 1 instead of tail. The former always returns a list, even if our original list was empty:

bpDecode :: [Char] -> Either String C.ByteString
bpDecode = decode . C.pack . drop 1

Some libraries provide a "I know it's correct" function and you can too:

bpDecode' :: [Char] -> C.ByteString
bpDecode' s = let (Right d) = bpDecode s in d

but that's an aside. However, now bpDecode is total: for any input it will produce an output.

Use hlint to find possible eta-reductions

While not necessary, this could be a good time to learn about hlint. It will report common improvements on your code. In this case, you can write bpDeflate as

bpDeflate :: C.ByteString -> [Char]
bpDeflate = L.unpack . decompress . L.fromStrict

and blueprintJson as

blueprintJson :: [Char] -> [Char]
blueprintJson = bpDeflate . bpDecode

Try to stay in a single character stream type

Haskell has 5 types that work with strings. ByteString (lazy and strict), Text (lazy and strict) and String (aka [Char]). If possible try to stick to one of them and don't switch between them all the time. For example, all your functions can be written with lazy ByteStrings:

import Codec.Compression.Zlib
import qualified Data.ByteString.Lazy.Char8 as L
import Data.ByteString.Base64.Lazy

blueprintJson :: L.ByteString -> Either String L.ByteString
blueprintJson = fmap bpDeflate . bpDecode

bpDecode :: L.ByteString -> Either String L.ByteString
bpDecode = decode . L.drop 1

bpDeflate :: L.ByteString -> L.ByteString
bpDeflate = decompress

Note that I changed blueprintJson's type, since we also changed bpDecode's type. In case you're not familiar with fmap: in this context you can think of fmap as

fmap f (Right r) = Right (f r)
fmap _ (Left  l) = Left l

Regardless, the string type depends of course on your later use case. Since blueprintJson hints that you will decode JSON, you will likely use aeson later, which uses lazy ByteStrings as well.

Given those functions above, we could now write blueprintJson as

blueprintJson :: L.ByteString -> Either String L.ByteString
blueprintJson = fmap decompress . decode . L.drop 1

blueprintJson' :: L.ByteString -> L.ByteString
blueprintJson' = fromRight . blueprintJson

fromRight :: Either a b -> b
fromRight e = -- left as an exercise

Prefer pattern matches instead of boolean guards

In main, you check whether the list is null. That still enables us to accide

main :: IO ()
main = do
    args <- getArgs
    if null args
        then putStrLn (blueprintJson (head args)) -- woops!
        else putStrLn "Usage: factorio-exe <blueprint-string>"

If you pattern match you cannot accidentally use head on an empty list:

main :: IO ()
main = do
    args <- getArgs
    case args of
      [b] -> putStrLn (blueprintJson b) 
      _   -> putStrLn "Usage: factorio-exe <blueprint-string>"
\$\endgroup\$
2

Your Answer

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

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