解决MultiParameterTypeClass中的歧义

根据对此问题的反馈,我使用 MultiParamTypeClass 来表示强化学习环境Environment,使用 3 个类型变量:e对于环境实例本身(例如,下面的 Nim 之类的游戏),s对于特定对象使用的状态数据类型游戏,以及a特定游戏使用的动作数据类型。

{-# LANGUAGE MultiParamTypeClasses #-}

class MultiAgentEnvironment e s a where
    baseState :: e -> s
    nextState :: e -> s -> a -> s
    reward :: (Num r) => e -> s -> a -> [r]

data Game = Game { players :: Int
                 , initial_piles :: [Int]
                 } deriving (Show)

data State = State { player :: Int
                   , piles :: [Int]} deriving (Show)

data Action = Action { removed :: [Int]} deriving (Show)

instance MultiAgentEnvironment Game State Action where
    baseState game = State{player=0, piles=initial_piles game}
    nextState game state action = State{player=player state + 1 `mod` players game,
                                        piles=zipWith (-) (piles state) (removed action)}
    reward game state action = [0, 0]

newGame :: Int -> [Int] -> Game
newGame players piles = Game{players=players, initial_piles=piles}


main = do
    print "Hello, world!"
    let game = newGame 2 [3,4,5]
    print game

正如预期的那样,我已经遇到了歧义问题。见下文,其中动作类型变量a在 typeclass 中被认为是不明确的Environment

(base) randm@soundgarden:~/Projects/games/src/Main$ ghc -o basic basic.hs
[1 of 1] Compiling Main             ( basic.hs, basic.o )

basic.hs:4:5: error:
    • Could not deduce (MultiAgentEnvironment e s a0)
      from the context: MultiAgentEnvironment e s a
        bound by the type signature for:
                   baseState :: forall e s a. MultiAgentEnvironment e s a => e -> s
        at basic.hs:4:5-23
      The type variable ‘a0’ is ambiguous
    • In the ambiguity check for ‘baseState’
      To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
      When checking the class method:
        baseState :: forall e s a. MultiAgentEnvironment e s a => e -> s
      In the class declaration for ‘MultiAgentEnvironment’
  |
4 |     baseState :: e -> s
  |     ^^^^^^^^^^^^^^^^^^^

我如何解决这种歧义?我是否错误地使用类型类来实现接口(即baseState, nextState, reward)?

回答

虽然您可以打开AllowAmbiguousTypes,但这只会将您的问题推到更远的地方。也就是说,最终,您将尝试调用baseState,而 GHC 将需要知道是什么a。您有三个不错的选择:

  1. 使用函数依赖,
  2. 使用关联的类型系列,或
  3. 不要为此使用类。

让我们详细看看每个选项。


功能依赖

该GHC遇到的问题是在于,它知道哪些es你想,当你调用像一个函数使用baseState(它可以决定那些从输入和输出baseState功能),但它不知道哪个a使用。GHC 都知道,MultiAgentEnvironment对于给定的e和,可能有多个实例化s。通过函数依赖,你可以告诉 GHC 一个给定的e并且s完全定义a应该是什么。通俗地说,通过对 的函数依赖a,你是说对于任何给定的环境和状态,只有一种可能的动作类型是有意义的。如果这是真的,那么您可以像这样添加它们:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

class MultiAgentEnvironment e s a | e s -> a where
    baseState :: e -> s
    nextState :: e -> s -> a -> s
    reward :: (Num r) => e -> s -> a -> [r]

由于您的每个函数都具有es在其中,a是唯一可能不明确的类型参数,并且有了这个基金,它就不再是模棱两可了。也就是说,如果知道的环境类型决定双方的状态和动作毫不含糊地(即,一个给定的环境永远只能有一种可能的状态和动作类型),那么你可以使用2个fundeps更以降低不确定性,如在:

class MultiAgentEnvironment e s a | e -> s a where

关联类型族

函数依赖名声不好,更现代的替代方法通常是使用关联类型系列。实际上,就您的目的而言,它们的工作方式非常相似。再一次,您必须接受环境类型决定操作类型(也许还有状态类型)。如果是这样,你可以这样写:

{-# LANGUAGE TypeFamilies #-}

class MultiAgentEnvironment e where
    type EState  e
    type EAction e
    baseState :: e -> EState e
    nextState :: e -> EState e -> a -> EState e
    reward :: (Num r) => e -> EState e -> EAction e -> [r]

然后,当您创建实例时,它将如下所示:

instance MultiAgentEnvironment Game where
    type EState  Game = State
    type EAction Game = Action
    baseState game = State{player=0, piles=initial_piles game}
    nextState game state action = ...

使用数据类型而不是类

最后一个选择是完全放弃使用类型类。相反,您可以通过将数据表示为数据类型来使数据显式。例如,您可以定义:

{-# LANGUAGE RankNTypes #-}

data MultiAgentEnvironment e s a = MultiAgentEnvironment
  { baseState :: e -> s
  , nextState :: e -> s -> a -> s
  , reward :: forall r. (Num r) => e -> s -> a -> [r]
  }

您只需创建数据类型的值,而不是创建类型类的实例:

gameStateActionMAE :: MultiAgentEnvironment Game State Action
gameStateActionMAE = MultiAgentEnvironment
  { baseState = game -> State{player=0, piles=initial_piles game}
  , nextState = game state action -> State{player=player state + 1 `mod` players game,
                                        piles=zipWith (-) (piles state) (removed action)}
  , reward = game state action -> [0, 0]
  }

这种方法的一个很好的优点是您可以MultiAgentEnvironment使用相同的类型创建多个不同的s,但它们具有不同的行为。使用它们也非常简单:MultiAgentEnvironment e s a您现在将其作为常规的旧参数而不是作为约束。事实上,如果你打开RecordWildCardspragma,那么任何用于开始的函数

foo :: MultiAgentEnvironment e s a => x -> y
foo x = ...

现在可以写成

foo :: MultiAgentEnvironment e s a -> x -> y
foo mae@MultiAgentEnvironment{..} x = ...

并且主体应该几乎相同(好吧,除非主体调用需要 的子函数MultiAgentEnvironment,在这种情况下,您需要手动传递mae)。


以上是解决MultiParameterTypeClass中的歧义的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>