|
| 1 | +# purescript-checked-exceptions |
| 2 | + |
| 3 | +Extensible checked exceptions using polymorphic variants |
| 4 | + |
| 5 | +[](https://github.com/natefaubion/purescript-checked-exceptions/releases) |
| 6 | +[](https://travis-ci.org/natefaubion/purescript-checked-exceptions) |
| 7 | + |
| 8 | +## The ~~Expression~~ Exception Problem |
| 9 | + |
| 10 | +Given some function for making HTTP requests which propagates an `HttpError` |
| 11 | +for failures cases: |
| 12 | + |
| 13 | +```purescript |
| 14 | +get |
| 15 | + ∷ ∀ m |
| 16 | + . MonadHttp m |
| 17 | + ⇒ String |
| 18 | + → ExceptT HttpError m String |
| 19 | +``` |
| 20 | + |
| 21 | +And another for writing files which propagates an `FsError` for failures cases: |
| 22 | + |
| 23 | +```purescript |
| 24 | +write |
| 25 | + ∷ ∀ m |
| 26 | + . MonadFs m |
| 27 | + ⇒ Path |
| 28 | + → String |
| 29 | + → ExceptT FsError m Unit |
| 30 | +``` |
| 31 | + |
| 32 | +What happens when we combine them? |
| 33 | + |
| 34 | +```purescript |
| 35 | +getPureScript |
| 36 | + ∷ ∀ m |
| 37 | + . MonadHttp m |
| 38 | + ⇒ MonadFs m |
| 39 | + ⇒ ExceptT _ m Unit |
| 40 | +getPureScript = |
| 41 | + get "http://purescript.org" >>= write "~/purescript.html" |
| 42 | +``` |
| 43 | +``` |
| 44 | +Could not match type |
| 45 | +
|
| 46 | + FsError |
| 47 | +
|
| 48 | +with type |
| 49 | +
|
| 50 | + HttpError |
| 51 | +``` |
| 52 | + |
| 53 | +Before we can get anywhere, we must unify the error types. |
| 54 | + |
| 55 | +```purescript |
| 56 | +getPureScript |
| 57 | + ∷ ∀ m |
| 58 | + . MonadHttp m |
| 59 | + ⇒ MonadFs m |
| 60 | + ⇒ ExceptT (Either HttpError FsError) m Unit |
| 61 | +getPureScript = do |
| 62 | + resp <- withExceptT Left (get "http://purescript.org") |
| 63 | + rethrow Right (write "~/purescript.html" resp) |
| 64 | +``` |
| 65 | + |
| 66 | +This gets very tedious, very quickly, because every new exception type we |
| 67 | +introduce breaks code we've already written. |
| 68 | + |
| 69 | +## Polymorphic Variants to the Rescue |
| 70 | + |
| 71 | +[`Variant`](https://github.com/natefaubion/purescript-variant) lets us define |
| 72 | +_structural_ sum types. Row types in a `Record` point to _fields_, while row |
| 73 | +types in a `Variant` point to _tags_. That means we only have to care about |
| 74 | +the cases we want to use, and they work together regardless of which module |
| 75 | +defined them. |
| 76 | + |
| 77 | +We'll start with a little bit of sugar (this helps the types go down easy): |
| 78 | + |
| 79 | +```purescript |
| 80 | +type RowApply (f :: # Type -> # Type) (a :: # Type) = f a |
| 81 | +
|
| 82 | +infixr 0 type RowApply as + |
| 83 | +``` |
| 84 | + |
| 85 | +We'll define our `HttpError` variants with _rows_ instead of the usual `data` |
| 86 | +declaration: |
| 87 | + |
| 88 | +```purescript |
| 89 | +type HttpServerError r = (httpServerError ∷ String | r) |
| 90 | +type HttpNotFound r = (httpNotFound ∷ Unit | r) |
| 91 | +type HttpOther r = (httpOther ∷ { status ∷ Int, body ∷ String } | r) |
| 92 | +``` |
| 93 | + |
| 94 | +And add constructors which lift them into `Variant`: |
| 95 | + |
| 96 | +```purescript |
| 97 | +httpServerError ∷ ∀ r. String → Variant (HttpServerError + r) |
| 98 | +httpServerError = inj (SProxy ∷ SProxy "httpServerError") |
| 99 | +
|
| 100 | +httpNotFound ∷ ∀ r. Variant (HttpNotFound + r) |
| 101 | +httpNotFound = inj (SProxy ∷ SProxy "httpNotFound") unit |
| 102 | +
|
| 103 | +httpOther ∷ ∀ r. Int → String → Variant (HttpOther + r) |
| 104 | +httpOther status body = inj (SProxy ∷ SProxy "httpOther") { status, body } |
| 105 | +``` |
| 106 | + |
| 107 | +We can then define a helpful alias for all of our HTTP exceptions: |
| 108 | + |
| 109 | +```purescript |
| 110 | +type HttpError r = |
| 111 | + ( HttpServerError |
| 112 | + + HttpNotFound |
| 113 | + + HttpOther |
| 114 | + + r |
| 115 | + ) |
| 116 | +``` |
| 117 | + |
| 118 | +Now in another module we might do the same for FS exceptions: |
| 119 | + |
| 120 | +```purescript |
| 121 | +type FsPermissionDenied r = (fsPermissionDenied ∷ Unit | r) |
| 122 | +type FsFileNotFound r = (fsFileNotFound ∷ Path | r) |
| 123 | +
|
| 124 | +fsPermissionDenied ∷ ∀ r. Variant (FsPermissionDenied + r) |
| 125 | +fsPermissionDenied = inj (SProxy ∷ SProxy "fsPermissionDenied") unit |
| 126 | +
|
| 127 | +fsFileNotFound ∷ ∀ r. Path → Variant (FsFileNotFound + r) |
| 128 | +fsFileNotFound = inj (SProxy ∷ SProxy "fsFileNotFound") |
| 129 | +
|
| 130 | +type FsError r = |
| 131 | + ( FsPermissionDenied |
| 132 | + + FsFileNotFound |
| 133 | + + r |
| 134 | + ) |
| 135 | +``` |
| 136 | + |
| 137 | +Let's go back to our original example, but instead of `ExceptT` we will |
| 138 | +substitute `ExceptV`: |
| 139 | + |
| 140 | +```purescript |
| 141 | +type ExceptV exc = ExceptT (Variant exc) |
| 142 | +``` |
| 143 | + |
| 144 | +```purescript |
| 145 | +get |
| 146 | + ∷ ∀ r m |
| 147 | + . MonadHttp m |
| 148 | + ⇒ String |
| 149 | + → ExceptV (HttpError + r) m String |
| 150 | +
|
| 151 | +write |
| 152 | + ∷ ∀ r m |
| 153 | + . MonadFs m |
| 154 | + ⇒ Path |
| 155 | + → String |
| 156 | + → ExceptV (FsError + r) m Unit |
| 157 | +``` |
| 158 | + |
| 159 | +When we go to combine them, _it just works_: |
| 160 | + |
| 161 | +```purescript |
| 162 | +getPureScript |
| 163 | + ∷ ∀ r m |
| 164 | + . MonadHttp m |
| 165 | + ⇒ MonadFs m |
| 166 | + ⇒ ExceptV (HttpError + FsError + r) m Unit |
| 167 | +getPureScript = |
| 168 | + get "http://purescript.org" >>= write "~/purescript.html" |
| 169 | +``` |
| 170 | + |
| 171 | +Additionally, these types are completely inferrable: |
| 172 | + |
| 173 | +``` |
| 174 | +Wildcard type definition has the inferred type |
| 175 | +
|
| 176 | + ( httpServerError :: String |
| 177 | + , httpNotFound :: Unit |
| 178 | + , httpOther :: { status :: Int |
| 179 | + , body :: String |
| 180 | + } |
| 181 | + , fsFileNotFound :: String |
| 182 | + , fsPermissionDenied :: Unit |
| 183 | + | t0 |
| 184 | + ) |
| 185 | +``` |
| 186 | + |
| 187 | +## Handling Errors |
| 188 | + |
| 189 | +This library exports the `handleError` function. Given a record of exception |
| 190 | +handlers, it will catch and route the corresponding exceptions, eliminating |
| 191 | +them from the type. |
| 192 | + |
| 193 | +```purescript |
| 194 | +getPureScript # handleError |
| 195 | + { httpServerError: \error -> log $ "Server error:" <> error |
| 196 | + , httpNotFound: \_ -> log "Not found" |
| 197 | + } |
| 198 | +``` |
| 199 | + |
| 200 | +``` |
| 201 | +Wildcard type definition has the inferred type |
| 202 | +
|
| 203 | + ( fsFileNotFound :: String |
| 204 | + , fsPermissionDenied :: Unit |
| 205 | + , httpOther :: { status :: Int |
| 206 | + , body :: String |
| 207 | + } |
| 208 | + | t0 |
| 209 | + ) |
| 210 | +``` |
| 211 | + |
| 212 | +This lets us prove that _all_ exceptions have been handled, which means |
| 213 | +we can safely remove the `ExceptV` wrapper using the `safe` combinator. |
| 214 | + |
| 215 | +```purescript |
| 216 | +getPureScriptSafe |
| 217 | + :: forall m |
| 218 | + . MonadHttp m |
| 219 | + => MonadFs m |
| 220 | + => MonadLog m |
| 221 | + -> m Unit |
| 222 | +getPureScriptSafe = |
| 223 | + safe $ getPureScript # handleError |
| 224 | + { httpServerError: ... |
| 225 | + , httpNotFound: ... |
| 226 | + , httpOther: ... |
| 227 | + , fsFileNotFound: ... |
| 228 | + , fsPermissionDenied ... |
| 229 | + } |
| 230 | +``` |
0 commit comments