简体   繁体   English

Haskell,定义类型多态的函数的专业化

[英]Haskell, define specialization of function that's polymorphic in type

When using state machines in hedgehog I have to define a function that updates my model state. 刺猬中使用状态机时,我必须定义一个更新模型状态的函数。 Its type should be forall v. Ord1 v => state v -> input v -> Var output v -> state v (see Update constructor of Callback ). 它的类型应为forall v. Ord1 v => state v -> input v -> Var output v -> state v (请参见Update Callback构造函数)。

Now, I want to get to output , but the only function I've found is concrete , however it's specifying that v of my update function. 现在,我想output ,但是我发现的唯一函数是concrete ,但是它指定了我的更新函数v

How do I define an update function that satisfies the type for Update while still letting me get to the output (presumably by using concrete )? 如何定义一个满足Update类型的更新函数,同时又仍然允许我到达输出(大概是通过使用concrete )?

Ah, I see. 知道了 What you want to do is use Vars in your Hedgehog model state and inputs (AKA transitions) wherever a state component depends on earlier actions. 您想要做的是在Hedgehog模型状态和输入(AKA转换)中使用Vars ,无论状态组件取决于先前的操作。 You then update the state in terms of these variables abstractly (ie, in a way that can work both symbolically and concretely). 然后,您可以根据这些变量抽象地更新状态(即,以既可以象征性地又可以具体地起作用的方式)。 It's only when you execute a command that you make those variables concrete. 只有在执行命令时,您才能使这些变量具体化。

Let me show you an example. 让我给你看一个例子。 I've used the following imports and extensions, if you want to follow along: 如果您想遵循以下说明,则使用了以下导入和扩展:

{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wall #-}

import Control.Monad
import Control.Monad.IO.Class
import Data.IORef
import Data.Map.Strict as Map
import Data.Map.Strict (Map)
import Data.Set as Set
import Data.Set (Set)
import System.IO.Unsafe

import Hedgehog
import Hedgehog.Gen as Gen
import Hedgehog.Range as Range

Suppose we have the following mock web API using global IORefs: 假设我们有以下使用全局IORef的模拟Web API:

type UUID = Int
type Content = String

uuidRef :: IORef UUID
uuidRef = unsafePerformIO (newIORef 0)

newUuid :: IO UUID
newUuid = do
  n <- readIORef uuidRef
  writeIORef uuidRef (n+1)
  return n

dbRef :: IORef (Map UUID Content)
dbRef = unsafePerformIO (newIORef Map.empty)

resetDatabase :: IO ()
resetDatabase = writeIORef dbRef Map.empty

postFoo :: Content -> IO UUID
postFoo bdy = do
  uuid <- newUuid
  modifyIORef dbRef (Map.insert uuid bdy)
  return uuid

getFoo :: UUID -> IO (Maybe Content)
getFoo uuid = Map.lookup uuid <$> readIORef dbRef

deleteFoo :: UUID -> IO ()
deleteFoo uuid =
  modifyIORef dbRef (Map.delete uuid)

In constructing the Hedgehog model, we need to keep in mind that UUIDs will be generated as output by postFoo actions for use in subsequent (get and delete) actions. 在构造Hedgehog模型时,我们需要记住, postFoo动作将生成UUID作为输出,以用于后续(获取和删除)动作。 This dependency of later actions on earlier ones means that these UUIDs should appear as variables in the state. 以后的操作对较早的操作的这种依赖性意味着这些UUID应该在状态中显示为变量。

In our state, we'll keep track of a Map of UUIDs (as variables) to Content to model the internal state of the database. 在我们的状态下,我们将跟踪UUID(作为变量)到ContentMap ,以对数据库的内部状态进行建模。 We'll also keep track of the set of all UUIDs seen even those no longer in the database, so we can test fetching of deleted UUIDs. 我们还将跟踪所有UUID的集合,甚至是那些不再存在于数据库中的UUID,因此我们可以测试已删除UUID的提取。

data ModelState (v :: * -> *)
  = S { uuids :: Set (Var UUID v)             -- UUIDs ever returned
      , content :: Map (Var UUID v) Content   -- active content
      }
  deriving (Eq, Ord, Show)

initialState :: ModelState v
initialState = S Set.empty Map.empty

Now, we'll want to model post, get, and delete commands. 现在,我们要对发布,获取和删除命令进行建模。 To "post", we'll want the following "input" (or transition, or whatever), which posts the given content: 要“发布”,我们需要下面的“输入”(或过渡,或任何其他形式),用于发布给定的内容:

data Post (v :: * -> *) = Post Content
  deriving (Eq, Show)

and the corresponding command looks like this: 相应的命令如下所示:

s_post :: (MonadGen n, MonadIO m, MonadTest m) => Command n m ModelState
s_post =
  let
    gen _state = Just $ Post <$> Gen.string (Range.constant 0 100) Gen.alpha
    execute (Post bdy) = liftIO $ postFoo bdy
  in
    Command gen execute [
        Update $ \S{..} (Post bdy) o -> S { uuids = Set.insert o uuids
                                          , content = Map.insert o bdy content }
      ]

Note that it's always possible to create a new post, whatever the current state, so gen ignores the current state and generate a random post. 请注意,无论当前状态如何,总是可以创建新的帖子,因此gen忽略当前状态并生成随机帖子。 execute translates this action into an IO action on the actual API. execute将此操作转换为实际API上的IO操作。 Note that the Update callback receives the result of the postFoo as a variable . 请注意, Update回调将postFoo的结果作为变量接收 That is, o will have type Var UUID v . 也就是说, o将具有Var UUID v类型。 That's fine, because our Update just needs to store a Var UUID v in the state -- it doesn't need a concrete UUID value because of the way we structured the ModelState . 很好,因为我们的Update仅需要在状态中存储Var UUID v由于我们构造ModelState的方式,它不需要具体的UUID值。

We'll also need an HTraversable instance for Post for this to typecheck. 我们还需要PostHTraversable实例来进行类型检查。 Since Post doesn't have any variables, this instance is trivial: 由于Post没有任何变量,因此该实例很简单:

instance HTraversable Post where
  htraverse _ (Post bdy) = pure (Post bdy)

For the "get" input and command, we have: 对于“获取”输入和命令,我们有:

data Get (v :: * -> *) = Get (Var UUID v)
  deriving (Eq, Show)

s_get :: (MonadGen n, MonadIO m, MonadTest m) => Command n m ModelState
s_get =
  let
    gen S{..} | not (Set.null uuids) = Just $ Get <$> Gen.element (Set.toList uuids)
              | otherwise            = Nothing
    execute (Get uuid) = liftIO $ getFoo $ concrete uuid
  in
    Command gen execute [
        Require $ \S{..} (Get uuid) -> uuid `Set.member` uuids
      , Ensure $ \before _after (Get uuid) o ->
          o === Map.lookup uuid (content before)
      ]

Here, gen consults the current state to get the set of ever-observed UUIDs (technically, as symbolic variables). 在这里, gen查询当前状态以获取一直观察到的UUID(在技术上,作为符号变量)的集合。 If the set is empty, we don't have any valid UUIDs to test, so no Get is possible, and gen returns Nothing . 如果该集合为空,则我们没有要测试的有效UUID,因此不可能进行Get操作,并且gen返回Nothing Otherwise, we generate a Get request for a random UUID (as a symbolic variable) in the set. 否则,我们将为集合中的随机UUID(作为符号变量)生成Get请求。 This may be a UUID still in the database or one that's been deleted. 这可能是仍在数据库中的UUID或已被删除的UUID。 The execute method then performs the IO action on the actual API. 然后execute方法对实际的API执行IO操作。 Here, finally, we're allowed to make the variable concrete (which we need to get an actual UUID for the API). 最后,在这里,我们允许将变量具体化(我们需要获取API的实际UUID )。

Note the callbacks -- we Require that the UUID variable be a member of the set of UUID variables in the current state (in case this was invalidated during shrinkage), and after the action executes, we Ensure that we can retrieve the appropriate content for this UUID. 请注意回调-我们Require UUID变量是当前状态下UUID变量集的成员(以防在收缩过程中无效),并且在执行操作之后,我们Ensure可以为以下内容检索适当的内容:这个UUID。 Note that we're allowed to make variables concrete in Ensure , but we didn't need to in this case. 请注意,我们可以在Ensure中将变量具体化,但是在这种情况下我们不需要这样做。 No Update was needed here, since Get doesn't affect the state. 这里不需要Update ,因为Get不会影响状态。

We also need an HTraversable instance for Get . 我们还需要GetHTraversable实例。 Since it has a variable, the instance is little more complicated: 由于它具有变量,因此实例稍微复杂一些:

instance HTraversable Get where
  htraverse f (Get uuid) = Get <$> htraverse f uuid

The code for the "delete" input and command is much the same as for "get", except it has an Update callback. “删除”输入和命令的代码与“获取”的代码非常相似,不同之处在于它具有Update回调。

data Delete (v :: * -> *) = Delete (Var UUID v)
  deriving (Eq, Show)
instance HTraversable Delete where
  htraverse f (Delete uuid) = Delete <$> htraverse f uuid

s_delete :: (MonadGen n, MonadIO m, MonadTest m) => Command n m ModelState
s_delete =
  let
    gen S{..} | not (Set.null uuids) = Just $ Delete <$> Gen.element (Set.toList uuids)
              | otherwise            = Nothing
    execute (Delete uuid) = liftIO $ deleteFoo $ concrete uuid
  in
    Command gen execute [
        Require $ \S{..} (Delete uuid) -> uuid `Set.member` uuids
      , Update $ \S{..} (Delete uuid) _o -> S { content = Map.delete uuid content, .. }
      , Ensure $ \_before after (Delete uuid) _o ->
          Nothing === Map.lookup uuid (content after)
      ]

The property we want to test is sequential application of a random collection of these actions. 我们要测试的属性是这些动作的随机集合的顺序应用。 Note that because our API has global state, we need to resetDatabase at the start of each test, or things will get bizarre: 请注意,由于我们的API具有全局状态, resetDatabase在每次测试开始时我们都需要resetDatabase ,否则情况会变resetDatabase奇怪:

prop_main :: Property
prop_main =
  property $ do
    liftIO $ resetDatabase
    actions <- forAll $
      Gen.sequential (Range.linear 1 100) initialState
          [ s_post, s_get, s_delete ]
    executeSequential initialState actions

Finally, then: 最后,然后:

main :: IO ()
main = void (check prop_main)

and running this gives: 并运行它给出:

> main
✓ <interactive> passed 100 tests.
>

Note that there was one thing we forgot to check above, namely that the API genuinely provides unique UUIDs when posting. 请注意,上面我们忘记了要检查的一件事,即该API在发布时确实提供了唯一的UUID。 For example, if we intentinoally break our UUID generator: 例如,如果我们故意破坏我们的UUID生成器:

newUuid :: IO UUID
newUuid = do
  n <- readIORef uuidRef
  writeIORef uuidRef $ (n+1) `mod` 2
  return n

the testing still passes -- the API gives us duplicate UUIDs and we dutifully overwrite old data in our model state, matching the broken API. 测试仍然通过-API给我们提供了重复的UUID,并且我们忠实地覆盖了模型状态下的旧数据,匹配了损坏的API。

To check this, we want to add an Ensure callback to s_post to ensure that each new UUID isn't one we've seen before. 为了检查这一点,我们想向s_post添加一个Ensure回调,以确保每个新的UUID都不是我们以前见过的。 However if we write: 但是,如果我们写:

, Ensure $ \before _after (Post _bdy) o ->
    assert $ o `Set.notMember` uuids before

this won't type check, because o is an actual, concrete UUID output value (ie, not a Var ), but uuids before is a set of concrete variables. 这不会进行类型检查,因为o是一个实际的,具体的UUID输出值(即,不是Var ),但是uuids before是一组具体变量。 We could map over the set to extract the concrete values from the variables: 我们可以映射到集合以从变量中提取具体值:

, Ensure $ \before _after (Post _bdy) o ->
    assert $ o `Set.notMember` Set.map concrete (uuids before)

or alternatively, we can construct a concrete variable for the value o like so: 或者,我们可以像这样构造一个具体的变量o

, Ensure $ \before _after (Post _bdy) o ->
    assert $ Var (Concrete o) `Set.notMember` uuids before

Both work fine and catch the buggy newUuid implementation above. 两者都可以正常工作,并且可以捕获上面有问题的newUuid实现。

For reference, the complete code is: 供参考,完整的代码是:

{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wall #-}

import Control.Monad
import Control.Monad.IO.Class
import Data.IORef
import Data.Map.Strict as Map
import Data.Map.Strict (Map)
import Data.Set as Set
import Data.Set (Set)
import System.IO.Unsafe

import Hedgehog
import Hedgehog.Gen as Gen
import Hedgehog.Range as Range

-- * Mock API

type UUID = Int
type Content = String

uuidRef :: IORef UUID
uuidRef = unsafePerformIO (newIORef 0)

newUuid :: IO UUID
newUuid = do
  n <- readIORef uuidRef
  writeIORef uuidRef $ (n+1)
  return n

dbRef :: IORef (Map UUID Content)
dbRef = unsafePerformIO (newIORef Map.empty)

resetDatabase :: IO ()
resetDatabase = writeIORef dbRef Map.empty

postFoo :: Content -> IO UUID
postFoo bdy = do
  uuid <- newUuid
  modifyIORef dbRef (Map.insert uuid bdy)
  return uuid

getFoo :: UUID -> IO (Maybe Content)
getFoo uuid = Map.lookup uuid <$> readIORef dbRef

deleteFoo :: UUID -> IO ()
deleteFoo uuid =
  modifyIORef dbRef (Map.delete uuid)

-- * Hedgehog model state

data ModelState (v :: * -> *)
  = S { uuids :: Set (Var UUID v)             -- UUIDs ever returned
      , content :: Map (Var UUID v) Content   -- active content
      }
  deriving (Eq, Ord, Show)

initialState :: ModelState v
initialState = S Set.empty Map.empty

-- * Post input/command

data Post (v :: * -> *) = Post Content
  deriving (Eq, Show)
instance HTraversable Post where
  htraverse _ (Post bdy) = pure (Post bdy)

s_post :: (MonadGen n, MonadIO m, MonadTest m) => Command n m ModelState
s_post =
  let
    gen _state = Just $ Post <$> Gen.string (Range.constant 0 100) Gen.alpha
    execute (Post bdy) = liftIO $ postFoo bdy
  in
    Command gen execute [
        Update $ \S{..} (Post bdy) o -> S { uuids = Set.insert o uuids
                                          , content = Map.insert o bdy content }
    , Ensure $ \before _after (Post _bdy) o ->
        assert $ Var (Concrete o) `Set.notMember` uuids before
      ]

-- * Get input/command

data Get (v :: * -> *) = Get (Var UUID v)
  deriving (Eq, Show)
instance HTraversable Get where
  htraverse f (Get uuid) = Get <$> htraverse f uuid

s_get :: (MonadGen n, MonadIO m, MonadTest m) => Command n m ModelState
s_get =
  let
    gen S{..} | not (Set.null uuids) = Just $ Get <$> Gen.element (Set.toList uuids)
              | otherwise            = Nothing
    execute (Get uuid) = liftIO $ getFoo $ concrete uuid
  in
    Command gen execute [
        Require $ \S{..} (Get uuid) -> uuid `Set.member` uuids
      , Ensure $ \before _after (Get uuid) o ->
          o === Map.lookup uuid (content before)
      ]

-- * Delete input/command

data Delete (v :: * -> *) = Delete (Var UUID v)
  deriving (Eq, Show)
instance HTraversable Delete where
  htraverse f (Delete uuid) = Delete <$> htraverse f uuid

s_delete :: (MonadGen n, MonadIO m, MonadTest m) => Command n m ModelState
s_delete =
  let
    gen S{..} | not (Set.null uuids) = Just $ Delete <$> Gen.element (Set.toList uuids)
              | otherwise            = Nothing
    execute (Delete uuid) = liftIO $ deleteFoo $ concrete uuid
  in
    Command gen execute [
        Require $ \S{..} (Delete uuid) -> uuid `Set.member` uuids
      , Update $ \S{..} (Delete uuid) _o -> S { content = Map.delete uuid content, .. }
      , Ensure $ \_before after (Delete uuid) _o ->
          Nothing === Map.lookup uuid (content after)
      ]

-- * Run the tests

prop_main :: Property
prop_main =
  property $ do
    liftIO $ resetDatabase
    actions <- forAll $
      Gen.sequential (Range.linear 1 100) initialState
          [ s_post, s_get, s_delete ]
    executeSequential initialState actions

main :: IO ()
main = void (check prop_main)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM