为什么一切都是函子,单态类型呢?

时间:2018-10-04 20:24:19

标签: haskell

我正在为haskell中的各种函数构建优化器,这是我的AST

 data Expr = ExprCall { _fname :: Text, _fparams :: [Expr] }
              | ExprVar { _var :: Text }
              | ExprNat { _nat :: Int }

这是语法示例,modrandom是外部函数,其语义对haskell不透明。

mod(random(), 10)

它所具有的只是语法的AST表示,对于上面的示例将是:

ExprCall "mod" [ExprCall "random" [], ExprNat 10]

我有很多类型为Expr->Expr的通行证,并且我有一个名为descend的函数,该函数遍历AST并将通行证应用于每个Expr。这是我的下降函数

{- # LANGUAGE RecordWildCards #-}
descend :: (Expr -> Expr) -> Expr -> Expr
descend f ExprCall   { .. } = ExprCall _fname $ map f _fparams
-- Base expressions
descend f other = f other

现在,我意识到这确实是Data.Traversable的工作,我想让Expr成为该实例的实例,但是,该层次结构FunctorFoldable,{{1 }}等仅接受Traversable种。为什么?

是否可以通过某种方法将* -> *设置为Expr,以便在逻辑上有意义?另外,有没有办法使单态可遍历?

与我拥有的(* -> *)相比,将数据声明为data Expr a有什么优势?

2 个答案:

答案 0 :(得分:3)

(我不太喜欢这个答案,但我保留了它,希望它能激发人们写出更好的答案。)


标准类型类仅适用于多态容器:可以容纳任何类型的值的容器。

例如,常规函子提供

fmap :: (a -> b) -> f a -> f b

允许ab是不同的类型。由于您的Expr不是多态的,因此无法实现。

对于像您的Expr这样的单态“容器”,我们有mono-traversable软件包提供了单态替代。

例如MonoFunctor类可以提供

omap :: (E -> E) -> C -> C    -- simplified

其中E是“包含”类型的类型,而C是容器的类型。对于您的Expr,它们是相同的,并且应该可以使用:

type instance Element Expr = Expr

instance MonoFunctor Expr where
   omap f expr = ...

很难决定保持Expr单态还是将其转换为多态类型是更好的选择。这取决于您打算做什么。

首先,单态更简单。您的递归类型正是您所需要的,没有麻烦。

多态性比较棘手,并且会涉及类型级别的不动点:

import Data.Functor.Foldable  -- not the "other" foldable

-- the functor
data ExprF k 
        = ExprCall { _fname :: Text, _fparams :: [k] }
        | ...
     deriving Functor

-- the fixed point
type Expr = Fix ExprF

缺点:现在,您需要在所有位置(例如,

)对Fix构造函数进行模式匹配
foo :: Expr -> ...
foo (Fix (ExprCall ...)) = ...

主要(?)缺点:与仿函数相关联的可折叠/可遍历实例仅关心Expr first 层,因此它们并不是那么有用。例如。 foldMap并没有真正访问整个表达式,因为它是在ExprF a上定义的,其中a不一定是表达式,而不是Expr上。这似乎不太有用。

上行:您可以免费获得变态和变形。

就个人而言,我建议您使用单态版本,除非您觉得冒险,并且对recursion-schemes理解得很好。

答案 1 :(得分:3)

这些类的多态性有一些好处:类型更具描述性,并且可以防止某些实现错误。

这两个优点的主要原因是,多态类型的实现通常比专用类型的实现少得多。如果我们排除异常和无限循环,那么实际上只有两种实现fmap :: (a -> b) -> Maybe a -> Maybe b的方法,一种是常量函数,因此只有另一种才是真正明智的:

fmap f (Just a) = Just (f a)
fmap f Nothing = Nothing

-- or

fmap f (Just _) = Nothing
fmap f Nothing = Nothing

相比之下,类型(Int -> Int) -> Maybe Int -> Maybe Int从技术上讲很少告诉我们这种功能的作用。特别是,人们可能会忘记使用第一个参数,这对于制作IMO来说不是一个太牵强的错误:

fmap :: (Int -> Int) -> Maybe Int -> Maybe Int
fmap f (Just a) = Just a
fmap f Nothing = Nothing