├── .npmignore ├── .gitignore ├── assets ├── banner.png └── banner_2.png ├── package.json ├── tsconfig.json ├── tests ├── fpts.test.ts └── index.test.ts ├── src └── index.ts ├── yarn.lock ├── LICENSE └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | assets 2 | tsconfig.tsbuildinfo 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tsconfig.tsbuildinfo 3 | *.js 4 | *.d.ts -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakenickels/let-anything/HEAD/assets/banner.png -------------------------------------------------------------------------------- /assets/banner_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fakenickels/let-anything/HEAD/assets/banner_2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fakenickels/let-anything", 3 | "version": "1.1.1", 4 | "main": "src/index.js", 5 | "license": "MIT", 6 | "typings": "src/index.d.ts", 7 | "devDependencies": { 8 | "fp-ts": "^2.8.2", 9 | "typescript": "^4.0.3" 10 | }, 11 | "scripts": { 12 | "build": "tsc --declaration" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "lib": ["ES2018", "DOM"], 7 | "rootDirs": ["./src", "./tests"], 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/fpts.test.ts: -------------------------------------------------------------------------------- 1 | import {either} from 'fp-ts' 2 | import {letAnything} from '../src/index' 3 | 4 | const letEither = letAnything>({ 5 | let_: (value, continuation) => either.chain(continuation)(value) 6 | }); 7 | 8 | const stuff = letEither(function*() { 9 | const value = yield either.right("d"); 10 | const anotherValue = yield either.right("e"); 11 | const anotherAnother = yield either.right("bug"); 12 | 13 | return either.right(value + anotherValue + anotherAnother); 14 | }) 15 | 16 | 17 | console.log( 18 | either.getOrElse(error => `Something went wrong: ${error}`)(stuff) 19 | ) 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | type Config = { 2 | let_: (value: T, continuation: ((value: T) => T)) => T, 3 | } 4 | 5 | interface LetAnything { 6 | (gen: () => Generator): T; 7 | (gen: () => Generator): T; 8 | } 9 | 10 | export function letAnything(config: Config): LetAnything { 11 | return (gen: () => Generator) => { 12 | const context = gen() 13 | 14 | const compose: (step: IteratorResult) => T = (step) => step.done 15 | ? step.value 16 | : config.let_(step.value, value => compose(context.next(value))) 17 | 18 | return compose(context.next()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | fp-ts@^2.8.2: 6 | version "2.8.2" 7 | resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.8.2.tgz#7948dea1ceef32e487d7f7f47bd2d3c4fcccfc0d" 8 | integrity sha512-YKLBW75Rp+L9DuY1jr7QO6mZLTmJjy7tOqSAMcB2q2kBomqLjBMyV7dotpcnZmUYY6khMsfgYWtPbUDOFcNmkA== 9 | 10 | typescript@^4.0.3: 11 | version "4.0.3" 12 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" 13 | integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gabriel Rubens Abreu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import {letAnything} from '../src/index' 2 | 3 | class Result { 4 | value: T 5 | constructor(value: T) { 6 | this.value = value; 7 | } 8 | 9 | flatMap(fn: (value: T) => Result) { 10 | return fn(this.value); 11 | } 12 | 13 | flatMapOk(fn: (value: T) => Result): Ok | Err { 14 | if (this instanceof (Ok as any)) { 15 | const newValue = fn(this.value); 16 | 17 | if (newValue instanceof Result) return newValue; 18 | else { 19 | throw new TypeError( 20 | "You should return a new Result from flatMap received instead the value above" 21 | ); 22 | } 23 | } else { 24 | return this; 25 | } 26 | } 27 | 28 | mapOkWithDefault(defaultValue: T, fn: ((value: T) => B )) { 29 | if (this instanceof (Ok as any)) { 30 | return fn(this.value); 31 | } else { 32 | return defaultValue; 33 | } 34 | } 35 | 36 | tap(fn: ((value: T) => void)) { 37 | fn(this.value); 38 | return this; 39 | } 40 | } 41 | 42 | class Ok extends Result {} 43 | class Err extends Result {} 44 | 45 | const ok = (value: T) => new Ok(value); 46 | const err = (value: T) => new Err(value); 47 | 48 | const letResultAsync = letAnything>({ 49 | let_: (value, continuation) => { return value.flatMapOk(continuation) } 50 | }); 51 | 52 | function* stuff() { 53 | const value = yield ok("d"); 54 | const anotherValue = yield ok("e"); 55 | const anotherAnother = yield err("bug"); 56 | 57 | return Promise.resolve(new Ok(value + anotherValue + anotherAnother)); 58 | } 59 | 60 | letResultAsync(stuff) 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 |

5 | 6 | It allows you to create monadic syntax for all sorts of monad-y values by just creating the proper context for them, heavily inspired by [OCaml's shiny new syntax](https://jobjo.github.io/2019/04/24/ocaml-has-some-new-shiny-syntax.html) and the name is inspired by [Jared's let-anything](https://github.com/jaredly/let-anything) 7 | 8 | 9 | # Installation 10 | ``` 11 | yarn add @fakenickels/let-anything 12 | ``` 13 | 14 | # Quick usage 15 | 16 |
17 | Show async/await demo 18 | 19 | [Run in CodeSandbox](https://codesandbox.io/s/modern-hill-z8lrc?file=/src/index.ts) 20 | 21 | ```js 22 | import { letAnything } from "@fakenickels/let-anything"; 23 | 24 | // define a context, in this case we are creating our own async-await! 25 | const letPromise = letAnything>({ 26 | let_: (value, continuation) => value.then(continuation) 27 | }); 28 | 29 | letPromise(function* () { 30 | const userName = yield Promise.resolve("Subaru-kun"); 31 | const deathCount = yield Promise.resolve(12909238409382); 32 | 33 | return Promise.resolve(`User ${userName} has a death count of ${deathCount}`); 34 | }).then(console.log).catch(console.log) 35 | // User Subaru-kun has a death count of 12909238409382 36 | ``` 37 | 38 |
39 | 40 |
41 | Show `fp-ts/either` demo 42 | 43 | [Run in CodeSandbox](https://codesandbox.io/s/wizardly-hopper-n1n1f?file=/src/index.ts) 44 | 45 | ```js 46 | import {either} from 'fp-ts' 47 | import {letAnything} from '@fakenickels/let-anything' 48 | 49 | // You could say I'm not a very good with TS types 50 | const letEither = letAnything>({ 51 | let_: (value, continuation) => either.chain(continuation)(value) 52 | }); 53 | 54 | function* stuff() { 55 | const value = yield either.right("d"); 56 | const anotherValue = yield either.right("e"); 57 | const anotherAnother = yield either.right("bug"); 58 | 59 | return either.right(value + anotherValue + anotherAnother); 60 | } 61 | 62 | 63 | console.log( 64 | either.getOrElse(error => `Something went wrong: ${error}`)(letEither(stuff)) 65 | ) 66 | // debug 67 | ``` 68 | 69 |
70 | 71 |
72 | Railway programming with `fp-ts` and promises 73 | 74 | [Run in CodeSandbox](https://codesandbox.io/s/exciting-cloud-d4141?file=/src/index.ts) 75 | 76 | ```js 77 | import {either} from 'fp-ts' 78 | import {letAnything} from '@fakenickels/let-anything' 79 | 80 | // You could say I'm not a very good with TS types, I'm more of a ReasonML guy so help would be appreciated! 81 | 82 | // Here we'll provide the context to combine both Either and Promises together 83 | const letEither = letAnything>({ 84 | let_: (value, continuation) => { 85 | return value.then(eitherValue => { 86 | return either.fold( 87 | error => Promise.resolve(either.left(error)), 88 | continuation, 89 | (eitherValue) 90 | }) 91 | } 92 | }); 93 | 94 | function* stuff() { 95 | const value = yield Promise.resolve(either.right("d")); 96 | const anotherValue = yield Promise.resolve(either.right("e")); 97 | const anotherAnother = yield Promise.resolve(either.right("bug")); 98 | 99 | return Promise.resolve(either.right(value + anotherValue + anotherAnother)); 100 | } 101 | 102 | letEither(stuff) 103 | .then(either.getOrElse(error => `Something went wrong: ${error}`)) 104 | .then(finalValue => { 105 | document.getElementById("app").innerHTML = finalValue 106 | }) 107 | ``` 108 | 109 |
110 | 111 | # Why? 112 | 113 | If you ever had to deal with error handling in JS with async/await you know how painful it can get. 114 | 115 | You can do it with try-catch 116 | 117 | ```js 118 | async function checkoutPurchase({productId, userId}) { 119 | try { 120 | const product = await Product.findOne(productId); 121 | const user = await User.findOne(userId) 122 | const card = await CreditCards.findOneByUserId(userId) 123 | 124 | await Charge.create({ 125 | customerId: user.customerId, 126 | source: card.source, 127 | price: product.price, 128 | }); 129 | 130 | return {success: true} 131 | } catch(e) { 132 | console.log(e.message) // not very helpul 133 | return {error: "Oopsie! Something went wrong and we have no clue about it!"} 134 | } 135 | } 136 | ``` 137 | 138 | A lot of things can go wrong there and try catch will just make it very hard to determine what. 139 | 140 | Some attempts to make that process better to handle have been offered by the community, like [eres](http://npmjs.com/eres) inspired by Go's way of handling with errors. 141 | 142 | ```js 143 | async function checkoutPurchase({productId, userId}) { 144 | const [productErr, product] = await eres(Product.findOne(productId)); 145 | 146 | if(productErr) { 147 | return {error: "Coulnd't find that product"} 148 | } 149 | 150 | const [userErr, user] = await eres(User.findOne(userId)); 151 | 152 | if(userErr) { 153 | return {error: "User not found"} 154 | } 155 | 156 | const [cardErr, card] = await eres(CreditCards.findOneByUserId(userId)); 157 | 158 | if(cardErr) { 159 | return {error: "User not found"} 160 | } 161 | 162 | const [chargeErr,] = await eres(Charge.create({ 163 | customerId: user.customerId, 164 | source: card.source, 165 | price: product.price, 166 | })); 167 | 168 | if(chargeErr) { 169 | return {error: "Failed to charge user"} 170 | } 171 | 172 | return {success: true} 173 | } 174 | ``` 175 | 176 | Well even though now we have a much more fine grained and correct error handling that's very awful and boilerplate-y and now our main logic is mixed with error handling making the code much harder to read. 177 | Looks like there is no way around that if we want to be good coders and handle our damn exceptions, or is it? *plays VSauce soung* 178 | Actually there is a much better way. And those FP smartass jerks (_just kidding peeps_) have been hiding it for themselves all along. 179 | 180 | ## A better way 181 | In the FP world there is a being called `result` and with it you can box values with two branches: the sucess branch and the error branch. 182 | 183 | ```js 184 | import {either} from 'fp-ts' 185 | 186 | // now this will have the type Promise> and we are not bound by the weird laws of Promise's .catch! 187 | async function findOne(id) { 188 | try { 189 | const value = await originalFindOne(id) 190 | 191 | return either.right(value) 192 | } catch(e) { 193 | return either.left({type: 'product/findOneErr', error: e.message}) 194 | } 195 | } 196 | // ... Charge.create will be changed to the same thing and for all the other repos ... 197 | ``` 198 | 199 | Now we can create a monadic context with `let-anything` and behold the power of FP with async/await easy to understand syntax! 200 | 201 | ```js 202 | import {either} from 'fp-ts' 203 | import {letAnything} from '@fakenickels/let-anything' 204 | 205 | // You could say I'm not a very good with TS types, I'm more of a ReasonML guy so help would be appreciated! 206 | 207 | // Here we'll provide the context to combine both Either and Promises together 208 | const letAsyncEither = letAnything>>({ 209 | let_: (value, continuation) => { 210 | return value.then(eitherValue => { 211 | return either.fold( 212 | error => Promise.resolve(either.left(error)), 213 | continuation, 214 | )(eitherValue) 215 | }) 216 | } 217 | }); 218 | 219 | function* checkoutPurchase({productId, userId}) { 220 | const product = yield Product.findOne(productId); 221 | const user = yield User.findOne(userId) 222 | const card = yield CreditCards.findOneByUserId(userId) 223 | 224 | yield Charge.create({ 225 | customerId: user.customerId, 226 | source: card.source, 227 | price: product.price, 228 | }); 229 | 230 | return Promise.resolve(either.right({success: true})) 231 | } 232 | 233 | letAsyncEither(checkoutPurchase) 234 | // we just got the error handling out of the way! 235 | .then(either.getOrElse(error => { 236 | switch(error.type) { 237 | // Just an idea, create your own abstraction for it. In ReasonML I do it with polymorphic variants. 238 | case 'user/findOneErr': return {error: 'Invalid user'} 239 | case 'product/findOneErr': return {error: 'Invalid product'} 240 | case 'card/findOneErr': return {error: 'Invalid card'} 241 | case 'charge/createErr': return {error: 'Failed to charge'} 242 | default: return {error: 'Something went terribly wrong'} 243 | } 244 | })) 245 | ``` 246 | 247 | Basically if you build your code around those following some juice mathmagical laws you get better error handling for free. 248 | You could even create an async/await now for something like [Fluture](https://github.com/fluture-js/Fluture) which are much better than JS promises. 249 | --------------------------------------------------------------------------------