在Factorio游戏中,我希望解码蓝图的字符串编码表示形式。
根据其维基的实现如下:

这是蓝图字符串格式的技术描述,用于与其他用户共享蓝图。
蓝图字符串是蓝图的json表示形式,它使用base 64进行编码,其版本字节位于前面(香草0.15中为0),然后使用zlib deflate进行压缩。因此,要从蓝图字符串中获取蓝图的json表示,请跳过第一个字节,对字符串进行base64解码,最后使用zlib deflate进行解压缩。

请记住,我对Haskell非常陌生,虽然我确实有一些函数式编程经验,但可能只是在我生命的早期才玩过它。
依赖项列表如下:

base> = 4.7 && <5
字节字符串
base64字节字符串
zlib

Main.hs文件:
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))

Lib.hs文件:
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))

要测试的示例蓝图字符串如下:


请注意,在解码时,可能会出现Windows命令提示符显示问题的字符。

#1 楼

老实说,没什么可说的。类型签名就在那里,这是一个加号。

传达可能的错误并使函数总计

但是,bpDecode有两个可能的运行时错误源:tail(部分错误)和Right _ = decode,因为后者可以返回Left。因此,bpDecode也很偏。在Haskell中,我们尝试将部分功能保持在最低水平。 bpDecode可能会失败,但是它的类型无法向我们传达。

相反,bpDecode应该将Either String C.ByteString返回为decode。我们可以使用String来传达另一个可能的错误,即一个空的String:或者
如果您不在乎空字符串的大小写,我们可以改用drop 1tail。即使我们的原始列表为空,前者也总是返回一个列表:

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


一些库提供了“我知道这是正确的”功能,您也可以:

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


但这是一个问题。但是,现在bpDecode总计了:对于任何输入,它都会产生输出。

使用hlint查找可能的eta减少量

虽然不是必需的,但这可能是个好时机了解有关hlint的信息。它将报告您代码的常见改进。在这种情况下,您可以将bpDeflate写为

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


,将blueprintJson写为

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


单个字符流类型

Haskell有5种类型适用于字符串。 ByteString(延迟和严格),Text(延迟和严格)和String(又称为[Char])。如果可能,请尝试坚持使用其中一种,不要一直在它们之间切换。例如,您的所有函数都可以用惰性ByteString编写:

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


请注意,我更改了blueprintJson的类型,因为我们也更改了bpDecode的类型。如果您不熟悉fmap:在这种情况下,您可以将fmap视为

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


无论如何,字符串类型当然取决于您以后的使用情况。由于blueprintJson暗示您将解码JSON,因此您稍后可能会使用aeson,后者也会使用惰性ByteString

鉴于上述功能,我们现在可以将blueprintJson编写为

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


首选模式匹配而不是布尔值保护。

main中,检查列表是否为null。这仍然使我们能够接受

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


如果您进行模式匹配,就不能在空白列表上意外使用head

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


评论


\ $ \ begingroup \ $
将惰性ByteString用于解码功能将如何工作?在hackage.haskell.org/package/base64-bytestring-1.0.0.1/docs/…上,它声明使用严格的变体,所以我不知道它如何工作。
\ $ \ endgroup \ $
–skiwi
18 Mar 25 '18在15:48

\ $ \ begingroup \ $
@skiwi也有一个懒惰的变体:hackage.haskell.org/package/base64-bytestring-1.0.0.1/docs/…。我不小心忘记了更改导入。
\ $ \ endgroup \ $
– Zeta
18-3-25在16:58