我正在为haskell中的各种函数构建优化器,这是我的AST
data Expr = ExprCall { _fname :: Text, _fparams :: [Expr] }
| ExprVar { _var :: Text }
| ExprNat { _nat :: Int }
这是语法示例,mod
和random
是外部函数,其语义对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成为该实例的实例,但是,该层次结构Functor
,Foldable
,{{1 }}等仅接受Traversable
种。为什么?
是否可以通过某种方法将* -> *
设置为Expr
,以便在逻辑上有意义?另外,有没有办法使单态可遍历?
与我拥有的(* -> *)
相比,将数据声明为data Expr a
有什么优势?
答案 0 :(得分:3)
(我不太喜欢这个答案,但我保留了它,希望它能激发人们写出更好的答案。)
标准类型类仅适用于多态容器:可以容纳任何类型的值的容器。
例如,常规函子提供
fmap :: (a -> b) -> f a -> f b
允许a
和b
是不同的类型。由于您的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