-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathError.hs
More file actions
155 lines (132 loc) · 4.7 KB
/
Copy pathError.hs
File metadata and controls
155 lines (132 loc) · 4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeInType #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Example uses and instances of the @HasError@ capability.
module Error where
import Capability.Error
import Control.Monad (when)
import Control.Monad.Except (ExceptT (..))
import Control.Monad.IO.Class (MonadIO, liftIO)
import GHC.Generics (Generic)
import Test.Common
import Test.Hspec
import Text.Read (readEither)
----------------------------------------------------------------------
-- Example Programs
-- Calculator Example ------------------------------------------------
data ParserError
= InvalidInput String
deriving (Show, Typeable)
deriving anyclass Exception
parseNumber :: HasThrow "parser" ParserError m
=> String -> m Int
parseNumber input = case readEither input of
Left err -> throw @"parser" $ InvalidInput err
Right num -> pure num
data MathError
= NegativeInput
deriving (Show, Typeable)
deriving anyclass Exception
sqrtNumber :: HasThrow "math" MathError m
=> Int -> m Int
sqrtNumber num
| num < 0 = throw @"math" NegativeInput
| otherwise = pure $ round $ sqrt @Double $ fromIntegral num
-- | Errors that can occur in the calculator application.
data CalcError
-- | The parser component failed.
= ParserError ParserError
-- | The math component failed.
| MathError MathError
deriving (Generic, Show, Typeable)
deriving anyclass Exception
-- | Calculator application
--
-- Prompts for positive numbers and prints their square roots.
calculator ::
( HasCatch "calc" CalcError m, MonadIO m )
=> m ()
calculator = do
liftIO $ putStr "Enter positive number or 'Q' to quit\n> "
line <- liftIO getLine
case line of
"Q" -> pure ()
input -> do
catch @"calc"
do
-- Errors in the parser or math component are converted to a
-- @CalcError@ by wrapping with the corresponding constructor.
let wrapParserError = wrapError @"parser"
@(Rename "ParserError" :.: Ctor "ParserError" "calc") @'[]
wrapMathError = wrapError @"math"
@(Rename "MathError" :.: Ctor "MathError" "calc") @'[]
num <- wrapParserError $ parseNumber input
root <- wrapMathError $ sqrtNumber num
liftIO $ putStrLn $ "sqrt = " ++ show root
\e -> liftIO $ putStrLn $ "Error: " ++ show e
calculator
-- Nested Example ----------------------------------------------------
nested :: (HasThrow "foo" String m, HasThrow "bar" () m) => Int -> m Int
nested n = do
when (n < 0) $
throw @"foo" "negative number"
when (odd n) $
throw @"bar" ()
pure n
----------------------------------------------------------------------
-- Instances
-- | Deriving @HasThrow/HasCatch@ from @unliftio@.
newtype Calculator a = Calculator { runCalculator :: IO a }
deriving newtype (Functor, Applicative, Monad, MonadIO)
deriving
( HasThrow "calc" CalcError
, HasCatch "calc" CalcError
) via MonadUnliftIO CalcError IO
-- | Deriving separate @HasThrow@ capabilities from different transformer
-- layers.
newtype MaybeEither a =
MaybeEither { runMaybeEither :: Maybe (Either String a) }
deriving (Functor, Applicative, Monad) via
ExceptT String Maybe
deriving (HasThrow "foo" String) via
MonadError (ExceptT String Maybe)
deriving (HasThrow "bar" ()) via
Lift (ExceptT String (MonadError Maybe))
----------------------------------------------------------------------
-- Test Cases
spec :: Spec
spec = do
describe "Calculator" $
it "evaluates calculator" $ do
let input = "4\n-1\nxyz\nQ\n"
output =
"Enter positive number or 'Q' to quit\n\
\> sqrt = 2\n\
\Enter positive number or 'Q' to quit\n\
\> Error: MathError NegativeInput\n\
\Enter positive number or 'Q' to quit\n\
\> Error: ParserError (InvalidInput \"Prelude.read: no parse\")\n\
\Enter positive number or 'Q' to quit\n\
\> "
runCalculator calculator
`withInput` input
`shouldPrint` output
describe "MaybeEither" $
it "evaluates nested" $ do
runMaybeEither (nested 2) `shouldBe` Just (Right 2)
runMaybeEither (nested (-1)) `shouldBe` Just (Left "negative number")
runMaybeEither (nested 1) `shouldBe` Nothing