给定一个具有固定点的任意数据结构,我们可以构造一个幺半代数而无需手动指定所有情况吗?
假设我们获得了如下的数据类型recursion-schemes
。使用ExprF
库,我们可以派生一个基本仿函数Functor
,它还会自动生成Foldable
,Traversable
和{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TemplateHaskell #-}
import Data.Semigroup (Sum(..))
import Data.Functor.Foldable
import Data.Functor.Foldable.TH
import Prelude hiding (fail)
data Expr = Var String
| Const Int
| Add [Expr]
| Mult [Expr]
deriving Show
$(makeBaseFunctor ''Expr)
expr :: Fix ExprF
expr = ana project $ Add [Const 1, Const 2, Mult [Const 3, Const 4], Var "hello"]
个实例。
expr
现在,让我们说我们要计算alg (ConstF _) = 1
alg (VarF _) = 1
alg (AddF xs) = sum xs
alg (MulF xs) = sum xs
中的叶数。我们可以轻松地为这么小的数据结构编写代数:
cata alg expr
现在,我们可以致电5
,返回Expr
,这是正确的结果。
让我们假设cata
变得非常庞大和复杂,我们不想手动为所有数据构造函数编写案例。 Monoid
如何知道如何结合所有案例的结果?我怀疑这可以使用Const
s,可能与fail = getSum $ foldMap (const (Sum 1) . unfix) $ unfix expr
仿函数一起使用(尽管不完全确定最后一部分)。
fail
4
返回5
,而我们实际上有Fix
个离开。我假设问题出在固定点上,因为我们只能剥离Mult [..]
一层,因此Monoid
只算作一片叶子。
是否有可能以某种方式通常折叠整个固定点并将结果收集到foldMap
- 类似结构中,而不用手动指定所有实例?我想要的是Table: Servers
Columns: ServerUrl ServerName
Rows: //server0001/space0/ Telescope1
//server0001/space1/ Space
//server0001/space2/ Planet
Table: Projects
Columns: ServerUrl Name Entity Type
Rows: //server0001/space0/ Field1 E T1
//server0001/space1/ Field2 T T1
//server0001/space2/ Field3 E T3
,但是更通用。
我有一种感觉,我错过了一些非常明显的东西。
答案 0 :(得分:10)
这是解决方案的本质。我已经开启了
{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable, PatternSynonyms #-}
让我们回顾一下固定点和咒语。
newtype Fix f = In {out :: f (Fix f)}
cata :: Functor f => (f t -> t) -> Fix f -> t
cata alg = alg . fmap (cata alg) . out
代数alg :: f t -> t
采用一个节点,其中子项已被t
值替换,然后返回父项的t
。 cata
运算符通过解压缩父节点,递归处理其所有子节点,然后应用alg
来完成作业来工作。
所以,如果我们想在这样的结构中计算叶子,我们就可以这样开始:
leaves :: (Foldable f, Functor f) => Fix f -> Integer
leaves = cata sumOrOne where
-- sumOrOne :: f Integer -> Integer
代数sumOrOne
可以看到父节点的每个子节点中的叶子数。我们可以使用cata
,因为f
是Functor
。由于f
是Foldable
,我们可以计算孩子们的叶子总数。
sumOrOne fl = case sum fl of
...
有两种可能性:如果父母没有孩子,它的叶子总和将是0
,我们可以检测到,但这意味着父亲本身就是一片叶子,所以1
应该是回。否则,叶子总和将是非零的,在这种情况下父叶子不是叶子,因此它的叶子总和确实是其子叶子的总叶子总和。这给了我们
leaves :: (Foldable f, Functor f) => Fix f -> Integer
leaves = cata sumOrOne where
sumOrOne fl{- number of leaves in each child-} = case sum fl of
0 -> 1 -- no leaves in my children means I am a leaf
l -> l -- otherwise, pass on the total
一个简单的例子,基于Hutton的Razor(带有整数和加法的表达式语言,这通常是说明这一点的最简单的事情)。表达式是从Hutton的仿函数生成的。
data HF h = Val Int | h :+: h deriving (Functor, Foldable, Traversable)
我介绍了一些模式同义词来恢复定制类型的外观。
pattern V x = In (Val x)
pattern s :+ t = In (s :+: t)
我做了一个快速示例表达式,其中有一些深度为三级的叶子。
example :: Fix HF
example = (V 1 :+ V 2) :+ ((V 3 :+ V 4) :+ V 5)
果然
Ok, modules loaded: Leaves.
*Leaves> leaves example
5
另一种方法是在感兴趣的子结构中进行编织和折叠,在这种情况下,是叶子上的东西。 (我们得到了完全免费的monad。)
data Tree f x = Leaf x | Node (f (Tree f x)) deriving (Functor, Foldable)
完成基本构造的叶子/节点分离后,您可以使用foldMap
直接访问叶子。扔了一点Control.Newtype
,我们得到了
ala' Sum foldMap (const 1) :: Foldable f => f x -> Integer
低于Fairbairn门槛(即,足够短,不需要名称,所有更清楚,因为没有名称)。
当然,问题在于数据结构在“感兴趣的子结构”中通常是多种有趣但相互矛盾的方式。 Haskell并不总是最好的让我们访问“找到的functoriality”:我们不得不预测在声明时参数化数据类型时我们需要的功能。但现在还有时间改变这一切......