如何在 Haskell 中解析整数矩阵?

Posted

技术标签:

【中文标题】如何在 Haskell 中解析整数矩阵?【英文标题】:How do I parse a matrix of integers in Haskell? 【发布时间】:2012-01-12 01:24:42 【问题描述】:

所以我已经阅读了理论,现在尝试在 Haskell 中解析文件 - 但我没有得到任何结果。这太奇怪了……

这是我的输入文件的外观:

        m n
        k1, k2...

        a11, ...., an
        a21,....   a22
        ...
        am1...     amn

其中 m,n 只是整数,K = [k1, k2...] 是整数列表,a11..amn 是“矩阵”(列表列表):A=[[a11,...a1n], ... [am1... amn]]

这是我的快速 python 版本:

def parse(filename):
    """
    Input of the form:
        m n
        k1, k2...

        a11, ...., an
        a21,....   a22
        ...
        am1...     amn

    """

    f = open(filename)
    (m,n) = f.readline().split()
    m = int(m)
    n = int(n)

    K = [int(k) for k in f.readline().split()]

    # Matrix - list of lists
    A = []
    for i in range(m):
        row = [float(el) for el in f.readline().split()]
        A.append(row)

    return (m, n, K, A)

以下是我在 Haskell 中的表现(不是很):

import System.Environment
import Data.List

main = do
    (fname:_) <- getArgs
    putStrLn fname --since putStrLn goes to IO ()monad we can't just apply it
    parsed <- parse fname
    putStrLn parsed

parse fname = do
    contents <- readFile fname
    -- ,,,missing stuff... ??? how can I get first "element" and match on it?

    return contents

我对 monads(以及我陷入其中的上下文!)和 do 语句感到困惑。我很想写这样的东西,但我知道这是错误的:

firstLine <- contents.head
(m,n) <- map read (words firstLine)

因为 contents 不是一个列表 - 而是一个单子。

下一步步骤的任何帮助都会很棒。

所以我只是 discovered 你可以这样做:

 liftM lines . readFile

从文件中获取行列表。但是,该示例仍然只转换了整个文件,并且不只使用第一行或第二行...

【问题讨论】:

【参考方案1】:

非常简单的版本可能是:

import Control.Monad (liftM)

-- this operates purely on list of strings
-- and also will fail horribly when passed something that doesn't 
-- match the pattern
parse_lines :: [String] -> (Int, Int, [Int], [[Int]])
parse_lines (mn_line : ks_line : matrix_lines) = (m, n, ks, matrix)
    where [m, n] = read_ints    mn_line
          ks     = read_ints    ks_line
          matrix = parse_matrix matrix_lines

-- this here is to loop through remaining lines to form a matrix
parse_matrix :: [String] -> [[Int]]
parse_matrix lines = parse_matrix' lines []
    where parse_matrix' []       acc = reverse acc
          parse_matrix' (l : ls) acc = parse_matrix' ls $ (read_ints l) : acc

-- this here is to give proper signature for read
read_ints :: String -> [Int]
read_ints = map read . words

-- this reads the file contents and lifts the result into IO
parse_file :: FilePath -> IO (Int, Int, [Int], [[Int]])
parse_file filename = do
    file_lines <- (liftM lines . readFile) filename
    return $ parse_lines file_lines

您可能希望使用 Parsec 进行更高级的解析,以及更好的错误处理。

*Main Control.Monad> parse_file "test.txt"
(3,3,[1,2,3],[[1,2,3],[4,5,6],[7,8,9]])

【讨论】:

我建议将readFile 移出parse 并改为使用String。纯函数更容易处理、测试等。 @hammar:是的,这是个好主意。我想我不应该在早上 8 点尝试写 Haskell。 很好:(mn_line:ks_line:matrix_lines)!完全忘记了你可以这样做:-( 另外我不知道你可以匹配这样的列表:[m,n]= stuff 这样 liftM 行。你正在做的 readFile - 它是只提升线条......还是提升整个作品?【参考方案2】:

一个易于编写的解决方案

import Control.Monad (replicateM)

-- Read space seperated words on a line from stdin
readMany :: Read a => IO [a]
readMany = fmap (map read . words) getLine

parse :: IO (Int, Int, [Int], [[Int]])
parse = do
    [m, n] <- readMany
    ks     <- readMany
    xss    <- replicateM m readMany
    return (m, n, ks, xss)

让我们试试吧:

*Main> parse
2 2
123 321
1 2
3 4
(2,2,[123,321],[[1,2],[3,4]])

虽然我提供的代码很有表现力。也就是说,你可以用很少的代码快速完成工作,它有一些不好的属性。虽然我认为如果你还在学习 haskell 并且还没有开始使用解析器库。这是要走的路。

我的解决方案的两个坏属性:

所有代码都在IO 中,没有任何东西可以单独测试 错误处理非常糟糕,正如您在[m, n] 中看到的模式匹配非常激进。如果我们在输入文件的第一行有 3 个元素会怎样?

【讨论】:

这行看起来很棒:xss &lt;- replicateM m readMany 如果需要,可以非常轻松地调整它以使用 hGetLine 从文件而不是标准输入中读取。【参考方案3】:

liftM 不是魔法!您可能会认为将函数 f 提升到 monad 中提升 确实有些神秘,但实际上它只是定义为:

liftM f x = do
  y <- x
  return (f y)

我们实际上可以使用liftM 来做你想做的事,即:

[m,n] <- liftM (map read . words . head . lines) (readFile fname)

但你要找的是 let 语句:

parseLine = map read . words

parse fname = do
  (x:y:xs) <- liftM lines (readFile fname)
  let [m,n]  = parseLine x
  let ks     = parseLine y
  let matrix = map parseLine xs
  return (m,n,ks,matrix)

如您所见,我们可以使用 let 来表示变量赋值,而不是一元计算。事实上,当我们对 do 符号进行脱糖时,let 语句就是让表达式:

parse fname = 
   liftM lines (readFile fname) >>= (\(x:y:xs) ->
   let [m,n]  = parseLine x
       ks     = parseLine y  
       matrix = map parseLine xs
   in return matrix )

【讨论】:

所以... let 允许我在类似单子的上下文中做类似“非单子”的事情? @drozzy - 呃...基本上,是的。您可能想查看有关let 的最新问题:***.com/questions/8274650/…【参考方案4】:

使用解析库的解决方案

由于您可能会有很多人使用将Ints 字符串解析为[[Int]] (map (map read . words) . lines $ contents) 的代码进行响应,因此我将跳过该内容并介绍其中一个解析库。如果您要为实际工作执行此任务,您可能会使用这样的库来解析 ByteString(而不是 String,这意味着您的 IO 将所有内容读入单个字符的链接列表)。

import System.Environment
import Control.Monad
import Data.Attoparsec.ByteString.Char8
import qualified Data.ByteString as B

首先,我导入了 Attoparsec 和 bytestring 库。您可以在 hackage 上查看这些库及其文档,并使用 cabal 工具安装它们。

main = do
    (fname:_) <- getArgs
    putStrLn fname
    parsed <- parseX fname
    print parsed

main基本不变。

parseX :: FilePath -> IO (Int, Int, [Int], [[Int]])
parseX fname = do
    bs <- B.readFile fname
    let res = parseOnly parseDrozzy bs
    -- We spew the error messages right here
    either (error . show) return res

parseX(从 parse 重命名以避免名称冲突)使用字节串库的 readfile,它以连续字节的形式读入打包的文件,而不是读入链表的单元格。解析后,如果解析器返回 Right result,我会使用一些速记来返回结果,或者如果解析器返回值 Left someErrorMessage,则打印错误。

-- Helper functions, more basic than you might think, but lets ignore it    
sint = skipSpace >> int
int = liftM floor number

parseDrozzy :: Parser (Int, Int, [Int], [[Int]])
parseDrozzy = do
   m <- sint
   n <- sint
   skipSpace
   ks  <- manyTill sint endOfLine
   arr <- count m (count n sint)              
   return (m,n,ks,arr)

然后真正的工作发生在parseDrozzy。我们使用上面的帮助程序获得了 mn Int 值。在大多数 Haskell 解析库中,我们必须显式处理空格 - 所以我跳过 n 之后的换行符以到达我们的 ksks 只是下一个换行符之前的所有 int 值。现在我们实际上可以使用之前指定的行数和列数来获取我们的数组了。

从技术上讲,最后一位 arr &lt;- count m (count n sint) 不符合您的格式。即使这意味着进入下一行,它也会抓取n ints。我们可以使用 count m (manyTill sint endOfLine) 复制 Python 的行为(不验证一行中的值的数量),或者我们可以更明确地检查每一行的结尾,如果我们缺少元素,则返回错误。

从列表到矩阵

列表列表不是二维数组——空间和性能特征完全不同。让我们使用 Data.Array.Repa (import Data.Array.Repa) 将列表打包成一个实数矩阵。这将允许我们有效地访问数组的元素以及对整个矩阵执行操作,可选择将工作分散到所有可用的 CPU 中。

Repa 使用稍微奇怪的语法定义数组的维度。如果您的行和列长度在变量mn 中,那么Z :. n :. m 很像C 声明int arr[m][n]。对于一维示例ks,我们有:

fromList (Z :. (length ks)) ks

这将我们的类型从 [Int] 更改为 Array DIM1 Int

对于我们有的二维数组:

let matrix = fromList (Z :. m :. n) (concat arr)

并将我们的类型从 [[Int]] 更改为 Array DIM2 Int

所以你有它。使用面向生产的库将文件格式解析为高效的 Haskell 数据结构。

【讨论】:

酷!这种 Array 类型是否允许我查询第一列或第二行之类的内容? 确实如此。关于该操作的一个问题是here。假设您想要列i,一般形式是slice arr (Z :. All :. i)【参考方案5】:

像这样简单的东西呢?

parse :: String -> (Int, Int, [Int], [[Int]])
parse stuff = (m, n, ks, xss)
        where (line1:line2:rest) = lines stuff
              readMany = map read . words
              (m:n:_) = readMany line1
              ks = readMany line2
              xss = take m $ map (take n . readMany) rest

main :: IO ()
main = do
        stuff <- getContents
        let (m, n, ks, xss) = parse stuff
        print m
        print n
        print ks
        print xss 

【讨论】:

你确定这是正确的吗? (line1:line2:rest) = lines stuff我以为应该是[line1:line2:rest] = lines stuff 是的,我确定:check this out。注意[1:2:[3]] == [[1,2,3]],但是(1:2:[3]) == [1,2,3] @drozzy:[foo](foo:[]) 的糖,[foo, bar](foo:bar:[]) 的糖。模式匹配总是使用括号,尽管使用列表糖你实际上看不到括号,直到它被去糖。

以上是关于如何在 Haskell 中解析整数矩阵?的主要内容,如果未能解决你的问题,请参考以下文章

如何在资源有限的 Haskell 中解析大型 XML 文件?

在Haskell中,如何创建具有多种类型的列表?

为什么会出现解析错误,如何在haskell中解决此错误?

Haskell 加速复制矩阵

如何在haskell中以二进制或十六进制打印整数文字?

在 haskell 中,如何从十进制数中生成二进制数字?