对于单子和单子函数的类约束

9
我正在尝试编写一个新的单子,它只能包含Num类型的值。当它失败时,它返回0,就像Maybe单子在失败时返回Nothing一样。
以下是目前的代码:
data (Num a) => IDnum a = IDnum a

instance Monad IDnum where
  return x = IDnum x
  IDnum x >>= f  = f x
  fail :: (Num a) => String -> IDnum a
  fail _ = return 0

Haskell抱怨存在问题。
No instance for (Num a) arising from a use of `IDnum'

建议为每个单子函数的类型签名上下文中添加一个add(Num a),但我尝试了这样做,然后它抱怨它们需要在“forall”a中工作。

例如:

Method signature does not match class; it should be
  return :: forall a. a -> IDnum a
In the instance declaration for `Monad IDnum'

有人知道如何解决这个问题吗?


3个回答

15
现有的Monad类型类希望您的类型能够适用于每个可能的类型参数。考虑Maybe:在Maybe a中,a根本没有受到任何限制。基本上,您不能使用带有约束条件的Monad
这是Monad类定义的一个根本性限制 - 我不知道有什么方法可以解决它而不修改它。
这对于为其他常见类型(例如Set)定义Monad实例也是一个问题。
实际上,这种限制非常重要。请考虑函数通常不是Num的实例。这意味着我们不能使用您的单子来包含函数! 这确实限制了一些重要操作,比如ap(来自Applicative<*>),因为它取决于包含函数的单子:
ap :: Monad m => m (a -> b) -> m a -> m b

你的单子不支持我们从普通单子中期望的许多常见用法和习惯用法!这将大大限制其实用性。

另外,顺便提一下,你通常应该避免使用fail。它并不真正符合Monad类型类:它更像是一个历史遗留问题。大多数人都同意一般应该避免使用它:它只是处理do-notation中失败的模式匹配的一个黑客方法。

话虽如此,研究如何定义受限单子类是一个理解一些Haskell扩展和学习一些中级/高级Haskell的很好练习。

替代方案

考虑到这些缺点,这里有几个选择——标准Monad类的替代方案,支持受限单子。

约束种类

我可以想到几个可能的替代方案。最现代的方法是利用GHC中的ConstraintKind扩展,它使您可以将类型类约束作为种类反射。 这篇博文详细介绍了如何使用约束种类实现受限单子;我读完后,将在此概括。

基本思想很简单:使用ConstraintKind,我们可以将我们的约束(Num a)转换为类型。然后我们可以有一个新的Monad类,它包含该类型作为成员(就像returnfail是成员一样),并允许我们用Num a重载约束。代码如下:

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeFamilies    #-}

module Main where

import Prelude hiding (Monad (..))

import GHC.Exts

class Monad m where
  type Restriction m a :: Constraint
  type Restriction m a = ()

  return :: Restriction m a => a -> m a
  (>>=)  :: Restriction m a => m a -> (a -> m b) -> m b
  fail   :: Restriction m a => String -> m a

data IDnum a = IDnum a

instance Monad IDnum where
  type Restriction IDnum a = Num a
  return        = IDnum
  IDnum x >>= f = f x
  fail _        = return 0

RMonad

现有一个名为rmonad的Hackage库(用于“受限制单子”),为提供更通用的类型类。你可以使用它来编写所需的单子实例(我自己没有使用过,所以很难说)。

rmonad不使用ConstraintKinds扩展,并且(我认为)支持旧版本的GHC。然而,我认为它有点丑陋,不确定它是否仍然是最佳选项。

这是我想出的代码:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

import Prelude hiding (Monad (..))

import Control.RMonad
import Data.Suitable

data IDnum a = IDnum a

data instance Constraints IDnum a = Num a => IDnumConstraints
instance Num a => Suitable IDnum a where
  constraints = IDnumConstraints

instance RMonad IDnum where
  return        = IDnum
  IDnum x >>= f = f x
  fail _        = withResConstraints $ \ IDnumConstraints -> return 0

延伸阅读

欲了解更多详细信息,请查看此 SO 问题

Oleg撰写了一篇有关于Set monad的文章,可能会很有趣:“如何限制一个单子而不破坏它”

最后,你也可以阅读几篇论文:


1
有一个基本上只是更现代化的rmonad版本,它使用约束类型;虽然似乎不太成熟。还有我的constrained-categories 项目,它处理这个约束问题以及更多内容,但比较复杂(它仍然不稳定,无法使用)。 - leftaroundabout
看起来你的 Constraint Kinds 示例缺少了一个限制条件:(>>=) 的限制条件应该是 (Restriction m a, Restriction m b) => ... - Pyry Jahkola

1
这个答案会很简短,但是这里有另一种方法可以和Tikhon的方法一起使用。您可以对您的类型应用一个codensity变换,从而基本上为其获得一个免费单子。只需使用它(在下面的代码中是IDnumM)代替您的基本类型,然后在最后将最终值转换为您的基本类型(在下面的代码中,您将使用runIDnumM)。您还可以将您的基本类型注入到转换后的类型中(在下面的代码中,这将是toIDnumM)。
这种方法的好处是它可以与标准Monad类一起使用。
data Num a => IDnum a = IDnum a

newtype IDnumM a = IDnumM { unIDnumM :: forall r. (a -> IDnum r) -> IDnum r }

runIDnumM :: Num a => IDnumM a -> IDnum a
runIDnumM (IDnumM n) = n IDnum

toIDnumM :: Num a => IDnum a -> IDnumM a
toIDnumM (IDnum x) = IDnumM $ \k -> k x

instance Monad IDnumM where
  return x = IDnumM $ \k -> k x
  IDnumM m >>= f = IDnumM $ \k -> m $ \x -> f x `unIDnumM` k

0

有一种更简单的方法来实现这个。可以使用多个函数。首先,在Maybe monad中编写一个函数。Maybe monad在失败时返回Nothing。其次,编写一个函数,如果不是Nothing,则返回Just值,否则返回某个安全值。第三,编写一个将这两个函数组合起来的函数。

这样做既容易编写又容易理解,同时也能产生所需的结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接