我有很多IO
基于的操作,其中最简单的一个如下:
-- Loop.hs
module Loop where
import System.Console.ANSI (setCursorPosition)
type Pos = (Int, Int)
setCursorPosition' :: Pos -> IO ()
setCursorPosition' = uncurry setCursorPosition
此时,从上面的开始,我决定根据实现的类型约束来编写这些函数,而不是像这个答案所建议的那样IO
硬编码。IO
所以我做的是
- 定义一个
FakeIO
类型class
及其简单实现IO
:-- Interfaces.hs module Interfaces where import qualified System.Console.ANSI as ANSI (setCursorPosition) class FakeIO m where setCursorPosition :: Int -> Int -> m () instance FakeIO IO where setCursorPosition = ANSI.setCursorPosition
- 更改
setCursorPosition'
为使用这个接口:-- Loop.hs module Loop where import Interfaces type Pos = (Int, Int) setCursorPosition' :: FakeIO m => Pos -> m () setCursorPosition' = uncurry setCursorPosition
这使得程序仍然可以正常工作(通过cabal run
),证明“重构”是正确的。
但当我尝试利用此重构进行测试时,我遇到了困难。我所做的就是编写以下测试:
-- test/Main.hs
module Main where
import Control.Monad (unless)
import System.Exit (exitFailure)
import MTLPrelude (State, execState, modify')
import Test.QuickCheck
import Loop
import Interfaces
data MockTerminal = MockTerminal {
pos :: (Int, Int)
} deriving Eq
instance FakeIO (State MockTerminal) where
setCursorPosition y x = modify' $ \m -> MockTerminal { pos = (y, x) }
main :: IO ()
main = do
result <- quickCheckResult tCenter
unless (isSuccess result) exitFailure
tCenter :: Bool
tCenter = (setCursorPosition' (1,1))
`execState` MockTerminal { pos = (0,0)}
== MockTerminal { pos = (1,1) }
编译失败(通过cabal test
),因为
error: [GHC-39999]
• No instance for ‘snakegame-0.1.0.0:Loop:Interfaces.FakeIO
(StateT MockTerminal Identity)’
arising from a use of ‘setCursorPosition'’
• In the first argument of ‘execState’, namely
‘(setCursorPosition' (1, 1))’
In the first argument of ‘(==)’, namely
‘(setCursorPosition' (1, 1))
`execState` MockTerminal {pos = (0, 0)}’
In the expression:
(setCursorPosition' (1, 1)) `execState` MockTerminal {pos = (0, 0)}
== MockTerminal {pos = (1, 1)}
|
41 | tCenter = (setCursorPosition' (1,1))
| ^^^^^^^^^^^^^^^^^^
我不明白,因为instance FakeIO (State MockTerminal)
应该正是snakegame-0.1.0.0:Loop:Interfaces.FakeIO (StateT MockTerminal Identity)
编译器声称不存在的实例。
此外,如果我将测试改为使用setCursorPosition 1 1
而不是setCursorPosition' (1,1)
,它会编译并通过,表明instance
确实在发挥作用。
instance
因此,这与 的定义相结合时一定出了问题setCursorPosition'
。
我把示例缩减为以下 4 个文件:
$ tree !(dist-newstyle)
cabal.project [error opening dir]
LICENSE [error opening dir]
Session.vim [error opening dir]
snakegame.cabal [error opening dir]
src
├── Interfaces.hs
├── Loop.hs
└── Main.hs
test
└── Main.hs
2 directories, 8 files
其中:
-- src/Main.hs
module Main where
import Loop
main :: IO ()
main = setCursorPosition' (1,1)
-- src/Loop.hs
module Loop (setCursorPosition') where
import Interfaces
type Pos = (Int, Int)
setCursorPosition' :: FakeIO m => Pos -> m ()
setCursorPosition' = uncurry setCursorPosition
-- test/Main.hs
module Main where
import Control.Monad (unless)
import System.Exit (exitFailure)
import MTLPrelude (State, execState, modify')
import Test.QuickCheck
import Loop
import Interfaces
data MockTerminal = MockTerminal {
pos :: (Int, Int)
} deriving Eq
instance FakeIO (State MockTerminal) where
setCursorPosition y x = modify' $ \m -> MockTerminal { pos = (y, x) }
putChar _ = modify' id
main :: IO ()
main = do
result <- quickCheckResult tCenter
unless (isSuccess result) exitFailure
tCenter :: Bool
tCenter = (setCursorPosition' (1,1))
`execState` MockTerminal { pos = (0,0)}
== MockTerminal { pos = (1,1)}
cabal-version: 3.0
name: snakegame
version: 0.1.0.0
common common
default-language: GHC2024
build-depends: base >= 4.19.1.0
, ansi-terminal
, mtl-prelude
common warnings
ghc-options: -Wall
executable snakegame
import: warnings, common
main-is: Main.hs
other-modules: Loop
, Interfaces
hs-source-dirs: src
library Loop
import: warnings, common
exposed-modules: Loop
hs-source-dirs: src
library Interfaces
import: warnings, common
exposed-modules: Interfaces
hs-source-dirs: src
test-suite Test
import: warnings, common
type: exitcode-stdio-1.0
main-is: Main.hs
build-depends: QuickCheck
, Interfaces
, Loop
hs-source-dirs: test
packages: .
with-compiler: ghc-9.10.1
您的 Haskell 源代码一切正常。您遇到的问题来自 Cabal。要修复它,请执行以下操作:
app
Main.hs
从移动src/
到app/
snakegame.cabal
为以下内容:问题是您说的是
Loop
和Interfaces
每个都属于多个库,所以您最终得到了它们的多个副本,并且FakeIO
您声明的instance
for 与约束中的副本不是同一个副本setCursorPosition'
。