首页 > 解决方案 > 解决 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

正如预期的那样,我已经遇到了模棱两可的问题。见下文,其中 action 类型变量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,,nextStatereward

标签: haskell

解决方案


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

  1. 使用功能依赖,
  2. 使用关联的类型族,或
  3. 不要为此使用课程。

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


功能依赖

eGHC 遇到的问题是,当您调用类似的函数时,它知道s您要使用哪个baseState(它可以从函数的输入和输出中确定那些baseState),但它不知道a要使用哪个。MultiAgentEnvironment据 GHC 所知,对于给定的eand ,可能会有多个实例化s。通过函数依赖,你可以告诉 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]

由于您的每个函数都有e并且s在其中,a是唯一可能模棱两可的类型参数,并且有了这个fundep,它就不再模棱两可了。也就是说,如果知道环境类型明确地确定状态和动作(即,给定的环境只能具有一种可能的状态和动作类型),那么您可以使用两个基金来进一步降低歧义,如在:

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)。


推荐阅读