Skip to content

Commit 146a282

Browse files
committed
Initial commit
0 parents  commit 146a282

File tree

7 files changed

+362
-0
lines changed

7 files changed

+362
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/bower_components/
2+
/node_modules/
3+
/.pulp-cache/
4+
/output/
5+
/.psc*
6+
/.psa*
7+
/.purs*
8+
/.vscode
9+
/package-lock.json
10+
/examples/*/app.js

.travis.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
language: node_js
2+
dist: trusty
3+
sudo: required
4+
node_js: stable
5+
install:
6+
- npm install
7+
- npm install -g bower
8+
- bower install --production
9+
script:
10+
- npm run -s build
11+
after_success:
12+
- >-
13+
test $TRAVIS_TAG &&
14+
echo $GITHUB_TOKEN | pulp login &&
15+
echo y | pulp publish --no-push ||
16+
git status

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Nathan Faubion
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# purescript-checked-exceptions
2+
3+
Extensible checked exceptions using polymorphic variants
4+
5+
[![Latest release](http://img.shields.io/github/release/natefaubion/purescript-checked-exceptions.svg)](https://github.com/natefaubion/purescript-checked-exceptions/releases)
6+
[![Build status](https://travis-ci.org/natefaubion/purescript-checked-exceptions.svg?branch=master)](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+
```

bower.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "purescript-checked-exceptions",
3+
"authors": [
4+
"Nathan Faubion <[email protected]>"
5+
],
6+
"license": "MIT",
7+
"repository": {
8+
"type": "git",
9+
"url": "git://github.com/natefaubion/purescript-checked-exceptions.git"
10+
},
11+
"ignore": [
12+
"**/.*",
13+
"node_modules",
14+
"bower_components",
15+
"output"
16+
],
17+
"dependencies": {
18+
"purescript-prelude": "^3.1.1",
19+
"purescript-transformers": "^3.5.0",
20+
"purescript-variant": "^4.1.0"
21+
}
22+
}

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"private": true,
3+
"scripts": {
4+
"build": "pulp build -- --strict --censor-lib"
5+
},
6+
"devDependencies": {
7+
"pulp": "^12.0.0",
8+
"purescript": "^0.11.7",
9+
"purescript-psa": "^0.5.0"
10+
}
11+
}

src/Control/Monad/Except/Checked.purs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
-- | ## Extensible checked exceptions with polymorphic variants
2+
-- |
3+
-- | This module provides helpers for using `Variant` with `ExceptT`. When
4+
-- | combined, we get a mechanism for extensible, checked exceptions. That
5+
-- | is, exceptions can be defined and used anywhere, and handled as needed.
6+
-- | Handling an exception eliminates it from the type, giving us proof
7+
-- | that it no longer occurs.
8+
module Control.Monad.Except.Checked
9+
( ExceptV
10+
, handleError
11+
, safe
12+
) where
13+
14+
import Prelude
15+
16+
import Control.Monad.Except (ExceptT, lift, throwError)
17+
import Data.Either (either, fromRight)
18+
import Data.Newtype (unwrap)
19+
import Data.Variant (class VariantMatchCases, Variant, onMatch)
20+
import Partial.Unsafe (unsafePartial)
21+
import Type.Row (class RowToList)
22+
23+
type ExceptV exc = ExceptT (Variant exc)
24+
25+
-- | Catches and eliminates exceptions given a record of handlers.
26+
-- | Unhandled exceptions are re-propragated. Record fields map
27+
-- | to the label for the exception being handled.
28+
-- |
29+
-- | An example for handling HTTP exceptions might be:
30+
-- | ```purescript
31+
-- | request # handleError
32+
-- | { httpNotFound: \_ -> ...
33+
-- | , httpServerError: \error -> ...
34+
-- | }
35+
-- | ```
36+
handleError
37+
m handlers excHandled excIn excOut rl a
38+
. RowToList handlers rl
39+
VariantMatchCases rl excHandled (ExceptV excOut m a)
40+
Union excHandled excOut excIn
41+
Monad m
42+
{ | handlers }
43+
ExceptV excIn m a
44+
ExceptV excOut m a
45+
handleError cases = unwrap >>> lift >=> either (onMatch cases throwError) pure
46+
47+
-- | Safely removes the `ExceptT` layer when all exceptions have been handled.
48+
safe
49+
m a
50+
. Functor m
51+
ExceptV () m a
52+
m a
53+
safe = unsafePartial $ unwrap >>> map fromRight

0 commit comments

Comments
 (0)