我认为制作像
这样的功能是不可能的(或需要某些语言扩展)f :: Maybe Int
f (Just n) = n
f Nothing = ... -- a compile-time error
你也无法做出类似的功能:
g :: MyClass a => Int -> a
g n | n < 10 = TypeClassInstance
| otherwise = OtherTypeClassInstance
所以,我正在使用着名NICTA FP course的is required to do things like:
来处理这个井字游戏的API。takeBack:选择一个完成的棋盘或一个至少有一次移动的棋盘,然后返回一个棋盘。在空板上调用此函数是编译时类型错误。
我认为可以做一些非常奇特的类型级编程。但即便如此,我也不认为刚刚参加了为期两天的函数式编程介绍的人可以知道它。或者我错过了什么?
基于@ user2407038给出的示例和来自@Cirdec的说明,我写了这个,当你在空板上尝试takeBack
时它确实会发生编译错误。
然而 - 稍微移动球门柱 - 这个技巧似乎有限。还有一个要求是你无法继续已经结束的游戏。
移动:采取一个井字板和位置并移动到该位置(如果没有占用)返回一块新板。此功能只能在空白或正在播放的电路板上调用。调用已完成的游戏板上的移动是编译时类型错误。
这似乎不是一个简单的技巧,可以在复杂逻辑的情况下使用类型计数来确定游戏是否结束。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
data N = S N | Z
data Stat
= Empty
| InPlay
| Won
deriving Show
data Board (n :: N)
= Board Stat [Int]
deriving Show
newBoard :: Board Z
newBoard = Board Empty []
move :: Int -> Board a -> Board (S a)
move x (Board Empty []) = Board InPlay [x]
move x (Board InPlay xs) = Board Won (x:xs)
takeBack :: Board (S a) -> Board a
takeBack (Board InPlay [x]) = Board Empty []
takeBack (Board InPlay (x:xs)) = Board InPlay xs
takeBack (Board Won (x:xs)) = Board InPlay xs
main :: IO ()
main = do
let
brd = newBoard -- Empty
m1 = move 1 brd -- InPlay
m2 = move 2 m1 -- Won
m3 = move 3 m2 -- Can't make this a compile-time error with this trick
tb2 = takeBack m2 -- Won
tb1 = takeBack tb2 -- InPlay
tb0 = takeBack tb1 -- Empty -> Compile-time error Yay!
return ()
答案 0 :(得分:5)
您可以使用GADT(广义代数数据类型)执行类似第一个示例的操作;
data SMaybe (a :: Maybe *) where
SJust :: a -> SMaybe (Just a)
SNothing :: SMaybe Nothing
f :: SMaybe (Just a) -> a
f (SJust a) = a
-- f SNothing = undefined -- Including this case is a compile time error
虽然我怀疑这有多大用处。对于电路板事物最简单的解决方案可能是Board
数据类型上有一个幻像类型参数:
type Empty = False
type NonEmpty = True
data Board (b :: Bool) = Board ...
newBoard :: Board Empty
newBoard = Board ...
setAt :: (Int, Int) -> Bool -> Board a -> Board NonEmpty
setAt p b (Board ...) = ...
takeBack :: Board NonEmpty -> Board NonEmpty
takeBack (Board ...) = ...
如果您愿意,可以增加存储在类型级别的信息量。例如,您可以拥有已填充的&#34;单元格的数量&#34;:
data N = S N | Z -- The naturals
data Board (n :: N) = Board ...
newBoard :: Board Z
newBoard = Board ...
setAt :: (Int, Int) -> Bool -> Board a -> Board (S a)
setAt = ...
takeBack :: Board (S n) -> Board (S n)
takeBack = ...
上面的示例为方便起见使用了DataKinds,但并不需要它。
答案 1 :(得分:1)
在不调用任何类型级编程的情况下完成此类操作的一种简单方法是smart constructor。要创建智能构造函数,不要导出数据类型的实际构造函数,而是提供仅创建符合其他规则的类型值的函数。
我们可以通过制作代表董事会成员Playable
,Finished
或NonEmpty
的证明的智能构造函数来解决示例问题。
type Position = (Int, Int)
type Player = Bool
data Board = Board -- ...
deriving (Eq, Show, Read, Ord)
newtype Playable = Playable {getPlayable :: Board}
deriving (Eq, Ord, Show)
newtype Finished = Finished {getFinished :: Board}
deriving (Eq, Ord, Show)
newtype NonEmpty = NonEmpty {getNonEmpty :: Board}
deriving (Eq, Ord, Show)
我们注意不要提供可以创建任何这些类型的实例;例如,我们没有为它们派生Read
个实例。创建这些函数的唯一导出函数将首先检查必要条件。
playable :: Board -> Maybe Playable
playable = undefined
finished :: Board -> Maybe Finished
finished = undefined
nonEmpty :: Board -> Maybe NonEmpty
nonEmpty = undefined
当我们从模块中导出类型时,我们不会导出它们的构造函数
module TicTacToe (
Playable (getPlayable),
Finished (getFinished),
NonEmpty (getNonEmpty),
playable,
finished,
nonEmpty,
Position,
Player,
Board (..),
move,
whoWon,
takeBack,
playerAt
) where
其余的函数可能要求客户端代码在调用函数之前已经获得了必要属性的类型级证明。
move :: Position -> Playable -> Board
move = undefined
whoWon :: Finished -> Player
whoWon = undefined
takeBack :: NonEmpty -> Board
takeBack = undefined
对于这个示例问题,智能构造函数完全没有完成。任何库用户都将定义辅助函数,这样他们只需要关注Maybe
而不关心任何其他特殊的董事会类型。
move' :: Position -> Board -> Maybe Board
move' p = fmap (move p) . playable
whoWon' :: Board -> Maybe Player
whoWon' = fmap whoWon . finished
takeBack' :: Board -> Maybe Board
takeBack' = fmap takeBack . nonEmpty
这表明在接口中使用Maybe
就足够了,并且练习中编译时错误的要求是多余的。这也符合以下要求的功能,该功能不需要在使用之前有人在该位置移动的类型级证据。
playerAt :: Position -> Board -> Maybe Player
playerAt = undefined
当存在许多转换时,使用属性的类型级证明是更有利的,这些转换的属性是不变的或者可以很容易地推导出来。