我必须从文本文件(空格分隔)中读取数据结构,每行一个数据项。我的第一个尝试是
data Person = Person {name :: String, surname :: String, age :: Int, ... dozens of other fields} deriving (Show,...)
main = do
string <- readFile "filename.txt"
let people = readPeople string
do_something people
readPeople s = map (readPerson.words) (lines s)
readPerson row = Person (read(row!!0)) (read(row!!1)) (read(row!!2)) (read(row!!3)) ... (read(row!!dozens))
此代码有效,但readPerson
的代码非常糟糕:我必须为我的数据结构中的所有字段复制粘贴read(row!!n))
!
所以,作为第二次尝试,我认为我可以利用Person
函数的Currying,并在当时传递一个参数。
readPerson row = readFields Person row
readFields f [x] = (f x)
readFields f (x:xs) = readFields (f (read x)) xs
啊,编码风格看起来好多了!
但是,它没有编译! Occurs check: cannot construct the infinite type: t ~ String -> t
实际上,我传递给f
的函数readFields
在每次调用时都有不同的类型签名;这就是为什么我无法想出它的类型签名......
所以,我的问题是:读取具有多个字段的数据结构的最简单,最优雅的方法是什么?
答案 0 :(得分:3)
首先,为所有顶级声明添加类型始终是一个好习惯。它使代码结构更好,更易读。
如何实现这一目标的一个简单方法是利用applicative functors。在解析过程中,你有一个&#34;有效的&#34;计算,其中效果消耗部分输入,其结果是一个解析的片段。我们可以使用State
monad来跟踪剩余的输入,并创建一个多态函数,它消耗输入的一个元素并read
它:
import Control.Applicative
import Control.Monad.State
data Person = Person { name :: String, surname :: String, age :: Int }
deriving (Eq, Ord, Show, Read)
readField :: (Read a) => State [String] a
readField = state $ \(x : xs) -> (read x, xs)
为了解析许多这样的字段,我们使用<$>
和<*>
组合器,它们允许按如下方式对操作进行排序:
readPerson :: [String] -> Person
readPerson = evalState $ Person <$> readField <*> readField <*> readField
表达式Person <$> ...
的类型为State [String] Person
,我们对给定的输入运行evalState
以运行有状态计算并提取输出。我们仍然需要与字段具有相同数量的readField
次数,但不必使用索引或显式类型。
对于真正的程序,您可能包含一些错误处理,因为read
失败并出现异常,以及patterm (x : xs)
如果输入列表太短。使用完整的解析器(例如parsec或attoparsec)允许您使用相同的表示法并进行适当的错误处理,自定义各个字段的解析等。
更通用的方法是使用generics自动将字段换行和展开到列表中。然后你只是派生Generic
。如果您有兴趣,我可举个例子。
或者,您可以使用现有的序列化软件包,例如 cereal 或 binary 等二进制文件包,或者基于文本的 aeson 或 yaml ,通常允许您同时执行这两项操作(从Generic
自动派生(de)序列化或提供自定义序列化)。
答案 1 :(得分:2)
编辑:如果您正在阅读字符串,则更简单的解决方案:
{-# LANGUAGE FlexibleInstances #-}
data Person = Person { name :: String, age :: Int, height :: Double }
deriving Show
class Person' a where
person :: a -> [String] -> Maybe Person
instance Person' Person where
person c [] = Just c
person _ _ = Nothing
instance (Read a, Person' b) => Person' (a -> b) where
person f (x:xs) = person (f $ read x) xs
person _ _ = Nothing
instance {-# OVERLAPPING #-} Person' a => Person' (String -> a) where
person f (x:xs) = person (f x) xs
person _ _ = Nothing
然后,如果列表的大小合适,那么:
\> person Person $ words "John 42 6.05"
Just (Person {name = "John", age = 42, height = 6.05})
如果没有,你什么也得不到:
\> person Person $ words "John 42"
Nothing
当所有记录字段属于同一类型时,Constructing Haskell data types with many fields提供解决方案。如果不是,那么稍微多一点的解决方案就是:
{-# LANGUAGE FlexibleInstances, CPP #-}
data Person = Person { name :: String, age :: Int, height :: Double }
deriving Show
data Val = IVal Int | DVal Double | SVal String
class Person' a where
person :: a -> [Val] -> Maybe Person
instance Person' Person where
person c [] = Just c
person _ _ = Nothing
#define PERSON(t, n) \
instance (Person' a) => Person' (t -> a) where { \
person f ((n i):xs) = person (f i) xs; \
person _ _ = Nothing; } \
PERSON(Int, IVal)
PERSON(Double, DVal)
PERSON(String, SVal)
然后,
\> person Person [SVal "John", IVal 42, DVal 6.05]
Just (Person {name = "John", age = 42, height = 6.05})
为了构造Val
类型,您可以创建另一个类类并创建所需的实例:
class Cast a where
cast :: a -> Val
instance Cast Int where cast = IVal
instance Cast Double where cast = DVal
instance Cast String where cast = SVal
然后,它会稍微简单一点:
\> person Person [cast "John", cast (42 :: Int), cast 6.05]
Just (Person {name = "John", age = 42, height = 6.05})