├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── domain │ └── index.ts ├── tests │ ├── free.test.ts │ └── tagless.test.ts ├── utils │ ├── atLeastOne.ts │ ├── db.ts │ └── throw.ts └── workshop │ ├── free-monads │ ├── README.md │ ├── api.ts │ ├── examples.ts │ ├── interpreters.ts │ └── run.ts │ └── tagless-final │ ├── README.md │ ├── api.ts │ ├── examples.ts │ ├── interpreters.ts │ └── run.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | # yarn v2 111 | 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .pnp.* 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building eDSLs in functional TypeScript 2 | 3 | Business logic could be expressed in a limited subset of host language, leading to correct by construction, robust, optimisable code. This process is known as building eDSL – embedded domain-specific languages – and interpreting them, and is a widely used practice in functional languages like Haskell, Scala, OCaml. Still, this topic is terra incognita for many JS/TS developers. 4 | 5 | During this workshop I will give an overview of two ways of building eDSLs in functional TypeScript using [fp-ts](https://github.com/gcanti/fp-ts) library: 6 | 1. Free Monads 7 | 2. Tagless Final 8 | 9 | ## Goals of this workshop 10 | 11 | 1. Introduce you to a concept of “effect abstraction”, allowing dynamic replacement of effects depending on the requirements. 12 | 2. Give you two new instruments for separating the business logic from a concrete effect — Free monads and Tagless Final style. 13 | 3. Provide you with a hands-on experience of using eDSLs as a pattern. 14 | 15 | More specifically, you'll write a set of functions for out business domain — blogging platform, — use them to express a few simple programs and finally write an interpreter, which will do the actual execution of the code. 16 | 17 | ## Business domain 18 | 19 | You're writing a REST API for a blogging platform, in which you have two entities: a user and a post. 20 | 21 | A user is described by this interface: 22 | 23 | ```ts 24 | interface User { 25 | readonly name: string; 26 | readonly email: string; 27 | } 28 | ``` 29 | 30 | And the blog post is defined as this: 31 | 32 | ```ts 33 | interface Post { 34 | readonly title: string; 35 | readonly body: string; 36 | readonly tags: string[]; 37 | readonly author: User; 38 | } 39 | ``` 40 | 41 | These two entities are stored in the relational database — PostgreSQL, MySQL, MSSQL, you name it — and are cached in some kind of key-value storage — Redis, KeyDB, memcached, etc. Your goal is to represent the commonly-used operations over those storages and network as a high-level composable API. 42 | 43 | ### Requirements 44 | 45 | - A notebook with code editor OR browser with CodeSandbox. 46 | - Working Node.js 10+ environment. 47 | - Downloaded workshop template (this repository). 48 | - Understanding basic concepts of functional programming: immutability, totality, purity, function composition, least power principle, etc. 49 | - Understanding what a monad and a functor are. 50 | 51 | If you want to prepare for this workshop better, I highly recommend reading these articles: 52 | 1. An overview of FP terminology with `fp-ts`: https://medium.com/@steve.hartken/typescript-and-fp-ts-terminology-da6ea5d30bdc 53 | 2. How higher-kinded types work in `fp-ts`: https://dev.to/urgent/fp-ts-hkt-and-higher-kinded-types-in-depth-1ila 54 | 3. An example of how to use Do-notation: https://gcanti.github.io/fp-ts-contrib/modules/Do.ts.html 55 | 56 | ## How to use this repository 57 | 58 | 1. Clone it to your local computer. 59 | 2. Install all the dependencies using `npm ci`. 60 | 3. Open the repository in editor of your choice and follow along with the explanations. Corresponding video is published here: https://www.youtube.com/watch?v=hTnxaB52awA. 61 | 4. If you stuck, feel free to use one of recovery points (see below) to catch-up. 62 | 5. Occasionally run tests (`npm test`) to see if you implemented the logic correctly. 63 | 64 | Recovery points are branches with implemented crucial for understaning the material checkpoints. Their names are: 65 | - 01-free-api 66 | - 02-free-example 67 | - 03-free-interpreters 68 | - 04-tagless-api 69 | - 05-tagless-examples 70 | - 06-tagless-interpreters 71 | 72 | ## Further reading 73 | 74 | When it comes to functional programming in TypeScript, there's not many resources I can recommend with confidence, but these two are really good: 75 | 1. Articles from `fp-ts` creator Giulio Canti on dev.to: https://dev.to/gcanti 76 | 2. Awesome cource by Brian Lonsdorf: https://github.com/MostlyAdequate/mostly-adequate-guide 77 | 78 | Also if you want to read in more details about Free monads and Tagless Final, I recommend reading these articles: 79 | 1. Typed Tagless Final Interpreters by Oleg Kiselyov: http://okmij.org/ftp/tagless-final/course/lecture.pdf 80 | 2. Free monads for cheap interpreters: https://www.tweag.io/posts/2018-02-05-free-monads.html 81 | 82 | And if you want to dive deeper in interpretation and optimization, I recommend reading more about Free applicatives: https://arxiv.org/pdf/1403.0749.pdf 83 | 84 | ## Contacts 85 | 86 | Created by [Yuriy Bogomolov](mailto:yuriy.bogomolov@gmail.com). 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workshop-edsl", 3 | "version": "1.0.0", 4 | "description": "Code template for \"Building eDSLs in functional TypeScript\" workshop", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "run:fm": "ts-node ./src/workshop/free-monads/run.ts", 9 | "run:tf": "ts-node ./src/workshop/tagless-final/run.ts" 10 | }, 11 | "keywords": [ 12 | "edsl", 13 | "fp", 14 | "ts", 15 | "fp-ts", 16 | "typescript", 17 | "workshop" 18 | ], 19 | "author": "Yuriy Bogomolov ", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@types/jest": "^25.2.1", 23 | "@types/node": "^13.13.2", 24 | "ts-node": "^8.9.0", 25 | "tslint": "^6.1.1", 26 | "typescript": "^3.8.3", 27 | "jest": "^25.5.4", 28 | "ts-jest": "^25.4.0" 29 | }, 30 | "dependencies": { 31 | "fp-ts": "^2.5.3", 32 | "fp-ts-contrib": "^0.1.15" 33 | }, 34 | "jest": { 35 | "testEnvironment": "jsdom", 36 | "preset": "ts-jest" 37 | } 38 | } -------------------------------------------------------------------------------- /src/domain/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file represents our domain model - blogging platform with users and posts. 3 | */ 4 | 5 | import { AtLeastOne } from '../utils/atLeastOne'; 6 | 7 | /** 8 | * User of our system – just a simple collection of fields 9 | */ 10 | export interface User { 11 | readonly id: number; 12 | readonly name: string; 13 | readonly email: string; 14 | } 15 | 16 | /** 17 | * Blog post in our domain, authored by some user 18 | */ 19 | export interface Post { 20 | readonly title: string; 21 | readonly body: string; 22 | readonly tags: string[]; 23 | readonly author: User; 24 | } 25 | 26 | /** 27 | * A representation of a post in the database. Contains an `id` obtained from the DB. 28 | */ 29 | export type DBPost = Post & { 30 | readonly id: number; 31 | }; 32 | 33 | /** 34 | * A convenience alias for a post update model. 35 | * 36 | * Contains a required field – `author` and at least one other field – `title`, `body` or `tags` – as required 37 | * 38 | * @example 39 | * const update1: PostUpdate = { author: john }; // ❌ ts(6133) 40 | * const update2: PostUpdate = { author: john, title: 'New post' } // ✅ 41 | * const update3: PostUpdate = { author: john, body: 'A new post!' } // ✅ 42 | * const update4: PostUpdate = { author: john, tags: ['funny'] } } // ✅ 43 | */ 44 | export type PostUpdate = AtLeastOne>> & Pick; 45 | 46 | // An example data for testing purposes 47 | 48 | export const john: User = { id: 1, name: 'John Smith', email: 'john.smith@example.com' }; 49 | 50 | export const coolPost: DBPost = { id: 1, title: 'Cool', body: 'Post', tags: [], author: john }; 51 | -------------------------------------------------------------------------------- /src/tests/free.test.ts: -------------------------------------------------------------------------------- 1 | import { foldFree } from 'fp-ts-contrib/lib/Free'; 2 | import * as O from 'fp-ts/lib/Option'; 3 | import { pipe } from 'fp-ts/lib/pipeable'; 4 | import { execState, State, state } from 'fp-ts/lib/State'; 5 | 6 | import { coolPost, john } from '../domain'; 7 | import { DB, getNextId } from '../utils/db'; 8 | import { ProgramF } from '../workshop/free-monads/api'; 9 | import { exampleProgram1, exampleProgram2, exampleProgram3 } from '../workshop/free-monads/examples'; 10 | 11 | interface TestState { 12 | readonly db: DB; 13 | readonly cache: Map; 14 | } 15 | 16 | type Interpreter = (program: ProgramF) => State; 17 | 18 | const stateInterpreter: Interpreter = (program) => { 19 | return ({ db, cache }) => { 20 | switch (program.tag) { 21 | case 'KVGet': return [program.next(O.fromNullable(cache.get(program.key))), { db, cache }]; 22 | case 'KVPut': return [program.next(true), { db, cache: cache.set(program.key, program.value) }]; 23 | case 'KVDelete': return [program.next(cache.delete(program.key)), { db, cache }]; 24 | 25 | case 'DBGetPosts': return [program.next(Object.values(db[program.userId])), { db, cache }]; 26 | case 'DBCreatePost': { 27 | const id = getNextId(db, program.post.author.id); 28 | const newPost = { ...program.post, id, }; 29 | db[program.post.author.id] = db[program.post.author.id] || {}; 30 | db[program.post.author.id][id] = newPost; 31 | 32 | return [program.next(newPost), { db, cache }]; 33 | } 34 | case 'DBUpdatePost': return pipe( 35 | O.fromNullable(db[program.update.author.id][program.postId]), 36 | O.fold( 37 | () => [program.next(O.none), { db, cache }], 38 | (existingPost) => { 39 | const updatedPost = { ...existingPost, ...program.update }; 40 | db[program.update.author.id][program.postId] = updatedPost; 41 | 42 | return [program.next(O.some(updatedPost)), { db, cache }]; 43 | }, 44 | ), 45 | ); 46 | 47 | case 'NetSend': return [program.next(), { db, cache }]; 48 | } 49 | }; 50 | }; 51 | 52 | describe('Free monads', () => { 53 | it('Example 1: Get a list of user posts, cache and send them for a review', () => { 54 | const initialState: TestState = { 55 | db: { 56 | [john.id]: { 57 | [coolPost.id]: coolPost, 58 | }, 59 | }, 60 | cache: new Map(), 61 | }; 62 | 63 | const nextState = execState( 64 | foldFree(state)(stateInterpreter, exampleProgram1(john.id)), 65 | initialState, 66 | ); 67 | 68 | const cachedPost = nextState.cache.get(`${john.id}`); 69 | expect(cachedPost).toBeDefined(); 70 | expect(cachedPost).toStrictEqual(JSON.stringify([coolPost])); 71 | }); 72 | 73 | it('Example 1: should not make a trip to DB when posts are cached', () => { 74 | const initialState: TestState = { 75 | db: {}, 76 | cache: new Map([[`${john.id}`, JSON.stringify([coolPost])]]), 77 | }; 78 | const mockInterpreter = jest.fn(stateInterpreter); 79 | 80 | const nextState = execState( 81 | foldFree(state)(mockInterpreter as Interpreter, exampleProgram1(john.id)), 82 | initialState, 83 | ); 84 | 85 | expect(mockInterpreter).toBeCalledTimes(2); 86 | expect(nextState).toStrictEqual(initialState); 87 | }); 88 | 89 | it(`Example 2: Create a post and send top-3 to author's email`, () => { 90 | const initialState: TestState = { 91 | db: {}, 92 | cache: new Map(), 93 | }; 94 | 95 | const nextState = execState( 96 | foldFree(state)(stateInterpreter, exampleProgram2(coolPost)), 97 | initialState, 98 | ); 99 | expect(nextState.db).toStrictEqual({ [john.id]: { [coolPost.id]: coolPost } }); 100 | }); 101 | 102 | it('Example 3: Update a post, invalidate cache if required and return the results', () => { 103 | const initialState: TestState = { 104 | db: { 105 | [john.id]: { 106 | [coolPost.id]: { ...coolPost, body: 'Some old and rusty text' }, 107 | }, 108 | }, 109 | cache: new Map([[`${john.id}`, JSON.stringify([coolPost])]]), 110 | }; 111 | 112 | const nextState = execState( 113 | foldFree(state)(stateInterpreter, exampleProgram3(coolPost.id, coolPost)), 114 | initialState, 115 | ); 116 | 117 | expect(nextState).toStrictEqual({ 118 | db: { 119 | [john.id]: { 120 | [coolPost.id]: coolPost, 121 | }, 122 | }, 123 | cache: new Map(), 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/tests/tagless.test.ts: -------------------------------------------------------------------------------- 1 | import * as O from 'fp-ts/lib/Option'; 2 | import { pipe } from 'fp-ts/lib/pipeable'; 3 | import { execState, State, state } from 'fp-ts/lib/State'; 4 | 5 | import { coolPost, john } from '../domain'; 6 | import { DB, getNextId } from '../utils/db'; 7 | import { Program } from '../workshop/tagless-final/api'; 8 | import { exampleProgram1, exampleProgram2, exampleProgram3 } from '../workshop/tagless-final/examples'; 9 | 10 | interface TestState { 11 | readonly db: DB; 12 | readonly cache: Map; 13 | } 14 | 15 | const URI = 'InterpreterState'; 16 | type URI = typeof URI; 17 | type InterpreterState = State; 18 | 19 | declare module 'fp-ts/lib/HKT' { 20 | interface URItoKind { 21 | InterpreterState: InterpreterState; 22 | } 23 | } 24 | 25 | const stateInterpreter: Program = { 26 | ...state, 27 | URI, 28 | kvGet: (key) => ({ db, cache }) => [O.fromNullable(cache.get(key)), { db, cache }], 29 | kvPut: (key, value) => ({ db, cache }) => [cache.set(key, value) != null, { db, cache }], 30 | kvDelete: (key) => ({ db, cache }) => [cache.delete(key), { db, cache }], 31 | 32 | getPosts: (userId) => ({ db, cache }) => [Object.values(db[userId]), { db, cache }], 33 | createPost: (post) => ({ db, cache }) => { 34 | const id = getNextId(db, post.author.id); 35 | const newPost = { ...post, id }; 36 | db[post.author.id] = db[post.author.id] || {}; 37 | db[post.author.id][id] = newPost; 38 | return [newPost, { db, cache }]; 39 | }, 40 | updatePost: (postId, update) => ({ db, cache }) => [pipe( 41 | O.fromNullable(db[update.author.id][postId]), 42 | O.map( 43 | existingPost => { 44 | const updatedPost = { ...existingPost, ...update }; 45 | db[update.author.id][postId] = updatedPost; 46 | return updatedPost; 47 | }, 48 | ), 49 | ), { db, cache }], 50 | 51 | netSend: (_payload, _address) => ({ db, cache }) => [undefined, { db, cache }], 52 | }; 53 | 54 | describe('Tagless Final', () => { 55 | it('Example 1: Get a list of user posts, cache and send them for a review', () => { 56 | const initialState: TestState = { 57 | db: { 58 | [john.id]: { 59 | [coolPost.id]: coolPost, 60 | }, 61 | }, 62 | cache: new Map(), 63 | }; 64 | 65 | const nextState = execState(exampleProgram1(stateInterpreter)(john.id), initialState); 66 | const cachedPost = nextState.cache.get(`${john.id}`); 67 | 68 | expect(cachedPost).toBeDefined(); 69 | expect(cachedPost).toStrictEqual(JSON.stringify([coolPost])); 70 | }); 71 | 72 | it('Example 1: should not make a trip to DB when posts are cached', () => { 73 | const initialState: TestState = { 74 | db: {}, 75 | cache: new Map([[`${john.id}`, JSON.stringify([coolPost])]]), 76 | }; 77 | const getPostsSpy = jest.spyOn(stateInterpreter, 'getPosts'); 78 | 79 | const nextState = execState(exampleProgram1(stateInterpreter)(john.id), initialState); 80 | 81 | expect(getPostsSpy).not.toBeCalled(); 82 | expect(nextState).toStrictEqual(initialState); 83 | }); 84 | 85 | it(`Example 2: Create a post and send top-3 to author's email`, () => { 86 | const initialState: TestState = { 87 | db: {}, 88 | cache: new Map(), 89 | }; 90 | 91 | const nextState = execState(exampleProgram2(stateInterpreter)(coolPost), initialState); 92 | 93 | expect(nextState.db).toStrictEqual({ [john.id]: { [coolPost.id]: coolPost } }); 94 | }); 95 | 96 | it('Example 3: Update a post, invalidate cache if required and return the results', () => { 97 | const initialState: TestState = { 98 | db: { 99 | [john.id]: { 100 | [coolPost.id]: { ...coolPost, body: 'Some old and rusty text' }, 101 | }, 102 | }, 103 | cache: new Map([[`${john.id}`, JSON.stringify([coolPost])]]), 104 | }; 105 | 106 | const nextState = execState(exampleProgram3(stateInterpreter)(coolPost.id, coolPost), initialState); 107 | 108 | expect(nextState).toStrictEqual({ 109 | db: { 110 | [john.id]: { 111 | [coolPost.id]: coolPost, 112 | }, 113 | }, 114 | cache: new Map(), 115 | }); 116 | }); 117 | }); -------------------------------------------------------------------------------- /src/utils/atLeastOne.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes a sum type out of given partial type, in which at least one field is required. 3 | */ 4 | export type AtLeastOne = Partial & { [K in Keys]: Required> }[Keys]; -------------------------------------------------------------------------------- /src/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { DBPost, User } from '../domain'; 2 | 3 | export type DB = Record>; 4 | 5 | export const getNextId = (db: DB, authorId: number): number => { 6 | const userPosts = db[authorId]; 7 | const id = Math.max(Math.max(...Object.keys(userPosts || {}).map(k => +k)) + 1, 1); 8 | return id; 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/throw.ts: -------------------------------------------------------------------------------- 1 | export const notImplemented = () => { 2 | throw new Error('Not defined, please check the implementation'); 3 | }; 4 | -------------------------------------------------------------------------------- /src/workshop/free-monads/README.md: -------------------------------------------------------------------------------- 1 | # Free monads 2 | 3 | > 'free' like in 'freedom' and not in 'free beer' 4 | 5 | First of all, what does "free" means? It means "free of computations", so a *[free object](https://en.wikipedia.org/wiki/Free_object)* in general is something that holds the meaning of its origin, merely describing it. In our example you can think that "free monad" is just something which mimic the structure of a monad, and could be *interpreted* into a real monad. 6 | 7 | ## Further reading 8 | 9 | If you want to learn more, I encourage you to read this great post about free monads in Haskell: https://www.tweag.io/posts/2018-02-05-free-monads.html -------------------------------------------------------------------------------- /src/workshop/free-monads/api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Here you'll write API for the eDSL – a set of operations which represent your core domain logic 3 | 4 | Our domain requires the following set of operations: 5 | 6 | * KV Store * 7 | 8 | kvGet : (key: string) => Option 9 | kvPut : (key: string, value: string) => boolean 10 | kvDelete : (key: string) => boolean 11 | 12 | * Database * 13 | 14 | getPosts : (userId: number) => DBPost[] 15 | createPost : (post: Post) => DBPost 16 | updatePost : (postId: number, update: PostUpdate) => Option 17 | 18 | * Network * 19 | 20 | netSend : (payload: T, email: string) => void 21 | */ 22 | 23 | import { Post, PostUpdate } from '../../domain'; 24 | import { notImplemented } from '../../utils/throw'; 25 | 26 | // * API 27 | 28 | /** 29 | * Tries to get user's posts from cache, returns either `none` in case of cache miss, 30 | * and `some` in case of hit. 31 | * 32 | * @param userId User ID for which we want to get cached posts 33 | */ 34 | export const cacheGetPosts = 35 | (userId: number) => notImplemented(); 36 | 37 | /** 38 | * Stores a list of posts of the given user (identified by `userId`) in the cache. 39 | * 40 | * @param userId User ID – a will be used as a key for cache 41 | * @param posts A list of posts to store in cache 42 | */ 43 | export const cacheStorePosts = 44 | (userId: number, posts: Post[]) => notImplemented(); 45 | 46 | /** 47 | * Clears cache for a given user ID. 48 | * 49 | * @param userId User ID 50 | */ 51 | export const cacheInvalidate = 52 | (userId: number) => notImplemented(); 53 | 54 | /** 55 | * Gets a list of posts belonging to the given user (identified by `userId`). 56 | * 57 | * @param userId User ID – author of posts 58 | */ 59 | export const dbGetPosts = 60 | (userId: number) => notImplemented(); 61 | 62 | /** 63 | * Stores post in the database. 64 | * 65 | * @param post Post data to store in the DB 66 | */ 67 | export const dbCreatePost = 68 | (post: Post) => notImplemented(); 69 | 70 | /** 71 | * Updated a post in the database, identified by its ID. 72 | * 73 | * @param postId Post ID 74 | * @param update A set of updated fields for this post ID 75 | */ 76 | export const dbUpdatePost = 77 | (postId: number, update: PostUpdate) => notImplemented(); 78 | 79 | /** 80 | * Sends a list of posts via network. 81 | * 82 | * @param posts A list of posts to send via network 83 | * @param to Email to send these `posts` to. 84 | */ 85 | export const netSendPosts = 86 | (posts: Post[], to: string) => notImplemented(); -------------------------------------------------------------------------------- /src/workshop/free-monads/examples.ts: -------------------------------------------------------------------------------- 1 | // * Write the following: 2 | 3 | import { Free } from 'fp-ts-contrib/lib/Free'; 4 | import { Option } from 'fp-ts/lib/Option'; 5 | 6 | import { Post, PostUpdate } from '../../domain'; 7 | import { notImplemented } from '../../utils/throw'; 8 | 9 | // 1. Get a list of user posts, cache and send them to 'review@example.com' for a review 10 | export const exampleProgram1 = 11 | (userId: number): Free<'???', void> => notImplemented(); 12 | 13 | // 2. Create a post and send top-3 to author's email 14 | export const exampleProgram2 = 15 | (newPost: Post): Free<'???', void> => notImplemented(); 16 | 17 | // 3. Update a post, invalidate the cache if required and return the updated post 18 | export const exampleProgram3 = 19 | (postId: number, update: PostUpdate): Free<'???', Option> => notImplemented(); -------------------------------------------------------------------------------- /src/workshop/free-monads/interpreters.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Here you'll write concrete interpreters for your eDSL, which will translate it to the specific monad 3 | */ -------------------------------------------------------------------------------- /src/workshop/free-monads/run.ts: -------------------------------------------------------------------------------- 1 | import { notImplemented } from '../../utils/throw'; 2 | 3 | (async () => { 4 | console.log('exampleProgram1: first run'); 5 | await notImplemented(); 6 | console.log('exampleProgram1: second run'); 7 | await notImplemented(); 8 | console.log('exampleProgram2'); 9 | await notImplemented(); 10 | console.log('exampleProgram3'); 11 | await notImplemented(); 12 | })(); -------------------------------------------------------------------------------- /src/workshop/tagless-final/README.md: -------------------------------------------------------------------------------- 1 | # Finally tagless, partially evaluated 2 | 3 | Tagless Final should be perceived as a functional design pattern. In its core lies an idea of abstracting a concrete effect away from the computation, and delay interpretation into a concrete effect as much as possible. 4 | 5 | ## Tagged vs. tagless 6 | 7 | We call a *tagged* encoding that which required construction of an object in order to represent the computations. In this sense free monads should be considered as a *tagged* encoding. 8 | 9 | We call a *tagless* encoding that which does not require creation of a new object while representing computations. 10 | 11 | ## Initial vs. final 12 | 13 | Strictly speaking, *initial* encoding means that an entity is defined by the way it's *constructed*, and *final* encoding means that an entity is defined by the way it's *consumed* (eliminated, viewed, computed). 14 | 15 | ## Further reading 16 | 17 | To read the original papers about Typed Tagless Final interpreters, please visit Oleg Kiselyov's website: http://okmij.org/ftp/tagless-final/index.html -------------------------------------------------------------------------------- /src/workshop/tagless-final/api.ts: -------------------------------------------------------------------------------- 1 | import { Post, PostUpdate } from '../../domain'; 2 | import { notImplemented } from '../../utils/throw'; 3 | 4 | /* 5 | Here you'll write API for the eDSL – a set of operations which represent your core domain logic 6 | 7 | Our domain requires the following set of operations: 8 | 9 | * KV Store * 10 | 11 | kvGet : (key: string) => Option 12 | kvPut : (key: string, value: string) => boolean 13 | kvDelete : (key: string) => boolean 14 | 15 | * Database * 16 | 17 | getPosts : (userId: number) => DBPost[] 18 | createPost : (post: Post) => DBPost 19 | updatePost : (postId: number, update: PostUpdate) => Option 20 | 21 | * Network * 22 | 23 | netSend : (payload: T, email: string) => void 24 | */ 25 | 26 | // * API 27 | 28 | /** 29 | * Tries to get user's posts from cache, returns either `none` in case of cache miss, 30 | * and `some` in case of hit. 31 | * 32 | * @param userId User ID for which we want to get cached posts 33 | */ 34 | export const cacheGetPosts = 35 | (userId: number) => notImplemented(); 36 | 37 | /** 38 | * Stores a list of posts of the given user (identified by `userId`) in the cache. 39 | * 40 | * @param userId User ID – a will be used as a key for cache 41 | * @param posts A list of posts to store in cache 42 | */ 43 | export const cacheStorePosts = 44 | (userId: number, posts: Post[]) => notImplemented(); 45 | 46 | /** 47 | * Clears cache for a given user ID. 48 | * 49 | * @param userId User ID 50 | */ 51 | export const cacheInvalidate = 52 | (userId: number) => notImplemented(); 53 | 54 | /** 55 | * Gets a list of posts belonging to the given user (identified by `userId`). 56 | * 57 | * @param userId User ID – author of posts 58 | */ 59 | export const dbGetPosts = 60 | (userId: number) => notImplemented(); 61 | 62 | /** 63 | * Stores post in the database. 64 | * 65 | * @param post Post data to store in the DB 66 | */ 67 | export const dbCreatePost = 68 | (post: Post) => notImplemented(); 69 | 70 | /** 71 | * Updated a post in the database, identified by its ID. 72 | * 73 | * @param postId Post ID 74 | * @param update A set of updated fields for this post ID 75 | */ 76 | export const dbUpdatePost = 77 | (postId: number, update: PostUpdate) => notImplemented(); 78 | 79 | /** 80 | * Sends a list of posts via network. 81 | * 82 | * @param posts A list of posts to send via network 83 | * @param to Email to send these `posts` to. 84 | */ 85 | export const netSendPosts = 86 | (posts: Post[], to: string) => notImplemented(); 87 | 88 | /** 89 | * **Bonus points**: implement a `getInstanceFor` method. Think what `P` parameter might be? 90 | */ 91 | export const getInstanceFor = (P: unknown) => { 92 | return { 93 | // ??? 94 | }; 95 | }; -------------------------------------------------------------------------------- /src/workshop/tagless-final/examples.ts: -------------------------------------------------------------------------------- 1 | import { Post } from '../../domain'; 2 | import { notImplemented } from '../../utils/throw'; 3 | 4 | // * Write the following: 5 | 6 | // 1. Get a list of user posts, cache and send them to 'review@example.com' for a review 7 | export const exampleProgram1 = 8 | (userId: number) => notImplemented(); 9 | 10 | // 2. Create a post and send top-3 to author's email 11 | export const exampleProgram2 = 12 | (newPost: Post) => notImplemented(); 13 | 14 | // 3. Update a post, invalidate the cache if required and return the updated post 15 | export const exampleProgram3 = 16 | (postId: number, update: Post) => notImplemented(); 17 | -------------------------------------------------------------------------------- /src/workshop/tagless-final/interpreters.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Here you'll write concrete interpreters for your eDSL, which will translate it to the specific monad 3 | */ -------------------------------------------------------------------------------- /src/workshop/tagless-final/run.ts: -------------------------------------------------------------------------------- 1 | import { notImplemented } from '../../utils/throw'; 2 | 3 | (async () => { 4 | console.log('exampleProgram1: first run'); 5 | await notImplemented(); 6 | console.log('exampleProgram1: second run'); 7 | await notImplemented(); 8 | console.log('exampleProgram2'); 9 | await notImplemented(); 10 | console.log('exampleProgram3'); 11 | await notImplemented(); 12 | })(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "incremental": true, 5 | "module": "commonjs", 6 | "target": "ESNext", 7 | "esModuleInterop": true, 8 | "sourceMap": false, 9 | "allowJs": false, 10 | "noImplicitAny": true, 11 | "lib": [ 12 | "es6", 13 | "dom" 14 | ], 15 | "rootDir": "src", 16 | "moduleResolution": "node", 17 | "types": [ 18 | "jest", 19 | "node" 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "defaultSeverity": "error", 6 | "rules": { 7 | "no-console": [ 8 | false 9 | ], 10 | "quotemark": [ 11 | true, 12 | "single" 13 | ], 14 | "ordered-imports": [ 15 | true, 16 | { 17 | "import-sources-order": "case-insensitive", 18 | "grouped-imports": true, 19 | "groups": [ 20 | { 21 | "name": "relative", 22 | "match": "^\\.\\./", 23 | "order": 30 24 | }, 25 | { 26 | "name": "local", 27 | "match": "(^\\./)|(^\\.$)", 28 | "order": 40 29 | }, 30 | { 31 | "name": "modules", 32 | "match": ".*", 33 | "order": 20 34 | } 35 | ] 36 | } 37 | ], 38 | "member-ordering": [ 39 | false 40 | ], 41 | "object-literal-sort-keys": [ 42 | false 43 | ], 44 | "trailing-comma": [ 45 | true, 46 | { 47 | "multiline": { 48 | "objects": "always", 49 | "arrays": "always", 50 | "functions": "always", 51 | "typeLiterals": "ignore" 52 | }, 53 | "esSpecCompliant": true 54 | } 55 | ], 56 | "indent": [ 57 | true, 58 | "spaces" 59 | ], 60 | "interface-name": [ 61 | true, 62 | "never-prefix" 63 | ], 64 | "max-line-length": [ 65 | true, 66 | 120 67 | ], 68 | "semicolon": [ 69 | true, 70 | "always" 71 | ], 72 | "object-literal-key-quotes": [ 73 | true, 74 | "as-needed" 75 | ], 76 | "linebreak-style": [ 77 | true, 78 | "LF" 79 | ], 80 | "whitespace": [ 81 | true, 82 | "check-branch", 83 | "check-decl", 84 | "check-operator", 85 | "check-module", 86 | "check-separator", 87 | "check-rest-spread", 88 | "check-type", 89 | "check-typecast", 90 | "check-type-operator", 91 | "check-preblock" 92 | ], 93 | "member-access": false, 94 | "no-any": true, 95 | "variable-name": false, 96 | "jsx-no-multiline-js": false, 97 | "no-implicit-dependencies": [ 98 | true, 99 | "dev" 100 | ], 101 | "max-classes-per-file": false, 102 | "interface-over-type-literal": false, 103 | "newline-before-return": false, 104 | "array-type": [ 105 | true, 106 | "array-simple" 107 | ], 108 | "ban-types": false 109 | } 110 | } --------------------------------------------------------------------------------