【问题标题】:Implementing predicate types as contravariant monads?将谓词类型实现为逆变单子?
【发布时间】:2017-09-07 03:01:36
【问题描述】:

我很确定我可以证明,原则上,Predicate 类型构造函数是 ContraMonad,是 Monad 的泛化,其中函子是逆变的,但我不确定它会如何实施。

首先,让我们定义一些术语:

class Contravariant f where
    contramap :: (b -> a) -> f a -> f b

class ContraMonad m where
    return :: a -> m a
    join :: m(m a) -> m a
    contrabind :: m a -> (m b -> a) -> m b

data Predicate a = Pred {getPred :: a -> Bool}

instance Contravariant Predicate where
    contramap f x = Pred ((getPred x).f)

这表明谓词是一个从 a 取值并从中生成命题的函数,是从 Hask 到 Hask^{op} 的逆变函子。谓词的一个例子是:

isEven :: Integer -> Bool
isEven n = if (mod n 2 == 0)
           then True
           else False

函数isEven 是数学意义上的谓词,而Pred isEven 是此处实现的意义上的Predicate

Predicate 实现为ContraMonad 比较棘手。

return只有一种自然选择。

return :: a -> Predicate a
return x = Pred(const True)

您可能会考虑“返回”给出一个谓词的可能性,该谓词 x 为真,并且没有其他类型为 a 的元素为真。问题是只有当 a 是 Eq 类型类的成员时才能实现。

考虑加入比contrabind 更容易。显然,我们想要

contrabind x f = join.contramap f x

这样 contrabind 在考虑 f 作为逆变函子的同时尽可能地类似于 bind。函数fPred b 转换为a,因此contramap fPred a 转换为Pred(Pred b)

那么,join 应该怎么做呢?它必须将 a 类型的谓词的谓词转换为 a 类型的谓词。 a 类型谓词的谓词对 a 类型的谓词进行判断。例如,考虑 a = Integer。 Pred( Pred Integer) 的一个例子是:

Pred("The predicate f is true for all even numbers.")

我用引号代替了实际的实现。如果 f 对所有偶数都为真,则此陈述为真。例如,Pred isEven 的计算结果为 True

考虑到集合 A 上的谓词对应于 A 的子集这一事实,Pred(Pred A) 是一个函数的包装器,它采用 A 的所有子集并将它们判断为“真”或“假”。我们希望join 给我们一个谓词,这与给A 的一个子集相同。这个子集应该尽可能无损。事实上,它应该关心每一个Pred X 的真值,w.r.t. Pred(Pred X) 判断。在我看来,自然的解决方案似乎是所有被判断为“真”的子集的交集,这与将所有真谓词“与”在一起

predAnd :: (a -> Bool) -> (a -> Bool) -> (a -> Bool)
predAnd p q = \x -> ((getPred p) $ x) && ((getPred q) $ x)

作为一个例子,让我们回到“谓词 f 对所有偶数都为真”。在此判断下,每个被评估为True 的谓词对于所有偶数都必须为真,因此每个可能的子集都包含偶数。所有这些集合的交集将只是偶数集合,因此join 将返回谓词Pred isEven

我的问题是,“加入”实际上是如何实现的?可以实施吗?我可以看到对于像“整数”这样的无限类型产生的不可判定集的潜在问题,但它甚至可以为像“Char”这样的有限类型实现,即使幂集也具有有限的基数?

【问题讨论】:

  • 我注意到\f x -> contrabind (return x) f :: (m b -> a) -> a -> m b。不知何故,我不喜欢我们如何改变这些函数的方向:每个 contramonad 都必须支持这一点,这似乎很难实现。此外,任何bcontrabind (return ()) (const ()) : m b 看起来都很奇怪。
  • 函数被翻转,因为 m 是一个逆变函子。因此,来自m b -> a 的函数变成来自m a -> m (m b)) 的函数。只要它是逆变的而不是协变的,每个反单子都可以实现这一点。你可以写contrabind x f = join.contramap (f x)contramap f 函数几乎总是通过使用 f 进行预合成来工作,而不是使用 f 进行正常的后合成。这就是大多数逆变函子的工作方式。 ContraMonad 应该与 Monad 完全相同,只是它是逆变的。因此,returnjoin 不应更改。

标签: haskell monads functor


【解决方案1】:

有一点相关,Applicative的逆变版本是Divisible,我这里简化了

class Contravariant f => Divisible f where
  divide  :: f b -> f c -> f (b, c)
  conquer :: f a

Data.Functor.Contravariant.Divisible 中的divide 是用a -> (b, c) 编写的,这突出了将a 的工作划分为bc 的工作的想法。

{-# LANGUAGE RankNTypes #-}

-- The divide from Data.Functor.Contravariant.Divisible
divide' :: Divisible f => (a -> (b, c)) -> f b -> f c -> f a
divide' f b c = contramap f $ divide b c

-- And a proof they types are equivalent
proof :: (forall a b c. (a -> (b, c)) -> f b -> f c -> f a) -> f b -> f c -> f (b, c)
proof divide' = divide' id

如果结果是Monoid,则任何Op 都是Divisible。这是Predicate的泛化,也就是OpAll

import Data.Monoid

newtype Op r a = Op {runOp :: a -> r}

instance Contravariant (Op r) where
  contramap f (Op g) = Op (g . f)

instance Monoid r => Divisible (Op r) where
  divide (Op f) (Op g) = Op (\(b, c) -> f b <> g c)
  conquer = Op (const mempty)

作为奖励,Op rMonoid,只要结果 r 是一个 moniod,它提供了一种直接定义 predAnd 的方法

instance Monoid a => Monoid (Op a b) where
  mempty = Op (const mempty)
  mappend (Op p) (Op q) = Op $ \a -> mappend (p a) (q a)

type Predicate a = Op All a

predAnd :: Predicate a -> Predicate a -> Predicate a
predAnd = mappend 

但那是hardly surprising

【讨论】:

  • 那么,如果我可以问,join 是什么Predicate?是否可以在 Haskell 中编写实现 join :: Predicate (Predicate a) -&gt; Predicate a 的代码?如果有,那代码是什么?
  • Predicate (Predicate a)(a -&gt; Bool) -&gt; Bool。无法将其转换为 a -&gt; Boola -&gt; Bool 正是您从 (a -&gt; Bool) -&gt; Bool 中获得任何重要信息所需要的!
  • 所以没有办法在Predicate( Predicate a) 中获取所有可能谓词的predAnd?我知道在 Haskell 中可能无法实现,但至少在理论上,它在很多情况下都可以计算出来。如果 a 是有限集,它总是可以计算出来的。
  • 试试小数据类型,比如a ~ Word8
  • 我有一些想法,但这里没有足够的空间将它们全部输入。首先,我们需要要求Pred a 中的a 属于Eq 类型类。然后,我们可以找到一种将Pred a 定义为在Eq 中的方法,通过说两个谓词相等当且仅当它们对于a 的完全相同的术语为真。最后,我们可以构造一个新的数据类型data Remove a = Rmv {getRmv :: [a]},它表示数据类型a,其中列表的元素被删除。这将允许我们递归调用join,一次从Pred a 中删除每个谓词。
【解决方案2】:

我想我可能已经想到了我自己问题的部分答案。首先,我们必须强制 aEq 类型类中。

join p = Pred(\x -> not ((getPred p) (\y -> y /= x)))

这是一个谓词,它接受a 类型的元素并从中构造一个谓词,即x 为假而其他一切都为真的谓词。然后根据原来的Pred( Pred a)判断对这个谓词求值。

如果此谓词为真,则表示 x 不在所有谓词的交集中,因此最终谓词将 x 发送为 False。另一方面,如果这个谓词是假的,那么x就不一定在交集里,除非我们做一个额外的规则:

如果Pred( Pred a)判断判定近最大谓词为假,则该近最大谓词判定为假的元素必须出现在所有判断为真的谓词中。

因此,判断“谓词 f 对所有偶数都为真”。在此规则下是允许的,但判断“谓词 f 仅对偶数为真”。不被允许。如果使用第一类判断,join 将给出所有真谓词的交集。在第二种情况下,join 将简单地给出一个谓词,它有时可以告诉您x 是否不必要。如果它返回Falsex 肯定是没有必要的。如果返回True,则测试没有结果。

我现在确信,除非 a 类型是有限的,否则“ANDing”的完整实现是不可能的,即使这样也是不切实际的,除非 a 非常小。

【讨论】:

  • 作为附录,另一个非常自然的运算符,它给出了所有真正的谓词的联合是isSufficient :: Predicate( Predicate a) -&gt; Predicate a,其中isSufficient p = Pred(\x -&gt; (getPred p) (\y -&gt; y == x))。这会让您知道x 是否足以让谓词被判断为真。与必要性不同,这很容易实现,不需要额外的规则。
猜你喜欢
  • 2020-07-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-05
  • 2015-01-03
  • 1970-01-01
  • 2014-01-26
相关资源
最近更新 更多