解决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在类型类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. 使用相关类型族,或

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


函数依赖

GHC的问题是,当你调用像baseState这样的函数时,它知道你想使用哪个es(它可以从baseState函数的输入和输出中确定那些),但它不知道要使用哪个a。对于所有GHC来说,可能有多个MultiAgentEnvironment的实例化可以为给定的es工作。通过功能依赖,您可以告诉GHC给定的es完全定义了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是唯一可能有二义性的类型参数,有了这个基础,它就不再是二义性了。也就是说,如果知道环境类型明确地决定状态和操作(也就是说,给定的环境只能有一种可能的状态和操作类型),那么您可以使用两个元素来进一步降低模糊性,如:

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]
}

该方法的一个优点是,您可以使用相同类型但具有不同行为的多个不同的MultiAgentEnvironments。使用它们也非常简单:现在您不再将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)。

相关内容

  • 没有找到相关文章

最新更新