意外地反向非二元函数会产生奇怪的行为

时间:2013-12-10 17:04:16

标签: haskell

以下是有问题的代码(also on lpaste.net):

module Data.Graph.Dijkstra
       ( dijkstra
       , dijkstraPath
       ) where

-- Graph library import
import Data.Graph.Inductive hiding (dijkstra)

-- Priority queue import
import qualified Data.PQueue.Prio.Min as PQ

-- Standard imports
import Data.List (find)
import Data.Maybe (fromJust)
import Data.Monoid

-- Internal routine implementing Dijkstra's shortest paths
-- algorithm. Deemed internal because it needs to be kickstarted with
-- a singleton node queue. Based on FGL's current implementation of
-- Dijkstra.
dijkstraInternal ::
  (Graph gr, Ord b, Monoid b) => gr a b -> PQ.MinPQueue b [Node] -> [[Node]]
dijkstraInternal g q
  | PQ.null q = []
  | otherwise =
    case match v g of
      (Just cxt,g') -> p:dijkstraInternal  g' (PQ.unions (q' : expand cxt minDist p))
      (Nothing, g') -> dijkstraInternal g' q'
  where ((minDist,p@(v:_)), q') = PQ.deleteFindMin q
        expand (_,_,_,s) dist pathToC =
          map (\(edgeCost,n) -> PQ.singleton (dist `mappend` edgeCost) (n:pathToC)) s

-- Given a graph and a start node, returns a list of lists of nodes
-- corresponding to the shortest paths from the start to all other
-- nodes, where the edge costs are accumulated according to the Monoid
-- instance of the edge label type and costs are compared by the edge
-- label's Ord instance.
dijkstra :: (Graph gr, Ord b, Monoid b) => gr a b -> Node -> [[Node]]
dijkstra g start = dijkstraInternal g (PQ.singleton `mempty` [start])  -- !!!

dijkstraPath :: (Graph gr, Ord b, Monoid b) => gr a b -> Node -> Node -> [LNode a]
dijkstraPath g start goal =
  let paths = dijkstra g start
      pathNodes  = find ((goal ==) . head) paths -- Can paths be empty?
  in
   case pathNodes of
     Nothing -> []
     Just ps -> reverse $ map (\n -> (n, fromJust $ lab g n)) ps

奇怪的是第39行,标有-- !!!评论。此代码编译,但运行时错误是无论如何,PQ.singleton函数返回一个空的优先级队列。我意识到我不小心向mempty添加了反引号,所以当我删除那些编译并按预期工作的代码时。

然而,这令我感到奇怪。如何使用mempty周围的反引号正确编译代码,这根本不是二进制函数(mempty :: a)?

在对#haskell提供了一些非常慷慨的帮助之后,我发现它与函数的Monoid实例有关:

instance Monoid b => Monoid (a -> b)

我现在非常模糊地理解为什么这个错误成功地进行了类型检查,但我仍然觉得在某种程度上受到了道德冤枉。有人能解释这是怎么发生的吗?

此外,我还想关注我正在使用的优先级队列的singleton函数:根据the source,它不会返回空队列。但是,在第24行,相同的优先级队列立即被评估为空。 (我通过跟踪调用验证了这一点。)

3 个答案:

答案 0 :(得分:10)

所以,通常,代码:

a `f` b

只是语法糖:

f a b

因此您的代码变为:

mempty PQ.singleton [start]

因此,类型检查器推断出该特定mempty的类型:

mempty :: (k -> a -> PQ.MinPQueue k a) -> [Node] -> PQ.MinPQueue b [Node]

您正确找到了正确的实例。 a -> b类型的任何内容都是Monoid,前提是b。所以我们将上面的类型括起来:

mempty :: (k -> a -> PQ.MinPQueue k a) -> ([Node] -> PQ.MinPQueue b [Node])

因此,如果Monoid[Node] -> PQ.MinPQueue b [Node],则该类型可以是Monoid。按照相同的逻辑,如果[Node] -> PQ.MinPQueue b [Node]MonoidPQ.MinPQueue b [Node]可以是instance Monoid => Monoid (a -> b) where mempty = const mempty 。它是什么。因此,使用此代码可以使用类型检查器。

据推测,我们麻烦的实例的实现是:

a -> b

总的来说,你得到一个空的优先级队列。实际上,我认为这归结为一个问题,即设计师是否明智地包含这个实例。它的净效果是返回一个monoid的任何函数都可以是一个monoid,它应该允许你组合结果。这里更有用的例子是mappend,它可以通过应用它们并使用mappend来组合结果来附加两个extremes = (return . minimum) `mappend` (return . maximum) 函数。例如:

extremes xs = [minimum xs, maximum xs]

而不是:

{{1}}
嗯,也许其他人可以产生一个明智的例子。

答案 1 :(得分:5)

所以反引号将二进制函数转换为中缀运算符,使

x `op` y

相当于

op x y

所以op必须属于a -> b -> c类型x :: ay :: b

在您的情况下,opmempty,类型为Monoid m => m。但是我们知道它的格式为a -> b -> c,所以替换它,你得到(这不再是有效的语法)Monoid (a -> b -> c) => a -> b -> c,因为我们可以用m代替任何东西,只要约束成立。

现在我们知道(由于实例声明)s -> t形式的任何函数,其中t是一个Monoid,是一个Monoid本身,我们也知道a -> b -> c实际上是a -> (b -> c),即一个函数接受一个参数并返回另一个函数。因此,如果我们将a替换为s而将(b -> c)替换为t,则我们将满足Monoid实例,如果t是Monoid。当然,t(b -> c),因此我们可以再次应用相同的Monoid实例(使用s = bt = c),因此如果c是Monoid,我们很好。

那么c是什么?你的表达是

PQ.singleton `mempty` [start]

mempty PQ.singleton [start]

Monoid (a -> b)的实例声明定义mempty _ = mempty,即它是一个忽略其参数并返回b Monoid的空元素的函数。换句话说,我们可以将上面的调用扩展到

mempty [start]

即。我们忽略了这个参数并使用了内部Monoid的mempty(b -> c)。然后我们重复一遍,再次忽略这个论点:

mempty

所以你所拥有的表达式只相当于一个mempty,其类型为Monoid c => c,即它可以是任何Monoid。

在您的情况下,较大的表达式会将c推断为PQ.MinPQueueMinPQueue是一个Monoid实例,mempty是空队列。

这就是你最终得到的结果。

答案 2 :(得分:1)

你已经有了几个很好的答案,我想我会发布这个,因为它有点简单并且帮助了我,因为我在ghci中对此感到困惑。

mempty :: (a -> b) = mempty _ = mempty所以它基本上是const mempty

λ> :t mempty :: (a -> b)
<interactive>:1:1:
    No instance for (Monoid b) arising from a use of `mempty'

所以b必须是Monoid,因为我们要求该类型的mempty,这是有道理的。

λ> :t mempty :: (a -> [b])
mempty :: (a -> [b]) :: a -> [b]
λ> :t mempty :: (a -> c -> [b])
mempty :: (a -> c -> [b]) :: a -> c -> [b]

我们可以递归链接这些。由于(->)是正确的关联(a -> b),因此(a -> c -> d)可能代表b == (c -> d)。所以我们可以提供任意数量的参数,函数的mempty将被递归地应用,直到消耗掉所有参数。

λ> import Data.Map
λ> (mempty :: (a -> c -> Map Int Int)) 4 5
fromList []
λ> (mempty :: (a -> c -> d -> Map Int Int)) 4 5 6
fromList []

因此,我们看到应用函数mempty将丢弃它给出的任何参数,并返回mempty表达式所在位置的预期类型。