├── .gitignore ├── README.md ├── chapter2 ├── .codesandbox │ └── workspace.json ├── index.html ├── package.json ├── src │ ├── clip3-1.ts │ ├── clip3-2.ts │ ├── clip4.ts │ ├── clip6.ts │ ├── clip7.ts │ └── index.ts └── tsconfig.json ├── chapter3 ├── .codesandbox │ └── workspace.json ├── clip7-1.txt ├── clip7-2.txt ├── index.html ├── package.json ├── src │ ├── cart.ts │ ├── clip4.ts │ ├── clip5.ts │ ├── clip6.ts │ ├── clip7.ts │ ├── clip8.ts │ ├── clip9.ts │ ├── index.css │ └── index.ts └── tsconfig.json ├── chapter4 ├── .codesandbox │ └── workspace.json ├── index.html ├── package.json ├── src │ ├── cart.ts │ ├── clip4.ts │ ├── clip5.ts │ ├── clip8.ts │ ├── index.css │ ├── index.ts │ ├── option.ts │ └── test.ts └── tsconfig.json ├── chapter5 ├── .codesandbox │ └── workspace.json ├── index.html ├── package.json ├── src │ ├── clip3-partial-application.ts │ ├── clip3.ts │ ├── clip4.ts │ ├── clip5.ts │ ├── compose.ts │ ├── index.ts │ ├── option.ts │ └── styles.css └── tsconfig.json ├── chapter6 ├── .codesandbox │ └── workspace.json ├── index.html ├── package.json ├── src │ ├── cart.ts │ ├── clip5.ts │ ├── clip8-original.ts │ ├── clip8.ts │ ├── index.css │ ├── index.ts │ ├── option.ts │ └── try.ts └── tsconfig.json ├── chapter7 ├── .codesandbox │ └── workspace.json ├── index.html ├── package.json ├── src │ ├── clip3.ts │ ├── clip4.ts │ ├── clip5.ts │ ├── clip6.ts │ ├── clip7.ts │ ├── clip8-1.ts │ ├── clip8-2.ts │ ├── index.ts │ ├── styles.css │ └── try.ts └── tsconfig.json ├── chapter8-clip8 ├── .codesandbox │ └── workspace.json ├── index.html ├── package.json ├── src │ ├── cart.ts │ ├── clip7.ts │ ├── index.css │ ├── index.ts │ ├── option.ts │ ├── renderButtons.ts │ ├── renderList.ts │ ├── styles.css │ └── try.ts └── tsconfig.json ├── chapter8 ├── .codesandbox │ └── workspace.json ├── index.html ├── package.json ├── src │ ├── cart.ts │ ├── clip3.ts │ ├── clip4.ts │ ├── clip5-1.ts │ ├── clip5-2.ts │ ├── clip6.ts │ ├── index.css │ ├── index.ts │ ├── option.ts │ ├── styles.css │ └── try.ts └── tsconfig.json └── chapter9 ├── .codesandbox └── workspace.json ├── index.html ├── package.json ├── src ├── clip3-complete.ts ├── clip3.ts ├── index.css ├── index.ts ├── option.ts ├── styles.css └── try.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/vim,visualstudiocode,osx,windows,linux,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=vim,visualstudiocode,osx,windows,linux,node 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### Node ### 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | lerna-debug.log* 28 | .pnpm-debug.log* 29 | 30 | # Diagnostic reports (https://nodejs.org/api/report.html) 31 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 32 | 33 | # Runtime data 34 | pids 35 | *.pid 36 | *.seed 37 | *.pid.lock 38 | 39 | # Directory for instrumented libs generated by jscoverage/JSCover 40 | lib-cov 41 | 42 | # Coverage directory used by tools like istanbul 43 | coverage 44 | *.lcov 45 | 46 | # nyc test coverage 47 | .nyc_output 48 | 49 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 50 | .grunt 51 | 52 | # Bower dependency directory (https://bower.io/) 53 | bower_components 54 | 55 | # node-waf configuration 56 | .lock-wscript 57 | 58 | # Compiled binary addons (https://nodejs.org/api/addons.html) 59 | build/Release 60 | 61 | # Dependency directories 62 | node_modules/ 63 | jspm_packages/ 64 | 65 | # Snowpack dependency directory (https://snowpack.dev/) 66 | web_modules/ 67 | 68 | # TypeScript cache 69 | *.tsbuildinfo 70 | 71 | # Optional npm cache directory 72 | .npm 73 | 74 | # Optional eslint cache 75 | .eslintcache 76 | 77 | # Microbundle cache 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | .node_repl_history 85 | 86 | # Output of 'npm pack' 87 | *.tgz 88 | 89 | # Yarn Integrity file 90 | .yarn-integrity 91 | 92 | # dotenv environment variables file 93 | .env 94 | .env.test 95 | .env.production 96 | 97 | # parcel-bundler cache (https://parceljs.org/) 98 | .cache 99 | .parcel-cache 100 | 101 | # Next.js build output 102 | .next 103 | out 104 | 105 | # Nuxt.js build / generate output 106 | .nuxt 107 | dist 108 | 109 | # Gatsby files 110 | .cache/ 111 | # Comment in the public line in if your project uses Gatsby and not Next.js 112 | # https://nextjs.org/blog/next-9-1#public-directory-support 113 | # public 114 | 115 | # vuepress build output 116 | .vuepress/dist 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | # TernJS port file 128 | .tern-port 129 | 130 | # Stores VSCode versions used for testing VSCode extensions 131 | .vscode-test 132 | 133 | # yarn v2 134 | .yarn/cache 135 | .yarn/unplugged 136 | .yarn/build-state.yml 137 | .yarn/install-state.gz 138 | .pnp.* 139 | 140 | ### Node Patch ### 141 | # Serverless Webpack directories 142 | .webpack/ 143 | 144 | # Optional stylelint cache 145 | .stylelintcache 146 | 147 | # SvelteKit build / generate output 148 | .svelte-kit 149 | 150 | ### OSX ### 151 | # General 152 | .DS_Store 153 | .AppleDouble 154 | .LSOverride 155 | 156 | # Icon must end with two \r 157 | Icon 158 | 159 | 160 | # Thumbnails 161 | ._* 162 | 163 | # Files that might appear in the root of a volume 164 | .DocumentRevisions-V100 165 | .fseventsd 166 | .Spotlight-V100 167 | .TemporaryItems 168 | .Trashes 169 | .VolumeIcon.icns 170 | .com.apple.timemachine.donotpresent 171 | 172 | # Directories potentially created on remote AFP share 173 | .AppleDB 174 | .AppleDesktop 175 | Network Trash Folder 176 | Temporary Items 177 | .apdisk 178 | 179 | ### Vim ### 180 | # Swap 181 | [._]*.s[a-v][a-z] 182 | !*.svg # comment out if you don't need vector files 183 | [._]*.sw[a-p] 184 | [._]s[a-rt-v][a-z] 185 | [._]ss[a-gi-z] 186 | [._]sw[a-p] 187 | 188 | # Session 189 | Session.vim 190 | Sessionx.vim 191 | 192 | # Temporary 193 | .netrwhist 194 | # Auto-generated tag files 195 | tags 196 | # Persistent undo 197 | [._]*.un~ 198 | 199 | ### VisualStudioCode ### 200 | .vscode/* 201 | !.vscode/settings.json 202 | !.vscode/tasks.json 203 | !.vscode/launch.json 204 | !.vscode/extensions.json 205 | *.code-workspace 206 | 207 | # Local History for Visual Studio Code 208 | .history/ 209 | 210 | ### VisualStudioCode Patch ### 211 | # Ignore all local history of files 212 | .history 213 | .ionide 214 | 215 | # Support for Project snippet scope 216 | !.vscode/*.code-snippets 217 | 218 | ### Windows ### 219 | # Windows thumbnail cache files 220 | Thumbs.db 221 | Thumbs.db:encryptable 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Dump file 226 | *.stackdump 227 | 228 | # Folder config file 229 | [Dd]esktop.ini 230 | 231 | # Recycle Bin used on file shares 232 | $RECYCLE.BIN/ 233 | 234 | # Windows Installer files 235 | *.cab 236 | *.msi 237 | *.msix 238 | *.msm 239 | *.msp 240 | 241 | # Windows shortcuts 242 | *.lnk 243 | 244 | # End of https://www.toptal.com/developers/gitignore/api/vim,visualstudiocode,osx,windows,linux,node 245 | 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Chapter 2 2 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter2?file=/src/clip3-1.ts) 3 | 4 | ## Chapter 3 5 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter3?file=/src/clip4.ts) 6 | 7 | ## Chapter 4 8 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter4?file=/src/clip4.ts) 9 | 10 | ## Chapter 5 11 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter5?file=/src/clip3.ts) 12 | 13 | ## Chapter 6 14 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter6?file=/src/clip5.ts) 15 | 16 | ## Chapter 7 17 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter7?file=/src/clip3.ts) 18 | 19 | ## Chapter 8 20 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter8?file=/src/clip3.ts) 21 | 22 | ## Chapter 8 - Clip 8 23 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter8-clip8?file=/src/renderButtons.ts) 24 | 25 | ## Chapter 9 26 | [codesandbox로 열기](https://codesandbox.io/s/github/JsonKim/fastcampus-fp-typescript/tree/main/chapter8-clip8?file=/src/clip3.ts) 27 | 28 | -------------------------------------------------------------------------------- /chapter2/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |
10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /chapter2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1" 12 | }, 13 | "devDependencies": { 14 | "typescript": "4.4.4" 15 | }, 16 | "resolutions": { 17 | "@babel/preset-env": "7.13.8" 18 | }, 19 | "keywords": [] 20 | } -------------------------------------------------------------------------------- /chapter2/src/clip3-1.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 토마토: 7000원 3 | 오렌지: 15000원 4 | 사과: 10000원 5 | */ 6 | 7 | export let totalPrice = 0; 8 | totalPrice += 7000; 9 | totalPrice += 15000; 10 | totalPrice += 10000; 11 | // 오렌지가 2개인지, 사과가 3개인지 불분명 12 | totalPrice += 30000; 13 | // 사소한 오타로 인한 실수 14 | totalPrice += 8000; 15 | // 단위가 잘못됐다면 실수라는 것을 인지하기조차 어려움 16 | totalPrice += 70000; 17 | -------------------------------------------------------------------------------- /chapter2/src/clip3-2.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 토마토: 7000원 3 | 오렌지: 15000원 4 | 사과: 10000원 5 | */ 6 | 7 | // 전역 변수라는 부수효과를 사용 8 | // 예측하기 어려운 오류의 발생 가능성이 있음 9 | export let totalPrice = 0; 10 | 11 | export function addTomato() { 12 | totalPrice += 7000; 13 | } 14 | 15 | export function addOrange() { 16 | totalPrice += 15000; 17 | } 18 | 19 | export function addApple() { 20 | totalPrice += 10000; 21 | } 22 | 23 | export function list1() { 24 | // 토마토, 오렌지, 사과 한 상자 25 | addTomato(); 26 | addOrange(); 27 | addApple(); 28 | } 29 | 30 | export function list2() { 31 | // 토마토 2상자 32 | addTomato(); 33 | addTomato(); 34 | } 35 | 36 | export function list3() { 37 | // addOrange 함수를 직접 100번 호출 하는 대신 38 | // 함수를 100번 호출 하도록 명령하는 코드를 작성 39 | // 오렌지 100상자 40 | for (let i = 0; i < 100; i++) { 41 | addOrange(); 42 | } 43 | } 44 | 45 | export function main() { 46 | list3(); 47 | } 48 | -------------------------------------------------------------------------------- /chapter2/src/clip4.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 토마토: 7000원 3 | 오렌지: 15000원 4 | 사과: 10000원 5 | */ 6 | export function priceOfTomato() { 7 | return 7000; 8 | } 9 | 10 | export function priceOfOrange() { 11 | return 15000; 12 | } 13 | 14 | export function priceOfApple() { 15 | return 10000; 16 | } 17 | 18 | export function getPrice(name: string) { 19 | if (name === "tomato") { 20 | return 7000; 21 | } else if (name === "orange") { 22 | return 15000; 23 | } else if (name === "apple") { 24 | return 10000; 25 | } 26 | } 27 | 28 | export const priceOfFruit = { 29 | tomato: 7000, 30 | orange: 15000, 31 | apple: 10000 32 | }; 33 | 34 | export const isEven = { 35 | tomato: true, 36 | orange: true, 37 | apple: false 38 | }; 39 | 40 | export const isEvenFn = (str: string) => str.length % 2 === 0; 41 | 42 | export function list1() { 43 | // 토마토, 오렌지, 사과 한 상자 44 | return priceOfTomato() + priceOfOrange() + priceOfApple(); 45 | } 46 | 47 | export function list2() { 48 | // 토마토 2상자 49 | return priceOfTomato() + priceOfTomato(); 50 | } 51 | 52 | export function list3() { 53 | // 오렌지 100상자 구입은 오렌지 가격을 100번 더하는 것과 동일하기 때문에 54 | // 오렌지 가격 * 100 으로 계산 가능 55 | // 오렌지 100상자 56 | return priceOfOrange() * 100; 57 | } 58 | 59 | export function getTotalPrice() { 60 | return list2(); 61 | } 62 | -------------------------------------------------------------------------------- /chapter2/src/clip6.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 토마토: 7000원 3 | 오렌지: 15000원 4 | 사과: 10000원 5 | */ 6 | 7 | export function getPrice(name: string): number | undefined { 8 | if (name === "tomato") { 9 | return 7000; 10 | } else if (name === "orange") { 11 | return 15000; 12 | } else if (name === "apple") { 13 | return 10000; 14 | } 15 | } 16 | 17 | export const isExpensive = (price: number | undefined) => { 18 | // getPrice의 결과에 undefined가 포함 되어 있기 때문에 19 | // getPrice와 합성을 위해 undefined에 대한 처리 필요 20 | if (price === undefined) { 21 | return false; 22 | } 23 | return price > 10000; 24 | }; 25 | 26 | // isExpensive와 getPrice를 합성해서 하나의 함수로 27 | export const isExpensivePrice = (name: string): boolean => { 28 | return isExpensive(getPrice(name)); 29 | }; 30 | 31 | export const main = () => { 32 | return isExpensive(getPrice("tomato")); 33 | }; 34 | -------------------------------------------------------------------------------- /chapter2/src/clip7.ts: -------------------------------------------------------------------------------- 1 | // 숫자를 그대로 돌려주는 함수 2 | export const idNumber = (n: number) => { 3 | return n; 4 | }; 5 | 6 | // 문자열을 그대로 돌려주는 함수 7 | export const idString = (s: string) => { 8 | return s; 9 | }; 10 | 11 | // boolean값을 그대로 돌려주는 함수 12 | export const idBoolean = (b: boolean) => { 13 | return b; 14 | }; 15 | 16 | // 어떤 타입의 값이라도 그대로 돌려주는 함수 17 | export const id = (x: T) => { 18 | return x; 19 | }; 20 | 21 | // 주석을 제거하면 에러 발생 22 | // export type T1 = Array; 23 | 24 | export type T2 = Array; 25 | 26 | export const compose = (g: (y: B) => C, f: (x: A) => B) => (x: A) => { 27 | return g(f(x)); 28 | }; 29 | 30 | // compose 함수의 원래 타입 31 | // (g: (y: B) => C, f: (x: A) => B) => (x: A) => C 32 | 33 | // 매개변수를 지워서 단순하게 표기 34 | // ((B) => C, (A) => B) => (A) => C 35 | -------------------------------------------------------------------------------- /chapter2/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as C from "./clip3-2"; 2 | 3 | const app = document.getElementById("app"); 4 | if (app !== null) { 5 | app.innerHTML = ` 6 |

Total price: ${Math.round(C.totalPrice)}

7 | `; 8 | } 9 | -------------------------------------------------------------------------------- /chapter2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "es6", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } -------------------------------------------------------------------------------- /chapter3/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter3/clip7-1.txt: -------------------------------------------------------------------------------- 1 | 장바구니를 그려야 한다. 2 | -> 장바구니를 순회하면서 3 | -> 화면에 상품 이름, 가격, 수량을 표시한다. 4 | 5 | 전체 가격과 전체 수량도 화면에 그려야 한다. 6 | -> 2번의 동작을 수행할 때 totalPrice, totalCount에 값을 누적한다. 7 | 8 | 재고 없는 상품의 처리 9 | -> 2번과 6번의 동작을 수행할 때 재고 여부에 따라 다르게 동작시킨다. -------------------------------------------------------------------------------- /chapter3/clip7-2.txt: -------------------------------------------------------------------------------- 1 | 아이템 목록 화면 2 | - 재고가 있는 아이템 3 | - 재고가 없는 아이템 4 | 5 | 전체 수량 표시 6 | 전체 가격 표시 7 | -------------------------------------------------------------------------------- /chapter3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |
10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /chapter3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter3-clip3-github", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1" 12 | }, 13 | "devDependencies": { 14 | "typescript": "4.4.4" 15 | }, 16 | "resolutions": { 17 | "@babel/preset-env": "7.13.8" 18 | }, 19 | "keywords": [] 20 | } -------------------------------------------------------------------------------- /chapter3/src/cart.ts: -------------------------------------------------------------------------------- 1 | export interface Item { 2 | code: string; 3 | outOfStock: boolean; 4 | name: string; 5 | price: number; 6 | quantity: number; 7 | } 8 | 9 | export const cart: Array = [ 10 | { 11 | code: "tomato", 12 | outOfStock: false, 13 | name: "토마토", 14 | price: 7000, 15 | quantity: 2 16 | }, 17 | { 18 | code: "orange", 19 | outOfStock: true, 20 | name: "오렌지", 21 | price: 15000, 22 | quantity: 3 23 | }, 24 | { 25 | code: "apple", 26 | outOfStock: false, 27 | name: "사과", 28 | price: 10000, 29 | quantity: 1 30 | } 31 | ]; 32 | -------------------------------------------------------------------------------- /chapter3/src/clip4.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart } from "./cart"; 3 | 4 | const list = () => { 5 | let html = "
    "; 6 | 7 | for (let i = 0; i < cart.length; i++) { 8 | if (cart[i].outOfStock === false) { 9 | html += "
  • "; 10 | html += `

    ${cart[i].name}

    `; 11 | html += `
    가격: ${cart[i].price}원
    `; 12 | html += `
    수량: ${cart[i].quantity}상자
    `; 13 | html += "
  • "; 14 | } else { 15 | html += "
  • "; 16 | html += `

    ${cart[i].name} (품절)

    `; 17 | html += `
    가격: ${cart[i].price}원
    `; 18 | html += `
    수량: ${cart[i].quantity}상자
    `; 19 | html += "
  • "; 20 | } 21 | } 22 | html += "
"; 23 | 24 | let totalCount = 0; 25 | for (let i = 0; i < cart.length; i++) { 26 | if (cart[i].outOfStock === false) { 27 | totalCount += cart[i].quantity; 28 | } 29 | } 30 | html += `

전체 수량: ${totalCount}상자

`; 31 | 32 | let totalPrice = 0; 33 | for (let i = 0; i < cart.length; i++) { 34 | if (cart[i].outOfStock === false) { 35 | totalPrice += cart[i].price * cart[i].quantity; 36 | } 37 | } 38 | html += `

전체 가격: ${totalPrice}원

`; 39 | 40 | return ` 41 | ${html} 42 | `; 43 | }; 44 | 45 | const app = document.getElementById("app"); 46 | if (app != null) { 47 | app.innerHTML = ` 48 |

장바구니

49 | ${list()} 50 | `; 51 | } 52 | -------------------------------------------------------------------------------- /chapter3/src/clip5.ts: -------------------------------------------------------------------------------- 1 | export const map = (array: Array, f: (a: A) => B) => { 2 | const result: Array = []; 3 | for (const value of array) { 4 | result.push(f(value)); 5 | } 6 | return result; 7 | }; 8 | -------------------------------------------------------------------------------- /chapter3/src/clip6.ts: -------------------------------------------------------------------------------- 1 | export type MapType = (xs: Array, f: (x: A) => B) => Array; 2 | // (Array, A => B) => Array 3 | 4 | export type MapType1 = MapType; 5 | // (Array, number => string) => Array 6 | 7 | export type Compose = (g: (y: B) => C, f: (x: A) => B) => (a: A) => C; 8 | // (B => C, A => B) => A => C 9 | 10 | export type Compose1 = Compose; 11 | // (number => boolean, string => number) => string => boolean 12 | -------------------------------------------------------------------------------- /chapter3/src/clip7.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | 4 | const stockItem = (item: Item): string => ` 5 |
  • 6 |

    ${item.name}

    7 |
    가격: ${item.price}원
    8 |
    수량: ${item.quantity}상자
    9 |
  • 10 | `; 11 | 12 | const outOfStockItem = (item: Item): string => ` 13 |
  • 14 |

    ${item.name} (품절)

    15 |
    가격: ${item.price}원
    16 |
    수량: ${item.quantity}상자
    17 |
  • 18 | `; 19 | 20 | const item = (item: Item): string => { 21 | if (item.outOfStock) { 22 | return outOfStockItem(item); 23 | } else { 24 | return stockItem(item); 25 | } 26 | }; 27 | 28 | const totalCalculator = ( 29 | list: Array, 30 | getValue: (item: Item) => number 31 | ) => { 32 | return ( 33 | list 34 | // 1. 재고가 있는 상품만 분류하기 35 | .filter((item) => item.outOfStock === false) 36 | // 2. 분류된 상품들에 대해서 getValue 실행하기 37 | .map(getValue) 38 | // 3. getValue가 실행된 값 모두 더하기 39 | .reduce((total, value) => total + value, 0) 40 | ); 41 | }; 42 | 43 | const totalCount = (list: Array): string => { 44 | const totalCount = totalCalculator(list, (item) => item.quantity); 45 | return `

    전체 수량: ${totalCount}상자`; 46 | }; 47 | 48 | const totalPrice = (list: Array): string => { 49 | const totalPrice = totalCalculator( 50 | list, 51 | (item) => item.price * item.quantity 52 | ); 53 | return `

    전체 가격: ${totalPrice}원`; 54 | }; 55 | 56 | const list = (list: Array) => { 57 | return ` 58 |
      59 | ${list 60 | // 1. 목록에 있는 모든 아이템을 태그로 변경 61 | .map(item) 62 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 63 | .reduce((tags, tag) => tags + tag, "")} 64 |
    65 | `; 66 | }; 67 | 68 | const app = document.getElementById("app"); 69 | if (app != null) { 70 | app.innerHTML = ` 71 |

    장바구니

    72 | ${list(cart)} 73 | ${totalCount(cart)} 74 | ${totalPrice(cart)} 75 | `; 76 | } 77 | -------------------------------------------------------------------------------- /chapter3/src/clip8.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | 4 | const stockItem = (item: Item): string => ` 5 |
  • 6 |

    ${item.name}

    7 |
    가격: ${item.price}원
    8 |
    수량: ${item.quantity}상자
    9 |
  • 10 | `; 11 | 12 | const outOfStockItem = (item: Item): string => ` 13 |
  • 14 |

    ${item.name} (품절)

    15 |
    가격: ${item.price}원
    16 |
    수량: ${item.quantity}상자
    17 |
  • 18 | `; 19 | 20 | const item = (item: Item): string => { 21 | if (item.outOfStock) { 22 | return outOfStockItem(item); 23 | } else { 24 | return stockItem(item); 25 | } 26 | }; 27 | 28 | const totalCalculator = ( 29 | list: Array, 30 | getValue: (item: Item) => number 31 | ) => { 32 | let result: Array = []; 33 | list.forEach(function (item) { 34 | if (item.outOfStock === false) { 35 | result.push(getValue(item)); 36 | } 37 | }); 38 | 39 | return result.reduce((total, value) => total + value, 0); 40 | }; 41 | 42 | const totalCount = (list: Array): string => { 43 | const totalCount = totalCalculator(list, (item) => item.quantity); 44 | return `

    전체 수량: ${totalCount}상자`; 45 | }; 46 | 47 | const totalPrice = (list: Array): string => { 48 | const totalPrice = totalCalculator( 49 | list, 50 | (item) => item.price * item.quantity 51 | ); 52 | return `

    전체 가격: ${totalPrice}원`; 53 | }; 54 | 55 | const list = (list: Array) => { 56 | return ` 57 |
      58 | ${list 59 | // 1. 목록에 있는 모든 아이템을 태그로 변경 60 | .map(item) 61 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 62 | .reduce((tags, tag) => tags + tag, "")} 63 |
    64 | `; 65 | }; 66 | 67 | const app = document.getElementById("app"); 68 | if (app != null) { 69 | app.innerHTML = ` 70 |

    장바구니

    71 | ${list(cart)} 72 | ${totalCount(cart)} 73 | ${totalPrice(cart)} 74 | `; 75 | } 76 | -------------------------------------------------------------------------------- /chapter3/src/clip9.ts: -------------------------------------------------------------------------------- 1 | const suits = ["♠", "♥", "♣", "♦"]; 2 | const numbers = [ 3 | "2", 4 | "3", 5 | "4", 6 | "5", 7 | "6", 8 | "7", 9 | "8", 10 | "9", 11 | "10", 12 | "J", 13 | "Q", 14 | "K", 15 | "A" 16 | ]; 17 | 18 | const cards: Array = []; 19 | for (const suit of suits) { 20 | for (const number of numbers) { 21 | cards.push(suit + number); 22 | } 23 | } 24 | 25 | // 모든 카드 목록은 아래의 작업이 완료된 것이다. 26 | const cards2 = 27 | // 아래의 작업을 모든 무늬에 적용한다. 28 | suits 29 | .map((suit) => 30 | // 아래의 작업을 모든 숫자에 적용한다. 31 | numbers.map( 32 | (number) => 33 | // 카드는 무늬와 숫자를 연결 한 문자열이다. 34 | suit + number 35 | ) 36 | ) 37 | // 무늬별로 나누어진 카드를 하나로 합친다. 38 | .flat(); 39 | // Array> => Array 40 | 41 | export const main = () => { 42 | const app = document.getElementById("app"); 43 | if (app === null) { 44 | return; 45 | } 46 | 47 | app.innerHTML = ` 48 |

    cards

    49 |
    ${JSON.stringify(cards2, null, 2)}
    50 |   
    51 | `; 52 | }; 53 | -------------------------------------------------------------------------------- /chapter3/src/index.css: -------------------------------------------------------------------------------- 1 | .gray { 2 | color: gray; 3 | } 4 | 5 | .strike { 6 | text-decoration: line-through; 7 | } 8 | -------------------------------------------------------------------------------- /chapter3/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./clip4"; 2 | -------------------------------------------------------------------------------- /chapter3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "esnext", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } -------------------------------------------------------------------------------- /chapter4/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /chapter4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter4-clip4-github", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1" 12 | }, 13 | "devDependencies": { 14 | "typescript": "4.4.4" 15 | }, 16 | "resolutions": { 17 | "@babel/preset-env": "7.13.8" 18 | }, 19 | "keywords": [] 20 | } -------------------------------------------------------------------------------- /chapter4/src/cart.ts: -------------------------------------------------------------------------------- 1 | export interface Item { 2 | readonly code: string; 3 | readonly outOfStock: boolean; 4 | readonly name: string; 5 | readonly price: number; 6 | readonly quantity: number; 7 | readonly discountPrice?: number; 8 | } 9 | 10 | export const cart: Array = [ 11 | { 12 | code: "tomato", 13 | outOfStock: false, 14 | name: "토마토", 15 | price: 7000, 16 | quantity: 2, 17 | discountPrice: 1000 18 | }, 19 | { 20 | code: "orange", 21 | outOfStock: true, 22 | name: "오렌지", 23 | price: 15000, 24 | quantity: 3, 25 | discountPrice: 2000 26 | }, 27 | { 28 | code: "apple", 29 | outOfStock: false, 30 | name: "사과", 31 | price: 10000, 32 | quantity: 1 33 | } 34 | ]; 35 | -------------------------------------------------------------------------------- /chapter4/src/clip4.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | 4 | const stockItem = (item: Item): string => { 5 | let saleText = ""; 6 | let discountPrice = 0; 7 | if (item.discountPrice !== undefined) { 8 | saleText = `(${item.discountPrice}원 할인)`; 9 | discountPrice = item.discountPrice; 10 | } 11 | 12 | return ` 13 |
  • 14 |

    ${item.name}

    15 |
    가격: ${item.price - discountPrice}원 ${saleText}
    16 |
    수량: ${item.quantity}상자
    17 |
  • 18 | `; 19 | }; 20 | 21 | const outOfStockItem = (item: Item): string => ` 22 |
  • 23 |

    ${item.name} (품절)

    24 |
    가격: ${item.price}원
    25 |
    수량: ${item.quantity}상자
    26 |
  • 27 | `; 28 | 29 | const item = (item: Item): string => { 30 | if (item.outOfStock) { 31 | return outOfStockItem(item); 32 | } else { 33 | return stockItem(item); 34 | } 35 | }; 36 | 37 | const totalCalculator = ( 38 | list: Array, 39 | getValue: (item: Item) => number 40 | ) => { 41 | let result: Array = []; 42 | list.forEach(function (item) { 43 | if (item.outOfStock === false) { 44 | result.push(getValue(item)); 45 | } 46 | }); 47 | 48 | return result.reduce((total, value) => total + value, 0); 49 | }; 50 | 51 | const totalCount = (list: Array): string => { 52 | const totalCount = totalCalculator(list, (item) => item.quantity); 53 | return `

    전체 수량: ${totalCount}상자`; 54 | }; 55 | 56 | const totalPrice = (list: Array): string => { 57 | const totalPrice = totalCalculator( 58 | list, 59 | (item) => item.price * item.quantity 60 | ); 61 | 62 | const totalDiscountPrice = totalCalculator(list, (item) => { 63 | let discountPrice = 0; 64 | if (item.discountPrice !== undefined) { 65 | discountPrice = item.discountPrice; 66 | } 67 | return discountPrice * item.quantity; 68 | }); 69 | 70 | return `

    전체 가격: ${ 71 | totalPrice - totalDiscountPrice 72 | }원 (총 ${totalDiscountPrice}원 할인)

    `; 73 | }; 74 | 75 | const list = (list: Array) => { 76 | return ` 77 |
      78 | ${list 79 | // 1. 목록에 있는 모든 아이템을 태그로 변경 80 | .map(item) 81 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 82 | .reduce((tags, tag) => tags + tag, "")} 83 |
    84 | `; 85 | }; 86 | 87 | const app = document.getElementById("app"); 88 | if (app != null) { 89 | app.innerHTML = ` 90 |

    장바구니

    91 | ${list(cart)} 92 | ${totalCount(cart)} 93 | ${totalPrice(cart)} 94 | `; 95 | } 96 | -------------------------------------------------------------------------------- /chapter4/src/clip5.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | import * as O from "./option"; 4 | 5 | const stockItem = (item: Item): string => { 6 | const optionDiscountPrice = O.fromUndefined(item.discountPrice); 7 | const discountPrice = O.getOrElse(optionDiscountPrice, 0); 8 | 9 | let saleText = ""; 10 | if (O.isSome(optionDiscountPrice)) { 11 | saleText = `(${item.discountPrice}원 할인)`; 12 | } 13 | 14 | return ` 15 |
  • 16 |

    ${item.name}

    17 |
    가격: ${item.price - discountPrice}원 ${saleText}
    18 |
    수량: ${item.quantity}상자
    19 |
  • 20 | `; 21 | }; 22 | 23 | const outOfStockItem = (item: Item): string => ` 24 |
  • 25 |

    ${item.name} (품절)

    26 |
    가격: ${item.price}원
    27 |
    수량: ${item.quantity}상자
    28 |
  • 29 | `; 30 | 31 | const item = (item: Item): string => { 32 | if (item.outOfStock) { 33 | return outOfStockItem(item); 34 | } else { 35 | return stockItem(item); 36 | } 37 | }; 38 | 39 | const totalCalculator = ( 40 | list: Array, 41 | getValue: (item: Item) => number 42 | ) => { 43 | let result: Array = []; 44 | list.forEach(function (item) { 45 | if (item.outOfStock === false) { 46 | result.push(getValue(item)); 47 | } 48 | }); 49 | 50 | return result.reduce((total, value) => total + value, 0); 51 | }; 52 | 53 | const totalCount = (list: Array): string => { 54 | const totalCount = totalCalculator(list, (item) => item.quantity); 55 | return `

    전체 수량: ${totalCount}상자`; 56 | }; 57 | 58 | const totalPrice = (list: Array): string => { 59 | const totalPrice = totalCalculator( 60 | list, 61 | (item) => item.price * item.quantity 62 | ); 63 | 64 | const discountPrice = totalCalculator(list, (item) => { 65 | // item.discountPrice |> O.fromUndefined(%) |> O.getOrElse(%, 0) 66 | const discountPrice = O.getOrElse(O.fromUndefined(item.discountPrice), 0); 67 | return discountPrice * item.quantity; 68 | }); 69 | 70 | return `

    전체 가격: ${ 71 | totalPrice - discountPrice 72 | }원 (총 ${discountPrice}원 할인)

    `; 73 | }; 74 | 75 | const list = (list: Array) => { 76 | return ` 77 |
      78 | ${list 79 | // 1. 목록에 있는 모든 아이템을 태그로 변경 80 | .map(item) 81 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 82 | .reduce((tags, tag) => tags + tag, "")} 83 |
    84 | `; 85 | }; 86 | 87 | const app = document.getElementById("app"); 88 | if (app != null) { 89 | app.innerHTML = ` 90 |

    장바구니

    91 | ${list(cart)} 92 | ${totalCount(cart)} 93 | ${totalPrice(cart)} 94 | `; 95 | } 96 | -------------------------------------------------------------------------------- /chapter4/src/clip8.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | import * as O from "./option"; 4 | 5 | const stockItem = (item: Item): string => { 6 | const optionDiscountPrice = O.fromUndefined(item.discountPrice); 7 | const discountPrice = O.getOrElse(optionDiscountPrice, 0); 8 | 9 | const saleText = O.mapOrElse( 10 | optionDiscountPrice, 11 | (discountPrice) => `${discountPrice}원 할인`, 12 | "" 13 | ); 14 | 15 | /* 16 | let saleText = ""; 17 | if (O.isSome(optionDiscountPrice)) { 18 | saleText = `(${item.discountPrice}원 할인)`; 19 | } 20 | */ 21 | 22 | return ` 23 |
  • 24 |

    ${item.name}

    25 |
    가격: ${item.price - discountPrice}원 ${saleText}
    26 |
    수량: ${item.quantity}상자
    27 |
  • 28 | `; 29 | }; 30 | 31 | const outOfStockItem = (item: Item): string => ` 32 |
  • 33 |

    ${item.name} (품절)

    34 |
    가격: ${item.price}원
    35 |
    수량: ${item.quantity}상자
    36 |
  • 37 | `; 38 | 39 | const item = (item: Item): string => { 40 | if (item.outOfStock) { 41 | return outOfStockItem(item); 42 | } else { 43 | return stockItem(item); 44 | } 45 | }; 46 | 47 | const totalCalculator = ( 48 | list: Array, 49 | getValue: (item: Item) => number 50 | ) => { 51 | let result: Array = []; 52 | list.forEach(function (item) { 53 | if (item.outOfStock === false) { 54 | result.push(getValue(item)); 55 | } 56 | }); 57 | 58 | return result.reduce((total, value) => total + value, 0); 59 | }; 60 | 61 | const totalCount = (list: Array): string => { 62 | const totalCount = totalCalculator(list, (item) => item.quantity); 63 | return `

    전체 수량: ${totalCount}상자`; 64 | }; 65 | 66 | const totalPrice = (list: Array): string => { 67 | const totalPrice = totalCalculator( 68 | list, 69 | (item) => item.price * item.quantity 70 | ); 71 | 72 | const discountPrice = totalCalculator(list, (item) => { 73 | // item.discountPrice |> O.fromUndefined(%) |> O.getOrElse(%, 0) 74 | const discountPrice = O.getOrElse(O.fromUndefined(item.discountPrice), 0); 75 | return discountPrice * item.quantity; 76 | }); 77 | 78 | return `

    전체 가격: ${ 79 | totalPrice - discountPrice 80 | }원 (총 ${discountPrice}원 할인)

    `; 81 | }; 82 | 83 | const list = (list: Array) => { 84 | return ` 85 |
      86 | ${list 87 | // 1. 목록에 있는 모든 아이템을 태그로 변경 88 | .map(item) 89 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 90 | .reduce((tags, tag) => tags + tag, "")} 91 |
    92 | `; 93 | }; 94 | 95 | const app = document.getElementById("app"); 96 | if (app != null) { 97 | app.innerHTML = ` 98 |

    장바구니

    99 | ${list(cart)} 100 | ${totalCount(cart)} 101 | ${totalPrice(cart)} 102 | `; 103 | } 104 | -------------------------------------------------------------------------------- /chapter4/src/index.css: -------------------------------------------------------------------------------- 1 | .gray { 2 | color: gray; 3 | } 4 | 5 | .strike { 6 | text-decoration: line-through; 7 | } 8 | -------------------------------------------------------------------------------- /chapter4/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./clip4"; 2 | -------------------------------------------------------------------------------- /chapter4/src/option.ts: -------------------------------------------------------------------------------- 1 | // 값이 있을수도, 없을수도 있는 자료구조. 2 | 3 | export type Some
    = { 4 | _tag: "some"; 5 | value: A; 6 | }; 7 | 8 | export type None = { 9 | _tag: "none"; 10 | }; 11 | 12 | export type Option = Some | None; 13 | 14 | export const some = (value: A): Option => ({ _tag: "some", value }); 15 | 16 | export const none = (): Option => ({ _tag: "none" }); 17 | 18 | export const isSome = (oa: Option): oa is Some => oa._tag === "some"; 19 | 20 | export const isNone = (oa: Option): oa is None => oa._tag === "none"; 21 | 22 | export const fromUndefined = (a: A | undefined): Option => { 23 | if (a === undefined) return none(); 24 | return some(a); 25 | }; 26 | 27 | export const getOrElse = (oa: Option, defaultValue: A): A => { 28 | // 값이 없으면 지정된 값을 사용하고 29 | if (isNone(oa)) return defaultValue; 30 | // 값이 있다면 해당 값을 사용한다. 31 | return oa.value; 32 | }; 33 | 34 | export const map = (oa: Option, f: (a: A) => B): Option => { 35 | // 값이 없으면 값이 없는 상태를 유지한다. 36 | if (isNone(oa)) return oa; 37 | // 값이 있으면 값을 함수에 적용한다. 38 | return some(f(oa.value)); 39 | }; 40 | 41 | export const mapOrElse = ( 42 | oa: Option, 43 | f: (a: A) => B, 44 | defaultValue: B 45 | ): B => { 46 | return getOrElse(map(oa, f), defaultValue); 47 | }; 48 | -------------------------------------------------------------------------------- /chapter4/src/test.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | 4 | const stockItem = (item: Item): string => ` 5 |
  • 6 |

    ${item.name}

    7 |
    가격: ${item.price}원
    8 |
    수량: ${item.quantity}상자
    9 |
  • 10 | `; 11 | 12 | const outOfStockItem = (item: Item): string => ` 13 |
  • 14 |

    ${item.name} (품절)

    15 |
    가격: ${item.price}원
    16 |
    수량: ${item.quantity}상자
    17 |
  • 18 | `; 19 | 20 | const item = (item: Item): string => 21 | item.outOfStock ? outOfStockItem(item) : stockItem(item); 22 | 23 | const totalCalculator = ( 24 | list: Array, 25 | getValue: (item: Item) => number 26 | ) => { 27 | // 전체 목록중 재고가 있는 상품만 getValue를 실행하고 그 값을 모두 더한다 28 | 29 | return ( 30 | list 31 | // 1. 재고가 있는 상품만 분류하기 32 | .filter((item) => item.outOfStock === false) 33 | // 2. 분류된 상품들에 대해서 getValue 실행하기 34 | .map(getValue) 35 | // 3. getValue가 실행된 값 모두 더하기 36 | .reduce((total, value) => total + value, 0) 37 | ); 38 | }; 39 | 40 | const totalCount = (list: Array): string => { 41 | const totalCount = totalCalculator(list, (item) => item.quantity); 42 | return `

    전체 수량: ${totalCount}상자`; 43 | }; 44 | 45 | const totalPrice = (list: Array): string => { 46 | const totalPrice = totalCalculator( 47 | list, 48 | (item) => item.price * item.quantity 49 | ); 50 | return `

    전체 가격: ${totalPrice}원`; 51 | }; 52 | 53 | const list = (list: Array) => { 54 | return ` 55 |
      56 | ${list 57 | // 1. 목록에 있는 모든 아이템을 태그로 변경 58 | .map(item) 59 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 60 | .reduce((tags, tag) => tags + tag, "")} 61 |
    62 | `; 63 | }; 64 | 65 | const app = document.getElementById("app"); 66 | if (app != null) { 67 | app.innerHTML = ` 68 |

    장바구니

    69 | ${list(cart)} 70 | ${totalCount(cart)} 71 | ${totalPrice(cart)} 72 | `; 73 | } 74 | -------------------------------------------------------------------------------- /chapter4/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "es6", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } -------------------------------------------------------------------------------- /chapter5/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /chapter5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter5-github", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1" 12 | }, 13 | "devDependencies": { 14 | "typescript": "4.4.4" 15 | }, 16 | "resolutions": { 17 | "@babel/preset-env": "7.13.8" 18 | }, 19 | "keywords": [] 20 | } -------------------------------------------------------------------------------- /chapter5/src/clip3-partial-application.ts: -------------------------------------------------------------------------------- 1 | const delivery = (present: string, from: string) => (to: string) => { 2 | return ` 3 | 보내는 물건: ${present} 4 | 보내는 사람: ${from} 5 | 받는 사람: ${to} 6 | `; 7 | }; 8 | 9 | export const main = () => { 10 | console.clear(); 11 | 12 | const momsPresent = delivery("상품권", "엄마"); 13 | console.log(momsPresent("아들")); 14 | console.log(momsPresent("딸")); 15 | console.log(momsPresent("할머니")); 16 | }; 17 | -------------------------------------------------------------------------------- /chapter5/src/clip3.ts: -------------------------------------------------------------------------------- 1 | const delivery = (present: string, from: string, to: string) => { 2 | return ` 3 | 보내는 물건: ${present} 4 | 보내는 사람: ${from} 5 | 받는 사람: ${to} 6 | `; 7 | }; 8 | 9 | export const main = () => { 10 | console.clear(); 11 | 12 | console.log(delivery("상품권", "엄마", "아들")); 13 | console.log(delivery("상품권", "엄마", "딸")); 14 | console.log(delivery("상품권", "엄마", "할머니")); 15 | }; 16 | -------------------------------------------------------------------------------- /chapter5/src/clip4.ts: -------------------------------------------------------------------------------- 1 | const delivery = (present: string, from: string, to: string) => { 2 | return ` 3 | 보내는 물건: ${present} 4 | 보내는 사람: ${from} 5 | 받는 사람: ${to} 6 | `; 7 | }; 8 | 9 | const currying3 = (f: (a: A, b: B, c: C) => D) => (a: A) => ( 10 | b: B 11 | ) => (c: C) => f(a, b, c); 12 | 13 | const curriedDelivery = currying3(delivery); 14 | 15 | export const main = () => { 16 | console.clear(); 17 | 18 | const momsPresent = curriedDelivery("상품권")("엄마"); 19 | console.log(momsPresent("아들")); 20 | }; 21 | -------------------------------------------------------------------------------- /chapter5/src/clip5.ts: -------------------------------------------------------------------------------- 1 | import * as O from "./option"; 2 | 3 | export const curry2 = (f: (a: A, b: B) => C) => (a: A) => (b: B): C => 4 | f(a, b); 5 | 6 | export const flip = (f: (a: A, b: B) => C) => (b: B, a: A): C => 7 | f(a, b); 8 | 9 | // Array
    == A[] 10 | // map :: (Array, (A => B)) => Array 11 | export const map = (array: Array, f: (a: A) => B): Array => { 12 | const result: Array = []; 13 | for (const value of array) { 14 | result.push(f(value)); 15 | } 16 | return result; 17 | }; 18 | 19 | export const main = () => { 20 | const numbers = [1, 2, 3]; 21 | const isEven = (x: number) => x % 2 === 0; 22 | 23 | map(numbers, isEven); 24 | 25 | // curriedMap :: Array => ((A => B) => Array) 26 | const curriedMap = curry2(map); 27 | curriedMap(numbers)(isEven); 28 | 29 | // map :: Array ~> (A => B) => Array 30 | numbers.map(isEven); 31 | 32 | // map_ :: (A => B) => (Array => Array) 33 | const map_ = curry2(flip(map)); 34 | map_(isEven)(numbers); 35 | 36 | // isEven :: number => boolean 37 | // mapIsEven :: Array => Array 38 | const mapIsEven = map_(isEven); 39 | 40 | isEven(42); 41 | isEven(7); 42 | 43 | mapIsEven(numbers); 44 | mapIsEven([]); 45 | mapIsEven([42]); 46 | 47 | const omap = curry2(flip(O.map)); 48 | // optionIsEven :: Option => Option 49 | const optionIsEven = omap(isEven); 50 | optionIsEven(O.some(42)); 51 | optionIsEven(O.none()); 52 | }; 53 | -------------------------------------------------------------------------------- /chapter5/src/compose.ts: -------------------------------------------------------------------------------- 1 | export const compose = (g: (y: B) => C, f: (x: A) => B) => (x: A) => { 2 | return g(f(x)); 3 | }; 4 | 5 | // compose 함수의 원래 타입 6 | // (g: (y: B) => C, f: (x: A) => B) => (x: A) => C 7 | 8 | // 매개변수를 지워서 단순하게 표기 9 | // ((B) => C, (A) => B) => (A) => C 10 | 11 | export function getPrice(name: string): number | undefined { 12 | if (name === "tomato") { 13 | return 7000; 14 | } else if (name === "orange") { 15 | return 15000; 16 | } else if (name === "apple") { 17 | return 10000; 18 | } 19 | } 20 | 21 | export const isExpensive = (price: number | undefined) => { 22 | // getPrice의 결과에 undefined가 포함 되어 있기 때문에 23 | // getPrice와 합성을 위해 undefined에 대한 처리 필요 24 | if (price === undefined) { 25 | return false; 26 | } 27 | return price > 10000; 28 | }; 29 | 30 | // isExpensive와 getPrice를 합성해서 하나의 함수로 31 | export const isExpensivePrice = compose(isExpensive, getPrice); 32 | -------------------------------------------------------------------------------- /chapter5/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./styles.css"; 2 | import * as C from "./clip4"; 3 | 4 | C.main(); 5 | 6 | document.getElementById("app").innerHTML = ` 7 |

    Hello Vanilla!

    8 |
    `; 13 | -------------------------------------------------------------------------------- /chapter5/src/option.ts: -------------------------------------------------------------------------------- 1 | // 값이 있을수도, 없을수도 있는 자료구조. 2 | 3 | export type Some = { 4 | _tag: "some"; 5 | value: A; 6 | }; 7 | 8 | export type None = { 9 | _tag: "none"; 10 | }; 11 | 12 | export type Option = Some | None; 13 | 14 | export const some = (value: A): Option => ({ _tag: "some", value }); 15 | 16 | export const none = (): Option => ({ _tag: "none" }); 17 | 18 | export const isSome = (oa: Option): oa is Some => oa._tag === "some"; 19 | 20 | export const isNone = (oa: Option): oa is None => oa._tag === "none"; 21 | 22 | export const fromUndefined = (a: A | undefined): Option => { 23 | if (a === undefined) return none(); 24 | return some(a); 25 | }; 26 | 27 | export const getOrElse = (oa: Option, defaultValue: A): A => { 28 | // 값이 없으면 지정된 값을 사용하고 29 | if (isNone(oa)) return defaultValue; 30 | // 값이 있다면 해당 값을 사용한다. 31 | return oa.value; 32 | }; 33 | 34 | export const map = (oa: Option, f: (a: A) => B): Option => { 35 | // 값이 없으면 값이 없는 상태를 유지한다. 36 | if (isNone(oa)) return oa; 37 | // 값이 있으면 값을 함수에 적용한다. 38 | return some(f(oa.value)); 39 | }; 40 | -------------------------------------------------------------------------------- /chapter5/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /chapter5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "es6", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } -------------------------------------------------------------------------------- /chapter6/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter6/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /chapter6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter6", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1" 12 | }, 13 | "devDependencies": { 14 | "typescript": "4.4.4" 15 | }, 16 | "resolutions": { 17 | "@babel/preset-env": "7.13.8" 18 | }, 19 | "keywords": [] 20 | } -------------------------------------------------------------------------------- /chapter6/src/cart.ts: -------------------------------------------------------------------------------- 1 | export interface Item { 2 | readonly code: string; 3 | readonly outOfStock: boolean; 4 | readonly name: string; 5 | readonly price: number; 6 | // 상품은 반드시 한 개 이상 선택하여야 한다. 7 | // 또한 한 번에 10개가 넘는 상품을 구매할 수 없다. 8 | readonly quantity: number; 9 | } 10 | 11 | export const cart: Array = [ 12 | { 13 | code: "tomato", 14 | outOfStock: false, 15 | name: "토마토", 16 | price: 7000, 17 | quantity: 2 18 | }, 19 | { 20 | code: "orange", 21 | outOfStock: false, 22 | name: "오렌지", 23 | price: 15000, 24 | quantity: -2 25 | }, 26 | { 27 | code: "apple", 28 | outOfStock: true, 29 | name: "사과", 30 | price: 10000, 31 | quantity: 2 32 | }, 33 | { 34 | code: "mango", 35 | outOfStock: false, 36 | name: "망고", 37 | price: 12000, 38 | quantity: 20 39 | }, 40 | { 41 | code: "grape", 42 | outOfStock: false, 43 | name: "포도", 44 | price: 5000, 45 | quantity: 10 46 | } 47 | ]; 48 | -------------------------------------------------------------------------------- /chapter6/src/clip5.ts: -------------------------------------------------------------------------------- 1 | console.clear(); 2 | 3 | const tenDivideBy = (n: number): number => { 4 | if (n === 0) { 5 | throw new Error("0으로 나눌 수 없습니다."); 6 | } 7 | return 10 / n; 8 | }; 9 | 10 | const test = () => { 11 | try { 12 | // y를 try 블럭 바깥으로 이동시키면 동작이 변경됨. 13 | const y = tenDivideBy(0); 14 | return y; 15 | } catch (e) { 16 | return 1; 17 | } 18 | }; 19 | 20 | export const main = () => { 21 | const x = test(); 22 | console.log(x); 23 | console.log("프로그램이 종료 되었습니다."); 24 | }; 25 | -------------------------------------------------------------------------------- /chapter6/src/clip8-original.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | import * as O from "./option"; 4 | 5 | console.clear(); 6 | 7 | type ArrayItem = Array; 8 | 9 | const stockItem = (item: Item): string => { 10 | return ` 11 |
  • 12 |

    ${item.name}

    13 |
    가격: ${item.price}원
    14 |
    수량: ${item.quantity}상자
    15 |
  • 16 | `; 17 | }; 18 | 19 | const outOfStockItem = (item: Item): string => ` 20 |
  • 21 |

    ${item.name} (품절)

    22 |
    가격: ${item.price}원
    23 |
  • 24 | `; 25 | 26 | const renderItem = (item: Item): string => { 27 | if (item.outOfStock) { 28 | return outOfStockItem(item); 29 | } else { 30 | return stockItem(item); 31 | } 32 | }; 33 | 34 | const totalCalculator = (list: ArrayItem, getValue: (item: Item) => number) => { 35 | return list 36 | .filter((item) => item.outOfStock === false) 37 | .map(getValue) 38 | .reduce((total, value) => total + value, 0); 39 | }; 40 | 41 | const totalCount = (list: ArrayItem): string => { 42 | const totalCount = totalCalculator(list, (item) => item.quantity); 43 | return `

    전체 수량: ${totalCount}상자`; 44 | }; 45 | 46 | const totalPrice = (list: ArrayItem): string => { 47 | const totalPrice = totalCalculator( 48 | list, 49 | (item) => item.price * item.quantity 50 | ); 51 | 52 | return `

    전체 가격: ${totalPrice}`; 53 | }; 54 | 55 | const list = (list: ArrayItem) => { 56 | return ` 57 |
      58 | ${list 59 | // 1. 목록에 있는 모든 아이템을 태그로 변경 60 | .map(renderItem) 61 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 62 | .reduce((tags, tag) => tags + tag, "")} 63 |
    64 | `; 65 | }; 66 | 67 | const render = (cart: ArrayItem) => { 68 | O.map(O.fromNull(document.getElementById("app")), (app) => { 69 | app.innerHTML = ` 70 |

    장바구니

    71 | ${list(cart)} 72 | ${totalCount(cart)} 73 | ${totalPrice(cart)} 74 | `; 75 | }); 76 | }; 77 | 78 | export const main = () => { 79 | render(cart); 80 | }; 81 | -------------------------------------------------------------------------------- /chapter6/src/clip8.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | import * as O from "./option"; 4 | import * as T from "./try"; 5 | 6 | console.clear(); 7 | 8 | type ParsedItem = { _tag: "parsedItem" } & Item; 9 | 10 | type ParseError = { 11 | name: string; 12 | message: string; 13 | }; 14 | 15 | const parseItem = (item: Item): T.Try => { 16 | if (item.quantity < 1) { 17 | return T.failed({ 18 | name: item.name, 19 | message: "상품은 반드시 한 개 이상 담아야 합니다." 20 | }); 21 | } else if (item.quantity > 10) { 22 | return T.failed({ 23 | name: item.name, 24 | message: "한 번에 10개를 초과하여 구매할 수 없습니다." 25 | }); 26 | } 27 | 28 | return T.success({ 29 | _tag: "parsedItem", 30 | ...item 31 | }); 32 | }; 33 | 34 | type ArrayItem = Array>; 35 | 36 | const stockItem = (item: ParsedItem): string => { 37 | return ` 38 |
  • 39 |

    ${item.name}

    40 |
    가격: ${item.price}원
    41 |
    수량: ${item.quantity}상자
    42 |
  • 43 | `; 44 | }; 45 | 46 | const errorItem = (e: ParseError): string => ` 47 |
  • 48 |

    ${e.name}

    49 |
    ${e.message}
    50 |
  • 51 | `; 52 | 53 | const outOfStockItem = (item: ParsedItem): string => ` 54 |
  • 55 |

    ${item.name} (품절)

    56 |
    가격: ${item.price}원
    57 |
  • 58 | `; 59 | 60 | const renderItem = (item: ParsedItem): string => { 61 | if (item.outOfStock) { 62 | return outOfStockItem(item); 63 | } else { 64 | return stockItem(item); 65 | } 66 | }; 67 | 68 | const totalCalculator = (list: ArrayItem, getValue: (item: ParsedItem) => number) => { 69 | return T.KeepSuccess(list) 70 | // Array> => Array 71 | .filter((item) => { 72 | try { 73 | return item.outOfStock === false; 74 | } catch (e) { 75 | return false; 76 | } 77 | }) 78 | .map(getValue) 79 | .reduce((total, value) => total + value, 0); 80 | }; 81 | 82 | const totalCount = (list: ArrayItem): string => { 83 | const totalCount = totalCalculator(list, (item) => item.quantity); 84 | return `

    전체 수량: ${totalCount}상자`; 85 | }; 86 | 87 | const totalPrice = (list: ArrayItem): string => { 88 | const totalPrice = totalCalculator( 89 | list, 90 | (item) => item.price * item.quantity 91 | ); 92 | 93 | return `

    전체 가격: ${totalPrice}`; 94 | }; 95 | 96 | const list = (list: ArrayItem) => { 97 | return ` 98 |
      99 | ${list 100 | // 1. 목록에 있는 모든 아이템을 태그로 변경 101 | .map(item => T.getOrElse( 102 | T.map(item, parsedItem => renderItem(parsedItem)), 103 | errorItem 104 | )) 105 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 106 | .reduce((tags, tag) => tags + tag, "")} 107 |
    108 | `; 109 | }; 110 | 111 | const render = (cart: ArrayItem) => { 112 | O.map(O.fromNull(document.getElementById("app")), (app) => { 113 | app.innerHTML = ` 114 |

    장바구니

    115 | ${list(cart)} 116 | ${totalCount(cart)} 117 | ${totalPrice(cart)} 118 | `; 119 | }); 120 | }; 121 | 122 | export const main = () => { 123 | render(cart.map(parseItem)); 124 | }; 125 | -------------------------------------------------------------------------------- /chapter6/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 0.8rem; 3 | } 4 | 5 | .gray { 6 | color: gray; 7 | } 8 | 9 | .strike { 10 | text-decoration: line-through; 11 | } 12 | -------------------------------------------------------------------------------- /chapter6/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as C from "./clip8"; 2 | 3 | C.main(); 4 | -------------------------------------------------------------------------------- /chapter6/src/option.ts: -------------------------------------------------------------------------------- 1 | // 값이 있을수도, 없을수도 있는 자료구조. 2 | 3 | export type Some
    = { 4 | _tag: "some"; 5 | value: A; 6 | }; 7 | 8 | export type None = { 9 | _tag: "none"; 10 | }; 11 | 12 | export type Option = Some | None; 13 | 14 | export const some = (value: A): Option => ({ _tag: "some", value }); 15 | 16 | export const none = (): Option => ({ _tag: "none" }); 17 | 18 | export const isSome = (oa: Option): oa is Some => oa._tag === "some"; 19 | 20 | export const isNone = (oa: Option): oa is None => oa._tag === "none"; 21 | 22 | export const fromUndefined = (a: A | undefined): Option => { 23 | if (a === undefined) return none(); 24 | return some(a); 25 | }; 26 | 27 | export const fromNull = (a: A | null): Option => { 28 | if (a === null) return none(); 29 | return some(a); 30 | }; 31 | 32 | export const getOrElse = (oa: Option, defaultValue: A): A => { 33 | // 값이 없으면 지정된 값을 사용하고 34 | if (isNone(oa)) return defaultValue; 35 | // 값이 있다면 해당 값을 사용한다. 36 | return oa.value; 37 | }; 38 | 39 | export const map = (oa: Option, f: (a: A) => B): Option => { 40 | // 값이 없으면 값이 없는 상태를 유지한다. 41 | if (isNone(oa)) return oa; 42 | // 값이 있으면 값을 함수에 적용한다. 43 | return some(f(oa.value)); 44 | }; 45 | 46 | export const mapOrElse = ( 47 | oa: Option, 48 | f: (a: A) => B, 49 | defaultValue: B 50 | ): B => { 51 | return getOrElse(map(oa, f), defaultValue); 52 | }; 53 | -------------------------------------------------------------------------------- /chapter6/src/try.ts: -------------------------------------------------------------------------------- 1 | // 실패하거나 성공했을때 각각 다른 값을 가지는 자료 구조 2 | 3 | type Success = { 4 | readonly _tag: "success"; 5 | readonly result: R; 6 | }; 7 | 8 | type Failed = { 9 | readonly _tag: "failed"; 10 | readonly error: E; 11 | }; 12 | 13 | export type Try = Failed | Success; 14 | 15 | export const success = (result: R): Try => ({ 16 | _tag: "success", 17 | result 18 | }); 19 | 20 | export const failed = (error: E): Try => ({ 21 | _tag: "failed", 22 | error 23 | }); 24 | 25 | export const isSuccess = (ta: Try): ta is Success => 26 | ta._tag === "success"; 27 | 28 | export const isFailed = (ta: Try): ta is Failed => 29 | ta._tag === "failed"; 30 | 31 | export const getOrElse = ( 32 | ta: Try, 33 | defaultValue: (e: E) => R 34 | ): R => { 35 | // 에러가 있을 경우 에러에 기반하여 기본 값을 결정한다. 36 | if (isFailed(ta)) return defaultValue(ta.error); 37 | // 결과가 성공이라면 해당 값을 사용한다. 38 | return ta.result; 39 | }; 40 | 41 | export const map = (ta: Try, f: (a: A) => B): Try => { 42 | if (isFailed(ta)) return ta; 43 | return success(f(ta.result)); 44 | } 45 | 46 | // Array> => Array 47 | export const KeepSuccess = 48 | (tas: Array>): Array => { 49 | const ret = tas.flatMap((ta) => { 50 | if (isSuccess(ta)) return [ta.result]; 51 | else return []; 52 | }) 53 | return ret; 54 | } 55 | 56 | export const KeepSuccessWithFor = (tas: Array>): Array => { 57 | const ret: Array = []; 58 | for (const ta of tas) { 59 | if (isSuccess(ta)) { 60 | ret.push(ta.result); 61 | } 62 | } 63 | return ret; 64 | } 65 | 66 | // flatMap :: (A => Array) => (Array => Array) 67 | // map :: (A => B) => (Array => Array) 68 | 69 | // flat :: Try> => Try 70 | export const flat = (tta: Try>): Try => { 71 | if (isSuccess(tta)) return tta.result; 72 | return tta; 73 | } 74 | 75 | export const flatMap = (ta: Try, f: (a: A) => Try): Try => { 76 | return flat(map(ta, f)); 77 | } 78 | -------------------------------------------------------------------------------- /chapter6/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "esnext", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } -------------------------------------------------------------------------------- /chapter7/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter7/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /chapter7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter7", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1" 12 | }, 13 | "devDependencies": { 14 | "typescript": "4.4.4" 15 | }, 16 | "resolutions": { 17 | "@babel/preset-env": "7.13.8" 18 | }, 19 | "keywords": [] 20 | } -------------------------------------------------------------------------------- /chapter7/src/clip3.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | const f = (str: string) => { 4 | setTimeout(() => { 5 | console.log("비동기로 출력: " + str); 6 | return str.length * 2; 7 | }, 500); 8 | }; 9 | 10 | const g = (n: number) => { 11 | return n + 1; 12 | }; 13 | 14 | const h = (x: number) => { 15 | return x % 3 === 0; 16 | }; 17 | 18 | const handleError = (e: unknown) => { 19 | // 사용자에게 에러를 알려주는 통합 함수 20 | console.log("handleError: " + e); 21 | } 22 | 23 | const program = (s: boolean) => { 24 | console.log(s); 25 | } 26 | 27 | const greeting = (name: string) => { 28 | console.log("Hello, " + name); 29 | } 30 | 31 | const id =
    (a: A): A => { 32 | return a; 33 | } 34 | 35 | const cpsId = (a: A, ret: (a: A) => void) => { 36 | ret(a); 37 | } 38 | 39 | // continuation-passing style -> CPS 40 | // direct style 41 | 42 | export const main = () => { 43 | const a = id("test"); 44 | console.log(a); 45 | 46 | cpsId("test", (a) => { 47 | console.log(a); 48 | }); 49 | 50 | greeting("World"); 51 | console.log("프로그램이 종료되었습니다."); 52 | } -------------------------------------------------------------------------------- /chapter7/src/clip4.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | const f = (str: string, ret: (x: number) => void) => { 4 | setTimeout(() => { 5 | console.log("f 호출: " + str); 6 | ret(str.length * 2); 7 | }, 500); 8 | }; 9 | 10 | const g = (n: number, ret: (x: number) => void) => { 11 | setTimeout(() => { 12 | console.log("g 호출: " + n); 13 | ret(n + 1); 14 | }, 500); 15 | }; 16 | 17 | const h = (x: number, ret: (x: boolean) => void) => { 18 | setTimeout(() => { 19 | console.log("h 호출: " + x); 20 | ret(x % 3 === 0); 21 | }, 500); 22 | }; 23 | 24 | const handleError = (e: unknown) => { 25 | // 사용자에게 에러를 알려주는 통합 함수 26 | console.log("handleError: " + e); 27 | } 28 | 29 | const program = (s: boolean) => { 30 | console.log(s); 31 | } 32 | 33 | const greeting = (name: string) => { 34 | console.log("Hello, " + name); 35 | } 36 | 37 | export const main = () => { 38 | f("test", (a) => { 39 | g(a, (b) => { 40 | h(b, (c) => { 41 | program(c); 42 | }); 43 | }); 44 | }); 45 | 46 | greeting("World"); 47 | console.log("프로그램이 종료되었습니다."); 48 | } -------------------------------------------------------------------------------- /chapter7/src/clip5.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | // Option = None | Some 4 | // Try = Failed | Success 5 | // Async = ??? 6 | 7 | type Async = (ret: (x: A) => void) => void; 8 | 9 | const resolve = (a: A): Async => { 10 | return (ret) => { 11 | ret(a); 12 | } 13 | } 14 | 15 | const flatMap = (a: Async, f: (a: A) => Async): Async => { 16 | return (ret) => { 17 | a((a_) => { 18 | const b = f(a_); 19 | b((b_) => ret(b_)); 20 | }); 21 | }; 22 | } 23 | 24 | const map = (a: Async, f: (a: A) => B): Async => { 25 | return flatMap(a, (a_) => resolve(f(a_))); 26 | } 27 | 28 | const run = (a: Async) => { 29 | a(() => { return }); 30 | } 31 | 32 | const f = (str: string): Async => (ret) => { 33 | setTimeout(() => { 34 | console.log("f 호출: " + str); 35 | ret(str.length * 2); 36 | }, 500); 37 | }; 38 | 39 | const g = (n: number): Async => (ret) => { 40 | setTimeout(() => { 41 | console.log("g 호출: " + n); 42 | ret(n + 1); 43 | }, 500); 44 | }; 45 | 46 | const h = (x: number): Async => (ret) => { 47 | setTimeout(() => { 48 | console.log("h 호출: " + x); 49 | ret(x % 3 === 0); 50 | }, 500); 51 | }; 52 | 53 | const handleError = (e: unknown) => { 54 | // 사용자에게 에러를 알려주는 통합 함수 55 | console.log("handleError: " + e); 56 | } 57 | 58 | const program = (s: boolean) => { 59 | console.log(s); 60 | } 61 | 62 | const greeting = (name: string) => { 63 | console.log("Hello, " + name); 64 | } 65 | 66 | export async function main() { 67 | const a = f("ab"); 68 | const b = flatMap(a, (a_) => g(a_)); 69 | const c = flatMap(b, (b_) => h(b_)); 70 | const result =map(c, (c_) => program(c_)); 71 | run(result); 72 | 73 | greeting("World"); 74 | console.log("프로그램이 종료되었습니다."); 75 | } 76 | -------------------------------------------------------------------------------- /chapter7/src/clip6.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | // Option = None | Some 4 | // Try = Failed | Success 5 | // Async = ??? 6 | 7 | type Async = (ret: (x: A) => void) => void; 8 | 9 | const resolve = (a: A): Async => { 10 | return (ret) => { 11 | ret(a); 12 | } 13 | } 14 | 15 | const flatMap = (a: Async, f: (a: A) => Async): Async => { 16 | return (ret) => { 17 | a((a_) => { 18 | const b = f(a_); 19 | b((b_) => ret(b_)); 20 | }) 21 | }; 22 | }; 23 | 24 | const map = (a: Async, f: (a: A) => B): Async => { 25 | return flatMap(a, (a_) => resolve(f(a_))); 26 | }; 27 | 28 | const run = (a: Async) => { 29 | a(() => { return }); 30 | } 31 | 32 | const f = (str: string): Promise => new Promise((resolve, reject) => { 33 | if (str === "") { 34 | reject("빈 문자열은 입력할 수 없습니다.") 35 | return; 36 | } 37 | 38 | setTimeout(() => { 39 | console.log("f 호출: " + str); 40 | resolve(str.length * 2); 41 | }, 500); 42 | }); 43 | 44 | const g = (n: number): Promise => new Promise((resolve, reject) => { 45 | if (n === 6) { 46 | reject("6은 입력할 수 없습니다."); 47 | return; 48 | } 49 | 50 | setTimeout(() => { 51 | console.log("g 호출: " + n); 52 | resolve(n + 1); 53 | }, 500); 54 | }); 55 | 56 | const h = (x: number): Promise => new Promise((resolve, reject) => { 57 | if (x === 5) { 58 | reject("5는 입력할 수 없습니다."); 59 | return; 60 | } 61 | 62 | setTimeout(() => { 63 | console.log("h 호출: " + x); 64 | resolve(x % 3 === 0); 65 | }, 500); 66 | }); 67 | 68 | const handleError = (e: unknown) => { 69 | // 사용자에게 에러를 알려주는 통합 함수 70 | console.log("handleError: " + e); 71 | } 72 | 73 | const program = (s: boolean) => { 74 | console.log(s); 75 | } 76 | 77 | const greeting = (name: string) => { 78 | console.log("Hello, " + name); 79 | } 80 | 81 | export const main = async () => { 82 | const a = f("abc"); 83 | const b = a.then(g); 84 | const c = b.then(h); 85 | const result = c.then(program); 86 | result.catch(handleError); 87 | 88 | f("abc").then(g).then(h).then(program).catch(handleError); 89 | 90 | greeting("World"); 91 | console.log("프로그램이 종료되었습니다."); 92 | } 93 | -------------------------------------------------------------------------------- /chapter7/src/clip7.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | // Option = None | Some 4 | // Try = Failed | Success 5 | // Async = ??? 6 | 7 | type Async = (ret: (x: A) => void) => void; 8 | 9 | const resolve = (a: A): Async => { 10 | return (ret) => { 11 | ret(a); 12 | } 13 | } 14 | 15 | const flatMap = (a: Async, f: (a: A) => Async): Async => { 16 | return (ret) => { 17 | a((a_) => { 18 | const b = f(a_); 19 | b((b_) => ret(b_)); 20 | }); 21 | }; 22 | } 23 | 24 | const map = (a: Async, f: (a: A) => B): Async => { 25 | return flatMap(a, (a_) => resolve(f(a_))); 26 | } 27 | 28 | const run = (a: Async) => { 29 | a(() => { return }); 30 | } 31 | 32 | const f = (str: string): Promise => new Promise((resolve, reject) => { 33 | if (str === "") { 34 | reject("빈 문자열은 입력할 수 없습니다."); 35 | return; 36 | } 37 | setTimeout(() => { 38 | console.log("f 호출: " + str); 39 | resolve(str.length * 2); 40 | }, 500); 41 | }); 42 | 43 | const g = (n: number): Promise => new Promise((resolve, reject) => { 44 | if (n === 6) { 45 | reject("6은 입력할 수 없습니다."); 46 | return; 47 | } 48 | setTimeout(() => { 49 | console.log("g 호출: " + n); 50 | resolve(n + 1); 51 | }, 500); 52 | }); 53 | 54 | const h = (x: number): Promise => new Promise((resolve, reject) => { 55 | if (x === 5) { 56 | reject("5는 입력할 수 없습니다."); 57 | return; 58 | } 59 | setTimeout(() => { 60 | console.log("h 호출: " + x); 61 | resolve(x % 3 === 0); 62 | }, 500); 63 | }); 64 | 65 | const handleError = (e: unknown) => { 66 | // 사용자에게 에러를 알려주는 통합 함수 67 | console.log("handleError: " + e); 68 | } 69 | 70 | const program = (s: boolean) => { 71 | console.log(s); 72 | } 73 | 74 | const greeting = (name: string) => { 75 | console.log("Hello, " + name); 76 | } 77 | 78 | export async function main() { 79 | try { 80 | const a = await f("abc"); 81 | const b = await g(a); 82 | const c = await h(b); 83 | program(c); 84 | } catch(e) { 85 | handleError(e); 86 | } 87 | 88 | greeting("World"); 89 | console.log("프로그램이 종료되었습니다."); 90 | } 91 | -------------------------------------------------------------------------------- /chapter7/src/clip8-1.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | // Option = None | Some 4 | // Try = Failed | Success 5 | // Async = ??? 6 | 7 | type Async = (ret: (x: A) => void) => void; 8 | 9 | const resolve = (a: A): Async => { 10 | return (ret) => { 11 | ret(a); 12 | } 13 | } 14 | 15 | const flatMap = (a: Async, f: (a: A) => Async): Async => { 16 | return (ret) => { 17 | a((a_) => { 18 | const b = f(a_); 19 | b((b_) => ret(b_)); 20 | }); 21 | }; 22 | } 23 | 24 | const map = (a: Async, f: (a: A) => B): Async => { 25 | return flatMap(a, (a_) => resolve(f(a_))); 26 | } 27 | 28 | const run = (a: Async) => { 29 | a(() => { return }); 30 | } 31 | 32 | const f = (str: string): Async => (ret) => { 33 | setTimeout(() => { 34 | console.log("f 호출: " + str); 35 | ret(str.length * 2); 36 | }, 500); 37 | }; 38 | 39 | const g = (n: number): Async => (ret) => { 40 | setTimeout(() => { 41 | console.log("g 호출: " + n); 42 | ret(n + 1); 43 | }, 500); 44 | }; 45 | 46 | const h = (x: number): Async => (ret) => { 47 | setTimeout(() => { 48 | console.log("h 호출: " + x); 49 | ret(x % 3 === 0); 50 | }, 500); 51 | }; 52 | 53 | const handleError = (e: unknown) => { 54 | // 사용자에게 에러를 알려주는 통합 함수 55 | console.log("handleError: " + e); 56 | } 57 | 58 | const program = (s: boolean) => { 59 | console.log(s); 60 | } 61 | 62 | const greeting = (name: string) => { 63 | console.log("Hello, " + name); 64 | } 65 | 66 | const four = 2 * 2; 67 | 68 | const lazyFour = () => 2 * 2; 69 | 70 | export async function main() { 71 | const a = f("test"); 72 | const b = flatMap(a, (a_) => g(a_)); 73 | const c = flatMap(b, (b_) => h(b_)); 74 | const result = map(c, (c_) => program(c_)); 75 | run(result); 76 | run(result); 77 | 78 | greeting("World"); 79 | console.log("프로그램이 종료되었습니다."); 80 | } 81 | -------------------------------------------------------------------------------- /chapter7/src/clip8-2.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | // Option = None | Some 4 | // Try = Failed | Success 5 | // Async = ??? 6 | 7 | type Async = (ret: (x: A) => void) => void; 8 | 9 | const resolve = (a: A): Async => { 10 | return (ret) => { 11 | ret(a); 12 | } 13 | } 14 | 15 | const flatMap = (a: Async, f: (a: A) => Async): Async => { 16 | return (ret) => { 17 | a((a_) => { 18 | const b = f(a_); 19 | b((b_) => ret(b_)); 20 | }) 21 | }; 22 | }; 23 | 24 | const map = (a: Async, f: (a: A) => B): Async => { 25 | return flatMap(a, (a_) => resolve(f(a_))); 26 | }; 27 | 28 | const run = (a: Async) => { 29 | a(() => { return }); 30 | } 31 | 32 | const f = (str: string): Promise => new Promise((resolve, reject) => { 33 | if (str === "") { 34 | reject("빈 문자열은 입력할 수 없습니다.") 35 | return; 36 | } 37 | 38 | setTimeout(() => { 39 | console.log("f 호출: " + str); 40 | resolve(str.length * 2); 41 | }, 500); 42 | }); 43 | 44 | const g = (n: number): Promise => new Promise((resolve, reject) => { 45 | if (n === 6) { 46 | reject("6은 입력할 수 없습니다."); 47 | return; 48 | } 49 | 50 | setTimeout(() => { 51 | console.log("g 호출: " + n); 52 | resolve(n + 1); 53 | }, 500); 54 | }); 55 | 56 | const h = (x: number): Promise => new Promise((resolve, reject) => { 57 | if (x === 5) { 58 | reject("5는 입력할 수 없습니다."); 59 | return; 60 | } 61 | 62 | setTimeout(() => { 63 | console.log("h 호출: " + x); 64 | resolve(x % 3 === 0); 65 | }, 500); 66 | }); 67 | 68 | const handleError = (e: unknown) => { 69 | // 사용자에게 에러를 알려주는 통합 함수 70 | console.log("handleError: " + e); 71 | } 72 | 73 | const program = (s: boolean) => { 74 | console.log(s); 75 | } 76 | 77 | const greeting = (name: string) => { 78 | console.log("Hello, " + name); 79 | } 80 | 81 | export const main = async () => { 82 | const a = f("abc"); 83 | const b = a.then(g); 84 | const c = b.then(h); 85 | const result = c.then(program); 86 | result.catch(handleError); 87 | 88 | f("abc").then(g).then(h).then(program).catch(handleError); 89 | 90 | greeting("World"); 91 | console.log("프로그램이 종료되었습니다."); 92 | } 93 | -------------------------------------------------------------------------------- /chapter7/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./styles.css"; 2 | 3 | import * as main from './clip8-1'; 4 | 5 | console.clear(); 6 | main.main(); 7 | 8 | const app = document.getElementById("app"); 9 | if (app !== null) { 10 | app.innerHTML = ` 11 |

    Hello Vanilla!

    12 |
    `; 17 | } -------------------------------------------------------------------------------- /chapter7/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /chapter7/src/try.ts: -------------------------------------------------------------------------------- 1 | // 실패하거나 성공했을때 각각 다른 값을 가지는 자료 구조 2 | 3 | type Success = { 4 | readonly _tag: "success"; 5 | readonly result: R; 6 | }; 7 | 8 | type Failed = { 9 | readonly _tag: "failed"; 10 | readonly error: E; 11 | }; 12 | 13 | export type Try = Failed | Success; 14 | 15 | export const success = (result: R): Try => ({ 16 | _tag: "success", 17 | result 18 | }); 19 | 20 | export const failed = (error: E): Try => ({ 21 | _tag: "failed", 22 | error 23 | }); 24 | 25 | export const isSuccess = (ta: Try): ta is Success => 26 | ta._tag === "success"; 27 | 28 | export const isFailed = (ta: Try): ta is Failed => 29 | ta._tag === "failed"; 30 | 31 | export const getOrElse = ( 32 | ta: Try, 33 | defaultValue: (e: E) => R 34 | ): R => { 35 | // 에러가 있을 경우 에러에 기반하여 기본 값을 결정한다. 36 | if (isFailed(ta)) return defaultValue(ta.error); 37 | // 결과가 성공이라면 해당 값을 사용한다. 38 | return ta.result; 39 | }; 40 | 41 | export const map = (ta: Try, f: (a: A) => B): Try => { 42 | if (isFailed(ta)) return ta; 43 | return success(f(ta.result)); 44 | } 45 | 46 | // Array> => Array 47 | export const KeepSuccess = 48 | (tas: Array>): Array => { 49 | const ret = tas.flatMap((ta) => { 50 | if (isSuccess(ta)) return [ta.result]; 51 | else return []; 52 | }) 53 | return ret; 54 | } 55 | 56 | export const KeepSuccessWithFor = (tas: Array>): Array => { 57 | const ret: Array = []; 58 | for (const ta of tas) { 59 | if (isSuccess(ta)) { 60 | ret.push(ta.result); 61 | } 62 | } 63 | return ret; 64 | } 65 | 66 | // flatMap :: (A => Try) => (Try => Try) 67 | // map :: (A => B) => (Try => Try) 68 | 69 | // flat :: Try> => Try 70 | export const flat = (tta: Try>): Try => { 71 | if (isSuccess(tta)) return tta.result; 72 | return tta; 73 | } 74 | 75 | export const flatMap = (ta: Try, f: (a: A) => Try): Try => { 76 | return flat(map(ta, f)); 77 | } 78 | -------------------------------------------------------------------------------- /chapter7/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "esnext", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } -------------------------------------------------------------------------------- /chapter8-clip8/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter8-clip8/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |
    10 |
    11 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /chapter8-clip8/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter8-clip8", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1", 12 | "rxjs": "7.5.2" 13 | }, 14 | "devDependencies": { 15 | "typescript": "4.4.4" 16 | }, 17 | "resolutions": { 18 | "@babel/preset-env": "7.13.8" 19 | }, 20 | "keywords": [] 21 | } -------------------------------------------------------------------------------- /chapter8-clip8/src/cart.ts: -------------------------------------------------------------------------------- 1 | export interface Item { 2 | readonly code: string; 3 | readonly outOfStock: boolean; 4 | readonly name: string; 5 | readonly price: number; 6 | readonly quantity: number; 7 | } 8 | 9 | export const cart: Array = [ 10 | { 11 | code: "tomato", 12 | outOfStock: false, 13 | name: "토마토", 14 | price: 7000, 15 | quantity: 2 16 | }, 17 | { 18 | code: "orange", 19 | outOfStock: false, 20 | name: "오렌지", 21 | price: 15000, 22 | quantity: 5 23 | }, 24 | { 25 | code: "apple", 26 | outOfStock: false, 27 | name: "사과", 28 | price: 10000, 29 | quantity: 3 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /chapter8-clip8/src/clip7.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | const pipeFunctions = (f: (a: A) => B, g: (b: B) => C) => (a: A): C => { 4 | return g(f(a)); 5 | } 6 | 7 | type Observer = (a: A) => void; 8 | type Observable = (subscribe: Observer) => void; 9 | 10 | const map = (f: (a: A) => B) => (source: Observable): Observable => { 11 | return (subscribe) => { 12 | source(a => { 13 | const b = f(a); 14 | subscribe(b); 15 | }) 16 | } 17 | } 18 | 19 | const filter = (pred: (a: A) => boolean) => (source: Observable): Observable => { 20 | return (subscribe) => { 21 | source(a => { 22 | if (pred(a)) { 23 | subscribe(a); 24 | } 25 | }) 26 | } 27 | } 28 | 29 | const filterObserver = (pred: (a: A) => boolean) => (subscribe: Observer): Observer => { 30 | return a => { 31 | if (pred(a)) { 32 | subscribe(a); 33 | } 34 | } 35 | } 36 | 37 | // map :: (A => B) => Array => Array 38 | // mapObserver :: (A => B) => Observer => Observer 39 | const mapObserver = (f: (a: A) => B) => (subscribe: Observer): Observer => { 40 | return a => { 41 | subscribe(f(a)); 42 | } 43 | } 44 | 45 | // map :: (A => B) => Observable => Observable 46 | // lift:: (Observer => Observer) => Observable => Observable 47 | const lift = (f: (b: Observer) => Observer) => (source: Observable): Observable => { 48 | return (subscribe) => { 49 | source(f(subscribe)) 50 | } 51 | } 52 | 53 | const liftedMap = pipeFunctions(mapObserver, lift); 54 | const liftedFilter = pipeFunctions(filterObserver, lift); 55 | 56 | export const main = () => { 57 | } -------------------------------------------------------------------------------- /chapter8-clip8/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 0.8rem; 3 | } 4 | 5 | .gray { 6 | color: gray; 7 | } 8 | 9 | .strike { 10 | text-decoration: line-through; 11 | } 12 | -------------------------------------------------------------------------------- /chapter8-clip8/src/index.ts: -------------------------------------------------------------------------------- 1 | import { cart } from "./cart"; 2 | import { render } from "./renderList"; 3 | import { renderButttons, createObservables } from "./renderButtons"; 4 | 5 | const main = () => { 6 | console.clear(); 7 | 8 | render(cart); 9 | renderButttons(cart); 10 | createObservables(cart); 11 | }; 12 | 13 | main(); 14 | -------------------------------------------------------------------------------- /chapter8-clip8/src/option.ts: -------------------------------------------------------------------------------- 1 | // 값이 있을수도, 없을수도 있는 자료구조. 2 | 3 | export type Some = { 4 | _tag: "some"; 5 | value: A; 6 | }; 7 | 8 | export type None = { 9 | _tag: "none"; 10 | }; 11 | 12 | export type Option = Some | None; 13 | 14 | export const some = (value: A): Option => ({ _tag: "some", value }); 15 | 16 | export const none = (): Option => ({ _tag: "none" }); 17 | 18 | export const isSome = (oa: Option): oa is Some => oa._tag === "some"; 19 | 20 | export const isNone = (oa: Option): oa is None => oa._tag === "none"; 21 | 22 | export const fromUndefined = (a: A | undefined): Option => { 23 | if (a === undefined) return none(); 24 | return some(a); 25 | }; 26 | 27 | export const fromNull = (a: A | null): Option => { 28 | if (a === null) return none(); 29 | return some(a); 30 | }; 31 | 32 | export const getOrElse = (oa: Option, defaultValue: A): A => { 33 | // 값이 없으면 지정된 값을 사용하고 34 | if (isNone(oa)) return defaultValue; 35 | // 값이 있다면 해당 값을 사용한다. 36 | return oa.value; 37 | }; 38 | 39 | export const map = (oa: Option, f: (a: A) => B): Option => { 40 | // 값이 없으면 값이 없는 상태를 유지한다. 41 | if (isNone(oa)) return oa; 42 | // 값이 있으면 값을 함수에 적용한다. 43 | return some(f(oa.value)); 44 | }; 45 | 46 | export const mapOrElse = ( 47 | oa: Option, 48 | f: (a: A) => B, 49 | defaultValue: B 50 | ): B => { 51 | return getOrElse(map(oa, f), defaultValue); 52 | }; 53 | -------------------------------------------------------------------------------- /chapter8-clip8/src/renderButtons.ts: -------------------------------------------------------------------------------- 1 | import { fromEvent, zip } from "rxjs"; 2 | import { map, scan, mergeAll, mergeMap } from "rxjs/operators"; 3 | 4 | import { Item } from './cart'; 5 | import { render } from './renderList'; 6 | 7 | const observableFromItem = (item: Item) => { 8 | const code = item.code; 9 | const element = document.getElementById(`add-${code}-button`)!; 10 | const observable = fromEvent(element, "click"); 11 | return observable; 12 | } 13 | 14 | const updateItem = (item: Item): Item => { 15 | return { ...item, quantity: item.quantity + 1 }; 16 | } 17 | 18 | const updateItemByCode = (code: string) => (item: Item): Item => { 19 | return item.code === code ? updateItem(item) : item; 20 | } 21 | 22 | const addFruit = (state: Array, code: string) => { 23 | return state.map(updateItemByCode(code)); 24 | } 25 | 26 | // [1,2,3], [a,b,c] => [[1,a], [2,b], [3,c]] 27 | 28 | export const createObservables = (cart: Array) => { 29 | const observables = cart.map(observableFromItem); 30 | const codes = cart.map((item) => item.code); 31 | zip(observables, codes) 32 | .pipe( 33 | mergeMap(([observable, code]) => { 34 | return observable.pipe(map(event => code)); 35 | }), 36 | scan(addFruit, cart), 37 | ) 38 | .subscribe((data) => { 39 | render(data); 40 | }); 41 | 42 | const x = ["tomato", "tomato", "apple"] 43 | .reduce(addFruit, cart); 44 | console.log(x); 45 | } 46 | 47 | const renderButton = (item: Item) => { 48 | return `
    49 | 52 |
    `; 53 | } 54 | 55 | export const renderButttons = (cart: Array) => { 56 | const buttons = document.getElementById("buttons"); 57 | if (buttons === null) { 58 | return; 59 | } 60 | 61 | buttons.innerHTML = cart.map(renderButton).join(""); 62 | } -------------------------------------------------------------------------------- /chapter8-clip8/src/renderList.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { cart, Item } from "./cart"; 3 | import * as O from "./option"; 4 | 5 | console.clear(); 6 | 7 | type ArrayItem = Array; 8 | 9 | const stockItem = (item: Item): string => { 10 | return ` 11 |
  • 12 |

    ${item.name}

    13 |
    가격: ${item.price}원
    14 |
    수량: ${item.quantity}상자
    15 |
  • 16 | `; 17 | }; 18 | 19 | const outOfStockItem = (item: Item): string => ` 20 |
  • 21 |

    ${item.name} (품절)

    22 |
    가격: ${item.price}원
    23 |
  • 24 | `; 25 | 26 | const renderItem = (item: Item): string => { 27 | if (item.outOfStock) { 28 | return outOfStockItem(item); 29 | } else { 30 | return stockItem(item); 31 | } 32 | }; 33 | 34 | const totalCalculator = (list: ArrayItem, getValue: (item: Item) => number) => { 35 | return list 36 | .filter((item) => item.outOfStock === false) 37 | .map(getValue) 38 | .reduce((total, value) => total + value, 0); 39 | }; 40 | 41 | const totalCount = (list: ArrayItem): string => { 42 | const totalCount = totalCalculator(list, (item) => item.quantity); 43 | return `

    전체 수량: ${totalCount}상자`; 44 | }; 45 | 46 | const totalPrice = (list: ArrayItem): string => { 47 | const totalPrice = totalCalculator( 48 | list, 49 | (item) => item.price * item.quantity 50 | ); 51 | 52 | return `

    전체 가격: ${totalPrice}`; 53 | }; 54 | 55 | const list = (list: ArrayItem) => { 56 | return ` 57 |
      58 | ${list 59 | // 1. 목록에 있는 모든 아이템을 태그로 변경 60 | .map(renderItem) 61 | // 2. 태그의 목록을 모두 하나의 문자열로 연결 62 | .reduce((tags, tag) => tags + tag, "")} 63 |
    64 | `; 65 | }; 66 | 67 | export const render = (cart: ArrayItem) => { 68 | O.map(O.fromNull(document.getElementById("app")), (app) => { 69 | app.innerHTML = ` 70 |

    장바구니

    71 | ${list(cart)} 72 | ${totalCount(cart)} 73 | ${totalPrice(cart)} 74 | `; 75 | }); 76 | }; 77 | 78 | export const main = () => { 79 | render(cart); 80 | }; 81 | -------------------------------------------------------------------------------- /chapter8-clip8/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | div { 6 | margin-bottom: 1rem; 7 | } 8 | -------------------------------------------------------------------------------- /chapter8-clip8/src/try.ts: -------------------------------------------------------------------------------- 1 | // 실패하거나 성공했을때 각각 다른 값을 가지는 자료 구조 2 | 3 | type Success = { 4 | readonly _tag: "success"; 5 | readonly result: R; 6 | }; 7 | 8 | type Failed = { 9 | readonly _tag: "failed"; 10 | readonly error: E; 11 | }; 12 | 13 | export type Try = Failed | Success; 14 | 15 | export const success = (result: R): Try => ({ 16 | _tag: "success", 17 | result 18 | }); 19 | 20 | export const failed = (error: E): Try => ({ 21 | _tag: "failed", 22 | error 23 | }); 24 | 25 | export const isSuccess = (ta: Try): ta is Success => 26 | ta._tag === "success"; 27 | 28 | export const isFailed = (ta: Try): ta is Failed => 29 | ta._tag === "failed"; 30 | 31 | export const getOrElse = ( 32 | ta: Try, 33 | defaultValue: (e: E) => R 34 | ): R => { 35 | // 에러가 있을 경우 에러에 기반하여 기본 값을 결정한다. 36 | if (isFailed(ta)) return defaultValue(ta.error); 37 | // 결과가 성공이라면 해당 값을 사용한다. 38 | return ta.result; 39 | }; 40 | 41 | export const map = (ta: Try, f: (a: A) => B): Try => { 42 | if (isFailed(ta)) return ta; 43 | return success(f(ta.result)); 44 | } 45 | 46 | // Array> => Array 47 | export const KeepSuccess = 48 | (tas: Array>): Array => { 49 | const ret = tas.flatMap((ta) => { 50 | if (isSuccess(ta)) return [ta.result]; 51 | else return []; 52 | }) 53 | return ret; 54 | } 55 | 56 | export const KeepSuccessWithFor = (tas: Array>): Array => { 57 | const ret: Array = []; 58 | for (const ta of tas) { 59 | if (isSuccess(ta)) { 60 | ret.push(ta.result); 61 | } 62 | } 63 | return ret; 64 | } 65 | 66 | // flatMap :: (A => Try) => (Try => Try) 67 | // map :: (A => B) => (Try => Try) 68 | 69 | // flat :: Try> => Try 70 | export const flat = (tta: Try>): Try => { 71 | if (isSuccess(tta)) return tta.result; 72 | return tta; 73 | } 74 | 75 | export const flatMap = (ta: Try, f: (a: A) => Try): Try => { 76 | return flat(map(ta, f)); 77 | } 78 | -------------------------------------------------------------------------------- /chapter8-clip8/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "esnext", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } -------------------------------------------------------------------------------- /chapter8/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter8/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |

    자연수 생성기

    10 |
    11 |
    12 |
    13 | 14 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /chapter8/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter8", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1", 12 | "rxjs": "7.5.2" 13 | }, 14 | "devDependencies": { 15 | "typescript": "4.4.4" 16 | }, 17 | "resolutions": { 18 | "@babel/preset-env": "7.13.8" 19 | }, 20 | "keywords": [] 21 | } -------------------------------------------------------------------------------- /chapter8/src/cart.ts: -------------------------------------------------------------------------------- 1 | export interface Item { 2 | readonly code: string; 3 | readonly outOfStock: boolean; 4 | readonly name: string; 5 | readonly price: number; 6 | readonly quantity: number; 7 | } 8 | 9 | export const cart: Array = [ 10 | { 11 | code: "tomato", 12 | outOfStock: false, 13 | name: "토마토", 14 | price: 7000, 15 | quantity: 2 16 | }, 17 | { 18 | code: "orange", 19 | outOfStock: false, 20 | name: "오렌지", 21 | price: 15000, 22 | quantity: -2 23 | }, 24 | { 25 | code: "apple", 26 | outOfStock: true, 27 | name: "사과", 28 | price: 10000, 29 | quantity: 2 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /chapter8/src/clip3.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | // Option
    = None | Some 4 | // Try = Failed | Success 5 | // Async = ??? 6 | 7 | type Async = (ret: (x: A) => void) => void; 8 | 9 | const resolve = (a: A): Async => { 10 | return (ret) => { 11 | ret(a); 12 | } 13 | } 14 | 15 | const flatMap = (a: Async, f: (a: A) => Async): Async => { 16 | return (ret) => { 17 | a((a_) => { 18 | const b = f(a_); 19 | b((b_) => ret(b_)); 20 | }) 21 | }; 22 | }; 23 | 24 | const map = (a: Async, f: (a: A) => B): Async => { 25 | return flatMap(a, (a_) => resolve(f(a_))); 26 | }; 27 | 28 | const run = (a: Async) => { 29 | a(() => { return }); 30 | } 31 | 32 | const promiseF = (str: string): Promise => new Promise((resolve, reject) => { 33 | setTimeout(() => { 34 | resolve("promiseF 1: " + str); 35 | }, 500); 36 | 37 | setTimeout(() => { 38 | resolve("promiseF 2: " + str); 39 | }, 1000); 40 | }) 41 | 42 | const asyncF = (str: string): Async => ((ret) => { 43 | setTimeout(() => { 44 | ret("asyncF 1: " + str); 45 | }, 500); 46 | 47 | setTimeout(() => { 48 | ret("asyncF 2: " + str); 49 | }, 1000); 50 | }); 51 | 52 | const handleError = (e: unknown) => { 53 | // 사용자에게 에러를 알려주는 통합 함수 54 | console.log("handleError: " + e); 55 | } 56 | 57 | const program = (s: boolean) => { 58 | console.log(s); 59 | } 60 | 61 | const greeting = (name: string) => { 62 | console.log("Hello, " + name); 63 | } 64 | 65 | export const main = () => { 66 | run(map(asyncF("test"), (x) => console.log(x))); 67 | promiseF("test").then(x => console.log(x)) ; 68 | 69 | greeting("World"); 70 | console.log("프로그램이 종료되었습니다."); 71 | } 72 | -------------------------------------------------------------------------------- /chapter8/src/clip4.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | type Async = (cb: (a: A) => void) => void; 4 | 5 | const ns: Array = [1,2,3,4,5]; 6 | 7 | const integers = (n: number): Array => { 8 | const ret: Array = []; 9 | let i = 0; 10 | while(i < n) { 11 | i = i + 1; 12 | ret.push(i); 13 | } 14 | return ret; 15 | } 16 | 17 | type Iterator = () => A; 18 | type Iterable = () => Iterator; 19 | 20 | const integerGenerator = () => { 21 | let i = 0; 22 | return () => { 23 | i = i + 1; 24 | return i; 25 | } 26 | } 27 | 28 | const promiseIntegers = (n: number): Promise> => new Promise((resolve) => { 29 | setTimeout(() => { 30 | resolve(integers(n)); 31 | }, 1000); 32 | }); 33 | 34 | promiseIntegers(5).then(ns => console.log(ns)); 35 | 36 | const integerObservable: Async = (ret) => { 37 | const iter = integerGenerator(); 38 | setInterval(() => { 39 | ret(iter()); 40 | }, 1000); 41 | }; 42 | 43 | // integerObservable(n => console.log(n)); 44 | 45 | const onManyIntegers = (n: number) => { 46 | const arr = integers(n); 47 | console.log(arr); 48 | } 49 | 50 | const iter = integerGenerator(); 51 | const onStep = () => { 52 | const n = iter(); 53 | console.log(n); 54 | } 55 | 56 | const render1 = () => { 57 | const app1 = document.getElementById("app1"); 58 | if (app1 === null) { 59 | return; 60 | } 61 | 62 | app1.innerHTML = ` 63 | 64 | 65 | `; 66 | 67 | const wantedNumber = document.getElementById("wanted-number"); 68 | const btn = document.getElementById("all"); 69 | 70 | if (!(wantedNumber && btn)) { 71 | return; 72 | } 73 | 74 | btn.onclick = (e) => { 75 | onManyIntegers(Number((wantedNumber as HTMLInputElement).value)); 76 | } 77 | } 78 | 79 | const render2 = () => { 80 | const app = document.getElementById("app2"); 81 | if (app === null) { 82 | return; 83 | } 84 | 85 | app.innerHTML = ` 86 | 87 | `; 88 | 89 | const btn1 = document.getElementById("step_1"); 90 | 91 | if (!(btn1)) { 92 | return; 93 | } 94 | 95 | btn1.onclick = (e) => { 96 | onStep(); 97 | } 98 | } 99 | 100 | const render = () => { 101 | render1(); 102 | render2(); 103 | } 104 | 105 | export const main = () => { 106 | render(); 107 | } -------------------------------------------------------------------------------- /chapter8/src/clip5-1.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | type Async = (ret: (a: A) => void) => void; 4 | 5 | // Iterable내의 상태 갱신과 상태를 기반으로 값을 생성하는 함수 6 | type Iterator = () => A 7 | // 상태를 초기화하고 iterator를 생성하는 함수 8 | type Iterable = () => Iterator 9 | 10 | type Observer = (a: A) => void; 11 | type Observable = (subscribe: Observer) => void; 12 | 13 | export const main = () => { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /chapter8/src/clip5-2.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | import { Observable, pipe } from 'rxjs'; 4 | import { filter, take } from 'rxjs/operators'; 5 | 6 | const isEven = (n: number) => n % 2 === 0; 7 | 8 | const intergerObservable: Observable = new Observable((subscribe) => { 9 | let i = 0; 10 | setInterval(() => { 11 | i = i + 1; 12 | subscribe.next(i); 13 | }, 1000); 14 | }); 15 | 16 | const ns: Array = [1,2,3,4,5,6,7,8,9,10]; 17 | 18 | export const main = () => { 19 | const xs = ns 20 | // Array ~> (A => boolean) => Array 21 | .filter(isEven) 22 | .slice(0, 3); 23 | console.log(xs); 24 | 25 | const take3EvenNumbers = pipe( 26 | filter(isEven), 27 | take(3), 28 | ) 29 | 30 | take3EvenNumbers(intergerObservable).subscribe({ 31 | next: (n) => { 32 | console.log(n); 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /chapter8/src/clip6.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | 3 | const pipeFunctions = (f: (a: A) => B, g: (b: B) => C) => (a: A): C => { 4 | return g(f(a)); 5 | } 6 | 7 | type Observer = (a: A) => void; 8 | type Observable = (subscribe: Observer) => void; 9 | 10 | const map = (f: (a: A) => B) => (source: Observable): Observable => { 11 | return (subscribe) => { 12 | source(a => { 13 | const b = f(a); 14 | subscribe(b); 15 | }) 16 | } 17 | } 18 | 19 | const filter = (pred: (a: A) => boolean) => (source: Observable): Observable => { 20 | return (subscribe) => { 21 | source(a => { 22 | if (pred(a)) { 23 | subscribe(a); 24 | } 25 | }) 26 | } 27 | } 28 | 29 | const filterObserver = (pred: (a: A) => boolean) => (subscribe: Observer): Observer => { 30 | return a => { 31 | if (pred(a)) { 32 | subscribe(a); 33 | } 34 | } 35 | } 36 | 37 | // map :: (A => B) => Array => Array 38 | // mapObserver :: (A => B) => Observer => Observer 39 | const mapObserver = (f: (a: A) => B) => (subscribe: Observer): Observer => { 40 | return a => { 41 | subscribe(f(a)); 42 | } 43 | } 44 | 45 | // map :: (A => B) => Observable => Observable 46 | // lift:: (Observer => Observer) => Observable => Observable 47 | const lift = (f: (b: Observer) => Observer) => (source: Observable): Observable => { 48 | return (subscribe) => { 49 | source(f(subscribe)) 50 | } 51 | } 52 | 53 | const liftedMap = pipeFunctions(mapObserver, lift); 54 | const liftedFilter = pipeFunctions(filterObserver, lift); 55 | 56 | export const main = () => { 57 | } -------------------------------------------------------------------------------- /chapter8/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 0.8rem; 3 | } 4 | 5 | .gray { 6 | color: gray; 7 | } 8 | 9 | .strike { 10 | text-decoration: line-through; 11 | } 12 | -------------------------------------------------------------------------------- /chapter8/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./styles.css"; 2 | 3 | import * as main from './clip6'; 4 | 5 | console.clear(); 6 | main.main(); 7 | -------------------------------------------------------------------------------- /chapter8/src/option.ts: -------------------------------------------------------------------------------- 1 | // 값이 있을수도, 없을수도 있는 자료구조. 2 | 3 | export type Some = { 4 | _tag: "some"; 5 | value: A; 6 | }; 7 | 8 | export type None = { 9 | _tag: "none"; 10 | }; 11 | 12 | export type Option = Some | None; 13 | 14 | export const some = (value: A): Option => ({ _tag: "some", value }); 15 | 16 | export const none = (): Option => ({ _tag: "none" }); 17 | 18 | export const isSome = (oa: Option): oa is Some => oa._tag === "some"; 19 | 20 | export const isNone = (oa: Option): oa is None => oa._tag === "none"; 21 | 22 | export const fromUndefined = (a: A | undefined): Option => { 23 | if (a === undefined) return none(); 24 | return some(a); 25 | }; 26 | 27 | export const fromNull = (a: A | null): Option => { 28 | if (a === null) return none(); 29 | return some(a); 30 | }; 31 | 32 | export const getOrElse = (oa: Option, defaultValue: A): A => { 33 | // 값이 없으면 지정된 값을 사용하고 34 | if (isNone(oa)) return defaultValue; 35 | // 값이 있다면 해당 값을 사용한다. 36 | return oa.value; 37 | }; 38 | 39 | export const map = (oa: Option, f: (a: A) => B): Option => { 40 | // 값이 없으면 값이 없는 상태를 유지한다. 41 | if (isNone(oa)) return oa; 42 | // 값이 있으면 값을 함수에 적용한다. 43 | return some(f(oa.value)); 44 | }; 45 | 46 | export const mapOrElse = ( 47 | oa: Option, 48 | f: (a: A) => B, 49 | defaultValue: B 50 | ): B => { 51 | return getOrElse(map(oa, f), defaultValue); 52 | }; 53 | -------------------------------------------------------------------------------- /chapter8/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | div { 6 | margin-bottom: 1rem; 7 | } 8 | -------------------------------------------------------------------------------- /chapter8/src/try.ts: -------------------------------------------------------------------------------- 1 | // 실패하거나 성공했을때 각각 다른 값을 가지는 자료 구조 2 | 3 | type Success = { 4 | readonly _tag: "success"; 5 | readonly result: R; 6 | }; 7 | 8 | type Failed = { 9 | readonly _tag: "failed"; 10 | readonly error: E; 11 | }; 12 | 13 | export type Try = Failed | Success; 14 | 15 | export const success = (result: R): Try => ({ 16 | _tag: "success", 17 | result 18 | }); 19 | 20 | export const failed = (error: E): Try => ({ 21 | _tag: "failed", 22 | error 23 | }); 24 | 25 | export const isSuccess = (ta: Try): ta is Success => 26 | ta._tag === "success"; 27 | 28 | export const isFailed = (ta: Try): ta is Failed => 29 | ta._tag === "failed"; 30 | 31 | export const getOrElse = ( 32 | ta: Try, 33 | defaultValue: (e: E) => R 34 | ): R => { 35 | // 에러가 있을 경우 에러에 기반하여 기본 값을 결정한다. 36 | if (isFailed(ta)) return defaultValue(ta.error); 37 | // 결과가 성공이라면 해당 값을 사용한다. 38 | return ta.result; 39 | }; 40 | 41 | export const map = (ta: Try, f: (a: A) => B): Try => { 42 | if (isFailed(ta)) return ta; 43 | return success(f(ta.result)); 44 | } 45 | 46 | // Array> => Array 47 | export const KeepSuccess = 48 | (tas: Array>): Array => { 49 | const ret = tas.flatMap((ta) => { 50 | if (isSuccess(ta)) return [ta.result]; 51 | else return []; 52 | }) 53 | return ret; 54 | } 55 | 56 | export const KeepSuccessWithFor = (tas: Array>): Array => { 57 | const ret: Array = []; 58 | for (const ta of tas) { 59 | if (isSuccess(ta)) { 60 | ret.push(ta.result); 61 | } 62 | } 63 | return ret; 64 | } 65 | 66 | // flatMap :: (A => Try) => (Try => Try) 67 | // map :: (A => B) => (Try => Try) 68 | 69 | // flat :: Try> => Try 70 | export const flat = (tta: Try>): Try => { 71 | if (isSuccess(tta)) return tta.result; 72 | return tta; 73 | } 74 | 75 | export const flatMap = (ta: Try, f: (a: A) => Try): Try => { 76 | return flat(map(ta, f)); 77 | } 78 | -------------------------------------------------------------------------------- /chapter8/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "esnext", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } -------------------------------------------------------------------------------- /chapter9/.codesandbox/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "responsive-preview": { 3 | "Mobile": [ 4 | 320, 5 | 675 6 | ], 7 | "Tablet": [ 8 | 1024, 9 | 765 10 | ], 11 | "Desktop": [ 12 | 1400, 13 | 800 14 | ], 15 | "Desktop HD": [ 16 | 1920, 17 | 1080 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /chapter9/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |

    자연수 생성기

    10 |
    11 |
    12 |
    13 | 14 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /chapter9/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter9", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "parcel-bundler": "^1.6.1", 12 | "rxjs": "7.5.2" 13 | }, 14 | "devDependencies": { 15 | "typescript": "4.4.4" 16 | }, 17 | "resolutions": { 18 | "@babel/preset-env": "7.13.8" 19 | }, 20 | "keywords": [] 21 | } -------------------------------------------------------------------------------- /chapter9/src/clip3-complete.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | import * as O from './option'; 3 | 4 | // 불변성, immutability 5 | 6 | const compose = (g: (b: B) => C) =>
    (f: (a: A) => B) => (a: A): C => g(f(a)); 7 | 8 | const getLength = (s: string): number => { 9 | return s.length; 10 | } 11 | 12 | const isEven = (n: number): boolean => { 13 | return n % 2 === 0; 14 | } 15 | 16 | const x = 3; 17 | // x = 4; // error 18 | 19 | const x1 = isEven(x); 20 | 21 | // apply :: (A => B) => A => B 22 | const apply = (f: (a: A) => B) => (a: A): B => f(a); 23 | 24 | // x2 :: boolean 25 | const x2 = apply(isEven)(x); 26 | 27 | // anotherIsEven :: number => boolean 28 | const anotherIsEven = apply(isEven); 29 | 30 | const anotherGetLength = apply(getLength); 31 | 32 | // f1 :: string => number 33 | const f1 = apply(compose(isEven)(getLength)); 34 | // f2 :: string => number 35 | const f2 = compose(apply(isEven))(apply(getLength)); 36 | 37 | // optionIsEven :: Option => Option 38 | const optionIsEven = O.map(isEven); 39 | // optionGetLength :: Option => Option 40 | const optionGetLength = O.map(getLength); 41 | 42 | // of1 :: Option => Option 43 | const of1 = O.map(compose(isEven)(getLength)); 44 | // of2 :: Option => Option 45 | const of2 = compose(O.map(isEven))(O.map(getLength)); 46 | 47 | type Iterator = () => A 48 | type Observer = (a: A) => void 49 | type Function = (a: A) => B; 50 | 51 | const map = (f: (a: A) => B) => (input: Function): Function => { 52 | return (r) => { 53 | return f(input(r)); 54 | } 55 | }; 56 | 57 | // mapIsEven :: (R => number) => (R => boolean) 58 | const mapIsEven = map(isEven); 59 | const c1 = mapIsEven(getLength); 60 | 61 | const contraMap = (f: (a: A) => B) => (input: Function): Function => { 62 | return (a) => { 63 | return input(f(a)); 64 | } 65 | }; 66 | 67 | const contraMapGetLength = contraMap(getLength); 68 | 69 | 70 | export const main = () => { 71 | 72 | } 73 | -------------------------------------------------------------------------------- /chapter9/src/clip3.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, @typescript-eslint/no-unused-vars, no-unused-vars, no-lone-blocks */ 2 | import * as O from './option'; 3 | 4 | const compose = (g: (b: B) => C) => (f: (a: A) => B) => (a: A): C => g(f(a)); 5 | 6 | const getLength = (s: string): number => { 7 | return s.length; 8 | } 9 | 10 | const isEven = (n: number): boolean => { 11 | return n % 2 === 0; 12 | } 13 | 14 | 15 | 16 | export const main = () => { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /chapter9/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 0.8rem; 3 | } 4 | 5 | .gray { 6 | color: gray; 7 | } 8 | 9 | .strike { 10 | text-decoration: line-through; 11 | } 12 | -------------------------------------------------------------------------------- /chapter9/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./styles.css"; 2 | 3 | import * as main from './clip3'; 4 | 5 | console.clear(); 6 | main.main(); 7 | -------------------------------------------------------------------------------- /chapter9/src/option.ts: -------------------------------------------------------------------------------- 1 | // 값이 있을수도, 없을수도 있는 자료구조. 2 | 3 | export type Some = { 4 | _tag: "some"; 5 | value: A; 6 | }; 7 | 8 | export type None = { 9 | _tag: "none"; 10 | }; 11 | 12 | export type Option = Some | None; 13 | 14 | export const some = (value: A): Option => ({ _tag: "some", value }); 15 | 16 | export const none = (): Option => ({ _tag: "none" }); 17 | 18 | export const isSome = (oa: Option): oa is Some => oa._tag === "some"; 19 | 20 | export const isNone = (oa: Option): oa is None => oa._tag === "none"; 21 | 22 | export const fromUndefined = (a: A | undefined): Option => { 23 | if (a === undefined) return none(); 24 | return some(a); 25 | }; 26 | 27 | export const fromNull = (a: A | null): Option => { 28 | if (a === null) return none(); 29 | return some(a); 30 | }; 31 | 32 | export const getOrElse = (oa: Option, defaultValue: A): A => { 33 | // 값이 없으면 지정된 값을 사용하고 34 | if (isNone(oa)) return defaultValue; 35 | // 값이 있다면 해당 값을 사용한다. 36 | return oa.value; 37 | }; 38 | 39 | export const map = (f: (a: A) => B) => (oa: Option): Option => { 40 | // 값이 없으면 값이 없는 상태를 유지한다. 41 | if (isNone(oa)) return oa; 42 | // 값이 있으면 값을 함수에 적용한다. 43 | return some(f(oa.value)); 44 | }; 45 | 46 | export const mapOrElse = ( 47 | oa: Option, 48 | f: (a: A) => B, 49 | defaultValue: B 50 | ): B => { 51 | return getOrElse(map(f)(oa), defaultValue); 52 | }; 53 | -------------------------------------------------------------------------------- /chapter9/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | } 4 | 5 | div { 6 | margin-bottom: 1rem; 7 | } 8 | -------------------------------------------------------------------------------- /chapter9/src/try.ts: -------------------------------------------------------------------------------- 1 | // 실패하거나 성공했을때 각각 다른 값을 가지는 자료 구조 2 | 3 | type Success = { 4 | readonly _tag: "success"; 5 | readonly result: R; 6 | }; 7 | 8 | type Failed = { 9 | readonly _tag: "failed"; 10 | readonly error: E; 11 | }; 12 | 13 | export type Try = Failed | Success; 14 | 15 | export const success = (result: R): Try => ({ 16 | _tag: "success", 17 | result 18 | }); 19 | 20 | export const failed = (error: E): Try => ({ 21 | _tag: "failed", 22 | error 23 | }); 24 | 25 | export const isSuccess = (ta: Try): ta is Success => 26 | ta._tag === "success"; 27 | 28 | export const isFailed = (ta: Try): ta is Failed => 29 | ta._tag === "failed"; 30 | 31 | export const getOrElse = ( 32 | ta: Try, 33 | defaultValue: (e: E) => R 34 | ): R => { 35 | // 에러가 있을 경우 에러에 기반하여 기본 값을 결정한다. 36 | if (isFailed(ta)) return defaultValue(ta.error); 37 | // 결과가 성공이라면 해당 값을 사용한다. 38 | return ta.result; 39 | }; 40 | 41 | export const map = (ta: Try, f: (a: A) => B): Try => { 42 | if (isFailed(ta)) return ta; 43 | return success(f(ta.result)); 44 | } 45 | 46 | // Array> => Array 47 | export const KeepSuccess = 48 | (tas: Array>): Array => { 49 | const ret = tas.flatMap((ta) => { 50 | if (isSuccess(ta)) return [ta.result]; 51 | else return []; 52 | }) 53 | return ret; 54 | } 55 | 56 | export const KeepSuccessWithFor = (tas: Array>): Array => { 57 | const ret: Array = []; 58 | for (const ta of tas) { 59 | if (isSuccess(ta)) { 60 | ret.push(ta.result); 61 | } 62 | } 63 | return ret; 64 | } 65 | 66 | // flatMap :: (A => Try) => (Try => Try) 67 | // map :: (A => B) => (Try => Try) 68 | 69 | // flat :: Try> => Try 70 | export const flat = (tta: Try>): Try => { 71 | if (isSuccess(tta)) return tta.result; 72 | return tta; 73 | } 74 | 75 | export const flatMap = (ta: Try, f: (a: A) => Try): Try => { 76 | return flat(map(ta, f)); 77 | } 78 | -------------------------------------------------------------------------------- /chapter9/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "jsx": "preserve", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "lib": [ 10 | "esnext", 11 | "dom" 12 | ], 13 | "rootDir": "src", 14 | "moduleResolution": "node" 15 | } 16 | } --------------------------------------------------------------------------------