├── .gitignore ├── README.md ├── bower.json ├── src └── Node │ └── Thunk.purs └── test └── Main.purs /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.Thunk 2 | 3 | `Node.Thunk` is a library for [PureScript][purescript] which helps to interface 4 | with asynchronous Node.js code: 5 | 6 | ## Wrapping Node.js async code with FFI and thunks 7 | 8 | This simple example shows how to use `Node.Thunk` to wrap a part of Node.js's 9 | `fs` module: 10 | 11 | import Node.Thunk 12 | 13 | foreign import fs "var fs = require('fs');" :: { 14 | readFile :: ThunkFn1 String String 15 | } 16 | 17 | readFile = runThunkFn1 fs.readFile 18 | 19 | The type `ThunkFn1 a b` means that a function takes a single argument of type 20 | `a` and produces a result of type `b`. To convert `ThunkFn1 a b` into a 21 | PureScript function `a -> Thunk b` we need to apply `runThunkFn1` function. 22 | 23 | Similarly to `ThunkFn1` there exists `ThunkFn2`, `ThunkFn3` for each arity up to 24 | 5. 25 | 26 | ## Computation with thunks 27 | 28 | This simple examples shows how to combine computations with thunks: 29 | 30 | readFileAndWait = do 31 | contents <- readFile "./README.md" 32 | delay 1000 33 | return contents 34 | 35 | `delay` is a computation which results into `Unit` value (nothing) after some 36 | milliseconds elapsed. 37 | 38 | To execute computation you should use `runThunk` function: 39 | 40 | main = runThunk readFileAndWait handle 41 | where 42 | handle (Left err) = -- handle error 43 | handle (Right result) = -- handle result 44 | 45 | `runThunk` function thunk as first argument and handler as second argument. 46 | Handler is supplied with either an error or a result. 47 | 48 | ## Executing `Eff` actions inside thunk computations 49 | 50 | There's a function `liftEff` which can lift `Eff` action into `Thunk`: 51 | 52 | readFileAndPrintContents = do 53 | contents <- readFile "./README.md" 54 | liftEff (print contents) 55 | 56 | 57 | [purescript]: http://purescript.org 58 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-node-thunk", 3 | "version": "0.1.0", 4 | "authors": [ 5 | "Andrey Popp <8mayday@gmail.com>" 6 | ], 7 | "description": "Node callbacks as thunks", 8 | "keywords": [ 9 | "purescript", 10 | "node", 11 | "thunk", 12 | "async" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "purescript-exceptions": "~0.2.2", 24 | "purescript-either": "~0.1.2" 25 | }, 26 | "homepage": "https://github.com/andreypopp/purescript-node-thunk" 27 | } 28 | -------------------------------------------------------------------------------- /src/Node/Thunk.purs: -------------------------------------------------------------------------------- 1 | module Node.Thunk where 2 | 3 | import Control.Monad.Eff 4 | import Control.Monad.Eff.Exception 5 | import Data.Either 6 | import Data.Function 7 | 8 | -- 9 | -- Thunk is represented as 10 | -- function(cb) { 11 | -- ... 12 | -- } 13 | -- where cb is expected to be function(err, result) { ... } 14 | -- 15 | foreign import data Thunk :: * -> * 16 | 17 | foreign import resolve 18 | """ 19 | function resolve(a) { 20 | return function(cb) { cb(null, a); }; 21 | } 22 | """ 23 | :: forall a. a -> Thunk a 24 | 25 | foreign import reject 26 | """ 27 | function reject(err) { 28 | return function(cb) { cb(err); }; 29 | } 30 | """ 31 | :: forall a. Error -> Thunk a 32 | 33 | foreign import _runThunk 34 | """ 35 | function _runThunk(left, right, thunk, handler) { 36 | return function() { 37 | return thunk(function(err, result) { 38 | if (err) { 39 | handler(left(err))(); 40 | } else { 41 | handler(right(result))(); 42 | } 43 | }); 44 | }; 45 | } 46 | """ 47 | :: forall a b eff eff2. Fn4 48 | (Error -> Either Error a) 49 | (a -> Either Error a) 50 | (Thunk a) 51 | (Either Error a -> Eff (eff) b) 52 | (Eff (eff2) Unit) 53 | 54 | runThunk = runFn4 _runThunk Left Right 55 | 56 | foreign import fmap 57 | """ 58 | function fmap(f) { 59 | return function(a) { 60 | return function(cb) { 61 | a(function(err, result) { 62 | if (err) return cb(err); 63 | try { 64 | result = f(result); 65 | } catch (err) { 66 | return cb(err); 67 | } 68 | cb(null, result); 69 | }); 70 | }; 71 | }; 72 | } 73 | """ 74 | :: forall a b. (a -> b) -> Thunk a -> Thunk b 75 | 76 | foreign import app 77 | """ 78 | function app(f) { 79 | return function(a) { 80 | return function(cb) { 81 | var latch = 2; 82 | var fVal, aVal; 83 | 84 | f(function(err, f) { 85 | if (err && latch !== 0) { 86 | latch = 0; 87 | return cb(err); 88 | } 89 | latch = latch - 1; 90 | fVal = f; 91 | if (latch === 0) { 92 | try { 93 | aVal = fVal(aVal); 94 | } catch(err) { 95 | return cb(err); 96 | } 97 | cb(null, aVal); 98 | } 99 | }); 100 | 101 | a(function(err, a) { 102 | if (err && latch !== 0) { 103 | latch = 0; 104 | return cb(err); 105 | } 106 | latch = latch - 1; 107 | aVal = a; 108 | if (latch === 0) { 109 | try { 110 | aVal = fVal(aVal); 111 | } catch(err) { 112 | return cb(err); 113 | } 114 | cb(null, aVal); 115 | } 116 | }); 117 | }; 118 | }; 119 | } 120 | """ 121 | :: forall a b. Thunk (a -> b) -> Thunk a -> Thunk b 122 | 123 | foreign import bind 124 | """ 125 | function bind(a) { 126 | return function(f) { 127 | return function(cb) { 128 | a(function(err, a) { 129 | if(err) return cb(err); 130 | try { 131 | f = f(a); 132 | } catch(err) { 133 | return cb(err); 134 | } 135 | f(cb); 136 | }); 137 | } 138 | } 139 | } 140 | """ 141 | :: forall a b. Thunk a -> (a -> Thunk b) -> Thunk b 142 | 143 | foreign import delay 144 | """ 145 | function delay(ms) { 146 | return function(cb) { 147 | setTimeout(function() { 148 | cb(null); 149 | }, ms); 150 | }; 151 | } 152 | """ 153 | :: Number -> Thunk Unit 154 | 155 | foreign import liftEff 156 | """ 157 | function liftEff(action) { 158 | return function(cb) { 159 | try { 160 | cb(null, action()); 161 | } catch(err) { 162 | cb(err); 163 | } 164 | } 165 | } 166 | """ 167 | :: forall a eff. Eff (eff) a -> Thunk a 168 | 169 | liftEither :: forall a. Either Error a -> Thunk a 170 | liftEither (Left err) = reject err 171 | liftEither (Right result) = resolve result 172 | 173 | instance thunkFunctor :: Functor Thunk where 174 | (<$>) = fmap 175 | 176 | instance thunkApply :: Apply Thunk where 177 | (<*>) = app 178 | 179 | instance thunkApplication :: Applicative Thunk where 180 | pure = resolve 181 | 182 | instance thunkBind :: Bind Thunk where 183 | (>>=) = bind 184 | 185 | instance thunkMonad :: Monad Thunk 186 | 187 | foreign import data ThunkFn1 :: * -> * -> * 188 | foreign import data ThunkFn2 :: * -> * -> * -> * 189 | foreign import data ThunkFn3 :: * -> * -> * -> * -> * 190 | foreign import data ThunkFn4 :: * -> * -> * -> * -> * -> * 191 | foreign import data ThunkFn5 :: * -> * -> * -> * -> * -> * -> * 192 | 193 | foreign import runThunkFn1 194 | """ 195 | function runThunkFn1(f) { 196 | return function(a) { 197 | return function(cb) { return f(a, cb); }; 198 | }; 199 | } 200 | """ 201 | :: forall a r. ThunkFn1 a r -> (a -> Thunk r) 202 | 203 | foreign import runThunkFn2 204 | """ 205 | function runThunkFn2(f) { 206 | return function(a) { 207 | return function(b) { 208 | return function(cb) { return f(a, b, cb); }; 209 | }; 210 | }; 211 | } 212 | """ 213 | :: forall a b r. ThunkFn2 a b r -> (a -> b -> Thunk r) 214 | 215 | foreign import runThunkFn3 216 | """ 217 | function runThunkFn3(f) { 218 | return function(a) { 219 | return function(b) { 220 | return function(c) { 221 | return function(cb) { 222 | return f(a, b, c, cb); 223 | }; 224 | }; 225 | }; 226 | }; 227 | } 228 | """ 229 | :: forall a b c r. ThunkFn3 a b c r -> (a -> b -> c -> Thunk r) 230 | 231 | foreign import runThunkFn4 232 | """ 233 | function runThunkFn4(f) { 234 | return function(a) { 235 | return function(b) { 236 | return function(c) { 237 | return function(d) { 238 | return function(cb) { 239 | return f(a, b, c, d, cb); 240 | }; 241 | }; 242 | }; 243 | }; 244 | }; 245 | } 246 | """ 247 | :: forall a b c d r. ThunkFn4 a b c d r -> (a -> b -> c -> d -> Thunk r) 248 | 249 | foreign import runThunkFn5 250 | """ 251 | function runThunkFn5(f) { 252 | return function(a) { 253 | return function(b) { 254 | return function(c) { 255 | return function(d) { 256 | return function(e) { 257 | return function(cb) { 258 | return f(a, b, c, d, e, cb); 259 | }; 260 | }; 261 | }; 262 | }; 263 | }; 264 | }; 265 | } 266 | """ 267 | :: forall a b c d e r. ThunkFn5 a b c d e r -> (a -> b -> c -> d -> e -> Thunk r) 268 | 269 | pair :: forall a b m. (Applicative m) => m a -> m b -> m {first :: a, second :: b} 270 | pair a b = 271 | pure collect <*> a <*> b 272 | where 273 | collect first second = {first: first, second: second} 274 | 275 | foreign import raise 276 | """ function raise(err) { return function() { throw err; } }""" 277 | :: forall eff. Error -> Eff (eff) Unit 278 | 279 | unsafeRunThunk thunk = runThunk thunk handle 280 | where 281 | handle (Left err) = raise err 282 | handle (Right result) = return unit 283 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Control.Monad.Eff 4 | import Control.Monad.Eff.Exception 5 | import Data.Either 6 | import Debug.Trace 7 | import Node.Thunk 8 | 9 | main = do 10 | runThunk (slowTwice 42) print 11 | runThunk (explode "let's cause an error...") print 12 | 13 | slowTwice n = do 14 | nn <- twice n 15 | delay 1000 16 | return nn 17 | 18 | foreign import someApi """ 19 | var someApi = { 20 | twice: function(n, callback) { 21 | callback(null, 2*n); 22 | }, 23 | 24 | explode: function(str, callback) { 25 | callback('Oops, something exploded: ' + str); 26 | } 27 | }; 28 | """ 29 | :: { twice :: ThunkFn1 Number Number 30 | , explode :: ThunkFn1 String Unit 31 | } 32 | 33 | twice = runThunkFn1 someApi.twice 34 | explode = runThunkFn1 someApi.explode 35 | --------------------------------------------------------------------------------