列表基于正确的Kan扩展

Kat*_* J. 14 haskell category-theory

在Ralf Hinze的" Kan Extensions for Program Optimization "中,有一个List类型的定义,基于monoid类别中遗忘函子的右Kan延伸(第7.4节).本文给出了Haskell实现如下:

newtype List a = Abstr {
  apply :: forall z . (Monoid z) => (a -> z) -> z
  } 
Run Code Online (Sandbox Code Playgroud)

我能够定义通常的nil和cons构造函数:

nil :: List a
nil = Abstr (\f -> mempty)

cons :: a -> List a -> List a
cons x (Abstr app) = Abstr (\f -> mappend (f x) (app f))
Run Code Online (Sandbox Code Playgroud)

使用以下为Maybe functor的Monoid类实例,我设法定义了head函数:

instance Monoid (Maybe a) where
  mempty = Nothing
  mappend Nothing m = m
  mappend (Just a) m = Just a

head :: List a -> Maybe a
head (Abstr app) = app Just
Run Code Online (Sandbox Code Playgroud)

问题:如何定义尾部函数?

gal*_*ais 11

这是一个相当原则的解决方案,一次性实现头部和尾部(全部要点):

首先,我们知道如何追加列表(稍后会有用):

append :: List a -> List a -> List a
append (Abstr xs) (Abstr ys) = Abstr (\ f -> xs f <> ys f)
Run Code Online (Sandbox Code Playgroud)

然后我们引入一个新类型Split,我们将用它来检测a是否List为空(并且在非空的情况下得到头部和尾部):

newtype Split a = Split { outSplit :: Maybe (a, List a) }
Run Code Online (Sandbox Code Playgroud)

这种新类型形成一个幺半群:确实我们知道如何附加两个列表.

instance Monoid (Split a) where
  mempty = Split Nothing
  mappend (Split Nothing)        (Split nns)            = Split nns
  mappend (Split mms)            (Split Nothing)        = Split mms
  mappend (Split (Just (m, ms))) (Split (Just (n, ns))) =
    Split $ Just (m, append ms (cons n ns))
Run Code Online (Sandbox Code Playgroud)

这意味着,我们可以从得到一个函数List aSplit a使用List aapply:

split :: List a -> Split a
split xs = apply xs $ \ a -> Split $ Just (a, nil)
Run Code Online (Sandbox Code Playgroud)

head并且tail最终可以从split以下方面得到:

head :: List a -> Maybe a
head = fmap fst . outSplit . split

tail :: List a -> Maybe (List a)
tail = fmap snd . outSplit . split
Run Code Online (Sandbox Code Playgroud)

  • 我相信我们从附录得到的monad恰好是有限列表中的monad,不是吗? (2认同)
  • 你和roconnor两个.:-)我对一个适用于无限列表的解决方案感兴趣,因为这是你在Haskell中实际结束的类型,并且因为无限情况下的终止可以对应于有限情况下的效率(遍历整个结构只是拿它的尾巴不理想).这个解决方案会产生很多不必要的麻烦. (2认同)

sha*_*haf 10

这个列表作为免费monoid的实现在包中提供fmlist,它注意到它的一些有趣的属性(不同于大多数列表的实现,它们是正确的,这个是真正无偏见的;你可以创建任意树,当然,当然幺半群法则强迫你把它看成扁平的,你仍然可以观察到无限情况下的一些差异.这几乎是一个Haskell的怪癖 - 通常是自由的幺半群.它也有一个实现tail,所以这是你的问题的答案(但见下文).

通过这些类型的表示(不仅仅是这个特定的一个,还有例如forall r. (a -> r -> r) -> r -> r列表),通常有一些操作(例如追加)变得更容易,而一些(例如拉链和尾部)变得更加困难.这在不同的地方进行了一些讨论,例如如何获取tail功能流.

fmlist但是,仔细观察一下,它的解决方案非常不令人满意:它只是将你给它的漂亮的平衡树转换为一个右偏置列表foldr,它允许它进行常规列表操作,但是失去了幺半群结构."中等无限"列表的尾部不再是"中间无限",它就像一个常规列表一样正确无限.

应该有可能想出一个聪明的Monoid实例来计算尾部,同时尽可能少地干扰结构的其余部分,但是一个明显的实例并不会让人想到.我可以想到一个非巧妙的"蛮力"解决方案:使用无效Monoid实例欺骗并将"列表" 重新整理到树中,检查树,然后将其折叠起来以使最终结果有效.这是我的nonfree包装的样子和fmlist:

nail :: FM.FMList a -> FM.FMList a
nail (FM.FM k) = FM.FM $ \f -> foldMap f (nail' (k N))

nail' :: N a -> N a
nail' NEmpty = error "nail' NEmpty"
nail' (N x) = NEmpty
nail' (NAppend l r) =
  case normalize l of
    NEmpty -> nail' r
    N x -> r
    l' -> NAppend (nail' l') r

-- Normalize a tree so that the left side of a root NAppend isn't an empty
-- subtree of any shape. If the tree is infinite in a particular way, this
-- won't terminate, so in that sense taking the tail of a list can make it
-- slightly worse (but you were already in pretty bad shape as far as
-- operations on the left side are concerned, and this is a pathological case
-- anyway).
normalize :: N a -> N a
normalize (NAppend l r) =
  case normalize l of
    NEmpty -> normalize r
    l' -> NAppend l' r
normalize n = n
Run Code Online (Sandbox Code Playgroud)