一种将 Haskell 的 Either 类型概括为任意多种类型的方法?

A way to generalize Haskell's Either type for arbitrarily many types?

我正在制作一款回合制游戏。我想定义一种数据类型,它对多种可能类型中的一种类型进行编码。这是激励人心的例子:

我已经使用 GADT 定义了一个 Turn 类型,因此 Turn a 的每个值的类型说明了它的值。

data Travel
data Attack
data Flee
data Quit

data Turn a where
   Travel :: Location -> Turn Travel
   Attack :: Int -> Turn Attack
   Flee :: Turn Flee
   Quit :: Turn Quit

现在我可以写出这样的类型了decideTravel :: GameState -> Turn Travel,非常有表​​现力和漂亮。

当我想 return 多种可能的转弯类型之一时,问题就出现了。我想写类似下面的函数:

-- OneOf taking two types
decideFightingTurn :: GameState -> OneOf (Turn Attack) (Turn Flee)
-- OneOf takes three types
decideTurn :: GameState -> OneOf (Turn Attack) (Turn Travel) (Turn Quit)

其中 OneOf 数据类型带有“多种可能类型中的一种类型”的概念。问题在于定义此数据类型,我需要以某种方式处理类型级别的类型列表。

到目前为止,我有两个低于标准的解决方案:

解决方案 1:创建包装总和类型

只需创建一个新类型,每个 Turn a 个构造函数都有一个构造函数:

data AnyTurn
   = TravelTurn (Turn Travel)
   | TravelAttack (Turn Attack)
   | TravelFlee (Turn Flee)
   | TravelQuit (Turn Quit)

这并没有以我想要的方式帮助我。现在我必须对 AnyTurn 的所有情况进行模式匹配并考虑无效的输入类型。此外,类型级别信息被 AnyTurn 类型所掩盖,因为它无法指示在类型级别上哪些特定回合实际上是可能的。

解决方案 2:为不同数量的类型创建“任一”类型

此解决方案在类型级别提供了我想要的内容,但使用起来很麻烦。基本上为任意数量的组合创建一个类似于 Either 的类型,如下所示:

data OneOf2 a b
   = OneOf2A a
   | OneOf2B b

data OneOf3 a b c
   = OneOf3A a
   | OneOf3B b
   | OneOf3C c

-- etc, for as many as are needed.

这在类型级别传达了我想要的内容,因此我现在可以将前面的示例写为:

decideFightingTurn :: GameState -> OneOf2 (Turn Travel) (Turn Flee)
decideTurn :: GameState -> OneOf3 (Turn Attack) (Turn Travel) (Turn Quit)

这是可行的,但是最好只用一种类型来表达它 OneOf。有什么方法可以在 Haskell 中编写通用的 OneOf 类型?

我猜是这样的:

data OneOf as where
    ThisOne :: a -> OneOf (a : as)
    Later :: OneOf as -> OneOf (a : as)

那你可以这样写,比如:

decideFightingTurn :: GameState -> OneOf [Turn Travel, Turn Flee]

您可能还想:

type family Map f xs where
    Map f '[] = '[]
    Map f (x:xs) = f x : Map f xs

这样你可以这样写:

decideFightingTurn :: GameState -> OneOf (Map Turn [Travel, Flee])

或者如果您认为自己会一直这样做的话,您可以将 Map 构建到 GADT 中:

data OneOfF f as where
    ThisOneF :: f a -> OneOfF f (a : as)
    LaterF :: OneOfF f as -> OneOfF f (a : as)

然后:

decideFightingTurn :: GameState -> OneOfF Turn [Travel, Flee]

如果这成为一个问题,可以采取各种措施来提高效率;我会尝试的第一件事是使用二进制索引而不是一元索引,如此处所示。

有一个名为 row-types that might give you what you want. Specifically, it provides an extensible variant 的 Haskell 库,有点像 Either,但带有任意多个选项。

在你的情况下,你可以创建你的类型:

import Data.Row

decideFightingTurn :: GameState -> Var ("travel" .== Turn Travel .+ "flee" .== Turn Flee)
decideTurn :: GameState -> Var ("attack" .== Turn Attack .+ "travel" .== Turn Travel .+ "quit" .== Turn Quit)

使用变体的最简单方法是打开 OverloadedLabels。然后,要创建一个,您可以这样写:

decideFightingTurn gs = case gs of
  Foo -> IsJust #travel $ Travel loc
  Bar -> IsJust #flee Flee

可以通过几种不同的方式进行解构。如果你只想检查特定类型,你可以使用 trialview(由 Data.Row.Variants 导出),但如果你想一次处理所有选项,你可能想使用 switch。你会这样写:

handleTravelOrFlee :: Var ("travel" .== Turn Travel .+ "flee" .== Turn Flee) -> GameState -> GameState
handleTravelOrFlee v gs = switch v $
     #travel .== (\(Travel loc) -> updateGameStateWithLoc gs loc)
  .+ #flee   .== (\Flee -> updateGameStateFlee gs)

查看文档并the examples file了解更多信息。

另一个选项可能是激活和停用求和类型的构造函数:

{-# LANGUAGE GADTs, TypeOperators, DataKinds, PolyKinds, 
             TypeFamilies, StandaloneKindSignatures #-}
{-# OPTIONS_GHC -Werror=incomplete-patterns -Werror=overlapping-patterns #-}
import Data.Void
import Data.Kind

data TurnType =
      Travel
    | Attack
    | Flee
    | Quit

type Turn :: TurnType -> Type
data Turn tt  where
   TurnTravel :: Location -> Turn Travel
   TurnAttack :: Int -> Turn Attack
   TurnFlee :: Turn Flee
   TurnQuit :: Turn Quit

type X :: TurnType -> [TurnType] -> Type
type family X x tts :: Type where
    X _ '[] = Void 
    X x (x : xs) = ()
    X x (_ : xs) = X x xs

-- Exhaustiveness checker knows that branches where
-- X returns Void are impossible.
type AnyTurn :: [TurnType] -> Type
data AnyTurn allowed
   = TravelTurn !(X Travel allowed) (Turn Travel)
   | TravelAttack !(X Attack allowed) (Turn Attack)
   | TravelFlee  !(X Flee allowed) (Turn Flee)
   | TravelQuit  !(X Quit allowed) (Turn Quit)

投入使用:

someTurn :: AnyTurn '[Travel, Attack]
someTurn = TravelAttack () (TurnAttack 0)

-- This doesn't give an exhaustiveness error because other branches are impossible.
-- The problem is that we now have those annoying () cluttering every match.
turn2string :: AnyTurn '[Travel, Attack] -> String
turn2string t = case t of
   TravelTurn () _ -> "foo" 
   TravelAttack () _ -> "bar"
   -- TravelQuit _ _ -> "doesn't compile"