Tenho muitas IO
ações baseadas em , sendo uma das mais simples a seguinte:
-- Loop.hs
module Loop where
import System.Console.ANSI (setCursorPosition)
type Pos = (Int, Int)
setCursorPosition' :: Pos -> IO ()
setCursorPosition' = uncurry setCursorPosition
Nesse ponto, partindo do exposto acima, decidi escrever essas funções em termos de uma restrição de tipo implementada por IO
, em vez de codificar IO
, como sugerido por esta resposta .
Então o que eu fiz consistiu em
- definindo um
FakeIO
tipoclass
e sua implementação trivial paraIO
:-- 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
- alterado
setCursorPosition'
para usar esta interface:-- Loop.hs module Loop where import Interfaces type Pos = (Int, Int) setCursorPosition' :: FakeIO m => Pos -> m () setCursorPosition' = uncurry setCursorPosition
Isso fez com que o programa continuasse funcionando bem (via cabal run
), atestando que a "refatoração" estava correta.
Mas quando tentei alavancar essa refatoração para fins de teste, fiquei preso. O que fiz foi escrever o seguinte teste:
-- 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) }
que falha ao compilar (via cabal test
) porque
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))
| ^^^^^^^^^^^^^^^^^^
o que eu não entendo, porque instance FakeIO (State MockTerminal)
deveria ser exatamente a snakegame-0.1.0.0:Loop:Interfaces.FakeIO (StateT MockTerminal Identity)
instância que o compilador está alegando que não existe.
Além disso, se eu alterar o teste para usar setCursorPosition 1 1
em vez de setCursorPosition' (1,1)
, ele compila e passa, revelando que o instance
está realmente fazendo seu trabalho.
Então deve haver algo errado com a forma como isso instance
se integra à definição de setCursorPosition'
.
Reduzi o exemplo para os 4 arquivos a seguir:
$ 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
dos quais:
-- 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
Seu código-fonte Haskell está tudo bem. O problema que você está enfrentando é do Cabal. Para consertá-lo, faça o seguinte:
app
Main.hs
desrc/
paraapp/
snakegame.cabal
pelo seguinte:O problema é que você estava dizendo isso
Loop
eInterfaces
cada um pertencia a várias bibliotecas, então você estava acabando com várias cópias delas, e oFakeIO
que você declarouinstance
para não era a mesma cópia que a da restrição emsetCursorPosition'
.