Haskell:被困在IO Monad中

6
我正在尝试使用haskell-src-exts包中的parseFile函数解析文件。 我正在尝试处理parseFile的输出,它当然是IO类型的,但我不知道如何处理IO类型。我发现了一个liftIO函数,但我不确定这是否是此情况的解决方案。以下是代码。
import Language.Haskell.Exts.Syntax
import Language.Haskell.Exts 
import Data.Map hiding (foldr, map)
import Control.Monad.Trans

increment :: Ord a => a -> Map a Int -> Map a Int
increment a = insertWith (+) a 1

fromName :: Name -> String
fromName (Ident s) = s
fromName (Symbol st) = st

fromQName :: QName -> String
fromQName (Qual _ fn) = fromName fn
fromQName (UnQual n) = fromName n

fromLiteral :: Literal -> String
fromLiteral (Int int) = show int

fromQOp :: QOp -> String
fromQOp (QVarOp qn) = fromQName qn

vars :: Exp -> Map String Int
vars (List (x:xs)) = vars x
vars (Lambda _ _ e1) = vars e1
vars (EnumFrom e1) = vars e1
vars (App e1 e2) = unionWith (+) (vars e1) (vars e2)
vars (Let _ e1) = vars e1
vars (NegApp e1) = vars e1
vars (Var qn) = increment (fromQName qn) empty
vars (Lit l) = increment (fromLiteral l) empty
vars (Paren e1) = vars e1
vars (InfixApp exp1 qop exp2) = 
                 increment (fromQOp qop) $ 
                     unionWith (+) (vars exp1) (vars exp2)



match :: [Match] -> Map String Int
match rhss = foldr (unionWith (+) ) empty 
                    (map (\(Match  a b c d e f) -> rHs e) rhss)

rHS :: GuardedRhs -> Map String Int
rHS (GuardedRhs _ _ e1) = vars e1

rHs':: [GuardedRhs] -> Map String Int
rHs' gr = foldr (unionWith (+)) empty 
                 (map (\(GuardedRhs a b c) -> vars c) gr)

rHs :: Rhs -> Map String Int
rHs (GuardedRhss gr) = rHs' gr
rHs (UnGuardedRhs e1) = vars e1

decl :: [Decl] -> Map String Int
decl decls =  foldr (unionWith (+) ) empty 
                     (map fun decls )
    where fun (FunBind f) = match f
          fun _ = empty

pMod' :: (ParseResult Module) -> Map String Int
pMod' (ParseOk (Module _ _ _ _ _ _ dEcl)) = decl dEcl 

pMod :: FilePath -> Map String Int
pMod = pMod' . liftIO . parseFile 

我希望能在parseFile的输出上使用pMod'函数。
请注意,所有类型和数据构造函数可在http://hackage.haskell.org/packages/archive/haskell-src-exts/1.13.5/doc/html/Language-Haskell-Exts-Syntax.html中找到。谢谢!

1
相关链接:https://dev59.com/rVrUa4cB1Zd3GeqPkoaX。 - atravers
2个回答

15

一旦进入IO,就无法逃脱。

使用fmap

-- parseFile :: FilePath -> IO (ParseResult Module)
-- pMod' :: (ParseResult Module) -> Map String Int
-- fmap :: Functor f => (a -> b) -> f a -> f b

-- fmap pMod' (parseFile filePath) :: IO (Map String Int)

pMod :: FilePath -> IO (Map String Int)
pMod = fmap pMod' . parseFile 

(补充:) 正如Levi Pearson在这个出色的答案中所解释的那样,还有一点需要注意:

Prelude Control.Monad> :t liftM
liftM :: (Monad m) => (a1 -> r) -> m a1 -> m r

但这也不是什么魔法。考虑一下:
Prelude Control.Monad> let g f = (>>= return . f)
Prelude Control.Monad> :t g
g :: (Monad m) => (a -> b) -> m a -> m b

所以你的函数也可以写成:
pMod fpath = fmap pMod' . parseFile $ fpath
     = liftM pMod' . parseFile $ fpath
     = (>>= return . pMod') . parseFile $ fpath   -- pushing it...
     = parseFile fpath >>= return . pMod'         -- that's better

pMod :: FilePath -> IO (Map String Int)
pMod fpath = do
    resMod <- parseFile fpath
    return $ pMod' resMod

不管你觉得什么更直观(记住,(.) 的优先级最高,紧接着是函数调用)。

顺便说一下,>>= return . f 部分是liftM 实际的实现方式,在 do 记法中;它真正展示了 fmapliftM 的等价性,因为对于任何单子来说,都应该成立:

fmap f m  ==  m >>= (return . f)

14
与Will的回答(当然是正确而简明扼要的)相比,一般来说,你通常会将操作“提升”到Monad中,而不是从它们中取出值,以便将纯函数应用于monadic值。
恰好,理论上来说,Monad是一种特定类型的Functor。Functor描述了表示将对象和操作映射到不同上下文的类型类。一个作为Functor实例的数据类型通过其数据构造函数将对象映射到其上下文中,并通过fmap函数将操作映射到其上下文中。要实现真正的functor,fmap必须以这样的方式工作,即将identity function提升到functor context中不会改变functor context中的值,并且将两个组合在一起的函数提升生成与在functor context中分别提升函数并将其组合产生相同的操作。
许多Haskell数据类型自然形成functor,fmap提供了一个通用界面,可以将函数提升,以使它们在functorized数据中“均匀”应用,而不必担心特定Functor实例的形式。其中几个很好的例子是list类型和Maybe类型; 函数fmap进入列表上下文的结果与列表上的熟悉的map操作完全相同,而将函数fmap进入Maybe上下文将通常为Just a值正常应用该函数,并且对于Nothing值不执行任何操作,使您可以在不必担心其是哪个的情况下执行操作。
话虽这样说,由于历史上的巧合,Haskell Prelude目前不要求Monad实例也具有Functor实例,因此Monad提供了一系列函数,还将操作提升到monadic context中。 在Monad实例也是Functor实例的情况下,操作liftM会做与fmap相同的事情(正如它们应该做的那样)。但是fmap和liftM仅将单参数函数提升。 Monad很周到地提供了一系列liftM2-liftM5函数,这些函数以相同的方式将多参数函数提升为monadic context。
最后,您询问了关于liftIO的问题,这涉及到单子变换器的相关思想,在其中通过将单子映射应用于已经单子化的值,将多个Monad实例组合成一个单一的数据类型,形成了一种基本纯类型上的单子映射堆栈mtl库提供了这个通用思想的一个实现,在其模块Control.Monad.Trans中定义了两个类:MonadTrans tMonad m => MonadIO mMonadTrans类提供了一个函数lift,它可以访问堆栈中下一个更高的单子“层”的操作,即(MonadTrans t,Monad m) => m a -> t m aMonadIO类提供了一个函数liftIO,它可以从堆栈中的任何“层”访问IO单子操作,即IO a -> m a。这使得在单子转换器堆栈中工作更加方便,但需要提供大量的转换器实例声明,当新的Monad实例引入到堆栈中时。

感谢您的详细解释!我现在感觉对单子和函子有了更深入的理解(之前我根本不理解)。 - user2548080
3
我回答问题的部分原因是为了帮助强化这些概念在我的头脑中,以便在编程时更自然地运用它们!当你思考不同类型类如何协同工作后,晦涩难懂的Haskell代码突然变得清晰易懂。祝好运! - Levi Pearson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接