根据对这个问题的反馈,我使用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
是什么。您有三个不错的选择:
- 使用功能依赖,
- 使用相关类型族,或
让我们详细查看每个选项。
函数依赖
GHC的问题是,当你调用像baseState
这样的函数时,它知道你想使用哪个e
和s
(它可以从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]
因为你的每个函数都有e
和s
,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]
}
该方法的一个优点是,您可以使用相同类型但具有不同行为的多个不同的MultiAgentEnvironment
s。使用它们也非常简单:现在您不再将MultiAgentEnvironment e s a
作为约束,而是将其作为常规的旧参数。实际上,如果您打开RecordWildCards
pragma,那么以前以
foo :: MultiAgentEnvironment e s a => x -> y
foo x = ...
现在可以写成
foo :: MultiAgentEnvironment e s a -> x -> y
foo mae@MultiAgentEnvironment{..} x = ...
和主体应该是完全相同的(除非主体调用需要MultiAgentEnvironment
的子函数,在这种情况下,您需要手动传递mae
)。