├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .vscode └── launch.json ├── dist ├── FiraCode-Regular.woff ├── NotoSansJP-Regular.otf ├── NotoSerifJP-Regular.otf ├── NotoSerifJP-Regular.woff ├── card.png ├── favicon-96x96.png ├── github-markdown.css ├── logo-shadow.png └── style.css ├── package-lock.json ├── package.json ├── res ├── cover.png ├── favicon-96x96.png ├── g4896.png ├── logo.svg ├── metadata.xml ├── ps_avatar.png ├── ps_avatar_bg.png └── translationnote.md ├── script ├── epub.mjs ├── html.mjs ├── pdf.mjs └── render.mjs ├── src ├── chapter01.md ├── chapter02.md ├── chapter03.md ├── chapter04.md ├── chapter05.md ├── chapter06.md ├── chapter07.md ├── chapter08.md ├── chapter09.md ├── chapter10.md ├── chapter11.md ├── chapter12.md ├── chapter13.md ├── chapter14.md └── index.md └── templates ├── default.epub └── default.html /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:prettier/recommended"], 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | "no-debugger": 0 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 8, 11 | "sourceType": "module" 12 | } 13 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | temp 2 | bower_components 3 | node_modules 4 | public 5 | dist/*.html 6 | dist/*.epub 7 | dist/*.pdf 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "type": "node", 10 | "request": "launch", 11 | "name": "Launch Program", 12 | "program": "${workspaceFolder}/script/html.mjs", 13 | "args": [ 14 | ], 15 | "runtimeArgs": [ 16 | "--experimental-modules" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /dist/FiraCode-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/dist/FiraCode-Regular.woff -------------------------------------------------------------------------------- /dist/NotoSansJP-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/dist/NotoSansJP-Regular.otf -------------------------------------------------------------------------------- /dist/NotoSerifJP-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/dist/NotoSerifJP-Regular.otf -------------------------------------------------------------------------------- /dist/NotoSerifJP-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/dist/NotoSerifJP-Regular.woff -------------------------------------------------------------------------------- /dist/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/dist/card.png -------------------------------------------------------------------------------- /dist/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/dist/favicon-96x96.png -------------------------------------------------------------------------------- /dist/github-markdown.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: octicons-link; 3 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); 4 | } 5 | 6 | .markdown-body { 7 | -ms-text-size-adjust: 100%; 8 | -webkit-text-size-adjust: 100%; 9 | line-height: 1.5; 10 | color: #24292e; 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 12 | font-size: 16px; 13 | line-height: 1.5; 14 | word-wrap: break-word; 15 | } 16 | 17 | .markdown-body .pl-c { 18 | color: #6a737d; 19 | } 20 | 21 | .markdown-body .pl-c1, 22 | .markdown-body .pl-s .pl-v { 23 | color: #005cc5; 24 | } 25 | 26 | .markdown-body .pl-e, 27 | .markdown-body .pl-en { 28 | color: #6f42c1; 29 | } 30 | 31 | .markdown-body .pl-smi, 32 | .markdown-body .pl-s .pl-s1 { 33 | color: #24292e; 34 | } 35 | 36 | .markdown-body .pl-ent { 37 | color: #22863a; 38 | } 39 | 40 | .markdown-body .pl-k { 41 | color: #d73a49; 42 | } 43 | 44 | .markdown-body .pl-s, 45 | .markdown-body .pl-pds, 46 | .markdown-body .pl-s .pl-pse .pl-s1, 47 | .markdown-body .pl-sr, 48 | .markdown-body .pl-sr .pl-cce, 49 | .markdown-body .pl-sr .pl-sre, 50 | .markdown-body .pl-sr .pl-sra { 51 | color: #032f62; 52 | } 53 | 54 | .markdown-body .pl-v, 55 | .markdown-body .pl-smw { 56 | color: #e36209; 57 | } 58 | 59 | .markdown-body .pl-bu { 60 | color: #b31d28; 61 | } 62 | 63 | .markdown-body .pl-ii { 64 | color: #fafbfc; 65 | background-color: #b31d28; 66 | } 67 | 68 | .markdown-body .pl-c2 { 69 | color: #fafbfc; 70 | background-color: #d73a49; 71 | } 72 | 73 | .markdown-body .pl-c2::before { 74 | content: "^M"; 75 | } 76 | 77 | .markdown-body .pl-sr .pl-cce { 78 | font-weight: bold; 79 | color: #22863a; 80 | } 81 | 82 | .markdown-body .pl-ml { 83 | color: #735c0f; 84 | } 85 | 86 | .markdown-body .pl-mh, 87 | .markdown-body .pl-mh .pl-en, 88 | .markdown-body .pl-ms { 89 | font-weight: bold; 90 | color: #005cc5; 91 | } 92 | 93 | .markdown-body .pl-mi { 94 | font-style: italic; 95 | color: #24292e; 96 | } 97 | 98 | .markdown-body .pl-mb { 99 | font-weight: bold; 100 | color: #24292e; 101 | } 102 | 103 | .markdown-body .pl-md { 104 | color: #b31d28; 105 | background-color: #ffeef0; 106 | } 107 | 108 | .markdown-body .pl-mi1 { 109 | color: #22863a; 110 | background-color: #f0fff4; 111 | } 112 | 113 | .markdown-body .pl-mc { 114 | color: #e36209; 115 | background-color: #ffebda; 116 | } 117 | 118 | .markdown-body .pl-mi2 { 119 | color: #f6f8fa; 120 | background-color: #005cc5; 121 | } 122 | 123 | .markdown-body .pl-mdr { 124 | font-weight: bold; 125 | color: #6f42c1; 126 | } 127 | 128 | .markdown-body .pl-ba { 129 | color: #586069; 130 | } 131 | 132 | .markdown-body .pl-sg { 133 | color: #959da5; 134 | } 135 | 136 | .markdown-body .pl-corl { 137 | text-decoration: underline; 138 | color: #032f62; 139 | } 140 | 141 | .markdown-body .octicon { 142 | display: inline-block; 143 | vertical-align: text-top; 144 | fill: currentColor; 145 | } 146 | 147 | .markdown-body a { 148 | background-color: transparent; 149 | } 150 | 151 | .markdown-body a:active, 152 | .markdown-body a:hover { 153 | outline-width: 0; 154 | } 155 | 156 | .markdown-body strong { 157 | font-weight: inherit; 158 | } 159 | 160 | .markdown-body strong { 161 | font-weight: bolder; 162 | } 163 | 164 | .markdown-body h1 { 165 | font-size: 2em; 166 | margin: 0.67em 0; 167 | } 168 | 169 | .markdown-body img { 170 | border-style: none; 171 | } 172 | 173 | .markdown-body code, 174 | .markdown-body kbd, 175 | .markdown-body pre { 176 | font-family: monospace, monospace; 177 | font-size: 1em; 178 | } 179 | 180 | .markdown-body hr { 181 | box-sizing: content-box; 182 | height: 0; 183 | overflow: visible; 184 | } 185 | 186 | .markdown-body input { 187 | font: inherit; 188 | margin: 0; 189 | } 190 | 191 | .markdown-body input { 192 | overflow: visible; 193 | } 194 | 195 | .markdown-body [type="checkbox"] { 196 | box-sizing: border-box; 197 | padding: 0; 198 | } 199 | 200 | .markdown-body * { 201 | box-sizing: border-box; 202 | } 203 | 204 | .markdown-body input { 205 | font-family: inherit; 206 | font-size: inherit; 207 | line-height: inherit; 208 | } 209 | 210 | .markdown-body a { 211 | color: #0366d6; 212 | text-decoration: none; 213 | } 214 | 215 | .markdown-body a:hover { 216 | text-decoration: underline; 217 | } 218 | 219 | .markdown-body strong { 220 | font-weight: 600; 221 | } 222 | 223 | .markdown-body hr { 224 | height: 0; 225 | margin: 15px 0; 226 | overflow: hidden; 227 | background: transparent; 228 | border: 0; 229 | border-bottom: 1px solid #dfe2e5; 230 | } 231 | 232 | .markdown-body hr::before { 233 | display: table; 234 | content: ""; 235 | } 236 | 237 | .markdown-body hr::after { 238 | display: table; 239 | clear: both; 240 | content: ""; 241 | } 242 | 243 | .markdown-body table { 244 | border-spacing: 0; 245 | border-collapse: collapse; 246 | } 247 | 248 | .markdown-body td, 249 | .markdown-body th { 250 | padding: 0; 251 | } 252 | 253 | .markdown-body h1, 254 | .markdown-body h2, 255 | .markdown-body h3, 256 | .markdown-body h4, 257 | .markdown-body h5, 258 | .markdown-body h6 { 259 | margin-top: 0; 260 | margin-bottom: 0; 261 | } 262 | 263 | .markdown-body h1 { 264 | font-size: 32px; 265 | font-weight: 600; 266 | } 267 | 268 | .markdown-body h2 { 269 | font-size: 24px; 270 | font-weight: 600; 271 | } 272 | 273 | .markdown-body h3 { 274 | font-size: 20px; 275 | font-weight: 600; 276 | } 277 | 278 | .markdown-body h4 { 279 | font-size: 16px; 280 | font-weight: 600; 281 | } 282 | 283 | .markdown-body h5 { 284 | font-size: 14px; 285 | font-weight: 600; 286 | } 287 | 288 | .markdown-body h6 { 289 | font-size: 12px; 290 | font-weight: 600; 291 | } 292 | 293 | .markdown-body p { 294 | margin-top: 0; 295 | margin-bottom: 10px; 296 | } 297 | 298 | .markdown-body blockquote { 299 | margin: 0; 300 | } 301 | 302 | .markdown-body ul, 303 | .markdown-body ol { 304 | padding-left: 0; 305 | margin-top: 0; 306 | margin-bottom: 0; 307 | } 308 | 309 | .markdown-body ol ol, 310 | .markdown-body ul ol { 311 | list-style-type: lower-roman; 312 | } 313 | 314 | .markdown-body ul ul ol, 315 | .markdown-body ul ol ol, 316 | .markdown-body ol ul ol, 317 | .markdown-body ol ol ol { 318 | list-style-type: lower-alpha; 319 | } 320 | 321 | .markdown-body dd { 322 | margin-left: 0; 323 | } 324 | 325 | .markdown-body code { 326 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 327 | font-size: 12px; 328 | } 329 | 330 | .markdown-body pre { 331 | margin-top: 0; 332 | margin-bottom: 0; 333 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 334 | font-size: 12px; 335 | } 336 | 337 | .markdown-body .octicon { 338 | vertical-align: text-bottom; 339 | } 340 | 341 | .markdown-body .pl-0 { 342 | padding-left: 0 !important; 343 | } 344 | 345 | .markdown-body .pl-1 { 346 | padding-left: 4px !important; 347 | } 348 | 349 | .markdown-body .pl-2 { 350 | padding-left: 8px !important; 351 | } 352 | 353 | .markdown-body .pl-3 { 354 | padding-left: 16px !important; 355 | } 356 | 357 | .markdown-body .pl-4 { 358 | padding-left: 24px !important; 359 | } 360 | 361 | .markdown-body .pl-5 { 362 | padding-left: 32px !important; 363 | } 364 | 365 | .markdown-body .pl-6 { 366 | padding-left: 40px !important; 367 | } 368 | 369 | .markdown-body::before { 370 | display: table; 371 | content: ""; 372 | } 373 | 374 | .markdown-body::after { 375 | display: table; 376 | clear: both; 377 | content: ""; 378 | } 379 | 380 | .markdown-body>*:first-child { 381 | margin-top: 0 !important; 382 | } 383 | 384 | .markdown-body>*:last-child { 385 | margin-bottom: 0 !important; 386 | } 387 | 388 | .markdown-body a:not([href]) { 389 | color: inherit; 390 | text-decoration: none; 391 | } 392 | 393 | .markdown-body .anchor { 394 | float: left; 395 | padding-right: 4px; 396 | margin-left: -20px; 397 | line-height: 1; 398 | } 399 | 400 | .markdown-body .anchor:focus { 401 | outline: none; 402 | } 403 | 404 | .markdown-body p, 405 | .markdown-body blockquote, 406 | .markdown-body ul, 407 | .markdown-body ol, 408 | .markdown-body dl, 409 | .markdown-body table, 410 | .markdown-body pre { 411 | margin-top: 0; 412 | margin-bottom: 16px; 413 | } 414 | 415 | .markdown-body hr { 416 | height: 0.25em; 417 | padding: 0; 418 | margin: 24px 0; 419 | background-color: #e1e4e8; 420 | border: 0; 421 | } 422 | 423 | .markdown-body blockquote { 424 | padding: 0 1em; 425 | color: #6a737d; 426 | border-left: 0.25em solid #dfe2e5; 427 | } 428 | 429 | .markdown-body blockquote>:first-child { 430 | margin-top: 0; 431 | } 432 | 433 | .markdown-body blockquote>:last-child { 434 | margin-bottom: 0; 435 | } 436 | 437 | .markdown-body kbd { 438 | display: inline-block; 439 | padding: 3px 5px; 440 | font-size: 11px; 441 | line-height: 10px; 442 | color: #444d56; 443 | vertical-align: middle; 444 | background-color: #fafbfc; 445 | border: solid 1px #c6cbd1; 446 | border-bottom-color: #959da5; 447 | border-radius: 3px; 448 | box-shadow: inset 0 -1px 0 #959da5; 449 | } 450 | 451 | .markdown-body h1, 452 | .markdown-body h2, 453 | .markdown-body h3, 454 | .markdown-body h4, 455 | .markdown-body h5, 456 | .markdown-body h6 { 457 | margin-top: 24px; 458 | margin-bottom: 16px; 459 | font-weight: 600; 460 | line-height: 1.25; 461 | } 462 | 463 | .markdown-body h1 .octicon-link, 464 | .markdown-body h2 .octicon-link, 465 | .markdown-body h3 .octicon-link, 466 | .markdown-body h4 .octicon-link, 467 | .markdown-body h5 .octicon-link, 468 | .markdown-body h6 .octicon-link { 469 | color: #1b1f23; 470 | vertical-align: middle; 471 | visibility: hidden; 472 | } 473 | 474 | .markdown-body h1:hover .anchor, 475 | .markdown-body h2:hover .anchor, 476 | .markdown-body h3:hover .anchor, 477 | .markdown-body h4:hover .anchor, 478 | .markdown-body h5:hover .anchor, 479 | .markdown-body h6:hover .anchor { 480 | text-decoration: none; 481 | } 482 | 483 | .markdown-body h1:hover .anchor .octicon-link, 484 | .markdown-body h2:hover .anchor .octicon-link, 485 | .markdown-body h3:hover .anchor .octicon-link, 486 | .markdown-body h4:hover .anchor .octicon-link, 487 | .markdown-body h5:hover .anchor .octicon-link, 488 | .markdown-body h6:hover .anchor .octicon-link { 489 | visibility: visible; 490 | } 491 | 492 | .markdown-body h1 { 493 | padding-bottom: 0.3em; 494 | font-size: 2em; 495 | border-bottom: 1px solid #eaecef; 496 | } 497 | 498 | .markdown-body h2 { 499 | padding-bottom: 0.3em; 500 | font-size: 1.5em; 501 | border-bottom: 1px solid #eaecef; 502 | } 503 | 504 | .markdown-body h3 { 505 | font-size: 1.25em; 506 | } 507 | 508 | .markdown-body h4 { 509 | font-size: 1em; 510 | } 511 | 512 | .markdown-body h5 { 513 | font-size: 0.875em; 514 | } 515 | 516 | .markdown-body h6 { 517 | font-size: 0.85em; 518 | color: #6a737d; 519 | } 520 | 521 | .markdown-body ul, 522 | .markdown-body ol { 523 | padding-left: 2em; 524 | } 525 | 526 | .markdown-body ul ul, 527 | .markdown-body ul ol, 528 | .markdown-body ol ol, 529 | .markdown-body ol ul { 530 | margin-top: 0; 531 | margin-bottom: 0; 532 | } 533 | 534 | .markdown-body li { 535 | word-wrap: break-all; 536 | } 537 | 538 | .markdown-body li>p { 539 | margin-top: 16px; 540 | } 541 | 542 | .markdown-body li+li { 543 | margin-top: 0.25em; 544 | } 545 | 546 | .markdown-body dl { 547 | padding: 0; 548 | } 549 | 550 | .markdown-body dl dt { 551 | padding: 0; 552 | margin-top: 16px; 553 | font-size: 1em; 554 | font-style: italic; 555 | font-weight: 600; 556 | } 557 | 558 | .markdown-body dl dd { 559 | padding: 0 16px; 560 | margin-bottom: 16px; 561 | } 562 | 563 | .markdown-body table { 564 | display: block; 565 | width: 100%; 566 | overflow: auto; 567 | } 568 | 569 | .markdown-body table th { 570 | font-weight: 600; 571 | } 572 | 573 | .markdown-body table th, 574 | .markdown-body table td { 575 | padding: 6px 13px; 576 | border: 1px solid #dfe2e5; 577 | } 578 | 579 | .markdown-body table tr { 580 | background-color: #fff; 581 | border-top: 1px solid #c6cbd1; 582 | } 583 | 584 | .markdown-body table tr:nth-child(2n) { 585 | background-color: #f6f8fa; 586 | } 587 | 588 | .markdown-body img { 589 | max-width: 100%; 590 | box-sizing: content-box; 591 | background-color: #fff; 592 | } 593 | 594 | .markdown-body img[align=right] { 595 | padding-left: 20px; 596 | } 597 | 598 | .markdown-body img[align=left] { 599 | padding-right: 20px; 600 | } 601 | 602 | .markdown-body code { 603 | padding: 0.2em 0.4em; 604 | margin: 0; 605 | font-size: 85%; 606 | background-color: rgba(27,31,35,0.05); 607 | border-radius: 3px; 608 | } 609 | 610 | .markdown-body pre { 611 | word-wrap: normal; 612 | } 613 | 614 | .markdown-body pre>code { 615 | padding: 0; 616 | margin: 0; 617 | font-size: 100%; 618 | word-break: normal; 619 | white-space: pre; 620 | background: transparent; 621 | border: 0; 622 | } 623 | 624 | .markdown-body .highlight { 625 | margin-bottom: 16px; 626 | } 627 | 628 | .markdown-body .highlight pre { 629 | margin-bottom: 0; 630 | word-break: normal; 631 | } 632 | 633 | .markdown-body .highlight pre, 634 | .markdown-body pre { 635 | padding: 16px; 636 | overflow: auto; 637 | font-size: 85%; 638 | line-height: 1.45; 639 | background-color: #f6f8fa; 640 | border-radius: 3px; 641 | } 642 | 643 | .markdown-body pre code { 644 | display: inline; 645 | max-width: auto; 646 | padding: 0; 647 | margin: 0; 648 | overflow: visible; 649 | line-height: inherit; 650 | word-wrap: normal; 651 | background-color: transparent; 652 | border: 0; 653 | } 654 | 655 | .markdown-body .full-commit .btn-outline:not(:disabled):hover { 656 | color: #005cc5; 657 | border-color: #005cc5; 658 | } 659 | 660 | .markdown-body kbd { 661 | display: inline-block; 662 | padding: 3px 5px; 663 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 664 | line-height: 10px; 665 | color: #444d56; 666 | vertical-align: middle; 667 | background-color: #fafbfc; 668 | border: solid 1px #d1d5da; 669 | border-bottom-color: #c6cbd1; 670 | border-radius: 3px; 671 | box-shadow: inset 0 -1px 0 #c6cbd1; 672 | } 673 | 674 | .markdown-body :checked+.radio-label { 675 | position: relative; 676 | z-index: 1; 677 | border-color: #0366d6; 678 | } 679 | 680 | .markdown-body .task-list-item { 681 | list-style-type: none; 682 | } 683 | 684 | .markdown-body .task-list-item+.task-list-item { 685 | margin-top: 3px; 686 | } 687 | 688 | .markdown-body .task-list-item input { 689 | margin: 0 0.2em 0.25em -1.6em; 690 | vertical-align: middle; 691 | } 692 | 693 | .markdown-body hr { 694 | border-bottom-color: #eee; 695 | } 696 | -------------------------------------------------------------------------------- /dist/logo-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/dist/logo-shadow.png -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Noto Serif Japanese'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url('NotoSerifJP-Regular.otf') format('opentype'); 6 | font-display: swap; 7 | } 8 | 9 | @font-face { 10 | font-family: 'Noto Sans Japanese'; 11 | font-style: normal; 12 | font-weight: 400; 13 | src: url('NotoSansJP-Regular.otf') format('opentype'); 14 | font-display: swap; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Fira Code'; 19 | font-style: normal; 20 | font-weight: 400; 21 | src: url('FiraCode-Regular.woff') format('woff'); 22 | font-display: swap; 23 | } 24 | 25 | ::selection { 26 | background: rgb(69, 63, 158); 27 | color: white; 28 | } 29 | 30 | body { 31 | background-color: #f5f5f5; 32 | margin: 0; 33 | } 34 | 35 | div.main { 36 | margin: 0 auto; 37 | background-color: white; 38 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.1); 39 | font-family: 'Noto Serif Japanese', "Yu Mincho", serif !important; 40 | overflow: hidden; 41 | } 42 | 43 | header .logo { 44 | display: none; 45 | } 46 | 47 | header img.logo:hover { 48 | background-color: rgba(255, 255, 255, 0.3); 49 | } 50 | 51 | header { 52 | background-color: #1d222d; 53 | color: white; 54 | padding: 50px; 55 | display: flex; 56 | margin-bottom: 30px; 57 | } 58 | 59 | header a { 60 | color: white; 61 | text-decoration: none; 62 | } 63 | 64 | header a:hover { 65 | background-color: rgba(255, 255, 255, 0.2); 66 | } 67 | 68 | header h1 { 69 | letter-spacing: 0.2em; 70 | border-bottom: solid 1px rgba(255, 255, 255, 0.4); 71 | padding: 0.4em; 72 | } 73 | 74 | header p.author { 75 | font-family: 'Yu Mincho', 'Noto Serif Japanese', serif; 76 | } 77 | 78 | div.content { 79 | padding: 8px; 80 | font-family: 'Yu Mincho', 'Noto Serif Japanese', serif !important; 81 | } 82 | 83 | div.content a { 84 | color: #c4953a; 85 | text-decoration: none; 86 | } 87 | 88 | div.content strong { 89 | font-family: 'Yu Gothic', 'Noto Sans Japanese', sans-serif !important; 90 | } 91 | 92 | .main ol { 93 | list-style-type: decimal; 94 | } 95 | 96 | .main ol ol { 97 | list-style-type: decimal; 98 | } 99 | 100 | .main ol ol ol { 101 | list-style-type: decimal; 102 | } 103 | 104 | .main blockquote { 105 | color: #333; 106 | } 107 | 108 | /* Bug Workaround for Readium */ 109 | 110 | .markdown-body.content { 111 | padding: 0px; 112 | padding-bottom: 40px; 113 | } 114 | 115 | .markdown-body.content h1, 116 | .markdown-body.content h2, 117 | .markdown-body.content h3, 118 | .markdown-body.content h4, 119 | .markdown-body.content h5, 120 | .markdown-body.content h6 { 121 | position: static !important; 122 | font-family: "Yu Mincho", 'Noto Serif Japanese', serif; 123 | font-weight: bold; 124 | letter-spacing: 0.12em; 125 | margin-left: 20px; 126 | margin-right: 20px; 127 | } 128 | 129 | .markdown-body.content h2 { 130 | margin-top: 3em; 131 | } 132 | 133 | .markdown-body.content p { 134 | text-indent: 1em; 135 | /* text-align: justify; */ 136 | line-height: 1.8; 137 | } 138 | 139 | .markdown-body.content h1, 140 | .markdown-body.content h2, 141 | .markdown-body.content h3, 142 | .markdown-body.content h4, 143 | .markdown-body.content h5, 144 | .markdown-body.content h6, 145 | .markdown-body.content p, 146 | .markdown-body.content pre { 147 | padding-left: 8px; 148 | padding-right: 8px; 149 | } 150 | 151 | .markdown-body.content ul, 152 | .markdown-body.content ol { 153 | padding-left: 8px; 154 | padding-right: 8px; 155 | } 156 | 157 | .markdown-body.content .exercise ul, 158 | .markdown-body.content .exercise ol { 159 | padding-left: 2em; 160 | padding-right: 1em; 161 | } 162 | 163 | .markdown-body.content li p { 164 | padding: 0; 165 | } 166 | 167 | .markdown-body code { 168 | font-family: "Fira Code", 'Courier New', monospace; 169 | white-space: pre; 170 | word-wrap: keep-all !important; 171 | } 172 | 173 | .markdown-body.content pre { 174 | background-color: #1d222d; 175 | color: white; 176 | } 177 | 178 | .markdown-body.content .next { 179 | text-align: center; 180 | color: white; 181 | background-color: #1d222d; 182 | padding: 10px; 183 | margin-top: 40px; 184 | margin-bottom: 40px; 185 | } 186 | 187 | .markdown-body.content .next:hover { 188 | background-color: #424b61; 189 | } 190 | 191 | .markdown-body.content .exercise { 192 | border: solid 1px lightgrey; 193 | margin: 30px; 194 | margin-top: 100px; 195 | margin-bottom: 100px; 196 | } 197 | 198 | .markdown-body.content .exercise h2 { 199 | letter-spacing: 1em; 200 | text-align: center; 201 | margin-top: 10px; 202 | } 203 | 204 | table.sourceCode tr { 205 | padding: 0; 206 | border: none; 207 | } 208 | 209 | table.sourceCode td { 210 | padding: 0; 211 | border: none; 212 | } 213 | 214 | table.sourceCode pre { 215 | margin: 0; 216 | border: none; 217 | } 218 | 219 | @media screen and (min-width: 800px) { 220 | body { 221 | margin: 10px; 222 | } 223 | header .logo { 224 | display: block; 225 | } 226 | div.main { 227 | min-width: 540px; 228 | max-width: 800px; 229 | } 230 | .markdown-body.content h1, 231 | .markdown-body.content h2, 232 | .markdown-body.content h3, 233 | .markdown-body.content h4, 234 | .markdown-body.content h5, 235 | .markdown-body.content h6, 236 | .markdown-body.content p, 237 | .markdown-body.content pre { 238 | padding-left: 40px; 239 | padding-right: 40px; 240 | } 241 | .markdown-body.content ul, 242 | .markdown-body.content ol { 243 | padding-left: 3em; 244 | padding-right: 2em; 245 | } 246 | } 247 | 248 | .pagebreak { 249 | margin-bottom: 14em; 250 | } 251 | 252 | @page { 253 | margin: 12mm; 254 | } 255 | 256 | @media print { 257 | body { 258 | background-color: transparent; 259 | font-size: 16pt; 260 | } 261 | .main { 262 | text-justify: inter-cluster; 263 | background-color: transparent; 264 | border: none 0px transparent; 265 | box-shadow: none; 266 | } 267 | .main h1, 268 | h2, 269 | h3, 270 | h4, 271 | h5, 272 | h6 { 273 | position: static !important; 274 | } 275 | pre { 276 | overflow: visible !important; 277 | } 278 | .pagebreak { 279 | margin-bottom: 2em; 280 | page-break-before: always; 281 | } 282 | } 283 | 284 | .sourceCode, 285 | .shell { 286 | font-family: 'Fira Code', 'Courier New', Monospace; 287 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-book-ja", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": " ", 6 | "scripts": { 7 | "clean": "rimraf ./dist/*.html ./dist/*.epub ./dist/*.pdf", 8 | "html": "npm run lint && npm run clean && node --experimental-modules script/html.mjs", 9 | "epub": "node --experimental-modules script/epub.mjs", 10 | "pdf": "node --experimental-modules script/pdf.mjs", 11 | "all": "npm run lint && npm run clean && npm run html && npm run epub && npm run pdf", 12 | "lint": "eslint --ext .mjs --fix script", 13 | "inspect": "npm run lint && npm run clean && node --experimental-modules --inspect-brk script/html.mjs" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": " " 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": " ", 22 | "dependencies": { 23 | "cheerio": "^1.0.0-rc.2", 24 | "commonmark": "^0.28.1", 25 | "epub-gen": "0.0.20", 26 | "eslint": "^4.19.1", 27 | "eslint-config-prettier": "^2.9.0", 28 | "eslint-plugin-import": "^2.11.0", 29 | "eslint-plugin-node": "^6.0.1", 30 | "eslint-plugin-prettier": "^2.6.0", 31 | "eslint-plugin-promise": "^3.7.0", 32 | "highlightjs": "^9.10.0", 33 | "prettier": "^1.13.4", 34 | "puppeteer": "^1.3.0" 35 | }, 36 | "devDependencies": { 37 | "fs-extra": "^5.0.0", 38 | "glob": "^7.1.2", 39 | "rimraf": "^2.3.2", 40 | "github-markdown-css": "^2.10.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /res/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/res/cover.png -------------------------------------------------------------------------------- /res/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/res/favicon-96x96.png -------------------------------------------------------------------------------- /res/g4896.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/res/g4896.png -------------------------------------------------------------------------------- /res/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 27 | 31 | 32 | 36 | 41 | 47 | 52 | 57 | 63 | 64 | 65 | 85 | 87 | 88 | 90 | image/svg+xml 91 | 93 | 94 | 95 | 96 | 97 | 102 | 109 | 115 | 118 | 123 | 124 | 127 | 132 | 133 | 136 | 141 | 142 | 145 | 150 | 151 | 154 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /res/metadata.xml: -------------------------------------------------------------------------------- 1 | Creative Commons 2 | es-AR 3 | 実例によるPureScript 4 | Phil Freeman 5 | 6 | ja 7 | -------------------------------------------------------------------------------- /res/ps_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/res/ps_avatar.png -------------------------------------------------------------------------------- /res/ps_avatar_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aratama/purescript-book-ja/573f9e8803e0d82673ddf346ba69574818a43ab1/res/ps_avatar_bg.png -------------------------------------------------------------------------------- /res/translationnote.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | http://www.kotha.net/ghcguide_ja/ 8 | 9 | http://www.sampou.org/cgi-bin/haskell.cgi?Haskell%3AReport -------------------------------------------------------------------------------- /script/epub.mjs: -------------------------------------------------------------------------------- 1 | import util from "util"; 2 | import glob from "glob"; 3 | import Epub from "epub-gen"; 4 | import { 5 | insertPageBreak, 6 | transformExercise, 7 | highlightCodes, 8 | markdownToHtml, 9 | numberHeadings, 10 | readMarkdown 11 | } from "./render"; 12 | 13 | async function main() { 14 | const files = await util.promisify(glob)("src/chapter*.md"); 15 | const css = await util.promisify(glob)( 16 | "node_modules/github-markdown-css/github-markdown.css", 17 | "utf8" 18 | ); 19 | const chapters = await Promise.all( 20 | files.map(async (file, i) => { 21 | const chapter = i + 1; 22 | const document = await readMarkdown(file); 23 | numberHeadings(document, chapter); 24 | const $ = markdownToHtml(document); 25 | highlightCodes($); 26 | transformExercise($); 27 | insertPageBreak($); 28 | return { 29 | title: `chapter${(i + 1).toString().padStart(2, "0")}`, 30 | data: $.html(), 31 | css: css 32 | }; 33 | }) 34 | ); 35 | 36 | await new Epub( 37 | { 38 | title: "実例によるPureScript", 39 | author: "Phil Freeman", 40 | cover: "./res/cover.png", 41 | content: chapters 42 | }, 43 | "./dist/purescript-book-ja.epub" 44 | ); 45 | } 46 | 47 | main().catch(e => console.error(e)); 48 | -------------------------------------------------------------------------------- /script/html.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import util from "util"; 3 | import glob from "glob"; 4 | import cheerio from "cheerio"; 5 | import { 6 | concatHtmls, 7 | writeHtml, 8 | renderMarkdown, 9 | readMarkdown, 10 | numberHeadings, 11 | insertPageBreak, 12 | insertNextChapterLink, 13 | insertLinkToHome, 14 | markdownToHtml, 15 | transformExercise, 16 | highlightCodes 17 | } from "./render"; 18 | import commonmark from "commonmark"; 19 | 20 | async function renderWithTemplate(path, content, template) { 21 | const $ = cheerio.load(template, { decodeEntities: false }); 22 | $(".content").append(content("body > *")); 23 | await writeHtml(path, $); 24 | } 25 | 26 | async function main() { 27 | debugger; 28 | await fs.ensureDir("dist"); 29 | const files = await util.promisify(glob)("src/chapter*.md"); 30 | 31 | // load the template 32 | const template = await fs.readFile("templates/default.html", "utf8"); 33 | 34 | // render each chapters 35 | const chapters = await Promise.all( 36 | files.map(async (file, i) => { 37 | const chapter = i + 1; 38 | const document = await readMarkdown(file); 39 | let result = numberHeadings(document, chapter); 40 | insertNextChapterLink(document, chapter, files.length - 1); 41 | insertLinkToHome(document); 42 | const $ = markdownToHtml(document); 43 | highlightCodes($); 44 | transformExercise($); 45 | await renderWithTemplate( 46 | `dist/chapter${chapter.toString().padStart(2, "0")}.html`, 47 | $, 48 | template 49 | ); 50 | return result; 51 | }) 52 | ); 53 | 54 | // render index page 55 | const indexDocument = await readMarkdown("src/index.md"); 56 | for (let child = indexDocument.firstChild; child; child = child.next) { 57 | if (child.type === "heading" && child.firstChild.literal === "目次") { 58 | const list = new commonmark.Node("list"); 59 | list.listType = commonmark.Ordered; 60 | list.listTight = true; 61 | list.listStart = 1; 62 | list.listDelimiter = "."; 63 | 64 | chapters.forEach((chapter, i) => { 65 | const text = new commonmark.Node("text"); 66 | text.literal = chapter.chapterTitle; 67 | 68 | const link = new commonmark.Node("link"); 69 | link.appendChild(text); 70 | link.destination = `chapter${(i + 1).toString().padStart(2, "0")}.html`; 71 | 72 | const item = new commonmark.Node("item"); 73 | item.appendChild(link); 74 | 75 | list.appendChild(item); 76 | 77 | const slist = new commonmark.Node("list"); 78 | slist.listType = commonmark.Ordered; 79 | slist.listTight = true; 80 | slist.listStart = 1; 81 | slist.listDelimiter = "."; 82 | 83 | /* 84 | chapter.sections.forEach((section, j) => { 85 | const text = new commonmark.Node("text"); 86 | text.literal = section; 87 | 88 | const item = new commonmark.Node("item"); 89 | item.appendChild(text); 90 | slist.appendChild(item); 91 | }); 92 | */ 93 | 94 | list.appendChild(slist); 95 | }); 96 | 97 | child.insertAfter(list); 98 | } 99 | } 100 | await renderWithTemplate( 101 | "dist/index.html", 102 | renderMarkdown(indexDocument), 103 | template 104 | ); 105 | 106 | // render integrated page 107 | const integrated = await Promise.all( 108 | files.map(async (file, i) => { 109 | const chapter = i + 1; 110 | const document = await readMarkdown(file); 111 | numberHeadings(document, chapter); 112 | const $ = markdownToHtml(document); 113 | highlightCodes($); 114 | transformExercise($); 115 | insertPageBreak($); 116 | return $; 117 | }) 118 | ); 119 | 120 | // render to file 121 | await renderWithTemplate( 122 | "dist/purescript-book-ja.html", 123 | concatHtmls(integrated), 124 | template 125 | ); 126 | 127 | // copy resources 128 | fs.copy( 129 | "node_modules/github-markdown-css/github-markdown.css", 130 | "dist/github-markdown.css" 131 | ); 132 | } 133 | 134 | main(); 135 | -------------------------------------------------------------------------------- /script/pdf.mjs: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | 3 | async function main() { 4 | const browser = await puppeteer.launch(); 5 | const page = await browser.newPage(); 6 | await page.goto(`file:///${process.cwd()}/dist/purescript-book-ja.html`); 7 | await page.pdf({ 8 | path: "./dist/purescript-book-ja.pdf", 9 | format: "A4", 10 | printBackground: true, 11 | displayHeaderFooter: false, 12 | margin: { 13 | left: "0.35cm" 14 | } 15 | }); 16 | await browser.close(); 17 | } 18 | 19 | main().catch(e => console.error(e)); 20 | -------------------------------------------------------------------------------- /script/render.mjs: -------------------------------------------------------------------------------- 1 | import commonmark from "commonmark"; 2 | import cheerio from "cheerio"; 3 | import highlightjs from "highlightjs"; 4 | import fs from "fs-extra"; 5 | 6 | export async function readMarkdown(path) { 7 | const content = await fs.readFile(path, "utf8"); 8 | const reader = new commonmark.Parser(); 9 | return reader.parse(content); 10 | } 11 | 12 | export function numberHeadings(document, chapter) { 13 | let sectionCounter = 1; 14 | let chapterTitle = null; 15 | let sections = []; 16 | for (let child = document.firstChild; child; child = child.next) { 17 | if (child.type === "heading") { 18 | if (child.level === 1) { 19 | chapterTitle = child.firstChild.literal; 20 | const text = new commonmark.Node("text"); 21 | text.literal = `第${chapter}章 `; 22 | child.prependChild(text); 23 | sectionCounter = 1; 24 | } else if (child.level === 2) { 25 | const text = child.firstChild.literal.trim(); 26 | if (text !== "まとめ" && text.indexOf("演習") < 0) { 27 | sections.push(child.firstChild.literal); 28 | const text = new commonmark.Node("text"); 29 | text.literal = chapter + "." + sectionCounter + " "; 30 | child.prependChild(text); 31 | sectionCounter += 1; 32 | } 33 | } 34 | } 35 | } 36 | return { chapterTitle, sections }; 37 | } 38 | 39 | export function insertNextChapterLink(document, chapter, lastChapter) { 40 | // insert link to the next chapter 41 | if (chapter !== null && lastChapter !== null && chapter < lastChapter) { 42 | const nextChapter = chapter + 1; 43 | const nextChapterLink = new commonmark.Node("html_block"); 44 | const chapterName = `chapter${nextChapter.toString().padStart(2, "0")}`; 45 | nextChapterLink.literal = `\n\n`; 46 | document.appendChild(nextChapterLink); 47 | } 48 | } 49 | 50 | export function insertLinkToHome(document) { 51 | const home = () => { 52 | const home = new commonmark.Node("html_block"); 53 | home.literal = `

目次に戻る

`; 54 | return home; 55 | }; 56 | document.appendChild(home()); 57 | document.prependChild(home()); 58 | } 59 | 60 | export function highlightCodes($) { 61 | $( 62 | `pre code[class="language-haskell"], pre code[class="language-purescript"], pre code[class="language-javascript"], pre code[class="language-html"]` 63 | ).map((i, node) => { 64 | const result = highlightjs.highlightAuto($(node).text(), [ 65 | "haskell", 66 | "javascript" 67 | ]); 68 | $(node).text(result.value); 69 | }); 70 | } 71 | 72 | export function transformExercise($) { 73 | $(`h2`).each((i, element) => { 74 | if ( 75 | $(element) 76 | .text() 77 | .trim() === "演習" 78 | ) { 79 | const div = $(`

演習

`); 80 | 81 | for ( 82 | let child = $(element).next(); 83 | child.length > 0; 84 | child = child.next() 85 | ) { 86 | // debugger 87 | if (child.is("h1, h2, h3, h4, h5, h6")) { 88 | break; 89 | } else { 90 | div.append(child); 91 | } 92 | } 93 | 94 | $(element).replaceWith(div); 95 | } 96 | }); 97 | } 98 | 99 | export function markdownToHtml(document) { 100 | const writer = new commonmark.HtmlRenderer(); 101 | try { 102 | const rendered = writer.render(document); // result is a String 103 | return cheerio.load(rendered, { decodeEntities: false }); 104 | } catch (e) { 105 | debugger; 106 | const rendered = writer.render(document); // result is a String 107 | return cheerio.load(rendered, { decodeEntities: false }); 108 | } 109 | } 110 | 111 | // renderMarkdown :: Markdown -> String 112 | export function renderMarkdown(document, options) { 113 | const chapter = (options && options.chapter) || null; 114 | const lastChapter = (options && options.lastChapter) || null; 115 | const homeLinks = (options && options.homeLinks) || false; 116 | 117 | // insert link to the next chapter 118 | insertNextChapterLink(document, chapter, lastChapter); 119 | 120 | // insert link to home 121 | if (homeLinks) { 122 | insertLinkToHome(document); 123 | } 124 | 125 | // render mamrkdown nodes to html html nodes 126 | const $ = markdownToHtml(document); 127 | 128 | // highlight codes 129 | highlightCodes($); 130 | 131 | // transform exercise 132 | transformExercise($); 133 | 134 | // render to html text 135 | return $; 136 | } 137 | 138 | export async function writeHtml(path, $) { 139 | await fs.writeFile(path, $.html()); 140 | } 141 | 142 | export function insertPageBreak($) { 143 | $("body").append($(`
`)); 144 | } 145 | 146 | export function concatHtmls(htmls) { 147 | const head = htmls.shift(); 148 | for (let i = 0; i < htmls.length; i++) { 149 | const $ = htmls[i]; 150 | head("body").append($("body > *")); 151 | } 152 | return head; 153 | } 154 | -------------------------------------------------------------------------------- /src/chapter01.md: -------------------------------------------------------------------------------- 1 | # はじめに 2 | 3 | ## 関数型JavaScript 4 | 5 | 関数型プログラミングの手法は、かねてよりJavaScriptでも用いられてきました。 6 | 7 | - [UnderscoreJS](http://underscorejs.org)などのライブラリは、 `map`や `filter`、 `reduce`といったよく知られた関数を活用して、小さいプログラムを組み合わせて大きなプログラムを作れるようにします。 8 | 9 | ```javascript 10 | var sumOfPrimes = 11 | _.chain(_.range(1000)) 12 | .filter(isPrime) 13 | .reduce(function(x, y) { 14 | return x + y; 15 | }) 16 | .value(); 17 | ``` 18 | 19 | - NodeJSにおける非同期プログラミングでは、第一級の値としての関数をコールバックを定義するために多用しています。 20 | 21 | ```javascript 22 | require('fs').readFile(sourceFile, function (error, data) { 23 | if (!error) { 24 | require('fs').writeFile(destFile, data, function (error) { 25 | if (!error) { 26 | console.log("File copied"); 27 | } 28 | }); 29 | } 30 | }); 31 | ``` 32 | 33 | - [React](http://facebook.github.io/react/)や[virtual-dom](https://github.com/Matt-Esch/virtual-dom)などのライブラリは、アプリケーションの状態についての純粋な関数としてその外観をモデル化しています。 34 | 35 | 関数は単純な抽象化を可能にし、優れた生産性をもたらしてくれます。しかし、JavaScriptでの関数型プログラミングには欠点があります。JavaScriptは冗長で、型付けされず、強力な抽象化を欠いているのです。また、無秩序に書かれたJavaScriptコードでは、式の理解がとても困難です。 36 | 37 | PureScriptはこのような問題を解決すべく作られたプログラミング言語です。PureScriptは、とても表現力豊かでありながらわかりやすく読みやすいコードを書けるようにする、軽量な構文を備えています。強力な抽象化を提供する豊かな型システムも採用しています。また、JavaScriptやJavaScriptへとコンパイルされる他の言語と相互運用するときに重要な、高速で理解しやすいコードを生成します。PureScriptをひとことで言えば、純粋関数型プログラミングの理論的な強力さと、JavaScriptのお手軽で緩いプログラミングスタイルとの、とても現実的なバランスを狙った言語だということを理解して頂けたらと思います。 38 | 39 | ## 型と型推論 40 | 41 | 動的型付けの言語と静的型付けの言語をめぐる議論についてはよく知られています。PureScriptは**静的型付け**の言語、つまり正しいプログラムはコンパイラによってその動作を示すような**型**を与えられる言語です。逆にいえば、型を​​与えることができないプログラムは**誤ったプログラム**であり、コンパイラによって拒否されます。動的型付けの言語とは異なり、PureScriptでは型は**コンパイル時**のみに存在し、実行時には型の表現はありません。 42 | 43 | PureScriptの型は、これまでJavaやC#のような他の言語で見たような型とは、いろいろな意味で異なっていることにも注意することが大切です。おおまかに言えばPureScriptの型はJavaやC#と同じ目的を持っているものの、PureScriptの型はMLとHaskellのような言語に影響を受けています。開発者がプログラムについての強い主張を表明できるので、PureScriptの型は表現力豊かなのです。最も重要なのは、PureScriptの型システムは**型推論**(type inference)をサポートしていることです。型推論があれば明示的な型注釈は必要最低限となり、型システムを厄介者ではなく**道具**にしてくれます。簡単な例を示すと、次のコードは**数**を定義していますが、それが `Number`型だという注釈はコードのどこにもありません。 44 | 45 | ```haskell 46 | iAmANumber = 47 | let square x = x * x 48 | in square 42.0 49 | ``` 50 | 51 | 次のもっと複雑な例では、**コンパイラにとって未知**の型が存在しているときでさえも、型注釈なしで型の正しさを確かめることができるということが示されています。 52 | 53 | ```haskell 54 | iterate f 0 x = x 55 | iterate f n x = iterate f (n - 1) (f x) 56 | ``` 57 | 58 | ここで `x`の型は不明ですが、 `x`がどんな型を持っているかにかかわらず、 `iterate`が型システムの規則に従っていることをコンパイラは検証することができます。 59 | 60 | 静的型はプログラムの正しさについての確信を得るためだけではなく、その正しさによって開発を助ける、ということをあなたに納得させる(もしくは、あなたの理解を確認する)ことをこの本では試みます。最も単純な抽象化を使わないかぎりJavaScriptでコードの大規模なリファクタリングすることは難しいですが、型検証器のある表現力豊かな型システムは、リファクタリングさえ楽しく対話的な体験にしてくれます。 61 | 62 | 加えて、型システムによって提供されたこのセーフティネットは、より高度な抽象化をも可能にします。実際に、関数型プログラミング言語Haskellによって知られるようになった、型主導の強力な抽象化である『型クラス』をPureScriptは備えています。 63 | 64 | ## 多言語Webプログラミング 65 | 66 | 関数型プログラミングはすでに多くの成功を収めています。特に成功している応用例をいくつか挙げると、データ解析、構文解析、コンパイラの実装、ジェネリックプログラミング、並列処理などがあります。 67 | 68 | PureScriptのような関数型言語は、アプリケーション開発の最初から最後までを​実施することが可能です。値や関数の型を提供することで既存のJavaScriptコードをインポートし、通常のPureScriptコードからこれらの関数を使用する機能をPureScriptは提供しています。この手法については本書の後半で見ていくことになります。 69 | 70 | しかしながら、PureScriptの強みのひとつは、JavaScriptを対象とする他​​の言語との相互運用性にあります。アプリケーションの開発の一部にだけPureScriptを使用し、JavaScriptの残りの部分を記述するのに他の言語を使用するという方法もあります。 71 | 72 | いくつかの例を示します。 73 | 74 | - 中核となる処理はPureScriptで記述し、ユーザーインターフェイスはJavaScriptで記述する 75 | - JavaScriptや、他のJavaScriptにコンパイルされる言語でアプリケーションを書き、PureScriptでそのテストを書く 76 | - 既存のアプリケーションのユーザインタフェースのテストを自動化するためにPureScriptを使用する 77 | 78 | この本では小規模な課題をPureScriptで解決することに焦点を当てますが、ここで学ぶ手法は大規模なアプリケーションに組み込むこともできます。JavaScriptからPureScriptコードを呼び出す方法、およびその逆についても見ていきます。 79 | 80 | ## ソフトウェア要件 81 | 82 | この本でのソフトウェア要件は最小限です。第1章では開発環境の構築を一から案内します。これから使用するツールは、ほとんどの現代のオペレーティングシステムの標準リポジトリで使用できるものです。 83 | 84 | PureScriptコンパイラ自体は、コンパイル済みバイナリ形式でダウンロードすることもできますし、最新のHaskellコンパイラが稼働しているシステム上でソースからビルドすることもできます。次の章ではこの手順を説明していきます。 85 | 86 | 本書のこのバージョンのコードは、 `0.11.*`バージョンのPureScriptコンパイラと互換性があります。 87 | 88 | ## 読者について 89 | 90 | 読者はJavaScriptの基本をすでに理解しているものと仮定します。すでにNPMやBowerのようなJavaScriptのエコシステムでの経験があれば、自身の好みに応じて標準設定をカスタマイズしたい場合などに役に立ちますが、そのような知識は必要ではありません。 91 | 92 | 関数型プログラミングの予備知識は必要ありませんが、あっても害にはならないでしょう。実例には新しいアイデアがつきものですから、これから使う関数型プログラミングからこうした概念に対する直感的な理解を得ることができるはずです。 93 | 94 | PureScriptはプログラミング言語Haskellに強く影響を受けているため、Haskellに通じている読者はこの本の中で提示された概念や構文の多くに見覚えがあるでしょう。しかしながら、読者はPureScriptとHaskellの間にはいくつか重要な違いがあることも理解しておかなければなりません。ここで紹介する概念の多くはHaskellでも同じように解釈できるとはいえ、どちらかの言語での考え方を他方の言語でそのまま応用しようとすることは、必ずしも適切ではありません。 95 | 96 | ## 本書の読み進めかた 97 | 98 | 本書の各章は、概ね章ごとに完結しています。しかしながら、多少の関数型プログラミングの経験がある初心者でも、まずは各章を順番に進めていくことをおすすめします。最初の数章では、本書の後半の内容を理解するために必要な基礎知識を養います。関数型プログラミングの考え方に十分通じた読者(特にMLやHaskellのよう強く型付けされた言語での経験を持つ読者)なら、本書の前半の章を読まなくても、後半の章のコードの大まかな理解を得ることがおそらく可能でしょう。 99 | 100 | 各章ではそれぞれひとつの実用的な例に焦点をあて、新しい考え方を導入するための動機付けとして用います。各章のコードは本書の[GitHubのリポジトリ](https://github.com/paf31/purescript-book)から入手できます。各章にはソースコードから抜粋したコード片が掲載されていますが、完全に理解するためには本書に掲載されたコードと平行してリポジトリのソースコードを読む必要があります。対話式環境 `PSCi`で実行し理解を確かめられるように、長めの節には短いコード片が掲載されていることがあります。 101 | 102 | コード例は次のように等幅フォントで示されています。 103 | 104 | ```haskell 105 | module Example where 106 | 107 | import Control.Monad.Eff.Console (log) 108 | 109 | main = log "Hello, World!" 110 | ``` 111 | 112 | 先頭にドル記号がついた行は、コマンドラインに入力されたコマンドです。 113 | 114 | ```text 115 | $ pulp build 116 | ``` 117 | 118 | 通常、これらのコマンドはLinuxやMac OSの利用者ならそのまま適用できますが、Windowsの利用者はファイル区切り文字を変更する、シェルの組み込み機能をWindowsの相当するものに置き換えるなどの小さな変更を加える必要があるかもしれません。 119 | 120 | `pulp repl`対話式プロンプトに入力するコマンドは、行の先頭に山括弧が付けられています。 121 | 122 | ```text 123 | > 1 + 2 124 | 3 125 | ``` 126 | 127 | 各章には演習が付いており、それぞれ難易度も示されています。各章の内容を完全に理解するために、演習に取り組むことを強くお勧めします。 128 | 129 | この本は初心者にPureScriptへの導入を提供することを目的としており、問題についてのお決まりの解決策の一覧を提供するような種類の本ではありません。初心者にとってこの本を読むのは楽しい挑戦になるはずですし、本書の内容を読み演習に挑戦すればだいたいの利益を得られるでしょうが、なにより重要なのは、あなたが自分自身のコードを書いてみることです。 130 | 131 | ## 困ったときには 132 | 133 | もしどこかでつまずいたときには、PureScriptを学べるオンラインで利用可能な資料がたくさんあります。 134 | 135 | - PureScript IRCチャン​​ネルはあなたが抱える問題についてチャットするのに最適な場所です。IRCクライアントでirc.freenode.netをポイントし、#purescriptチャンネルに接続してください。 136 | - [PureScriptのウェブサイト](http://purescript.org)にはPureScriptの開発者によって書かれたブログ記事や、初心者向けの動画、その他のリソースへのリンクがあります。 137 | - [PureScriptコンパイラのドキュメント](https://github.com/purescript/documentation)は、言語の主要な機能についての簡単​​なコード例があります。 138 | - [Try PureScript!](http://try.purescript.org)ではユーザーがWebブラウザでPureScriptコードをコンパイルすることができます。また、ウェブサイトにはコードの簡単な例がいくつか含まれています。 139 | - [Pursuit](http://pursuit.purescript.org)は、PureScriptの型や関数を検索することができるデータベースです。 140 | 141 | もしあなたが例を読んで学ぶことを好むなら、GitHubの `purescript`組織、 `purescript-node`組織および `purescript-contrib`組織にはPureScriptコードの例がたくさんあります。 142 | 143 | ## 著者について 144 | 145 | 私はPureScriptコンパイラの最初の開発者です。私はカリフォルニア州ロサンゼルスを拠点にしており、8ビットパーソナルコンピュータ、Amstrad CPC上のBASICでまだ幼い時にプログラミングを始めました。それ以来、私はいくつものプログラミング言語(JavaやScala、C#、F#、HaskellそしてPureScript)で業務に携わってきました。 146 | 147 | プロとしての経歴が始まって間もなく、私は関数型プログラミングと数学の関係を理解するようになり、そしてプログラミング言語Haskellとの恋に落ちました。 148 | 149 | JavaScriptでの経験をもとに、私はPureScriptコンパイラの開発を始めることにしました。私は自分がHaskellのような言語から取り上げた関数型プログラミングの手法を使っていることに気が付きましたが、それを応用するためのもっと理にかなった環境を求めていました。そのとき検討した案のなかには、Haskellをその意味論を維持しながらJavaScriptへとコンパイルするいろいろな試み(Fay、Haste、GHCJS)もありましたが、私が興味を持っていたのは、この問題への別の切り口からのアプローチ、すなわちHaskellのような言語の構文と型システムを楽しみながらJavaScriptの意味論も維持するということが、どのようにすれば可能になるのかでした。 150 | 151 | 私は[ウェブサイト](http://blog.functorial.com)を運営しており、[Twitterで連絡をとる](http://twitter.com/paf31)こともできます。 152 | 153 | ## 謝辞 154 | 155 | 現在の状態に到達するまでPureScriptを手伝ってくれた多くの協力者に感謝したいと思います。コンパイラやツール、ライブラリ、ドキュメント、テストでの組織的で弛まぬ努力がなかったら、プロジェクトは間違いなく失敗していたことでしょう。 156 | 157 | この本の表紙に表示されたPureScriptのロゴはGareth Hughesによって作成されたもので、[Creative Commons Attribution 4.0 license](https://creativecommons.org/licenses/by/4.0/)の条件の下で再利用させて頂いています 。 158 | 159 | 最後に、この本の内容に関する反応や訂正をくださったすべての方に、心より感謝したいと思います。 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/chapter02.md: -------------------------------------------------------------------------------- 1 | # 開発環境の準備 2 | 3 | ## この章の目標 4 | 5 | この章の目標は、作業用のPureScript開発環境を準備し、最初のPureScriptプログラムを書くことです。 6 | 7 | これから書く最初のコードはごく単純なPureScriptライブラリで、直角三角形の対角線の長さを計算する関数ひとつだけを提供します。 8 | 9 | ## 導入 10 | 11 | PureScript開発環境を準備するために、次のツールを使います。 12 | 13 | - [`purs`](http://purescript.org) - PureScriptコンパイラ本体 14 | - [`npm`](http://npmjs.org) - 残りの開発ツールをインストールできるようにする、Nodeパッケージマネージャ 15 | - [`Pulp`](https://github.com/purescript-contrib/pulp) ​​- さまざまな作業をパッケージマネージャと連動して自動化するコマンドラインツール 16 | 17 | 18 | この章ではこれらのツールのインストール方法と設定を説明します。 19 | 20 | ## PureScriptのインストール 21 | 22 | PureScriptコンパイラをインストールするときにお勧めなのは、[PureScriptのウェブサイト](http://purescript.org)からバイナリ配布物としてダウンロードする方法です。PureScriptコンパイラおよび関連する実行ファイルが、パス上で利用できるかどうか確認をしてください。試しに、コマンドラインでPureScriptコンパイラを実行してみましょう。 23 | 24 | ```text 25 | $ purs 26 | ``` 27 | 28 | PureScriptコンパイラをインストールする他の選択肢としては、次のようなものがあります。 29 | 30 | - NPMを使用する。`npm install -g purescript` 31 | - ソースコードからコンパイルを行う。この方法については、PureScriptのWebサイトが参考になります。 32 | 33 | ## 各ツールのインストール 34 | 35 | もし[NodeJS](http://nodejs.org/)がインストールされていないなら、NodeJSをインストールする必要があります。そうするとシステムに `npm`パッケージマネージャもインストールされるはずです。 `npm`がインストールされ、パス上で利用可能であることを確認してください。 36 | 37 | `npm`がインストールされたら、 `pulp`と `bower`もインストールする必要があります。プロジェクトがどこで作業しているかにかかわらずこれらのコマンドラインツールが利用可能であるようにするため、通常はグローバルにインストールしておくのがいいでしょう。 38 | 39 | ```text 40 | $ npm install -g pulp bower 41 | ``` 42 | 43 | これで、最初のPureScriptプロジェクトを作成するために必要なすべてのツールの用意ができたことになります。 44 | 45 | ## Hello, PureScript! 46 | 47 | まずはシンプルに始めましょう。PureScriptコンパイラ`pulp`を直接使用して、基本的なHello World! プログラムをコンパイルします。 48 | 最初に空のディレクトリ`my-project`を作成し、そこで`pulp init`を実行します。 49 | 50 | ```text 51 | $ mkdir my-project 52 | $ cd my-project 53 | $ pulp init 54 | 55 | * Generating project skeleton in ~/my-project 56 | 57 | $ ls 58 | 59 | bower.json src test 60 | ``` 61 | 62 | Pulpは`src`と`test`という2つのディレクトリと設定ファイル`bower.json`を作成してくれます。`src`ディレクトリにはソースコードファイルを保存し、`test`ディレクトリにはテストコードファイルを保存します。`test`ディレクトリはこの本の後半で使います。 63 | 64 | `src/Main.purs`という名前のファイルに、以下のコードを貼り付けてください。 65 | 66 | ```haskell 67 | module Main where 68 | 69 | import Control.Monad.Eff.Console 70 | 71 | main = log "Hello, World!" 72 | ``` 73 | 74 | これは小さなサンプルコードですが、​​いくつかの重要な概念を示しています。 75 | 76 | - すべてのソースファイルはモジュールヘッダから始まります。モジュール名は、ドットで区切られた大文字で始まる1つ以上の単語から構成されています。ここではモジュール名としてひとつの単語だけが使用されていますが、 `My.First.Module`というようなモジュール名も有効です。 77 | - モジュールは、モジュール名の各部分を区切るためのドットを含めた、完全な名前を使用してインポートされます。ここでは `log`関数を提供する `Control.Monad.Eff.Console`モジュールをインポートしています。 78 | - この `main`プログラムの定義本体は、関数適用の式になっています。PureScriptでは、関数適用は関数名のあとに引数を空白で区切って書くことで表します。 79 | 80 | それではこのコードをビルドして実行してみましょう。次のコマンドを実行します。 81 | 82 | ```text 83 | $ pulp run 84 | 85 | * Building project in ~/my-project 86 | * Build successful. 87 | Hello, World! 88 | ``` 89 | 90 | おめでとうございます! はじめてPureScriptで作成されたプログラムのコンパイルと実行ができました。 91 | 92 | ## ブラウザ向けのコンパイル 93 | 94 | Pulpは `pulp browserify`を実行して、PureScriptコードをブラウザで使うことに適したJavaScriptに変換することができます。 95 | 96 | ```text 97 | $ pulp browserify 98 | 99 | * Browserifying project in ~/my-project 100 | * Building project in ~/my-project 101 | * Build successful. 102 | * Browserifying... 103 | ``` 104 | 105 | これに続いて、大量のJavaScriptコードがコンソールに表示されます。 これは[Browserify](http://browserify.org/)の出力で、**Prelude**と呼ばれる標準のPureScriptライブラリに加え、`src`ディレクトリのコードにも適用されます。このJavaScriptコードをファイルに保存し、HTML文書に含めることもできます。これを試しに実行してみると、ブラウザのコンソールに"Hello、World!"という文章が出力されます。 106 | 107 | ## 使用されていないコードを取り除く 108 | 109 | Pulpは代替コマンド `pulp build`を提供しています。 `-O`オプションで**未使用コードの削除**を適用すると、不要なJavaScriptを出力から取り除くことができます。 110 | 111 | ```text 112 | $ pulp build -O --to output.js 113 | 114 | * Building project in ~/my-project 115 | * Build successful. 116 | * Bundling Javascript... 117 | * Bundled. 118 | ``` 119 | この場合も、生成されたコードはHTML文書で使用できます。 `output.js`を開くと、次のようなコンパイルされたモジュールがいくつか表示されます。 120 | 121 | ```javascript 122 | (function(exports) { 123 | "use strict"; 124 | 125 | var Control_Monad_Eff_Console = PS["Control.Monad.Eff.Console"]; 126 | 127 | var main = Control_Monad_Eff_Console.log("Hello, World!"); 128 | exports["main"] = main; 129 | })(PS["Main"] = PS["Main"] || {}); 130 | ``` 131 | 132 | ここでPureScriptコンパイラがJavaScriptコードを生成する方法の要点が示されています。 133 | 134 | - すべてのモジュールはオブジェクトに変換され、そのオブジェクトにはそのモジュールのエクスポートされたメンバが含まれています。モジュールは即時関数パターンによってスコープが限定されたコードで初期化されています。 135 | - PureScriptは可能な限り変数の名前をそのまま使おうとします。 136 | - PureScriptにおける関数適用は、そのままJavaScriptの関数適用に変換されます。 137 | - 引数のない単純な呼び出しとしてメインメソッド呼び出しが生成され、すべてのモジュールが定義された後に実行されます。 138 | - PureScriptコードはどんな実行時ライブラリにも依存しません。コンパイラによって生成されるすべてのコードは、あなたのコードが依存するいずれかのPureScriptモジュールをもとに出力されているものです。 139 | 140 | PureScriptはシンプルで理解しやすいコードを生成すること重視しているので、これらの点は大切です。実際に、ほとんどのコード生成処理はごく軽い変換です。PureScriptについての理解が比較的浅くても、ある入力からどのようなJavaScriptコードが生成されるかを予測することは難しくありません。 141 | 142 | ## CommonJSモジュールのコンパイル 143 | 144 | pulpは、PureScriptコードからCommonJSモジュールを生成するためにも使用できます。 これは、NodeJSを使用する場合やCommonJSモジュールを使用してコードを小さなコンポーネントに分割する大きなプロジェクトを開発する場合に便利です。 145 | 146 | CommonJSモジュールをビルドするには、( `-O`オプションなしで) `pulp build`コマンドを使います。 147 | 148 | ```text 149 | $ pulp build 150 | 151 | * Building project in ~/my-project 152 | * Build successful. 153 | ``` 154 | 155 | 生成されたモジュールはデフォルトで `output`ディレクトリに置かれます。 各PureScriptモジュールは、それ自身のサブディレクトリにある独自のCommonJSモジュールにコンパイルされます。 156 | 157 | ## Bowerによる依存関係の追跡 158 | 159 | この章の目的となっている `diagonal`関数を書くためには、平方根を計算できるようにする必要があります。 `purescript-math`パッケージにはJavaScriptの `Math`オブジェクトのプロパティとして定義されている関数の型定義が含まれていますので、 `purescript-math`パッケージをインストールしてみましょう。 `npm`の依存関係でやったのと同じように、次のようにコマンドラインに入力すると直接このパッケージをダウンロードできます。 160 | 161 | ```text 162 | $ bower install purescript-math --save 163 | ``` 164 | 165 | `--save`オプションは依存関係を `bower.json`設定ファイルに追加させます。 166 | 167 | `purescript-math`ライブラリは、依存するライブラリと一緒に `bower_components`サブディレクトリにインストールされます。 168 | 169 | ## 対角線の長さの計算 170 | 171 | それでは外部ライブラリの関数を使用する例として `diagonal`関数を書いてみましょう。 172 | 173 | まず、 `src/Main.purs`ファイルの先頭に次の行を追加し、 `Math`モジュールをインポートします。 174 | 175 | ```haskell 176 | import Math (sqrt) 177 | ``` 178 | 179 | また、数値の加算や乗算のようなごく基本的な演算を定義する `Prelude`モジュールをインポートすることも必要です。 180 | 181 | ```haskell 182 | import Prelude 183 | ``` 184 | 185 | そして、次のように `diagonal`関数を定義します。 186 | 187 | ```haskell 188 | diagonal w h = sqrt (w * w + h * h) 189 | ``` 190 | 191 | この関数の型を定義する必要はないことに注意してください。 `diagonal`は2つの数を取り数を返す関数である、とコンパイラは推論することができます。しかし、ドキュメントとしても役立つので、通常は型注釈を提供しておくことをお勧めします。 192 | 193 | それでは、新しい `diagonal`関数を使うように `main`関数も変更してみましょう。 194 | 195 | ```haskell 196 | main = logShow (diagonal 3.0 4.0) 197 | ``` 198 | 199 | `pulp run`を使用して、モジュールを再コンパイルします。 200 | 201 | ```text 202 | $ pulp run 203 | 204 | * Building project in ~/my-project 205 | * Build successful. 206 | 5.0 207 | ``` 208 | 209 | ## 対話式処理系を使用したコードのテスト 210 | 211 | PureScriptコンパイラには `PSCi`と呼ばれる対話式のREPL(Read-eval-print loop)が付属しています。 `PSCi`はコードをテストなど思いついたことを試すのにとても便利です。それでは、 `psci`を使って `diagonal`関数をテストしてみましょう。 212 | 213 | `pulp repl`コマンドを使ってソースモジュールを自動的に `PSCi`にロードすることができます。 214 | 215 | ```text 216 | $ pulp repl 217 | > 218 | ``` 219 | 220 | コマンドの一覧を見るには、 `:?`と入力します。 221 | 222 | ```text 223 | > :? 224 | The following commands are available: 225 | 226 | :? Show this help menu 227 | :quit Quit PSCi 228 | :reset Reset 229 | :browse Browse 230 | :type Show the type of 231 | :kind Show the kind of 232 | :show import Show imported modules 233 | :show loaded Show loaded modules 234 | :paste paste Enter multiple lines, terminated by ^D 235 | ``` 236 | 237 | Tabキーを押すと、自分のコードで利用可能なすべての関数、及びBowerの依存関係とプレリュードモジュールのリストをすべて見ることができるはずです。 238 | 239 | `Prelude`モジュールを読み込んでください。 240 | 241 | ```text 242 | > import Prelude 243 | ``` 244 | 245 | 幾つか数式を評価してみてください。 `PSCi`で評価を行うには、1行以上の式を入力し、Ctrl+ Dで入力を終了します。 246 | 247 | ```text 248 | > 1 + 2 249 | 3 250 | 251 | > "Hello, " <> "World!" 252 | "Hello, World!" 253 | ``` 254 | 255 | それでは `PSCi`で `diagonal`関数を試してみましょう。 256 | 257 | ```text 258 | > import Main 259 | > diagonal 5.0 12.0 260 | 261 | 13.0 262 | ``` 263 | 264 | また、 `PSCi`で関数を定義することもできます。 265 | 266 | ```text 267 | > double x = x * 2 268 | 269 | > double 10 270 | 20 271 | ``` 272 | 273 | コード例の構文がまだよくわからなくても心配はいりません。 この本を読み進めるうちにわかるようになっていきます。 274 | 275 | 最後に、 `:type`コマンドを使うと式の型を確認することができます。 276 | 277 | ```text 278 | > :type true 279 | Boolean 280 | 281 | > :type [1, 2, 3] 282 | Array Int 283 | ``` 284 | 285 | `PSCi`で試してみてください。もしどこかでつまずいた場合は、メモリ内にあるコンパイル済みのすべてのモジュールをアンロードするリセットコマンド `:reset`を使用してみてください。 286 | 287 | ## 演習 288 | 289 | 1. (簡単) `Math`モジュールで定義されている `pi`定数を使用し、指定された半径の円の面積を計算する関数 `circleArea`を書いてみましょう。また、 `PSCi`を使用してその関数をテストしてください。 (**ヒント**: `import math`文を修正して、 `pi`をインポートすることを忘れないようにしましょう) 290 | 1. (やや難しい) `purescript-globals`パッケージを依存関係としてインストールするには、`bower install`を使います。PSCiでその機能を試してみてください。 (**ヒント**: PSCiの `:browse`コマンドを使うと、モジュールの内容を閲覧することができます) 291 | 292 | ## まとめ 293 | 294 | この章では、Pulpツールを使用して簡単なPureScriptプロジェクトを設定しました。 295 | 296 | また、最初のPureScript関数を書き、コンパイルし、NodeJSを使用して実行することができました。 297 | 298 | 以降の章では、コードをコンパイルやデバッグ、テストするためにこの開発設定を使用しますので、これらのツールや使用手順に十分習熟しておくとよいでしょう。 299 | -------------------------------------------------------------------------------- /src/chapter03.md: -------------------------------------------------------------------------------- 1 | # 関数とレコード 2 | 3 | ## この章の目標 4 | 5 | この章では、関数およびレコードというPureScriptプログラムのふたつの構成要素を導入します。さらに、どのようにPureScriptプログラムを構造化するのか、どのように型をプログラム開発に役立てるかを見ていきます。  6 | 7 | 連絡先のリストを管理する簡単​​な住所録アプリケーションを作成していきます。このコード例により、PureScriptの構文からいくつかの新しい概念を導入します。 8 | 9 | このアプリケーションのフロントエンドは対話式処理系 `PSCi`を使うようにしていますが、JavaScriptでフロントエンドを書くこともできるでしょう。実際に後の章で、フォームの検証と保存および復元の機能追加について詳しく説明します。 10 | 11 | ## プロジェクトの準備 12 | 13 | この章のソースコードは `src/Data/AddressBook.purs`というファイルに含まれています。このファイルは次のようなモジュール宣言とインポート一覧から始まります。 14 | 15 | ```haskell 16 | module Data.AddressBook where 17 | 18 | import Prelude 19 | 20 | import Control.Plus (empty) 21 | import Data.List (List(..), filter, head) 22 | import Data.Maybe (Maybe) 23 | ``` 24 | 25 | ここでは、いくつかのモジュールをインポートします。 26 | 27 | - `Control.Plus`モジュールには後ほど使う `empty`値が定義されています。 28 | - `purescript-lists`パッケージで提供されている `Data.List`モジュールをインポートしています。 `purescript-lists`パッケージはbowerを使用してインストールすることができ、連結リストを使うために必要ないくつかの関数が含まれています。 29 | - `Data.Maybe`モジュールは、値が存在したりしなかったりするような、オプショナルな値を扱うためのデータ型と関数を定義しています。 30 | - (訳者注・ダブルドット(..)を使用すると、指定された型コンストラクタのすべてのデータコンストラクタをインポートできます。) 31 | 32 | このモジュールのインポート内容が括弧内で明示的に列挙されていることに注目してください。明示的な列挙はインポート内容の衝突を避けるのに役に立つので、一般に良い習慣です。 33 | 34 | ソースコードリポジトリを複製したと仮定すると、この章のプロジェクトは次のコマンドを使用してPulpを使用して構築できます。 35 | 36 | ```text 37 | $ cd chapter3 38 | $ bower update 39 | $ pulp build 40 | ``` 41 | 42 | ## 単純な型 43 | 44 | JavaScriptのプリミティブ型に対応する組み込みデータ型として、PureScriptでは数値型と文字列型、真偽型の3つが定義されており、それぞれ `Number`、 `String`、 `Boolean`と呼ばれています。これらの型はすべてのモジュールに暗黙にインポートされる `Prim`モジュールで定義されています。`pulp repl`の `:type`コマンドを使用すると、簡単な値の型を確認できます。 45 | 46 | ```text 47 | $ pulp repl 48 | 49 | > :type 1.0 50 | Number 51 | 52 | > :type "test" 53 | String 54 | 55 | > :type true 56 | Boolean 57 | ``` 58 | 59 | PureScriptには他にも、配列とレコード、関数などの組み込み型が定義されています。 60 | 61 | 整数は、小数点以下を省くことによって、型 `Number`の浮動小数点数の値と区別されます。 62 | 63 | ```text 64 | > :type 1 65 | Int 66 | ``` 67 | 68 | 二重引用符を使用する文字列リテラルとは異なり、文字リテラルは一重引用符で囲みます。 69 | 70 | ```text 71 | > :type 'a' 72 | Char 73 | ``` 74 | 75 | 配列はJavaScriptの配列に対応していますが、JavaScriptの配列とは異なり、PureScriptの配列のすべての要素は同じ型を持つ必要があります。 76 | 77 | ```text 78 | > :type [1, 2, 3] 79 | Array Int 80 | 81 | > :type [true, false] 82 | Array Boolean 83 | 84 | > :type [1, false] 85 | Could not match type Int with Boolean. 86 | ``` 87 | 88 | 最後の例で起きているエラーは型検証器によって報告されたもので、配列の2つの要素の型を**単一化**(Unification)しようとして失敗したこと示しています。 89 | 90 | レコードはJavaScriptのオブジェクトに対応しており、レコードリテラルはJavaScriptのオブジェクトリテラルと同じ構文になっています。 91 | 92 | ```text 93 | > author = { name: "Phil", interests: ["Functional Programming", "JavaScript"] } 94 | 95 | > :type author 96 | { name :: String 97 | , interests :: Array String 98 | } 99 | ``` 100 | 101 | この型が示しているのは、オブジェクト `author`は、 102 | 103 | - `String`型のフィールド `name` 104 | - `Array String`つまり `String`の配列の型のフィールド `interests` 105 | 106 | という2つの**フィールド**(field)を持っているということです。 107 | 108 | レコードのフィールドは、ドットに続けて参照したいフィールドのラベルを書くと参照することができます。 109 | 110 | ```text 111 | > author.name 112 | "Phil" 113 | 114 | > author.interests 115 | ["Functional Programming","JavaScript"] 116 | ``` 117 | 118 | PureScriptの関数はJavaScriptの関数に対応しています。PureScriptの標準ライブラリは多くの関数の例を提供しており、この章ではそれらをもう少し詳しく見ていきます。 119 | 120 | ```text 121 | > import Prelude 122 | > :type flip 123 | forall a b c. (a -> b -> c) -> b -> a -> c 124 | 125 | > :type const 126 | forall a b. a -> b -> a 127 | ``` 128 | 129 | ファイルのトップレベルでは、等号の直前に引数を指定することで関数を定義することができます。 130 | 131 | ```haskell 132 | add :: Int -> Int -> Int 133 | add x y = x + y 134 | ``` 135 | 136 | バックスラッシュに続けて空白文字で区切られた引数名のリストを書くことで、関数をインラインで定義することもできます。PSCiで複数行の宣言を入力するには、 `:paste`コマンドを使用して"paste mode"に入ります。このモードでは、**Control-D**キーシーケンスを使用して宣言を終了します。 137 | 138 | ```text 139 | > :paste 140 | … add :: Int -> Int -> Int 141 | … add = \x y -> x + y 142 | … ^D 143 | ``` 144 | 145 | `PSCi`でこの関数が定義されていると、次のように関数の隣に2つの引数を空白で区切って書くことで、関数をこれらの引数に**適用**(apply)することができます。 146 | 147 | ```text 148 | > add 10 20 149 | 30 150 | ``` 151 | 152 | ## 量化された型 153 | 154 | 前の節ではPreludeで定義された関数の型をいくつか見てきました。たとえば `flip`関数は次のような型を持っていました。 155 | 156 | ```text 157 | > :type flip 158 | forall a b c. (a -> b -> c) -> b -> a -> c 159 | ``` 160 | 161 | この `forall`キーワードは、 `flip`が**全称量化された型**(universally quantified type)を持っていることを示しています。これは、 `a`や `b`、 `c`をどの型に置き換えても、 `flip`はその型でうまく動作するという意味です。 162 | 163 | 例えば、 `a`を `Int`、 `b`を `String`、 `c`を `String`というように選んでみたとします。この場合、 `flip`の型を次のように**特殊化**(specialize)することができます。 164 | 165 | ```text 166 | (Int -> String -> String) -> String -> Int -> String 167 | ``` 168 | 169 | 量化された型を特殊化したいということをコードで示す必要はありません。特殊化は自動的に行われます。たとえば、すでにその型の `flip`を持っていたかのように、次のように単に `flip`を使用することができます。 170 | 171 | ```text 172 | > flip (\n s -> show n <> s) "Ten" 10 173 | 174 | "10Ten" 175 | ``` 176 | 177 | `a`、 `b`、 `c`の型はどんな型でも選ぶことができるといっても、型の不整合は生じないようにしなければなりません。 `flip`に渡す関数の型は、他の引数の型と整合性がなくてはなりません。第2引数として文字列 `"Ten"`、第3引数として数 `10`を渡したのはそれが理由です。もし引数が逆になっているとうまくいかないでしょう。 178 | 179 | ```text 180 | > flip (\n s -> show n <> s) 10 "Ten" 181 | 182 | Could not match type Int with type String 183 | ``` 184 | 185 | ## 字下げについての注意 186 | 187 | JavaScriptとは異なり、PureScriptのコードは字下げの大きさに影響されます(indentation-sensitive)。これはHaskellと同じようになっています。コード内の空白の多寡は無意味ではなく、Cのような言語で中括弧によってコードのまとまりを示しているように、PureScriptでは空白がコードのまとまりを示すのに使われているということです。 188 | 189 | 宣言が複数行にわたる場合は、2つめの行は最初の行の字下げより深く字下げしなければなりません。 190 | 191 | したがって、次は正しいPureScriptコードです。 192 | 193 | ```haskell 194 | add x y z = x + 195 | y + z 196 | ``` 197 | 198 | しかし、次は正しいコードではありません。 199 | 200 | ```haskell 201 | add x y z = x + 202 | y + z 203 | ``` 204 | 205 | 後者では、PureScriptコンパイラはそれぞれの行ごとにひとつ、つまり**2つ**の宣言であると構文解析します。 206 | 207 | 一般に、同じブロック内で定義された宣言は同じ深さで字下げする必要があります。例えば `PSCi`でlet文の宣言は同じ深さで字下げしなければなりません。次は正しいコードです。 208 | 209 | ```text 210 | > :paste 211 | … x = 1 212 | … y = 2 213 | … ^D 214 | ``` 215 | 216 | しかし、これは正しくありません。 217 | 218 | ```text 219 | > :paste 220 | … x = 1 221 | … y = 2 222 | … ^D 223 | ``` 224 | 225 | PureScriptのいくつかの予約語(例えば `where`や `of`、 `let`)は新たなコードのまとまりを導入しますが、そのコードのまとまり内の宣言はそれより深く字下げされている必要があります。 226 | 227 | ```haskell 228 | example x y z = foo + bar 229 | where 230 | foo = x * y 231 | bar = y * z 232 | ``` 233 | 234 | ここで `foo`や `bar`の宣言は `example`の宣言より深く字下げされていることに注意してください。 235 | 236 | ただし、ソースファイルの先頭、最初の `module`宣言における予約語 `where`だけは、この規則の唯一の例外になっています。 237 | 238 | ## 独自の型の定義 239 | 240 | PureScriptで新たな問題に取り組むときは、まずはこれから扱おうとする値の型の定義を書くことから始めるのがよいでしょう。最初に、住所録に含まれるレコードの型を定義してみます。 241 | 242 | ```haskell 243 | type Entry = { firstName :: String, lastName :: String, address :: Address } 244 | ``` 245 | 246 | これは `Entry`という**型同義語**(type synonym、型シノニム)を定義しています。 型 `Entry`は等号の右辺と同じ型ということです。レコードの型はいずれも文字列である `firstName`、 `lastName`、 `phone`という3つのフィールドからなります。前者の2つのフィールドは型 `String`を持ち、 `address`は以下のように定義された型 `Address`を持っています。 247 | 248 | ```haskell 249 | type Address = { street :: String, city :: String, state :: String } 250 | ``` 251 | 252 | それでは、2つめの型同義語も定義してみましょう。住所録のデータ構造としては、単に項目の連結リストとして格納することにします。 253 | 254 | ```haskell 255 | type AddressBook = List Entry 256 | ``` 257 | 258 | `List Entry`は `Array Entry`とは同じではないということに注意してください。 `Array Entry`は住所録の項目の**配列**を意味しています。 259 | 260 | ## 型構築子と種 261 | 262 | `List`は**型構築子**(type constructor、型コンストラクタ)の一例になっています。 `List`そのものは型ではなく、何らかの型 `a`があるとき `List a`が型になっています。つまり、 `List`は**型引数**(type argument)`a`をとり、新たな型 `List a`を構築するのです。 263 | 264 | ちょうど関数適用と同じように、型構築子は他の型に並べることで適用されることに注意してください。型 `List Entry`は実は型構築子 `List`が型 `Entry`に**適用**されたものです。これは住所録項目のリストを表しています。 265 | 266 | (型注釈演算子 `::`を使って)もし型 `List`の値を間違って定義しようとすると、今まで見たことのないような種類のエラーが表示されるでしょう。 267 | 268 | ```text 269 | > import Data.List 270 | > Nil :: List 271 | In a type-annotated expression x :: t, the type t must have kind Type 272 | ``` 273 | 274 | これは**種エラー**(kind error)です。値がその**型**で区別されるのと同じように、型はその**種**(kind)によって区別され、間違った型の値が**型エラー**になるように、**間違った種**の型は種エラーを引き起こします。 275 | 276 | `Number`や `String`のような、値を持つすべての型の種を表す `Type`と呼ばれる特別な種があります。 277 | 278 | 型構築子にも種があります。たとえば、種 `Type -> Type`はちょうど `List`のような型から型への関数を表しています。ここでエラーが発生したのは、値が種 `Type`であるような型を持つと期待されていたのに、 `List`は種 `Type -> Type`を持っているためです。 279 | 280 | `PSCi`で型の種を調べるには、 `:kind`命令を使用します。例えば次のようになります。 281 | 282 | ```text 283 | > :kind Number 284 | Type 285 | 286 | > import Data.List 287 | > :k List 288 | Type -> Type 289 | 290 | > :kind List String 291 | Type 292 | ``` 293 | 294 | PureScriptの**種システム**は他にも面白い種に対応していますが、それらについては本書の他の部分で見ていくことになるでしょう。 295 | 296 | ## 住所録の項目の表示 297 | 298 | それでは最初に、文字列で住所録の項目を表現するような関数を書いてみましょう。まずは関数に型を与えることから始めます。型の定義は省略することも可能ですが、ドキュメントとしても役立つので型を書いておくようにすると良いでしょう。型宣言は関数の名前とその型を `::`記号で区切るようにして書きます。 299 | 300 | ```haskell 301 | showEntry :: Entry -> String 302 | ``` 303 | 304 | `showEntry`は引数として `Entry`を取り `string`を返す関数であるということを、この型シグネチャは言っています。 `showEntry`の定義は次のとおりです。 305 | 306 | ```haskell 307 | showEntry entry = entry.lastName <> ", " <> 308 | entry.firstName <> ": " <> 309 | showAddress entry.address 310 | ``` 311 | 312 | この関数は `Entry`レコードの3つのフィールドを連結し、単一の文字列にします。ここで使用される `showAddress`は `address`フィールドを連接し、単一の文字列にする関数です。 `showAddress`の定義は次のとおりです。 313 | 314 | ```haskell 315 | showAddress :: Address -> String 316 | showAddress addr = addr.street <> ", " <> 317 | addr.city <> ", " <> 318 | addr.state 319 | ``` 320 | 321 | 関数定義は関数の名前で始まり、引数名のリストが続きます。関数の結果は等号の後ろに定義します。フィールドはドットに続けてフィールド名を書くことで参照することができます。PureScriptでは、文字列連結はJavaScriptのような単一のプラス記号ではなく、ダイアモンド演算子( `<>`)を使用します。 322 | 323 | ## はやめにテスト、たびたびテスト 324 | 325 | `PSCi`対話式処理系では反応を即座に得られるので、試行錯誤を繰り返したいときに向いています。それではこの最初の関数が正しく動作するかを `PSCi`を使用して確認してみましょう。 326 | 327 | まず、これまで書かれたコードをビルドします。 328 | 329 | ```text 330 | $ pulp build 331 | ``` 332 | 333 | 次に、 `PSCi`を起動し、この新しいモジュールをインポートするために `import`命令を使います。 334 | 335 | ```text 336 | $ pulp repl 337 | 338 | > import Data.AddressBook 339 | ``` 340 | 341 | レコードリテラルを使うと、住所録の項目を作成することができます。レコードリテラルはJavaScriptの無名オブジェクトと同じような構文で名前に束縛します。 342 | 343 | ```text 344 | > address = { street: "123 Fake St.", city: "Faketown", state: "CA" } 345 | ``` 346 | 347 | ​それでは、この例に関数を適用してみてください。 348 | 349 | ```text 350 | > showAddress address 351 | 352 | "123 Fake St., Faketown, CA" 353 | ``` 354 | 355 | そして、例で作成した `address`を含む住所録の `entry`レコードを作成し `showEntry`に適用させましょう。 356 | 357 | ```text 358 | > entry = { firstName: "John", lastName: "Smith", address: address } 359 | > showEntry entry 360 | 361 | "Smith, John: 123 Fake St., Faketown, CA" 362 | ``` 363 | 364 | ## 住所録の作成 365 | 366 | 今度は住所録の操作を支援する関数をいくつか書いてみましょう。空の住所録を表す値として、空のリストを使います。 367 | 368 | ```haskell 369 | emptyBook :: AddressBook 370 | emptyBook = empty 371 | ``` 372 | 373 | 既存の住所録に値を挿入する関数も必要でしょう。この関数を `insertEntry`と呼ぶことにします。関数の型を与えることから始めましょう。 374 | 375 | ```haskell 376 | insertEntry :: Entry -> AddressBook -> AddressBook 377 | ``` 378 | 379 | `insertEntry`は、最初の引数として `Entry`、第二引数として `AddressBook`を取り、新しい `AddressBook`を返すということを、この型シグネチャは言っています。 380 | 381 | 既存の `AddressBook`を直接変更することはしません。その代わりに、同じデータが含まれている新しい `AddressBook`を返すようにします。このように、 `AddressBook`は**永続データ構造**(persistent data structure)の一例となっています。これはPureScriptにおける重要な考え方です。変更はコードの副作用であり、コードの振る舞いについての判断するのを難しくします。そのため、我々は可能な限り純粋な関数や不変のデータを好むのです。 382 | 383 | `Data.List`の `Cons`関数を使用すると `insertEntry`を実装できます。 `PSCi`を起動し `:type`コマンドを使って、この関数の型を見てみましょう。 384 | 385 | ```text 386 | $ pulp repl 387 | 388 | > import Data.List 389 | > :type Cons 390 | 391 | forall a. a -> List a -> List a 392 | ``` 393 | 394 | `Cons`は、なんらかの型 `a`の値と、型 `a`を要素に持つリストを引数にとり、同じ型の要素を持つ新しいリストを返すということを、この型シグネチャは言っています。 `a`を `Entry`型として特殊化してみましょう。 395 | 396 | ```haskell 397 | Entry -> List Entry -> List Entry 398 | ``` 399 | 400 | しかし、 `List Entry`はまさに `AddressBook`ですから、次と同じになります。 401 | 402 | ```haskell 403 | Entry -> AddressBook -> AddressBook 404 | ``` 405 | 406 | 今回の場合、すでに適切な入力があります。 `Entry`と `AddressBook`に `Cons`を適用すると、新しい `AddressBook`を得ることができます。これこそまさに私たちが求めていた関数です! 407 | 408 | `insertEntry`の実装は次のようになります。 409 | 410 | ```haskell 411 | insertEntry entry book = Cons entry book 412 | ``` 413 | 414 | 等号の左側にある2つの引数 `entry`と `book`がスコープに導入されますから、これらに `Cons`関数を適用して結果の値を作成しています。 415 | 416 | ## カリー化された関数 417 | 418 | PureScriptでは、関数は常にひとつの引数だけを取ります。 `insertEntry`関数は2つの引数を取るように見えますが、これは実際には**カリー化された関数**(curried function)の一例となっています。 419 | 420 | `insertEntry`の型に含まれる `->`は右結合の演算子であり、つまりこの型はコンパイラによって次のように解釈されます。 421 | 422 | ```haskell 423 | Entry -> (AddressBook -> AddressBook) 424 | ``` 425 | 426 | すなわち、 `insertEntry`は関数を返す関数である、ということです!この関数は単一の引数 `Entry`を取り、それから単一の引数 `AddressBook`を取り新しい `AddressBook`を返す新しい関数を返すのです。 427 | 428 | これは例えば、最初の引数だけを与えると `insertEntry`を**部分適用**(partial application)できることを意味します。 `PSCi`でこの結果の型を見てみましょう。 429 | 430 | ```text 431 | > :type insertEntry example 432 | 433 | AddressBook -> AddressBook 434 | ``` 435 | 436 | 期待したとおり、戻り値の型は関数になっていました。この結果の関数に、ふたつめの引数を適用することもできます。 437 | 438 | ```text 439 | > :type (insertEntry example) emptyBook 440 | AddressBook 441 | ``` 442 | 443 | ここで括弧は不要であることにも注意してください。次の式は同等です。 444 | 445 | ```text 446 | > :type insertEntry example emptyBook 447 | AddressBook 448 | ``` 449 | 450 | これは関数適用が左結合であるためで、なぜ単に空白で区切るだけで関数に引数を与えることができるのかも説明にもなっています。 451 | 452 | 本書では今後、「2引数の関数」というように表現することがあることに注意してください。これはあくまで、最初の引数を取り別の関数を返す、カリー化された関数を意味していると考えてください。 453 | 454 | 今度は `insertEntry`の定義について考えてみます。 455 | 456 | ```haskell 457 | insertEntry :: Entry -> AddressBook -> AddressBook 458 | insertEntry entry book = Cons entry book 459 | ``` 460 | 461 | もし式の右辺に明示的に括弧をつけるなら、 `(Cons entry)book`となります。 `insertEntry entry`はその引数が単に関数 `(Cons entry)`に渡されるような関数だということです。この2つの関数はどんな入力についても同じ結果を返しますから、つまりこれらは同じ関数です!よって、両辺から引数 `book`を削除できます。 462 | 463 | ```haskell 464 | insertEntry :: Entry -> AddressBook -> AddressBook 465 | insertEntry entry = Cons entry 466 | ``` 467 | 468 | そして、同様の理由で両辺から `entry`も削除することができます。 469 | 470 | ```haskell 471 | insertEntry :: Entry -> AddressBook -> AddressBook 472 | insertEntry = Cons 473 | ``` 474 | 475 | この処理は**イータ変換**(eta conversion)と呼ばれ、引数を参照することなく関数を定義する**ポイントフリー形式**(point-free form)へと関数を書き換えるのに使うことができます。 476 | 477 | `insertEntry`の場合には、イータ変換によって「 `insertEntry`は単にリストに対する `cons`だ」と関数の定義はとても明確になりました。しかしながら、常にポイントフリー形式のほうがいいのかどうかには議論の余地があります。 478 | 479 | ## あなたの住所録は? 480 | 481 | 最小限の住所録アプリケーションの実装で必要になる最後の関数は、名前で人を検索し適切な `Entry`を返すものです。これは小さな関数を組み合わせることでプログラムを構築するという、関数型プログラミングで鍵となる考え方のよい応用例になるでしょう。 482 | 483 | まずは住所録をフィルタリングし、該当する姓名を持つ項目だけを保持するようにするのがいいでしょう。それから、結果のリストの先頭の(head)要素を返すだけです。 484 | 485 | この大まかな仕様に従って、この関数の型を計算することができます。まず `PSCi`を起動し、 `filter`関数と `head`関数の型を見てみましょう。 486 | 487 | ```text 488 | $ pulp repl 489 | 490 | > import Data.List 491 | > :type filter 492 | 493 | forall a. (a -> Boolean) -> List a -> List a 494 | 495 | > :type head 496 | 497 | forall a. List a -> Maybe a 498 | ``` 499 | 500 | 型の意味を理解するために、これらの2つの型の一部を取り出してみましょう。 501 | 502 | `filter`はカリー化された2引数の関数です。最初の引数は、リストの要素を取り `Boolean`値を結果として返す関数です。第2引数は要素のリストで、返り値は別のリストです。 503 | 504 | `head`は引数としてリストをとり、 `Maybe a`という今まで見たことがないような型を返します。 `Maybe a`は型 `a`のオプショナルな値、つまり `a`の値を持つか持たないかのどちらかの値を示しており、JavaScriptのような言語で値がないことを示すために使われる `null`の型安全な代替手段を提供します。これについては後の章で詳しく扱います。 505 | 506 | `filter`と `head`の全称量化された型は、PureScriptコンパイラによって次のように**特殊化**(specialized)されます。 507 | 508 | ```haskell 509 | filter :: (Entry -> Boolean) -> AddressBook -> AddressBook 510 | 511 | head :: AddressBook -> Maybe Entry 512 | ``` 513 | 514 | 検索する関数の引数として姓と名前を渡す必要があるのもわかっています。 515 | 516 | `filter`に渡す関数も必要になることもわかります。この関数を `filterEntry`と呼ぶことにしましょう。 `filterEntry`は `Entry -> Boolean`という型を持っています。 `filter filterEntry`という関数適用の式は、 `AddressBook -> AddressBook`という型を持つでしょう。もしこの関数の結果を `head`関数に渡すと、型 `Maybe Entry`の結果を得ることになります。 517 | 518 | これまでのことをまとめると、この `findEntry`関数の妥当な型シグネチャは次のようになります。 519 | 520 | ```haskell 521 | findEntry :: String -> String -> AddressBook -> Maybe Entry 522 | ``` 523 | 524 | `findEntry`は、姓と名前の2つの文字列、および `AddressBook`を引数にとり、 `Maybe Entry`という型の値を結果として返すということを、この型シグネチャは言っています。結果の `Maybe Entry`という型は、名前が住所録で発見された場合にのみ `Entry`の値を持ちます。 525 | 526 | そして、 `findEntry`の定義は次のようになります。 527 | 528 | ```haskell 529 | findEntry firstName lastName book = head $ filter filterEntry book 530 | where 531 | filterEntry :: Entry -> Boolean 532 | filterEntry entry = entry.firstName == firstName && entry.lastName == lastName 533 | ``` 534 | 535 | 一歩づつこのコードの動きを調べてみましょう。 536 | 537 | `findEntry`は、どちらも文字列型である `firstName`と `lastName`、 `AddressBook`型の `book`という3つの名前をスコープに導入します 538 | 539 | 定義の右辺では `filter`関数と `head`関数が組み合わされています。まず項目のリストをフィルタリングし、その結果に `head`関数を適用しています。 540 | 541 | 真偽型を返す関数 `filterEntry`は `where`節の内部で補助的な関数として定義されています。このため、 `filterEntry`関数はこの定義の内部では使用できますが、外部では使用することができません。また、 `filterEntry`はそれを包む関数の引数に依存することができ、 `filterEntry`は指定された `Entry`をフィルタリングするために引数 `firstName`と `lastName`を使用しているので、 `filterEntry`が `findEntry`の内部にあることは必須になっています。 542 | 543 | 最上位での宣言と同じように、必ずしも `filterEntry`の型シグネチャを指定しなくてもよいことに注意してください。ただし、ドキュメントとしても役に立つので型シグネチャを書くことは推奨されています。 544 | 545 | ## 中置の関数適用 546 | 547 | 上でみた `findEntry`のコードでは、少し異なる形式の関数適用が使用されています。 `head`関数は中置の `$`演算子を使って式 `filter filterEntry book`に適用されています。 548 | 549 | これは `head (filter filterEntry book)`という通常の関数適用と同じ意味です。 550 | 551 | `($)`はPreludeで定義されている `apply`関数の別名で、次のように定義されています。 552 | 553 | ```haskell 554 | apply :: forall a b. (a -> b) -> a -> b 555 | apply f x = f x 556 | 557 | infixr 0 apply as $ 558 | ``` 559 | 560 | ここで、 `apply`は関数と値をとり、その値にその関数を適用します。 `infixr`キーワードは `($)`を `apply`の別名として定義します。 561 | 562 | しかし、なぜ通常の関数適用の代わりに `$`を使ったのでしょうか? その理由は `$`は右結合で優先順位の低い演算子だということにあります。これは、深い入れ子になった関数適用のための括弧を、 `$`を使うと取り除くことができることを意味します。 563 | 564 | たとえば、ある従業員の上司の住所がある道路を見つける、次の入れ子になった関数適用を考えてみましょう。 565 | 566 | ```haskell 567 | street (address (boss employee)) 568 | ``` 569 | 570 | これは `$`を使用して表現すればずっと簡単になります。 571 | 572 | ```haskell 573 | street $ address $ boss employee 574 | ``` 575 | 576 | ## 関数合成 577 | 578 | イータ変換を使うと `insertEntry`関数を簡略化できたのと同じように、引数をよく考察すると `findEntry`の定義を簡略化することができます。 579 | 580 | 引数 `book`が関数 `filter filterEntry`に渡され、この適用の結果が `head`に渡されることに注目してください。これは言いかたを変えれば、 `filter filterEntry`と `head`の**合成**(composition) に `book`は渡されるということです。 581 | 582 | PureScriptの関数合成演算子は `<<<`と `>>>`です。前者は「逆方向の合成」であり、後者は「順方向の合成」です。 583 | 584 | いずれかの演算子を使用して `findEntry`の右辺を書き換えることができます。逆順の合成を使用すると、右辺は次のようになります。 585 | 586 | ```haskell 587 | (head <<< filter filterEntry) book 588 | ``` 589 | 590 | この形式なら最初の定義にイータ変換の技を適用することができ、 `findEntry`は最終的に次のような形式に到達します。 591 | 592 | ```haskell 593 | findEntry firstName lastName = head <<< filter filterEntry 594 | where 595 | ... 596 | ``` 597 | 598 | 右辺を次のようにしても同じです。 599 | 600 | ```haskell 601 | filter filterEntry >>> head 602 | ``` 603 | 604 | どちらにしても、これは「 `findEntry`はフィルタリング関数と `head`関数の合成である」という `findEntry`関数のわかりやすい定義を与えます。 605 | 606 | どちらの定義のほうがわかりやすいかの判断はお任せしますが、このように関数を部品として捉え、関数はひとつの役目だけをこなし、機能を関数合成で組み立てるというように考えると有用なことがよくあります。 607 | 608 | ## テスト、テスト、テスト…… 609 | 610 | これでこのアプリケーションの中核部分が完成しましたので、 `PSCi`を使って試してみましょう。 611 | 612 | ```text 613 | $ pulp repl 614 | 615 | > import Data.AddressBook 616 | ``` 617 | 618 | まずは空の住所録から項目を検索してみましょう(これは明らかに空の結果が返ってくることが期待されます)。 619 | 620 | ```text 621 | > findEntry "John" "Smith" emptyBook 622 | 623 | No type class instance was found for 624 | 625 | Data.Show.Show { firstName :: String 626 | , lastName :: String 627 | , address :: { street :: String 628 | , city :: String 629 | , state :: String 630 | } 631 | } 632 | ``` 633 | 634 | エラーです!でも心配しないでください。これは単に 型 `Entry`の値を文字列として出力する方法を `PSCi`が知らないという意味のエラーです。 635 | 636 | `findEntry`の返り値の型は `Maybe Entry`ですが、これは手作業で文字列に変換することができます。 637 | 638 | `showEntry`関数は `Entry`型の引数を期待していますが、今あるのは `Maybe Entry`型の値です。この関数は `Entry`型のオプショナルな値を返すことを忘れないでください。行う必要があるのは、オプショナルな値の中に項目の値が存在すれば `showEntry`関数を適用し、そうでなければ存在しないという値をそのまま伝播することです。 639 | 640 | 幸いなことに、Preludeモジュールはこれを行う方法を提供しています。 `map`演算子は `Maybe`のような適切な型構築子まで関数を「持ち上げる」ことができます(この本の後半で関手について説明するときに、この関数やそれに類似する他のものについて詳しく見ていきます)。 641 | 642 | ```text 643 | > import Prelude 644 | > map showEntry (findEntry "John" "Smith" emptyBook) 645 | 646 | Nothing 647 | ``` 648 | 649 | 今度はうまくいきました。この返り値 `Nothing`は、オプショナルな返り値に値が含まれていないことを示しています。期待していたとおりです。 650 | 651 | もっと使いやすくするために、 `Entry`を文字列として出力するような関数を定義し、毎回 `showEntry`を使わなくてもいいようにすることもできます。 652 | 653 | ```haskell 654 | printEntry firstName lastName book 655 | = map showEntry (findEntry firstName lastName book) 656 | ``` 657 | 658 | それでは空でない住所録を作成してもう一度試してみましょう。先ほどの項目の例を再利用します。 659 | 660 | ```text 661 | > book1 = insertEntry entry emptyBook 662 | 663 | > printEntry "John" "Smith" book1 664 | 665 | Just ("Smith, John: 123 Fake St., Faketown, CA") 666 | ``` 667 | 668 | 今度は結果が正しい値を含んでいました。 `book1`に別の名前で項目を挿入して、ふたつの名前がある住所録 `book2`を定義し、それぞれの項目を名前で検索してみてください。 669 | 670 | ## 演習 671 | 672 | 1. (簡単) `findEntry`関数の定義の主な部分式の型を書き下し、 `findEntry`関数についてよく理解しているか試してみましょう。たとえば、 `findEntry`の定義のなかにある `head`関数の型は `AddressBook -> Maybe Entry`と特殊化されています。 673 | 674 | 1. (簡単) `findEntry`の既存のコードを再利用し、与えられた電話番号から `Entry`を検索する関数を書いてみましょう。また、 `PSCi`で実装した関数をテストしてみましょう。 675 | 676 | 1. (やや難しい) 指定された名前が `AddressBook`に存在するかどうかを調べて真偽値で返す関数を書いてみましょう。 (**ヒント**:リストが空かどうかを調べる `Data.List.null`関数の型を `psci`で調べてみてみましょう) 677 | 678 | 1. (難しい) 姓名が重複している項目を住所録から削除する関数 `removeDuplicates`を書いてみましょう。 (**ヒント**:値どうしの等価性を定義する述語関数に基づいてリストから重複要素を削除する関数 `Data.List.nubBy`の型を、 `psci`を使用して調べてみましょう) 679 | 680 | ## まとめ 681 | 682 | この章では、関数型プログラミングの新しい概念をいくつか導入しました。 683 | 684 | - 対話的モード `PSCi`を使用して関数を調べるなど思いついたことを試す方法 685 | - 検証や実装の道具としての型の役割 686 | - 多引数関数を表現する、カリー化された関数の使用 687 | - 関数合成で小さな部品を組み合わせてのプログラムの構築 688 | - `where`節を利用したコードの構造化 689 | - `Maybe`型を使用してnull値を回避する方法 690 | - イータ変換や関数合成のような手法を利用した、よりわかりやすいコードへの再構成 691 | 692 | 次の章からは、これらの考えかたに基づいて進めていきます。 693 | -------------------------------------------------------------------------------- /src/chapter04.md: -------------------------------------------------------------------------------- 1 | # 再帰、マップ、畳み込み 2 | 3 | ## この章の目標 4 | 5 | この章では、アルゴリズムを構造化するときに再帰関数をどのように使うかについて見ていきましょう。再帰は関数型プログラミングの基本的な手法であり、この本の全体に渡って使われます。 6 | 7 | また、PureScriptの標準ライブラリから標準的な関数をいくつか取り扱います。`map`や`fold`のようなよく知られた関数だけでなく、`filter`や`concatMap`といった珍しいけれど便利なものについても見ていきます。 8 | 9 | この章では、仮想的なファイルシステムを操作する関数のライブラリを動機付けに用います。この章で学ぶ手法を応用して、擬似的なファイルシステムによって表されるファイルのプロパティを計算する関数を記述します。 10 | 11 | ## プロジェクトの準備 12 | 13 | この章のソースコードには、`src/Data/Path.purs`と`src/FileOperations.purs`という2つのファイルが含まれています。 14 | 15 | `Data.Path`モジュールには、仮想ファイルシステムが含まれています。このモジュールの内容を変更する必要はありません。 16 | 17 | `FileOperations`モジュールには、`Data.Path`APIを使用する関数が含まれています。演習への回答はこのファイルだけで完了することができます。 18 | 19 | このプロジェクトには以下のBower依存関係があります。 20 | 21 | - `purescript-maybe`: `Maybe`型構築子が定義されています 22 | - `purescript-arrays`: 配列を扱うための関数が定義されています 23 | - `purescript-strings`: JavaScriptの文字列を扱うための関数が定義されています 24 | - `purescript-foldable-traversable`: 配列の畳み込みやその他のデータ構造に関する関数が定義されています 25 | - `purescript-console`: コンソールへの出力を扱うための関数が定義されています 26 | 27 | ## はじめに 28 | 29 | 再帰は一般のプログラミングでも重要な手法ですが、特に純粋関数型プログラミングでは当たり前のように用いられます。この章で見ていくように、再帰はプログラムの変更可能な状態を減らすために役立つからです。 30 | 31 | 再帰は**分割統治**(divide and conquer)戦略と密接な関係があります。分割統治とはすなわち、いろいろな入力に対する問題を解決するために、入力を小さな部分に分割し、それぞれの部分について問題を解いて、部分ごとの答えから最終的な答えを組み立てるということです。 32 | 33 | それでは、PureScriptにおける再帰の簡単な例をいくつか見てみましょう。 34 | 35 | 次は**階乗関数**(factorial function)のよくある例です。 36 | 37 | ```haskell 38 | fact :: Int -> Int 39 | fact 0 = 1 40 | fact n = n * fact (n - 1) 41 | ``` 42 | 43 | 部分問題へ問題を分割することによって階乗関数がどのように計算されるかがわかります。より小さい数へと階乗を計算していくということです。ゼロに到達すると、答えは直ちに求まります。 44 | 45 | 次は、**フィボナッチ関数**(Fibonacci function)を計算するという、これまたよくある例です。 46 | 47 | ```haskell 48 | fib :: Int -> Int 49 | fib 0 = 1 50 | fib 1 = 1 51 | fib n = fib (n - 1) + fib (n - 2) 52 | ``` 53 | 54 | やはり、部分問題の解決策を考えることで全体を解決していることがわかります。このとき、`fib (n - 1)`と`fib (n - 2)`という式に対応した、2つの部分問題があります。これらの2つの部分問題が解決されていれば、この部分的な答えを加算することで、全体の答えを組み立てることができます。 55 | 56 | ## 配列上での再帰 57 | 58 | 再帰関数の定義は、`Int`型だけに限定されるものではありません!本書の後半で**パターン照合**(pattern matching)を扱うときに、いろいろなデータ型の上での再帰関数について見ていきますが、ここでは数と配列に限っておきます。 59 | 60 | 入力がゼロでないかどうかについて分岐するのと同じように、配列の場合も、配列が空でないかどうかについて分岐していきます。再帰を使用して配列の長さを計算する次の関数を考えてみます。 61 | 62 | ```haskell 63 | import Prelude 64 | 65 | import Data.Array (null) 66 | import Data.Array.Partial (tail) 67 | import Partial.Unsafe (unsafePartial) 68 | 69 | length :: forall a. Array a -> Int 70 | length arr = 71 | if null arr 72 | then 0 73 | else 1 + length (unsafePartial tail arr) 74 | ``` 75 | 76 | この関数では配列が空かどうかで分岐するために`if ... then ... else`式を使っています。この`null`関数は配列が空のときに`true`を返します。空の配列の長さはゼロであり、空でない配列の長さは配列の先頭を取り除いた残りの部分の長さより1大きいというわけです。 77 | 78 | JavaScriptで配列の長さを調べるのには、この例はどうみても実用的な方法とはいえませんが、次の演習を完了するための手がかりとしては充分でしょう。 79 | 80 | ## 演習 81 | 82 | 1. (簡単) 入力が偶数であるとき、かつそのときに限り`true`に返すような再帰関数を書いてみましょう。 83 | 84 | 1. (少し難しい) 配列内の偶数の数を数える再帰関数を書いてみましょう。**ヒント**:`Data.Array.Partial`モジュールの`unsafePartial head`関数を使うと、空でない配列の最初の要素を見つけることができます。 85 | 86 | ## マップ 87 | 88 | `map`関数は配列に対する再帰関数のひとつです。この関数を使うと、配列の各要素に順番に関数を適用することで、配列の要素を変換することができます。そのため、配列の**内容**は変更されますが、その**形状**(ここでは「長さ」)は保存されます。 89 | 90 | 本書で後ほど**型クラス**(type class)を扱うとき、形状を保存しながら型構築子のクラスを変換する**関手**(functor)と呼ばれる関数を紹介しますが、その時に`map`関数は関手の具体例であることがわかるでしょう。 91 | 92 | それでは、`PSCi`で`map`関数を試してみましょう。 93 | 94 | ```text 95 | $ pulp repl 96 | 97 | import Prelude 98 | map (\n -> n + 1) [1, 2, 3, 4, 5] 99 | 100 | [2, 3, 4, 5, 6] 101 | ``` 102 | 103 | `map`がどのように使われているかに注目してください。最初の引数には配列がどのように対応付けられるかを示す関数、第2引数には配列そのものを渡します。 104 | 105 | ## 中置演算子 106 | 107 | バッククォート(\`)で関数名を囲むと、対応関係を表す関数と配列のあいだに、`map`関数を書くことができます。 108 | 109 | ```text 110 | (\n -> n + 1) `map` [1, 2, 3, 4, 5] 111 | 112 | [2, 3, 4, 5, 6] 113 | ``` 114 | 115 | この構文は**中置関数適用**と呼ばれ、どんな関数でもこのように中置することができます。普通は2引数の関数に対して使うのが最も適切でしょう。 116 | 117 | 配列を扱うときは、`map`関数と等価な`<$>`という演算子が存在します。この演算子は他の二項演算子と同じように中置で使用することができます。 118 | 119 | ```text 120 | (\n -> n + 1) <$> [1, 2, 3, 4, 5] 121 | 122 | [2, 3, 4, 5, 6] 123 | ``` 124 | 125 | それでは`map`の型を見てみましょう。 126 | 127 | ```text 128 | :type map 129 | forall a b f. Functor f => (a -> b) -> f a -> f b 130 | ``` 131 | 132 | 実は`map`の型は、この章で必要とされているものよりも一般的な型になっています。今回の目的では、`map`は次のようなもっと具体的な型であるかのように考えるとよいでしょう。 133 | 134 | ```text 135 | forall a b. (a -> b) -> Array a -> Array b 136 | ``` 137 | 138 | この型では、`map`関数に適用するときには`a`と`b`という2つの型を自由に選ぶことができる、ということも示されています。`a`は元の配列の要素の型で、`b`は目的の配列の要素の型です。もっと言えば、`map`が配列要素の型を変化させても構わないということです。たとえば、`map`を使用すると数値を文字列に変換することができます。 139 | 140 | ```text 141 | show <$> [1, 2, 3, 4, 5] 142 | 143 | ["1","2","3","4","5"] 144 | ``` 145 | 146 | 中置演算子`<$>`は特別な構文のように見えるかもしれませんが、実はPureScriptの普通の関数です。中置構文を使用した単なる**適用**にすぎません。実際、括弧でその名前を囲むと、この関数を通常の関数のように使用することができます。これは、`map`代わりに、括弧で囲まれた`(<$>)`という名前を使って配列に関数を適用できるということです。 147 | 148 | ```text 149 | (<$>) show [1, 2, 3, 4, 5] 150 | ["1","2","3","4","5"] 151 | ``` 152 | 153 | 新しい中置演算子を定義するには、関数と同じ記法を使います。演算子名を括弧で囲み、あとは普通の関数のようにその中置演算子を定義します。たとえば、`Data.Array`モジュールでは次のように`range`関数と同じ振る舞いの中置演算子`(..)`を定義しています。 154 | 155 | ```haskell 156 | infix 8 range as .. 157 | ``` 158 | 159 | この演算子は次のように使うことができます。 160 | 161 | ```text 162 | import Data.Array 163 | 1 .. 5 164 | [1, 2, 3, 4, 5] 165 | 166 | show <$> (1 .. 5) 167 | ["1","2","3","4","5"] 168 | ``` 169 | 170 | **注意**: 独自の中置演算子は、自然な構文を持った領域特化言語を定義するのに優れた手段になりえます。ただし、使用には充分注意してください。初心者が読めないコードになることがありますから、新たな演算子の定義には慎重になるのが賢明です。 171 | 172 | 上記の例では、`1 .. 5`という式は括弧で囲まれていましたが、実際にはこれは必要ありません。なぜなら、`Data.Array`モジュールは、`<$>`に割り当てられた優先順位より高い優先順位を`..`演算子に割り当てているからです。上の例では、`..`の優先順位は、予約語`infix`のあとに書かれた数の`8` と定義されていました。ここでは`<$>`の優先順位よりも高い優先順位を`..`に割り当てており、このため括弧を付け加える必要がないということです。 173 | 174 | ```text 175 | show <$> 1 .. 5 176 | 177 | ["1","2","3","4","5"] 178 | ``` 179 | 180 | 中置演算子に左結合性または右結合性を与えたい場合は、代わりに予約語`infixl`と`infixr`を使います。 181 | 182 | ## 配列のフィルタリング 183 | 184 | `Data.Array`モジュールでは他にも、`map`と同様によく使われる関数`filter`も提供しています。この関数は、述語関数に適合する要素のみを残し、既存の配列から新しい配列を作成する機能を提供します。 185 | 186 | たとえば、1から10までの数で、偶数であるような数の配列を計算したいとします。これは次のように行うことができます。 187 | 188 | ```text 189 | import Data.Array 190 | 191 | filter (\n -> n `mod` 2 == 0) (1 .. 10) 192 | [2,4,6,8,10] 193 | ``` 194 | 195 | ## 演習 196 | 197 | 1. (簡単)`map`関数や`<$>`関数を使用して、 配列に格納された数のそれぞれの平方を計算する関数を書いてみましょう。 198 | 199 | 1. (簡単)`filter`関数を使用して、数の配列から負の数を取り除く関数を書いてみましょう。 200 | 201 | 1. (やや難しい)`filter`関数と同じ意味の中置演算子`<$?>`を定義してみましょう。先ほどの演習の回答を、この新しい演算子を使用して書き換えてください。また、`PSCi`でこの演算子の優先順位と結合性を試してみてください。 202 | 203 | ## 配列の平坦化 204 | 205 | 配列に関する標準的な関数として`Data.Array`で定義されているものには、`concat`関数もあります。`concat`は配列の配列をひとつの配列へと平坦化します。 206 | 207 | ```text 208 | import Data.Array 209 | :type concat 210 | forall a. Array (Array a) -> Array a 211 | 212 | concat [[1, 2, 3], [4, 5], [6]] 213 | [1, 2, 3, 4, 5, 6] 214 | ``` 215 | 216 | 関連する関数として、`concat`と`map`を組み合わせたような`concatMap`と呼ばれる関数もあります。`map`は(相異なる型も可能な)値からの値への関数を引数に取りますが、それに対して`concatMap`は値から値の配列の関数を取ります。 217 | 218 | 実際に動かして見てみましょう。 219 | 220 | ```text 221 | import Data.Array 222 | 223 | :type concatMap 224 | forall a b. (a -> Array b) -> Array a -> Array b 225 | 226 | concatMap (\n -> [n, n * n]) (1 .. 5) 227 | [1,1,2,4,3,9,4,16,5,25] 228 | ``` 229 | 230 | ここでは、数をその数とその数の平方の2つの要素からなる配列に写す関数`\n -> [n, n * n]`を引数に`concatMap`を呼び出しています。結果は、1から5の数と、そのそれぞれの数の平方からなる、10個の数になります。 231 | 232 | `concatMap`がどのように結果を連結しているのかに注目してください。渡された関数を元の配列のそれぞれの要素について一度づつ呼び出し、その関数はそれぞれ配列を生成します。最後にそれらの配列を単一の配列に押し潰し、それが結果となります。 233 | 234 | `map`と`filter`、`concatMap`は、「配列内包表記」(array comprehensions)と呼ばれる、配列に関するあらゆる関数の基盤を形成しています。 235 | 236 | ## 配列内包表記 237 | 238 | 数`n`のふたつの因数を見つけたいとしましょう。これを行うための簡単​​な方法としては、総当りで調べる方法があります。つまり、`1`から`n`の数のすべての組み合わせを生成し、それを乗算してみるわけです。もしその積が`n`なら、`n`の因数の組み合わせを見つけたということになります。 239 | 240 | 配列内包表記を使用すると、この計算を実行することができます。`PSCi`を対話式の開発環境として使用し、ひとつづつこの手順を進めていきましょう。 241 | 242 | `n`以下の数の組み合わせの配列を生成する最初の手順は、`concatMap`を使えば行うことができます。 243 | 244 | `1 .. n`のそれぞれの数を配列`1 .. n`へとマッピングすることから始めましょう。 245 | 246 | ```text 247 | pairs n = concatMap (\i -> 1 .. n) (1 .. n) 248 | ``` 249 | 250 | この関数をテストしてみましょう。 251 | 252 | ```text 253 | pairs 3 254 | [1,2,3,1,2,3,1,2,3] 255 | ``` 256 | 257 | これは求めているものとはぜんぜん違います。単にそれぞれの組み合わせの2つ目の要素を返すのではなく、ペア全体を保持することができるように、内側の`1 .. n`の複製について関数をマッピングする必要があります。 258 | 259 | ```text 260 | :paste 261 | … pairs' n = 262 | … concatMap (\i -> 263 | … map (\j -> [i, j]) (1 .. n) 264 | … ) (1 .. n) 265 | … ^D 266 | 267 | pairs' 3 268 | [[1,1],[1,2],[1,3],[2,1],[2,2],[2,3],[3,1],[3,2],[3,3]] 269 | ``` 270 | 271 | いい感じになってきました。しかし、`[1, 2]`と`[2, 1]`の両方があるように、重複した組み合わせが生成されています。`j`を`i`から`n`の範囲に限定することで、2つ目の場合を取り除くことができます。 272 | 273 | ```text 274 | :paste 275 | … pairs'' n = 276 | … concatMap (\i -> 277 | … map (\j -> [i, j]) (i .. n) 278 | … ) (1 .. n) 279 | … ^D 280 | 281 | pairs'' 3 282 | [[1,1],[1,2],[1,3],[2,2],[2,3],[3,3]] 283 | ``` 284 | 285 | すばらしいです!因数の候補のすべての組み合わせを手に入れたので、`filter`を使えば、その積が`n`であるような組み合わせを選び出すことができます。 286 | 287 | ```text 288 | import Data.Foldable 289 | 290 | factors n = filter (\pair -> product pair == n) (pairs'' n) 291 | 292 | factors 10 293 | [[1,10],[2,5]] 294 | ``` 295 | 296 | このコードでは、`purescript-foldable-traversable`ライブラリの`Data.Foldable`モジュールにある`product`関数を使っています。 297 | 298 | うまくいきました!重複のなく、因数の組み合わせの正しい集合を見つけることができました。 299 | 300 | ## do記法 301 | 302 | 機能は実現できましたが、このコードの可読性は大幅に向上することができます。`map`や`concatMap`は基本的な関数であり、**do記法**(do notation)と呼ばれる特別な構文の基礎になっています(もっと厳密にいえば、それらの一般化である`map`と`bind`が基礎をなしています)。 303 | 304 | **注意**:`map`と`concatMap`が**配列内包表記**を書けるようにしているように、もっと一般的な演算子である`map`と`bind`は**モナド内包表記**(monad comprehensions)と呼ばれているものを書けるようにします。本書の後半では**モナド**(monad)の例をたっぷり見ていくことになります。 305 | 306 | do記法を使うと、先ほどの`factors`関数を次のように書き直すことができます。 307 | 308 | ```haskell 309 | factors :: Int -> Array (Array Int) 310 | factors n = filter (\xs -> product xs == n) $ do 311 | i <- 1 .. n 312 | j <- i .. n 313 | pure [i, j] 314 | ``` 315 | 316 | 予約語`do`はdo記法を使うコードのブロックを導入します。このブロックは幾つかの型の式で構成されています。 317 | 318 | - 配列の要素を名前に束縛する式。これは後ろ向きの矢印`<-`で 示されていて、その左側は名前、右側は配列の型を持つ式です。 319 | - 名前に配列の要素を束縛しない式。最後の行の`pure [i, j]`が、この種類の式の一例です。 320 | -`let`キーワードを使用し、式に名前を与える式(ここでは使われていません)。 321 | 322 | この新しい記法を使うと、アルゴリズムの構造がわかりやすくなることがあります。心のなかで`<-`を「選ぶ」という単語に置き換えるとすると、「1からnの間の要素`i`を選び、それからiからnの間の要素`j`を選び、`[i, j]`を返す」というように読むことができるかもしれません。 323 | 324 | 最後の行では、`pure`関数を使っています。この関数は`PSCi`で評価することができますが、型を明示する必要があります。 325 | 326 | ```text 327 | pure [1, 2] :: Array (Array Int) 328 | [[1, 2]] 329 | ``` 330 | 331 | 配列の場合、`pure`は単に1要素の配列を作成します。実際に、`factors`関数を変更して、`pure`の代わりにこの形式を使うようにすることもできます。 332 | 333 | ```haskell 334 | factors :: Int -> Array (Array Int) 335 | factors n = filter (\xs -> product xs == n) $ do 336 | i <- 1 .. n 337 | j <- i .. n 338 | [[i, j]] 339 | ``` 340 | 341 | そして、結果は同じになります。 342 | 343 | ## ガード 344 | 345 | `factors`関数を更に改良する方法としては、このフィルタを配列内包表記の内側に移動するというものがあります。これは`purescript-control`ライブラリにある`Control.MonadZero`モジュールの`guard`関数を使用することで可能になります。 346 | 347 | ```haskell 348 | import Control.MonadZero (guard) 349 | 350 | factors :: Int -> Array (Array Int) 351 | factors n = do 352 | i <- 1 .. n 353 | j <- i .. n 354 | guard $ i * j == n 355 | pure [i, j] 356 | ``` 357 | 358 | `pure`と同じように、`guard`関数がどのように動作するかを理解するために、`PSCi`で`guard`関数を適用して調べてみましょう。`guard`関数の型は、ここで必要とされるものよりもっと一般的な型になっています。 359 | 360 | 361 | ```text 362 | import Control.MonadZero 363 | 364 | :type guard 365 | forall m. MonadZero m => Boolean -> m Unit 366 | ``` 367 | 368 | 今回の場合は、`PSCi`は次の型を報告するものと考えてください。 369 | 370 | ```haskell 371 | Boolean -> Array Unit 372 | ``` 373 | 374 | 次の計算の結果から、配列における`guard`関数について今知りたいことはすべてわかります。 375 | 376 | ```text 377 | import Data.Array 378 | 379 | length $ guard true 380 | 1 381 | 382 | length $ guard false 383 | 0 384 | ``` 385 | 386 | つまり、`guard`が`true`に評価される式を渡された場合、単一の要素を持つ配列を返すのです。もし式が`false`と評価された場合は、その結果は空です。 387 | 388 | ガードが失敗した場合、配列内包表記の現在の分岐は、結果なしで早めに終了されることを意味します。これは、`guard`の呼び出しが、途中の配列に対して`filter`を使用するのと同じだということです。これらが同じ結果になることを確認するために、`factors`の二つの定義を試してみてください。 389 | 390 | ## 演習 391 | 392 | 1. (簡単) `factors`関数を使用して、整数の引数が素数であるかどうかを調べる関数`isPrime`を定義してみましょう。 393 | 394 | 1. (やや難しい) 2つの配列の**直積集合**を見つけるための関数を書いてみましょう。直積集合とは、要素`a`、`b`のすべての組み合わせの集合のことです。ここで`a`は最初の配列の要素、`b`は2つ目の配列の要素です。 395 | 396 | 1. (やや難しい) **ピタゴラスの三つ組数**とは、`a² + b² = c²`を満たすような3つの数の配列`[a, b, c]`のことです。配列内包表記の中で`guard`関数を使用して、数`n`を引数に取り、どの要素も`n`より小さいようなピタゴラスの三つ組数すべてを求める関数を書いてみましょう。その関数は`Int -> Array (Array Int)`という型を持っていなければなりません。 397 | 398 | 1. (難しい) `factors`関数を使用して、数`n`のすべての**因数分解**を求める関数`factorizations`を定義してみましょう。数`n`の因数分解とは、それらの積が`n`であるような整数の配列のことです。**ヒント**:1は因数ではないと考えてください。また、無限再帰に陥らないように注意しましょう。 399 | 400 | ## 畳み込み 401 | 402 | 再帰を利用して実装される興味深い関数としては、配列に対する左畳み込み(left fold)と右畳み込み(right fold)があります。 403 | 404 | `PSCi`を使って、`Data.Foldable`モジュールをインポートし、`foldl`と`foldr`関数の型を調べることから始めましょう。 405 | 406 | ```text 407 | import Data.Foldable 408 | 409 | :type foldl 410 | forall a b f. Foldable f => (b -> a -> b) -> b -> f a -> b 411 | 412 | :type foldr 413 | forall a b f. Foldable f => (a -> b -> b) -> b -> f a -> b 414 | ``` 415 | 416 | これらの型は、現在興味があるものよりも一般的です。この章の目的では、`PSCi`は以下の(より具体的な)答えを与えていたと考えておきましょう。 417 | 418 | ```text 419 | :type foldl 420 | forall a b. (b -> a -> b) -> b -> Array a -> b 421 | 422 | :type foldr 423 | forall a b. (a -> b -> b) -> b -> Array a -> b 424 | ``` 425 | 426 | どちらの型でも、`a`は配列の要素の型に対応しています。型`b`は、配列を走査(traverse)したときの結果を累積する「累積器」(accumulator)の型だと考えることができます。 427 | 428 | `foldl`関数と`foldr`関数の違いは走査の方向です。`foldr`が「右から」配列を畳み込むのに対して、`foldl`は「左から」配列を畳み込みます。 429 | 430 | これらの関数の動きを見てみましょう。`foldl`を使用して数の配列の和を求めてみます。型`a`は`Number`になり、結果の型`b`も`Number`として選択することができます。ここでは、次の要素を累積器に加算する`Number -> Number -> Number`という型の関数、`Number`型の累積器の初期値、和を求めたい`Number`の配列という、3つの引数を提供する必要があります。最初の引数としては、加算演算子を使用することができますし、累積器の初期値はゼロになります。 431 | 432 | ```text 433 | foldl (+) 0 (1 .. 5) 434 | 15 435 | ``` 436 | 437 | この場合では、引数が逆になっていても`(+)`関数は同じ結果を返すので、`foldl`と`foldr`のどちらでも問題ありません。 438 | 439 | ```text 440 | foldr (+) 0 (1 .. 5) 441 | 15 442 | ``` 443 | 444 | `foldl`と`foldr`の違いを説明するために、畳み込み関数の選択が影響する例も書いてみましょう。加算関数の代わりに、文字列連結を使用して文字列を作ってみます。 445 | 446 | ```text 447 | foldl (\acc n -> acc <> show n) "" [1,2,3,4,5] 448 | "12345" 449 | 450 | foldr (\n acc -> acc <> show n) "" [1,2,3,4,5] 451 | "54321" 452 | ``` 453 | 454 | これは、2つの関数の​​違いを示しています。左畳み込み式は、以下の関数適用と同等です。 455 | 456 | ```text 457 | ((((("" <> show 1) <> show 2) <> show 3) <> show 4) <> show 5) 458 | ``` 459 | 460 | それに対し、右畳み込みは以下に相当します。 461 | 462 | ```text 463 | ((((("" <> show 5) <> show 4) <> show 3) <> show 2) <> show 1) 464 | ``` 465 | 466 | ## 末尾再帰 467 | 468 | 再帰はアルゴリズムを定義するための強力な手法ですが、問題も抱えています。JavaScriptで再帰関数を評価するとき、入力が大きすぎるとスタックオーバーフローでエラーを起こす可能性があるのです。 469 | 470 | `PSCi`で次のコードを入力すると、この問題を簡単に検証できます。 471 | 472 | ```text 473 | f 0 = 0 474 | f n = 1 + f (n - 1) 475 | 476 | f 10 477 | 10 478 | 479 | f 10000 480 | RangeError: Maximum call stack size exceeded 481 | ``` 482 | 483 | これは問題です。関数型プログラミングの基本的な手法として再帰を採用しようとするなら、無限かもしれない再帰でも扱える方法が必要です。 484 | 485 | PureScriptは**末尾再帰最適化**(tail recursion optimization)の形でこの問題に対する部分的な解決策を提供しています。 486 | 487 | **注意**:この問題へのより完全な解決策としては、いわゆる**トランポリン**(trampolining)を使用したライブラリで実装する方法がありますが、それはこの章で扱う範囲を超えています。この内容に興味のある読者は`purescript-free`や`purescript-tailrec`パッケージのドキュメントを参照してみてください。 488 | 489 | 末尾再帰の最適化が可能かどうかには条件があります。**末尾位置**(tail position)にある関数の再帰的な呼び出しは、スタックフレームが確保されない**ジャンプ**に置き換えることができます。呼び出しは、関数が戻るより前の最後の呼び出しであるとき、**末尾位置**にあるといいます。なぜこの例でスタックオーバーフローを観察したのかはこれが理由です。この`f`の再帰呼び出しは、末尾位置**ではない**からです。 490 | 491 | 実際には、PureScriptコンパイラは再帰呼び出しをジャンプに置き換えるのではなく、再帰的な関数全体を**whileループ**に置き換えます。 492 | 493 | 以下はすべての再帰呼び出しが末尾位置にある再帰関数の例です。 494 | 495 | ```haskell 496 | fact :: Int -> Int -> Int 497 | fact 0 acc = acc 498 | fact n acc = fact (n - 1) (acc * n) 499 | ``` 500 | 501 | `fact`への再帰呼び出しは、この関数の中で起こる最後のものである、つまり末尾位置にあることに注意してください。 502 | 503 | ## 累積器 504 | 505 | 末尾再帰ではない関数を末尾再帰関数に変える一般的な方法としては、**累積器引数**(accumulator parameter)を使用する方法があります。結果を累積するために返り値を使うと末尾再帰を妨げることがありますが、それとは対照的に累積器引数は返り値を**累積**する関数へ追加される付加的な引数です。 506 | 507 | たとえば、入力配列を逆順にする、この配列の再帰を考えてみましょう。 508 | 509 | ```haskell 510 | reverse :: forall a. Array a -> Array a 511 | reverse [] = [] 512 | reverse xs = snoc (reverse (unsafePartial tail xs)) 513 | (unsafePartial head xs) 514 | ``` 515 | 516 | この実装は末尾再帰ではないので、大きな入力配列に対して実行されると、生成されたJavaScriptはスタックオーバーフローを発生させるでしょう。しかし、代わりに、結果を蓄積するための2つ目の引数を関数に導入することで、これを末尾再帰に変えることができます。 517 | 518 | ```haskell 519 | reverse :: forall a. Array a -> Array a 520 | reverse = reverse' [] 521 | where 522 | reverse' acc [] = acc 523 | reverse' acc xs = reverse' (unsafePartial head xs : acc) 524 | (unsafePartial tail xs) 525 | ``` 526 | 527 | ここでは、配列を逆転させる作業を補助関数`reverse'`に委譲しています。関数`reverse'が末尾再帰的であることに注目してください。 その唯一の再帰呼び出しは、最後の場合の末尾位置にあります。これは、生成されたコードが**whileループ**となり、大きな入力でもスタックが溢れないことを意味します。 528 | 529 | `reverse`のふたつめの実装を理解するためには、部分的に構築された結果を状態として扱うために、補助関数`reverse'`で累積器引数の使用することが必須であることに注意してください。結果は空の配列で始まりますが、入力配列の要素ひとつごとに、ひとつづつ大きくなっていきます。後の要素は配列の先頭に追加されるので、結果は元の配列の逆になります! 530 | 531 | 累積器を「状態」と考えることもできますが、直接に変更がされているわけではないことにも注意してください。この累積器は不変の配列であり、計算に沿って状態受け渡すために、単に関数の引数を使います。 532 | 533 | ## 明示的な再帰より畳み込みを選ぶ 534 | 535 | 末尾再帰を使用して再帰関数を記述することができれば末尾再帰最適化の恩恵を受けることができるので、すべての関数をこの形で書こうとする誘惑にかられます。しかし、多くの関数は配列やそれに似たデータ構造に対する折り畳みとして直接書くことができることを忘れがちです。`map`や`fold`のようなコンビネータを使って直接アルゴリズムを書くことには、コードの単純さという利点があります。これらのコンビネータはよく知られており、アルゴリズムの**意図**をはっきりとさせるのです。 536 | 537 | 例えば、 先ほどの`reverse`の例は、畳み込みとして少なくとも2つの方法で書くことができます。`foldr`を使用すると次のようになります。 538 | 539 | ```text 540 | import Data.Foldable 541 | :paste 542 | … reverse :: forall a. Array a -> Array a 543 | … reverse = foldr (\x xs -> xs <> [x]) [] 544 | … ^D 545 | reverse [1, 2, 3] 546 | 547 | [3,2,1] 548 | ``` 549 | 550 | `foldl`を使って`reverse`を書くことは、読者への課題として残しておきます。 551 | 552 | ## 演習 553 | 554 | 1. (簡単)`foldl`を使って、真偽値の配列の要素すべてが真かどうか調べてみてください。 555 | 556 | 1. (やや難しい) 関数`foldl (==) false xs`が真を返すような配列`xs`とはどのようなものか説明してください。 557 | 558 | 1. (やや難しい) 累積器引数を使用して、次の関数を末尾再帰形に書きなおしてください。 559 | 560 | ```haskell 561 | import Prelude 562 | import Data.Array.Partial (head, tail) 563 | count :: forall a. (a -> Boolean) -> Array a -> Int 564 | count _ [] = 0 565 | count p xs = if p (unsafePartial head xs) 566 | then count p (unsafePartial tail xs) + 1 567 | else count p (unsafePartial tail xs) 568 | ``` 569 | 570 | 1. (やや難しい)`foldl`を使って`reverse`を書いてみましょう。 571 | 572 | ## 仮想ファイルシステム 573 | 574 | この節では、これまで学んだことを応用して、模擬的なファイルシステムで動作する関数を書いていきます。事前に定義されたAPIで動作するように、マップ、畳み込み、およびフィルタを使用します。 575 | 576 | `Data.Path`モジュールでは、次のように仮想ファイルシステムのAPIが定義されています。 577 | 578 | - ファイルシステム内のパスを表す型`Path`があります。 579 | - ルートディレクトリを表すパス`root`があります。 580 | - `ls`関数はディレクトリ内のファイルを列挙します。 581 | - `filename`関数は`Path`のファイル名を返します。 582 | - `size`関数は`Path`が示すファイルの大きさを返します。 583 | - `isDirectory`関数はファイルかディレクトリかを調べます。 584 | 585 | 型については、型定義は次のようになっています。 586 | 587 | ```haskell 588 | root :: Path 589 | 590 | ls :: Path -> Array Path 591 | 592 | filename :: Path -> String 593 | 594 | size :: Path -> Maybe Number 595 | 596 | isDirectory :: Path -> Boolean 597 | ``` 598 | 599 | `PSCi`でこのAPIを試してみましょう。 600 | 601 | ```text 602 | $ pulp repl 603 | import Data.Path 604 | 605 | root 606 | / 607 | 608 | isDirectory root 609 | true 610 | 611 | ls root 612 | [/bin/,/etc/,/home/] 613 | ``` 614 | 615 | `FileOperations`モジュールでは、`Data.Path`APIを操作するための関数を定義されています。`Data.Path`モジュールを変更したり実装を理解したりする必要はありません。すべて`FileOperations`モジュールだけで作業を行います。 616 | 617 | ## すべてのファイルの一覧 618 | 619 | それでは、内側のディレクトリまで、すべてのファイルを列挙する関数を書いてみましょう。この関数は以下のような型を持つでしょう。 620 | 621 | ```haskell 622 | allFiles :: Path -> Array Path 623 | ``` 624 | 625 | 再帰を使うとこの関数を定義することができます。まずは`ls`を使用してディレクトリの直接の子を列挙します。それぞれの子について再帰的に`allFiles`を適用すると、それぞれパスの配列が返ってくるでしょう。`concatMap`を適用すると、この結果を同時に平坦化することができます。 626 | 627 | 最後に、`:`演算子を使って現在のファイルも含めます。 628 | 629 | ```haskell 630 | allFiles file = file : concatMap allFiles (ls file) 631 | ``` 632 | 633 | **注意**:cons演算子`:`は、実際には不変な配列に対してパフォーマンスが悪いので、一般的には推奨されません。 リンクリストやシーケンスなどの他のデータ構造を使用すると、パフォーマンスを向上させることができます。 634 | 635 | それでは`PSCi`でこの関数を試してみましょう。 636 | 637 | ```text 638 | import FileOperations 639 | import Data.Path 640 | 641 | allFiles root 642 | 643 | [/,/bin/,/bin/cp,/bin/ls,/bin/mv,/etc/,/etc/hosts, ...] 644 | ``` 645 | 646 | すばらしい!do記法で配列内包表記を使ってもこの関数を書くことができるので見ていきましょう。 647 | 648 | 逆向きの矢印は配列から要素を選択するのに相当することを思い出してください。最初の手順は、引数の直接の子から要素を選択することです。それから、単にそのファイルに対してこの再帰関数を呼びします。do記法を使用しているので、再帰的な結果をすべて連結する`concatMap`が暗黙に呼び出されています。 649 | 650 | 新しいコードは次のようになります。 651 | 652 | ```haskell 653 | allFiles' :: Path -> Array Path 654 | allFiles' file = file : do 655 | child <- ls file 656 | allFiles' child 657 | ``` 658 | 659 | `PSCi`で新しいコードを試してみてください。同じ結果が返ってくるはずです。どちらのほうがわかりやすいかの選択はお任せします。 660 | 661 | ## 演習 662 | 663 | 1. (簡単) ディレクトリのすべてのサブディレクトリの中まで、ディレクトリを除くすべてのファイルを返すような関数`onlyFiles`を書いてみてください。 664 | 665 | 1. (やや難しい) このファイルシステムで最大と最小のファイルを決定するような畳み込みを書いてください。 666 | 667 | 1. (難しい) ファイルを名前で検索する関数`whereIs`を書いてください。この関数は型`Maybe Path`の値を返すものとします。この値が存在するなら、そのファイルがそのディレクトリに含まれているということを表します。この関数は次のように振る舞う必要があります。 668 | 669 | ```text 670 | > whereIs "/bin/ls" 671 | Just (/bin/) 672 | 673 | > whereIs "/bin/cat" 674 | Nothing 675 | ``` 676 | 677 | **ヒント**:do記法で配列内包表記を使用して、この関数を記述してみてください。 678 | 679 | ## まとめ 680 | 681 | この章では、アルゴリズムを簡潔に表現する手段として、PureScriptでの再帰の基本を説明しました。また、独自の中置演算子や、マップ、フィルタリングや畳み込みなどの配列に対する標準関数、およびこれらの概念を組み合わせた配列内包表記を導入しました。最後に、スタックオーバーフローエラーを回避するために末尾再帰を使用することの重要性、累積器引数を使用して末尾再帰形に関数を変換する方法を示しました。 682 | 683 | -------------------------------------------------------------------------------- /src/chapter05.md: -------------------------------------------------------------------------------- 1 | # パターン照合 2 | 3 | ## この章の目標 4 | 5 | この章では、代数的データ型とパターン照合という、ふたつの新しい概念を導入します。また、行多相というPureScriptの型システムの興味深い機能についても簡単に取り扱います。 6 | 7 | **パターン照合**(Pattern matching)は関数​​型プログラミングにおける一般的な手法で、複数の場合に実装を分解することにより、開発者は潜在的に複雑な動作の関数を簡潔に書くことができます。 8 | 9 | 代数的データ型はPureScriptの型システムの機能で、パターン照合とも密接に関連しています。 10 | 11 | この章の目的は、代数的データ型やパターン照合を使用して、単純なベクターグラフィックスを描画し操作するためのライブラリを書くことです。 12 | 13 | ## プロジェクトの準備 14 | 15 | この章のソースコードはファイル `src/Data/Picture.purs`で定義されています。 16 | 17 | このプロジェクトでは、これまで見てきたBowerパッケージを引き続き使用しますが、それに加えて次の新しい依存関係が追加されます。 18 | 19 | - `purescript-globals`: 一般的なJavaScriptの値や関数の取り扱いを可能にします。 20 | - `purescript-math`: JavaScriptの `Math`オブジェクトの関数群を利用可能にします。 21 | 22 | `Data.Picture`モジュールは、簡単な図形を表すデータ型 `Shape`や、図形の集合である型 `Picture`、及びこれらの型を扱うための関数を定義しています。 23 | 24 | このモジュールでは、データ構造の畳込みを行う関数を提供する `Data.Foldable`モジュールもインポートします。 25 | 26 | ```haskell 27 | module Data.Picture where 28 | 29 | import Prelude 30 | import Data.Foldable (foldl) 31 | ``` 32 | `Data.Picture`モジュールでは、 `Global`と `Math`モジュールもインポートするため `as`キーワードを使用します。 33 | 34 | ```haskell 35 | import Global as Global 36 | import Math as Math 37 | ``` 38 | 39 | これは型や関数をモジュール内で使用できるようにしますが、`Global.infinity`や`Math.max`といった**修飾名**でのみ使用にできるようにします。これは重複したインポートをさけ、使用するモジュールを明確にするのに有効な方法です。 40 | 41 | **注意**:同じモジュール名を修飾名に使用する場合には不要な作業です。一般的には`import Math as M`などの短い名前がよく使われています。 42 | 43 | ## 単純なパターン照合 44 | 45 | それではコード例を見ることから始めましょう。パターン照合を使用して2つの整数の最大公約数を計算する関数は、次のようになります。 46 | 47 | ```haskell 48 | gcd :: Int -> Int -> Int 49 | gcd n 0 = n 50 | gcd 0 m = m 51 | gcd n m = if n > m 52 | then gcd (n - m) m 53 | else gcd n (m - n) 54 | ``` 55 | 56 | このアルゴリズムはユークリッドの互除法と呼ばれています。その定義をオンラインで検索すると、おそらく上記のコードによく似た数学の方程式が見つかるでしょう。パターン照合の利点のひとつは、上記のようにコードを場合分けして定義することができ、数学関数の定義と似たような簡潔で宣言型なコードを書くことができることです。 57 | 58 | パターン照合を使用して書かれた関数は、条件と結果の組み合わせによって動作します。この定義の各行は**選択肢**(alternative)や**場合**(case)と呼ばれています。等号の左辺の式は**パターン**と呼ばれており、それぞれの場合は空白で区切られた1つ以上のパターンで構成されています。等号の右側の式が評価され値が返される前に引数が満たさなければならない条件について、これらの場合は説明しています。それぞれの場合は上からこの順番に試されていき、最初に入力に適合した場合が返り値を決定します。 59 | 60 | たとえば、 `gcd`関数は次の手順で評価されます。 61 | 62 | - まず最初の場合が試されます。第2引数がゼロの場合、関数は `n`(最初の引数)を返します。 63 | - そうでなければ、2番目の場合が試されます。最初の引数がゼロの場合、関数は `m`(第2引数)を返します。 64 | - それ以外の場合、関数は最後の行の式を評価して返します。 65 | 66 | パターンは値を名前に束縛することができることに注意してください。この例の各行では `n`という名前と `m`という名前の両方、またはどちらか一方に、入力された値を束縛しています。これより、入力の引数から名前を選ぶためのさまざまな方法に対応した、さまざまな種類のパターンを見ていくことになります。 67 | 68 | ## 単純なパターン 69 | 70 | 上記のコード例では、2種類のパターンを示しました。 71 | 72 | - `Int`型の値が正確に一致する場合にのみ適合する、数値リテラルパターン 73 | - 引数を名前に束縛する、変数パターン 74 | 75 | 単純なパターンには他にも種類があります。 76 | 77 | - 文字列リテラルと真偽リテラル 78 | - どんな引数とも適合するが名前に束縛はしない、アンダースコア( `_`)で表されるワイルドカードパターン 79 | 80 | ここではこれらの単純なパターンを使用した、さらに2つの例を示します。 81 | 82 | ```haskell 83 | fromString :: String -> Boolean 84 | fromString "true" = true 85 | fromString _ = false 86 | 87 | toString :: Boolean -> String 88 | toString true = "true" 89 | toString false = "false" 90 | ``` 91 | 92 | `PSCi`でこれらの関数を試してみてください。 93 | 94 | ## ガード 95 | 96 | ユークリッドの互除法の例では、 `m > n`のときと `m <= n`のときの2つに分岐するために `if .. then .. else`式を使っていました。こういうときには他に**ガード**(guard)を使うという選択肢もあります。 97 | 98 | ガードは真偽値の式で、パターンによる制約に加えてそのガードが満たされたときに、その場合の結果になります。ガードを使用してユークリッドの互除法を書き直すと、次のようになります。 99 | 100 | ```haskell 101 | gcd :: Int -> Int -> Int 102 | gcd n 0 = n 103 | gcd 0 n = n 104 | gcd n m | n > m = gcd (n - m) m 105 | | otherwise = gcd n (m - n) 106 | ``` 107 | 108 | 3行目ではガードを使用して、最初の引数が第2引数よりも厳密に大きいという条件を付け加えています。 109 | 110 | この例が示すように、ガードは等号の左側に現れ、パイプ文字( `|`)でパターンのリストと区切られています。 111 | 112 | ## 演習 113 | 114 | 1. (簡単)パターン照合を使用して、階乗関数を書いてみましょう。**ヒント**:入力がゼロのときとゼロでないときの、ふたつの場合を考えてみてください。 115 | 116 | 1. (やや難しい)二項係数を計算するための**パスカルの公式**(Pascal's Rule、パスカルの三角形を参照のこと)について調べてみてください。パスカルの公式を利用し、パターン照合を使って二項係数を計算する関数を記述してください。 117 | 118 | ## 配列リテラルパターン 119 | 120 | **配列リテラルパターン**(array literal patterns)は、固定長の配列に対して照合を行う方法を提供します。たとえば、空の配列であることを特定する関数 `isEmpty`を書きたいとします。最初の選択肢に空の配列パターン( `[]`)を用いるとこれを実現できます。 121 | 122 | ```haskell 123 | isEmpty :: forall a. Array a -> Boolean 124 | isEmpty [] = true 125 | isEmpty _ = false 126 | ``` 127 | 128 | 次の関数では、長さ5の配列と適合し、配列の5つの要素をそれぞれ異なった方法で束縛しています。   129 | 130 | ```haskell 131 | takeFive :: Array Int -> Int 132 | takeFive [0, 1, a, b, _] = a * b 133 | takeFive _ = 0 134 | ``` 135 | 136 | 最初のパターンは、第1要素と第2要素がそれぞれ0と1であるような、5要素の配列にのみ適合します。その場合、関数は第3要素と第4要素の積を返します。それ以外の場合は、関数は0を返します。 `PSCi`で試してみると、たとえば次のようになります。 137 | 138 | ```text 139 | > :paste 140 | … takeFive [0, 1, a, b, _] = a * b 141 | … takeFive _ = 0 142 | … ^D 143 | 144 | > takeFive [0, 1, 2, 3, 4] 145 | 6 146 | 147 | > takeFive [1, 2, 3, 4, 5] 148 | 0 149 | 150 | > takeFive [] 151 | 0 152 | ``` 153 | 154 | 配列のリテラルパターンでは、固定長の配列と一致させることはできますが、不特定の長さの配列を照合させる手段を提供していません。PureScriptでは、そのような方法で不変な配列を分解すると、実行速度が低下する可能性があるためです。不特定の長さの配列に対して照合を行うことができるデータ構造が必要な場合は、`Data.List`を使うことをお勧めします。そのほかの操作について、より優れた漸近性能を提供するデータ構造も存在します。 155 | 156 | ## レコードパターンと行多相 157 | 158 | **レコードパターン**(Record patterns)は(ご想像のとおり)レコードに照合します。 159 | 160 | レコードパターンはレコードリテラルに見た目が似ていますが、レコードリテラルでラベルと式を**コロン**で区切るのとは異なり、レコードパターンではラベルとパターンを**等号**で区切ります。 161 | 162 | たとえば、次のパターンは `first`と `last`と呼ばれるフィールドが含まれた任意のレコードにマッチし、これらのフィールドの値はそれぞれ `x`と `y`という名前に束縛されます。 163 | 164 | ```haskell 165 | showPerson :: { first :: String, last :: String } -> String 166 | showPerson { first: x, last: y } = y <> ", " <> x 167 | ``` 168 | 169 | レコードパターンはPureScriptの型システムの興味深い機能である**行多相**(row polymorphism)の良い例となっています。もし上の`showPerson`を型シグネチャなしで定義していたとすると、この型はどのように推論されるのでしょうか?面白いことに、推論される型は上で与えた型とは同じではありません。 170 | 171 | ```text 172 | > showPerson { first: x, last: y } = y <> ", " <> x 173 | 174 | > :type showPerson 175 | forall r. { first :: String, last :: String | r } -> String 176 | ``` 177 | 178 | この型変数 `r`とは何でしょうか?`PSCi`で `showPerson`を使ってみると、面白いことがわかります。 179 | 180 | ```text 181 | > showPerson { first: "Phil", last: "Freeman" } 182 | "Freeman, Phil" 183 | 184 | > showPerson { first: "Phil", last: "Freeman", location: "Los Angeles" } 185 | "Freeman, Phil" 186 | ``` 187 | 188 | レコードにそれ以外のフィールドが追加されていても、 `showPerson`関数はそのまま動作するのです。型が `String`であるようなフィールド `first`と `last`がレコードに少なくとも含まれていれば、関数適用は正しく型付けされます。しかし、フィールドが**不足**していると、 `showPerson`の呼び出しは**不正**となります。 189 | 190 | ```text 191 | > showPerson { first: "Phil" } 192 | 193 | Type of expression lacks required label "last" 194 | ``` 195 | 196 | `showPerson`の推論された型シグネチャは、 `String`であるような `first`と `last`というフィールドと、**それ以外の任意のフィールドを**持った任意のレコードを引数に取り、 `String`を返す、というように読むことができます。 197 | 198 | この関数はレコードフィールドの行 `r`について多相的なので、行多相と呼ばれるわけです。 199 | 200 | 次のように書くことができることにも注意してください。 201 | 202 | ```haskell 203 | > showPerson p = p.last <> ", " <> p.first 204 | ``` 205 | 206 | この場合も、 `PSCi`は先ほどと同じ型を推論するでしょう。 207 | 208 | 後ほど**拡張可能作用**(Extensible effects)について議論するときに、再び行多相について見ていくことになります。 209 | 210 | ## 入れ子になったパターン 211 | 212 | 配列パターンとレコードパターンはどちらも小さなパターンを組み合わせることで大きなパターンを構成しています。これまでの例では配列パターンとレコードパターンの内部に単純なパターンを使用していましたが、パターンが自由に**入れ子**にすることができることも知っておくのが大切です。入れ子になったパターンを使うと、潜在的に複雑なデータ型に対して関数が条件分岐できるようになります。 213 | 214 | たとえば、次のコードでは、レコードパターンと配列パターンを組み合わせて、レコードの配列と照合させています。 215 | 216 | ```haskell 217 | type Address = { street :: String, city :: String } 218 | 219 | type Person = { name :: String, address :: Address } 220 | 221 | livesInLA :: Person -> Boolean 222 | livesInLA { address: { city: "Los Angeles" } } = true 223 | livesInLA _ = false 224 | ``` 225 | 226 | ## 名前付きパターン 227 | 228 | パターンには**名前を付ける**ことができ、入れ子になったパターンを使うときにスコープに追加の名前を導入することができます。任意のパターンに名前を付けるには、 `@`記号を使います。 229 | 230 | たとえば、次のコードは1つ以上の要素を持つ任意の配列と適合しますが、配列の先頭を `x`という名前、配列全体を `arr`という名前に束縛します。 231 | 232 | ```haskell 233 | sortPair :: Array Int -> Array Int 234 | sortPair arr@[x, y] 235 | | x <= y = arr 236 | | otherwise = [y, x] 237 | sortPair arr = arr 238 | ``` 239 | 240 | その結果、ペアがすでにソートされている場合は、新しい配列を複製する必要がありません。 241 | 242 | ## 演習 243 | 244 | 1. (簡単)レコードパターンを使って、2つの `Person`レコードが同じ都市にいるか探す関数 `sameCity`を定義してみましょう。 245 | 246 | 1. (やや難しい)行多相を考慮すると、 `sameCity`関数の最も一般的な型は何でしょうか?先ほど定義した `livesInLA`関数についてはどうでしょうか? 247 | 248 | 1. (やや難しい)配列リテラルパターンを使って、1要素の配列の唯一のメンバーを抽出する関数`fromSingleton`を書いてみましょう。1要素だけを持つ配列でない場合、関数は指定されたデフォルト値を返さなければなりません。この関数は `forall a. a -> Array a -> a`.という型を持っていなければなりません。 249 | 250 | ## Case式 251 | 252 | パターンはソースコードの最上位にある関数だけに現れるわけではありません。 `case`式を使用すると計算の途中の値に対してパターン照合を使うことができます。case式には無名関数に似た種類の便利さがあります。関数に名前を与えることがいつも望ましいわけではありません。パターン照合を使いたいためだけで関数に名前をつけるようなことを避けられるようになります。 253 | 254 | 例を示しましょう。次の関数は、配列の"longest zero suffix"(和がゼロであるような、最も長い配列の末尾)を計算します。 255 | 256 | ```haskell 257 | import Data.Array.Partial (tail) 258 | import Partial.Unsafe (unsafePartial) 259 | 260 | lzs :: Array Int -> Array Int 261 | lzs [] = [] 262 | lzs xs = case sum xs of 263 | 0 -> xs 264 | _ -> lzs (unsafePartial tail xs) 265 | ``` 266 | 267 | 例えば次のようになります。 268 | 269 | ```text 270 | > lzs [1, 2, 3, 4] 271 | [] 272 | 273 | > lzs [1, -1, -2, 3] 274 | [-1, -2, 3] 275 | ``` 276 | 277 | この関数は場合ごとの分析によって動作します。もし配列が空なら、唯一の選択肢は空の配列を返すことです。配列が空でない場合は、さらに2つの場合に分けるためにまず `case`式を使用します。配列の合計がゼロであれば、配列全体を返します。そうでなければ、配列の残りに対して再帰します。 278 | 279 | ## パターン照合の失敗 280 | 281 | case式のパターンを順番に照合していって、もし選択肢のいずれの場合も入力が適合しなかった時は何が起こるのでしょうか?この場合、**パターン照合失敗**によって、case式は実行時に失敗します。 282 | 283 | 簡単な例でこの動作を見てみましょう。 284 | 285 | ```haskell 286 | import Partial.Unsafe (unsafePartial) 287 | 288 | partialFunction :: Boolean -> Boolean 289 | partialFunction = unsafePartial \true -> true 290 | ``` 291 | 292 | この関数はゼロの入力に対してのみ適合する単一の場合を含みます。このファイルをコンパイルして `PSCi`でそれ以外の値を与えてテストすると、実行時エラーが発生します。 293 | 294 | ```text 295 | > partialFunction false 296 | 297 | Failed pattern match 298 | ``` 299 | 300 | どんな入力の組み合わせに対しても値を返すような関数は**全関数**(total function)と呼ばれ、そうでない関数は**部分関数**(partial function)と呼ばれています。 301 | 302 | 一般的には、可能な限り全関数として定義したほうが良いと考えられています。もしその関数が正しい入力に対して値を返さないことがあるとわかっているなら、大抵は `a`に対して型 `Maybe a`の返り値にし、失敗を示すときには `Nothing`を使うようにしたほうがよいでしょう。この方法なら、型安全な方法で値の有無を示すことができます。 303 | 304 | PureScriptコンパイラは、パターンマッチが不完全で関数が全関数ではないことを検出するとエラーを生成します。部分関数が安全である場合、`unsafePartial`関数を使ってこれらのエラーを抑制することができます(その部分関数が安全だとあなたが言い切れるなら!)。もし上記の `unsafePartial`関数の呼び出しを取り除くと、コンパイラは次のエラーを生成します。 305 | 306 | ```text 307 | A case expression could not be determined to cover all inputs. 308 | The following additional cases are required to cover all inputs: 309 | 310 | false 311 | ``` 312 | 313 | これは値`false`が、定義されたどのパターンとも一致しないことを示しています。これらの警告には、複数の不一致のケースが含まれることがあります。 314 | 315 | 上記の型シグネチャも省略した場合は、次のようになります。 316 | 317 | ```purescript 318 | partialFunction true = true 319 | ``` 320 | 321 | このとき、PSCiは興味深い型を推論します。 322 | 323 | ```text 324 | :type partialFunction 325 | 326 | Partial => Boolean -> Boolean 327 | ``` 328 | 329 | 本書ではのちに`=>`記号を含むいろいろな型を見ることができます(これらは**型クラス**に関連しています)。しかし、今のところは、PureScriptは型システムを使って部分関数を追跡していること、開発者は型検証器にコードが安全であることを明示する必要があることを確認すれば十分です。 330 | 331 | コンパイラは、定義されたパターンが**冗長**であることを検出した場合(すでに定義されたパターンに一致するケースのみ)でも警告を生成します。 332 | 333 | ```purescript 334 | redundantCase :: Boolean -> Boolean 335 | redundantCase true = true 336 | redundantCase false = false 337 | redundantCase false = false 338 | ``` 339 | 340 | このとき、最後のケースは冗長であると正しく検出されます。 341 | 342 | ```text 343 | Redundant cases have been detected. 344 | The definition has the following redundant cases: 345 | 346 | false 347 | ``` 348 | 349 | **注意**:PSCiは警告を表示しないので、この例を再現するには、この関数をファイルとして保存し、 `pulp build`を使ってコンパイルします。 350 | 351 | ## 代数的データ型 352 | 353 | この節では、PureScriptの型システムでパターン照合に原理的に関係している**代数的データ型**(Algebraic data type, ADT)と呼ばれる機能を導入します。 354 | 355 | しかしまずは、ベクターグラフィックスライブラリの実装というこの章の課題を解決する基礎として、簡単な例を切り口にして考えていきましょう。 356 | 357 | 直線、矩形、円、テキストなどの単純な図形の種類を表現する型を定義したいとします。オブジェクト指向言語では、おそらくインタフェースもしくは抽象クラス `Shape`を定義し、使いたいそれぞれの図形について具体的なサブクラスを定義するでしょう。 358 | 359 | しかしながら、この方針は大きな欠点をひとつ抱えています。 `Shape`を抽象的に扱うためには、実行したいと思う可能性のあるすべての操作を事前に把握し、 `Shape`インタフェースに定義する必要があるのです。このため、モジュール性を壊さずに新しい操作を追加することが難しくなります。 360 | 361 | もし図形の種類が事前にわかっているなら、代数的データ型はこうした問題を解決する型安全な方法を提供します。モジュール性のある方法で `Shape`に新たな操作を定義し、型安全なまま保守することを可能にします。 362 | 363 | 代数的データ型として表現された `Shape`がどのように記述されるかを次に示します。 364 | 365 | ```haskell 366 | data Shape 367 | = Circle Point Number 368 | | Rectangle Point Number Number 369 | | Line Point Point 370 | | Text Point String 371 | ``` 372 | 373 | 次のように `Point`型を代数的データ型として定義することもできます。 374 | 375 | ```haskell 376 | data Point = Point 377 | { x :: Number 378 | , y :: Number 379 | } 380 | ``` 381 | 382 | この `Point`データ型は、興味深い点をいくつか示しています。 383 | 384 | - 代数的データ型の構築子に格納されるデータは、プリミティブ型に限定されるわけではありません。構築子はレコード、配列、あるいは他の代数的データ型を含めることもできます。 385 | - 代数的データ型は複数の構築子があるデータを記述するのに便利ですが、構築子がひとつだけのときでも便利です。 386 | - 代数的データ型の構築子は、代数的データ型自身と同じ名前の場合もあります。これはごく一般的であり、 `Point`**データ構築子**と `Point`**型構築子**を混同しないようにすることが大切です。これらは異なる名前空間にあります。 387 | 388 | この宣言ではいくつかの構築子の和として `Shape`を定義しており、各構築子に含まれたデータはそれぞれ区別されます。 `Shape`は、中央 `Point`と半径を持つ `Circle`か、 `Rectangle`、 `Line`、 `Text`のいずれかです。他には `Shape`型の値を構築する方法はありません。 389 | 390 | 代数的データ型の定義は予約語 `data`から始まり、それに新しい型の名前と任意個の型引数が続きます。その型のデータ構築子は等号の後に定義され、パイプ文字( `|`)で区切られます。 391 | 392 | それではPureScriptの標準ライブラリから別の例を見てみましょう。オプショナルな値を定義するのに使われる `Maybe`型を本書の冒頭で扱いました。 `purescript-maybe`パッケージでは `Maybe`を次のように定義しています。 393 | 394 | ```haskell 395 | data Maybe a = Nothing | Just a 396 | ``` 397 | 398 | この例では型引数 `a`の使用方法を示しています。パイプ文字を「または」と読むことにすると、この定義は「 `Maybe a`型の値は、無い(`Nothing`)、またはただの(`Just`)型 `a`の値だ」と英語のように読むことができます。 399 | 400 | データ構築子は再帰的なデータ構造を定義するために使用することもできます。更に例を挙げると、要素が型 `a`の単方向連結リストのデータ型を定義はこのようになります。 401 | 402 | ```haskell 403 | data List a = Nil | Cons a (List a) 404 | ``` 405 | 406 | この例は `purescript-lists`パッケージから持ってきました。ここで `Nil`構築子は空のリストを表しており、 `Cons`は先頭となる要素と他の配列から空でないリストを作成するために使われます。 `Cons`の2つ目のフィールドでデータ型 `List a`を使用しており、再帰的なデータ型になっていることに注目してください。 407 | 408 | ## 代数的データ型の使用 409 | 410 | 代数的データ型の構築子を使用して値を構築するのはとても簡単です。対応する構築子に含まれるデータに応じた引数を用意し、その構築子を単に関数のように適用するだけです。 411 | 412 | 例えば、上で定義した `Line`構築子は2つの `Point`を必要としていますので、 `Line`構築子を使って `Shape`を構築するには、型 `Point`のふたつの引数を与えなければなりません。 413 | 414 | ```haskell 415 | exampleLine :: Shape 416 | exampleLine = Line p1 p2 417 | where 418 | p1 :: Point 419 | p1 = Point { x: 0.0, y: 0.0 } 420 | 421 | p2 :: Point 422 | p2 = Point { x: 100.0, y: 50.0 } 423 | ``` 424 | 425 | `p1`及び `p2`を構築するため、レコードを引数として `Point`構築子を適用しています。 426 | 427 | 代数的データ型で値を構築することは簡単ですが、これをどうやって使ったらよいのでしょうか?ここで代数的データ型とパターン照合との重要な接点が見えてきます。代数的データ型の値がどの構築子から作られたかを調べたり、代数的データ型からフィールドの値を取り出す唯一の方法は、パターン照合を使用することです。 428 | 429 | 例を見てみましょう。 `Shape`を `String`に変換したいとしましょう。 `Shape`を構築するのにどの構築子が使用されたかを調べるには、パターン照合を使用しなければなりません。これには次のようにします。 430 | 431 | ```haskell 432 | showPoint :: Point -> String 433 | showPoint (Point { x: x, y: y }) = 434 | "(" <> show x <> ", " <> show y <> ")" 435 | 436 | showShape :: Shape -> String 437 | showShape (Circle c r) = ... 438 | showShape (Rectangle c w h) = ... 439 | showShape (Line start end) = ... 440 | showShape (Text p text) = ... 441 | ``` 442 | 443 | 各構築子はパターンとして使用することができ、構築子への引数はそのパターンで束縛することができます。 `showShape`の最初の場合を考えてみましょう。もし `Shape`が `Circle`構築子適合した場合、2つの変数パターン `c`と `r`を使って `Circle`の引数(中心と半径)がスコープに導入されます。その他の場合も同様です。 444 | 445 | `showPoint`は、パターン照合の別の例にもなっています。 `showPoint`はひとつの場合しかありませんが、 `Point`構築子の中に含まれたレコードのフィールドに適合する、入れ子になったパターンが使われています。 446 | 447 | ## レコード同名利用 448 | 449 | `showPoint`関数は引数内のレコードと一致し、 `x`と `y`プロパティを同じ名前の値に束縛します。 PureScriptでは、このようなパターン一致を次のように単純化できます。 450 | 451 | ```haskell 452 | showPoint :: Point -> String 453 | showPoint (Point { x, y }) = ... 454 | ``` 455 | 456 | ここでは、プロパティの名前のみを指定し、名前に導入したい値を指定する必要はありません。 これは**レコード同名利用**(record pun)と呼ばれます。 457 | 458 | レコード同名利用をレコードの**構築**に使用することもできます。例えば、スコープ内に `x`と `y`という名前の値があれば、 `Point {x、y}`を使って `Point`を作ることができます。 459 | 460 | ```haskell 461 | origin :: Point 462 | origin = Point { x, y } 463 | where 464 | x = 0.0 465 | y = 0.0 466 | ``` 467 | 468 | これは、状況によってはコードの可読性を向上させるのに役立ちます。 469 | 470 | ## 演習 471 | 472 | 1. (簡単)半径 `10`で中心が原点にある円を表す `Shape`の値を構築してください。 473 | 474 | 1. (やや難しい)引数の `Shape`を原点を中心として `2.0`倍に拡大する、 `Shape`から `Shape`への関数を書いてみましょう。 475 | 476 | 1. (やや難しい) `Shape`からテキストを抽出する関数を書いてください。この関数は `Maybe String`を返さなければならず、もし入力が `Text`を使用して構築されたのでなければ、返り値には `Nothing`構築子を使ってください。 477 | 478 | ## newtype宣言 479 | 480 | 代数的データ型の特別な場合に、**newtype**と呼ばれる重要なものあります。newtypeは予約語 `data`の代わりに予約語 `newtype`を使用して導入します。 481 | 482 | newtype宣言では**過不足なくひとつだけの**構築子を定義しなければならず、その構築子は**過不足なくひとつだけの**引数を取る必要があります。つまり、newtype宣言は既存の型に新しい名前を与えるものなのです。実際、newtypeの値は、元の型と同じ実行時表現を持っています。しかし、これらは型システムの観点から区別されます。これは型安全性の追加の層を提供するのです。 483 | 484 | 例として、ピクセルとインチのような単位を表現するために、 `Number`の型レベルの別名を定義したくなる場合があるかもしれません。 485 | 486 | ```haskell 487 | newtype Pixels = Pixels Number 488 | newtype Inches = Inches Number 489 | ``` 490 | 491 | こうすると `Inches`を期待している関数に `Pixels`型の値を渡すことは不可能になりますが、実行時の効率に余計な負荷が加わることはありません。 492 | 493 | newtypeは次の章で**型クラス**を扱う際に重要になります。newtypeは実行時の表現を変更することなく型に異なる振る舞いを与えることを可能にするからです。 494 | 495 | ## ベクターグラフィックスライブラリ 496 | 497 | これまで定義してきたデータ型を使って、ベクターグラフィックスを扱う簡単なライブラリを作成していきましょう。 498 | 499 | ただの `Shape`の配列であるような、 `Picture`という型同義語を定義しておきます。 500 | 501 | ```haskell 502 | type Picture = Array Shape 503 | ``` 504 | 505 | デバッグしていると `Picture`を `String`として表示できるようにしたくなることもあるでしょう。これはパターン照合を使用して定義された `showPicture`関数で行うことができます。 506 | 507 | ```haskell 508 | showPicture :: Picture -> Array String 509 | showPicture = map showShape 510 | ``` 511 | 512 | それを試してみましょう。 モジュールを `pulp build`でコンパイルし、 `pulp repl`でPSCiを開きます。 513 | 514 | ```text 515 | $ pulp build 516 | $ pulp repl 517 | 518 | > import Data.Picture 519 | 520 | > :paste 521 | … showPicture 522 | … [ Line (Point { x: 0.0, y: 0.0 }) 523 | … (Point { x: 1.0, y: 1.0 }) 524 | … ] 525 | … ^D 526 | 527 | ["Line [start: (0.0, 0.0), end: (1.0, 1.0)]"] 528 | ``` 529 | 530 | ## 外接矩形の算出 531 | 532 | このモジュールのコード例には、 `Picture`の最小外接矩形を計算する関数 `bounds`が含まれています。 533 | 534 | `Bounds`は外接矩形を定義するデータ型です。また、構築子をひとつだけ持つ代数的データ型として定義されています。 535 | 536 | ```haskell 537 | data Bounds = Bounds 538 | { top :: Number 539 | , left :: Number 540 | , bottom :: Number 541 | , right :: Number 542 | } 543 | ``` 544 | 545 | `Picture`内の `Shape`の配列を走査し、最小の外接矩形を累積するため、 `bounds`は `Data.Foldable`の `foldl`関数を使用しています。 546 | 547 | ```haskell 548 | bounds :: Picture -> Bounds 549 | bounds = foldl combine emptyBounds 550 | where 551 | combine :: Bounds -> Shape -> Bounds 552 | combine b shape = union (shapeBounds shape) b 553 | ``` 554 | 555 | 畳み込みの初期値として空の `Picture`の最小外接矩形を求める必要がありますが、 `emptyBounds`で定義される空の外接矩形がその条件を満たしています。 556 | 557 | 累積関数 `combine`は `where`ブロックで定義されています。 `combine`は `foldl`の再帰呼び出しで計算された外接矩形と、配列内の次の `Shape`を引数にとり、ユーザ定義の演算子 `union`を使ってふたつの外接矩形の和を計算しています。 `shapeBounds`関数は、パターン照合を使用して、単一の図形の外接矩形を計算します。 558 | 559 | ## 演習 560 | 561 | 1. (やや難しい) ベクターグラフィックライブラリを拡張し、 `Shape`の面積を計算する新しい操作 `area`を追加してください。この演習では、テキストの面積は0であるものとしてください。 562 | 563 | 1. (難しい) `Shape`を拡張し、新しいデータ構築子 `Clipped`を追加してください。 `Clipped`は他の `Picture`を矩形に切り抜き出ます。切り抜かれた `Picture`の境界を計算できるよう、 `shapeBounds`関数を拡張してください。これは `Shape`を再帰的なデータ型にすることに注意してください。 564 | 565 | ## まとめ 566 | 567 | この章では、関数型プログラミングから基本だが強力なテクニックであるパターン照合を扱いました。複雑なデータ構造の部分と照合するために、簡単なパターンだけでなく配列パターンやレコードパターンをどのように使用するかを見てきました。 568 | 569 | またこの章では、パターン照合に密接に関連する代数的データ型を導入しました。代数的データ型がデータ構造のわかりやすい記述をどのように可能にするか、新たな操作でデータ型を拡張するためのモジュール性のある方法を提供することを見てきました。 570 | 571 | 最後に、多くの既存のJavaScript関数に型を与えるために、強力な抽象化である行多相を扱いました。この本の後半ではこれらの概念を再び扱います。 572 | 573 | 本書では今後も代数的データ型とパターン照合を使用するので、今のうちにこれらに習熟しておくと後で役立つでしょう。これ以外にも独自の代数的データ型を作成し、パターン照合を使用してそれらを使う関数を書くことを試してみてください。 574 | 575 | -------------------------------------------------------------------------------- /src/chapter07.md: -------------------------------------------------------------------------------- 1 | # Applicativeによる検証 2 | 3 | ## この章の目標 4 | 5 | この章では、`Applicative`型クラスによって表現される**Applicative関手**(applicative functor)という重要な抽象化と新たに出会うことになります。名前が難しそうに思えても心配しないでください。フォームデータの検証という実用的な例を使ってこの概念を説明していきます。Applicative関手を使うと、大量の決まり文句を伴うような入力項目の内容を検証するためのコードを、簡潔で宣言的な記述へと変えることができるようになります。 6 | 7 | また、**Traversable関手**(traversable functor)を表現する`Traversable`という別の型クラスにも出会います。現実の問題への解決策からこの概念が自然に生じるということがわかるでしょう。 8 | 9 | この章では第3章に引き続き住所録を例として扱います。今回は住所録のデータ型を拡張し、これらの型の値を検証する関数を書きます。これらの関数は、例えばデータ入力フォームの一部で、使用者へエラーを表示するウェブユーザインタフェースで使われると考えてください。     10 | 11 | ## プロジェクトの準備 12 | 13 | この章のソース·コードは、次のふたつのファイルで定義されています。 14 | 15 | * `src/Data/AddressBook.purs` 16 | * `src/Data/AddressBook/Validation.purs` 17 | 18 | このプロジェクトは多くのBower依存関係を持っていますが、その大半はすでに見てきたものです。新しい依存関係は2つです。 19 | 20 | - `purescript-control` - `Applicative`のような型クラスを使用して制御フローを抽象化する関数が定義されています 21 | - `purescript-validation` - この章の主題である **`Applicative`による検証** のための関手が定義されています。 22 | 23 | `Data.AddressBook`モジュールには、このプロジェクトのデータ型とそれらの型に対する`Show`インスタンスが定義されており、`Data.AddressBook.Validation`モジュールにはそれらの型の検証規則含まれています。 24 | 25 | ## 関数適用の一般化 26 | 27 | **Applicative関手**の概念を理解するために、まずは以前扱った型構築子`Maybe`について考えてみましょう。 28 | 29 | このモジュールのソースコードでは、次のような型を持つ`address`関数が定義されています。 30 | 31 | ```haskell 32 | address :: String -> String -> String -> Address 33 | ``` 34 | 35 | この関数は、通りの名前、市、州という3つの文字列から型`Address`の値を構築するために使います。 36 | 37 | この関数は簡単に適用できますので、`PSCi`でどうなるか見てみましょう。 38 | 39 | ```text 40 | > import Data.AddressBook 41 | 42 | > address "123 Fake St." "Faketown" "CA" 43 | Address { street: "123 Fake St.", city: "Faketown", state: "CA" } 44 | ``` 45 | 46 | しかし、通り、市、州の三つすべてが必ずしも入力されないものとすると、三つの場合がそれぞれ省略可能であることを示すために`Maybe`型を使用したくなります。 47 | 48 | 考えられる場合としては、市が省略されている場合があるでしょう。もし`address`関数を直接適用しようとすると、型検証器からエラーが表示されます。 49 | 50 | ```text 51 | > import Data.Maybe 52 | > address (Just "123 Fake St.") Nothing (Just "CA") 53 | 54 | Could not match type Maybe String with type String 55 | ``` 56 | 57 | `address`は`Maybe String`型ではなく文字列型の引数を取るので、もちろんこれは型エラーになります。 58 | 59 | しかし、もし`address`関数を「持ち上げる」ことができれば、`Maybe`型で示される省略可能な値を扱うことができるはずだと期待することは理にかなっています。実際に、`Control.Apply`で提供されている関数`lift3`が、まさに求めているものです。 60 | 61 | ```text 62 | > import Control.Apply 63 | > lift3 address (Just "123 Fake St.") Nothing (Just "CA") 64 | 65 | Nothing 66 | ``` 67 | 68 | このとき、引数のひとつ(市)が欠落していたので、結果は`Nothing`になります。もし3つの引数すべてが`Just`構築子を使って与えられれば、結果は値を含むことになります。 69 | 70 | ```text 71 | > lift3 address (Just "123 Fake St.") (Just "Faketown") (Just "CA") 72 | 73 | Just (Address { street: "123 Fake St.", city: "Faketown", state: "CA" }) 74 | ``` 75 | 76 | `lift3`という関数の名前は、3引数の関数を持ち上げるために使用できることを示しています。関数を持ち上げる同様の関数で、引数の数が異なるものが、`Control.Apply`で定義されています。 77 | 78 | ## 任意個の引数を持つ関数の持ち上げ 79 | 80 | これで、`lift2`や`lift3`のような関数を使えば、引数が2個や3個の関数を持ち上げることができるのはわかりました。でも、これを任意個の引数の関数へと一般化することはできるのでしょうか。 81 | 82 | `lift3`の型を見てみるとわかりやすいでしょう。 83 | 84 | ```text 85 | > :type lift3 86 | forall a b c d f. Apply f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d 87 | ``` 88 | 89 | 上の`Maybe`の例では型構築子`f`は`Maybe`ですから、`lift3`は次のように特殊化されます。 90 | 91 | ```haskell 92 | forall a b c d. (a -> b -> c -> d) -> Maybe a -> Maybe b -> Maybe c -> Maybe d 93 | ``` 94 | 95 | この型が言っているのは、3引数の任意の関数を取り、その関数を引数と返り値が`Maybe`で包まれた新しい関数へと持ち上げる、ということです。 96 | 97 | もちろんどんな型構築子`f`についても持ち上げができるわけではないのですが、それでは`Maybe`型を持ち上げができるようにしているものは何なのでしょうか。さて、先ほどの型の特殊化では、`f`に対する型クラス制約から`Apply`型クラスを取り除いていました。`Apply`はPreludeで次のように定義されています。 98 | 99 | ```haskell 100 | class Functor f where 101 | map :: forall a b. (a -> b) -> f a -> f b 102 | class Functor f <= Apply f where 103 | apply :: forall a b. f (a -> b) -> f a -> f b 104 | ``` 105 | 106 | `Apply`型クラスは`Functor`の下位クラスであり、追加の関数`apply`が定義しています。`Prelude`モジュールでは`<$>`を、`map`の別名として、`<*>`を`apply`の別名として定義しています。`map`とよく似た型を持つ追加の関数`apply`が定義されています。`map`と`apply`の違いは、`map`がただの関数を引数に取るのに対し、`apply`の最初の引数は型構築子`f`で包まれているという点です。これをどのように使うのかはこれからすぐに見ていきますが、その前にまず`Maybe`型について`Apply`型クラスをどう実装するのかを見ていきましょう。 107 | 108 | ```haskell 109 | instance functorMaybe :: Functor Maybe where 110 | map f (Just a) = Just (f a) 111 | map f Nothing = Nothing 112 | 113 | instance applyMaybe :: Apply Maybe where 114 | apply (Just f) (Just x) = Just (f x) 115 | apply _ _ = Nothing 116 | ``` 117 | 118 | この型クラスのインスタンスが言っているのは、任意のオプショナルな値にオプショナルな関数を適用することができ、その両方が定義されている時に限り結果も定義される、ということです。 119 | 120 | それでは、`map`と`apply`を一緒に使ってどうやって引数が任意個の関数を持ち上げるのかを見ていきましょう。 121 | 122 | 1引数の関数については、`map`をそのまま使うだけです。 123 | 124 | 2引数の関数についても考えてみます。型`a -> b -> c`を持つカリー化された関数`f`があるとしましょう。これは型`a -> (b -> c)`と同じですから、`map`を`f`に適用すると型`f a -> f (b -> c)`の新たな関数を得ることになります。持ち上げられた(型`f a`の)最初の引数にその関数を部分適用すると、型`f (b -> c)`の新たな包まれた関数が得られます。それから、2番目の持ち上げられた(型`f b`の)引数へ`apply`を適用することができ、型`f c`の最終的な値を得ます。 125 | 126 | まとめると、`x :: f a`と`y :: f b`があるとき、式`(f <$> x) <*> y`の型は`f c`になります(この式は`apply (map f x) y`と同じ意味だということを思い出しましょう)。Preludeで定義された優先順位の規則に従うと、`f <$> x <*> y`というように括弧を外すことができます。 127 | 128 | 一般的にいえば、最初の引数に`<$>`を使い、残りの引数に対しては`<*>`を使います。`lift3`で説明すると次のようになります。 129 | 130 | ```haskell 131 | lift3 :: forall a b c d f 132 | . Apply f 133 | => (a -> b -> c -> d) 134 | -> f a 135 | -> f b 136 | -> f c 137 | -> f d 138 | lift3 f x y z = f <$> x <*> y <*> z 139 | ``` 140 | 141 | この式の型がちゃんと整合しているかの確認は、読者への演習として残しておきます。 142 | 143 | 例として、`<$>`と`<*>`をそのまま使うと、`Maybe`上に`address`関数を持ち上げることができます。 144 | 145 | ```text 146 | > address <$> Just "123 Fake St." <*> Just "Faketown" <*> Just "CA" 147 | Just (Address { street: "123 Fake St.", city: "Faketown", state: "CA" }) 148 | 149 | > address <$> Just "123 Fake St." <*> Nothing <*> Just "CA" 150 | Nothing 151 | ``` 152 | 153 | このように、引数が異なる他のいろいろな関数を`Maybe`上に持ち上げてみてください。 154 | 155 | ## Applicative型クラス 156 | 157 | これに関連する`Applicative`という型クラスが存在しており、次のように定義されています。 158 | 159 | ```haskell 160 | class Apply f <= Applicative f where 161 | pure :: forall a. a -> f a 162 | ``` 163 | 164 | `Applicative`は`Apply`の下位クラスであり、`pure`関数が定義されています。`pure`は値を取り、その型の型構築子`f`で包まれた値を返します。 165 | 166 | `Maybe`についての`Applicative`インスタンスは次のようになります。 167 | 168 | ```haskell 169 | instance applicativeMaybe :: Applicative Maybe where 170 | pure x = Just x 171 | ``` 172 | 173 | Applicative関手は関数を持ち上げることを可能にする関手だと考えるとすると、`pure`は引数のない関数の持ち上げだというように考えることができます。 174 | 175 | ## Applicativeに対する直感的理解 176 | 177 | PureScriptの関数は純粋であり、副作用は持っていません。Applicative関手は、関手`f`によって表現されたある種の副作用を提供するような、より大きな「プログラミング言語」を扱えるようにします。 178 | 179 | たとえば、関手`Maybe`はオプショナルな値の副作用を表現しています。その他の例としては、型`err`のエラーの可能性の副作用を表す`Either err`や、大域的な構成を読み取る副作用を表すArrow関手(arrow functor)`r ->`があります。ここでは`Maybe`関手についてだけを考えることにします。 180 | 181 | もし関手`f`が作用を持つより大きなプログラミング言語を表すとすると、`Apply`と`Applicative`インスタンスは小さなプログラミング言語(PureScript)から新しい大きな言語へと値や関数を持ち上げることを可能にします。 182 | 183 | `pure`は純粋な(副作用がない)値をより大きな言語へと持ち上げますし、関数については上で述べたとおり`map`と`apply`を使うことができます。 184 | 185 | ここで新たな疑問が生まれます。もしPureScriptの関数と値を新たな言語へ埋め込むのに`Applicative`が使えるなら、どうやって新たな言語は大きくなっているというのでしょうか。この答えは関手`f`に依存します。もしなんらかの`x`について`pure x`で表せないような型`f a`の式を見つけたなら、その式はそのより大きな言語だけに存在する項を表しているということです。 186 | 187 | `f`が`Maybe`のときの式`Nothing`がその例になっています。`Nothing`を何らかの`x`について`pure x`というように書くことはできません。したがって、PureScriptは省略可能な値を表す新しい項`Nothing`を含むように拡大されたと考えることができます。 188 | 189 | ## その他の作用について 190 | 191 | それでは、他にも`Applicative`関手へと関数を持ち上げる例をいろいろ見ていきましょう。 192 | 193 | 次は、`PSCi`で定義された3つの名前を結合して完全な名前を作る簡単なコード例です。 194 | 195 | ```text 196 | > import Prelude 197 | > fullName first middle last = last <> ", " <> first <> " " <> middle 198 | > fullName "Phillip" "A" "Freeman" 199 | Freeman, Phillip A 200 | ``` 201 | 202 | この関数は、クエリパラメータとして与えられた3つの引数を持つ、(とても簡単な!)ウェブサービスの実装であるとしましょう。使用者が3つの引数すべてを与えたことを確かめたいので、引数が存在するかどうかを表す`Maybe`型をつかうことになるでしょう。`fullName`を`Maybe`の上へ持ち上げると、省略された引数を確認するウェブサービスを実装することができます。 203 | 204 | ```text 205 | > import Data.Maybe 206 | > fullName <$> Just "Phillip" <*> Just "A" <*> Just "Freeman" 207 | Just ("Freeman, Phillip A") 208 | > fullName <$> Just "Phillip" <*> Nothing <*> Just "Freeman" 209 | Nothing 210 | ``` 211 | 212 | この持ち上げた関数は、引数のいずれかが`Nothing`なら`Nothing`返すことに注意してください。 213 | 214 | これで、もし引数が不正ならWebサービスからエラー応答を送信することができるので、なかなかいい感じです。しかし、どのフィールドが間違っていたのかを応答で表示できると、もっと良くなるでしょう。 215 | 216 | `Meybe`上へ持ち上げる代わりに`Either String`上へ持ち上げるようにすると、エラーメッセージを返すことができるようになります。まずは入力を`Either String`を使ってエラーを発信できる計算に変換する演算子を書きましょう。 217 | 218 | ```text 219 | > :paste 220 | … withError Nothing err = Left err 221 | … withError (Just a) _ = Right a 222 | … ^D 223 | ``` 224 | 225 | **注意**:`Either err`Applicative関手において、`Left`構築子は失敗を表しており、`Right`構築子は成功を表しています。 226 | 227 | これで`Either String`上へ持ち上げることで、それぞれの引数について適切なエラーメッセージを提供できるようになります。 228 | 229 | ```text 230 | > :paste 231 | … fullNameEither first middle last = 232 | … fullName <$> (first `withError` "First name was missing") 233 | … <*> (middle `withError` "Middle name was missing") 234 | … <*> (last `withError` "Last name was missing") 235 | … ^D 236 | 237 | > :type fullNameEither 238 | Maybe String -> Maybe String -> Maybe String -> Either String String 239 | ``` 240 | 241 | この関数は`Maybe`の3つの省略可能な引数を取り、`String`のエラーメッセージか`String`の結果のどちらかを返します。 242 | 243 | いろいろな入力でこの関数を試してみましょう。 244 | 245 | ```text 246 | > fullNameEither (Just "Phillip") (Just "A") (Just "Freeman") 247 | (Right "Freeman, Phillip A") 248 | 249 | > fullNameEither (Just "Phillip") Nothing (Just "Freeman") 250 | (Left "Middle name was missing") 251 | 252 | > fullNameEither (Just "Phillip") (Just "A") Nothing 253 | (Left "Last name was missing") 254 | ``` 255 | 256 | このとき、すべてのフィールドが与えられば成功の結果が表示され、そうでなければ省略されたフィールドのうち最初のものに対応するエラーメッセージが表示されます。しかし、もし複数の入力が省略されているとき、最初のエラーしか見ることができません。 257 | 258 | ```text 259 | > fullNameEither Nothing Nothing Nothing 260 | 261 | (Left "First name was missing") 262 | ``` 263 | 264 | これでも十分なときもありますが、エラー時に**すべての**省略されたフィールドの一覧がほしいときは、`Either String`よりも強力なものが必要です。この章の後半でこの解決策を見ていきます。 265 | 266 | ## 作用の結合 267 | 268 | 抽象的にApplicative関手を扱う例として、Applicative関手`f`によって表現された副作用を総称的に組み合わせる関数をどのように書くのかをこの節では示します。 269 | 270 | これはどういう意味でしょうか?何らかの`a`について型`f a`の包まれた引数の配列があるとしましょう。型`List (f a)`の配列があるということです。直感的には、これは`f`によって追跡される副作用を持つ、返り値の型が`a`の計算の配列を表しています。これらの計算のすべてを順番に実行することができれば、`List a`型の結果の配列を得るでしょう。しかし、まだ`f`によって追跡される副作用が残ります。つまり、元の配列の中の作用を「結合する」ことにより、型`List (f a)`の何かを型`List a`の何かへと変換することができると考えられます。 271 | 272 | 任意の固定長配列の長さ`n`について、その引数を要素に持った長さ`n`の配列を構築するような`n`引数の関数が存在します。たとえば、もし`n`が`3`なら、関数は`\x y z -> x : y : z : Nil`です。 この関数の型は`a -> a -> a -> List a`です。`Applicative`インスタンスを使うと、この関数を`f`の上へ持ち上げて関数型`f a -> f a -> f a -> f (List a)`を得ることができます。しかし、いかなる`n`についてもこれが可能なので、いかなる引数の**配列**についても同じように持ち上げられることが確かめられます。 273 | 274 | したがって、次のような関数を書くことができるはずです。 275 | 276 | ```haskell 277 | combineList :: forall f a. Applicative f => List (f a) -> f (List a) 278 | ``` 279 | 280 | この関数は副作用を持つかもしれない引数の配列をとり、それぞれの副作用を適用することで、`f`に包まれた単一の配列を返します。 281 | 282 | この関数を書くためには、引数の配列の長さについて考えます。配列が空の場合はどんな作用も実行する必要はありませんから、`pure`を使用して単に空の配列を返すことができます。 283 | 284 | ```haskell 285 | combineList Nil = pure Nil 286 | ``` 287 | 288 | 実際のところ、これが可能な唯一の​​定義です! 289 | 290 | 入力の配列が空でないならば、型`f a`の先頭要素と、型`List (f a)`の配列の残りについて考えます。また、再帰的に配列の残りを結合すると、型`f (List a)`の結果を得ることができます。`<$>`と`<*>`を使うと、`cons`関数を先頭と配列の残りの上に持ち上げることができます。 291 | 292 | ```haskell 293 | combineList (Cons x xs) = Cons <$> x <*> combineList xs 294 | ``` 295 | 296 | 繰り返しになりますが、これは与えられた型に基づいている唯一の妥当な実装です。 297 | 298 | `Maybe`型構築子を例にとって、`PSCi`でこの関数を試してみましょう。 299 | 300 | ```text 301 | > import Data.List 302 | > import Data.Maybe 303 | > combineList (fromFoldable [Just 1, Just 2, Just 3]) 304 | (Just (Cons 1 (Cons 2 (Cons 3 Nil)))) 305 | > combineList (fromFoldable [Just 1, Nothing, Just 2]) 306 | Nothing 307 | ``` 308 | 309 | `Meybe`へ特殊化して考えると、配列のすべての要素が`Just`であるとき、そのときに限りこの関数は`Just`を返します。そうでなければ、`Nothing`を返します。オプショナルな結果を返す計算の配列は、そのすべての計算が結果を持っていたときに全体も結果を持っているという、オプショナルな値に対応したより大きな言語での振る舞いに対する直感的な理解とこれは一致しています。 310 | 311 | しかも、`combineArray`関数はどんな`Applicative`に対しても機能します!`Either err`を使ってエラーを発信するかもしれなかったり、`r ->`を使って大域的な状態を読み取る計算を連鎖させるときにも`combineArray`関数を使うことができるのです。 312 | 313 | `combineArray`関数については、後ほど`Traversable`関手について考えるときに再び扱います。 314 | 315 | ## 演習 316 | 317 | 1. (簡単)`lift2`を使って、オプショナルな引数に対して働く、数に対する演算子`+`、`-`、`*`、`/`の持ち上げられたバージョンを書いてください。 318 | 319 | 2. (やや難しい) 上で与えられた`lift3`の定義について、`<$>`と`<*>`の型が整合していることを確認して下さい。 320 | 321 | 3. (難しい) 次の型を持つ関数`combineMaybe`を書いてください。 322 | 323 | ```haskell 324 | combineMaybe : forall a f. (Applicative f) => Maybe (f a) -> f (Maybe a) 325 | ``` 326 | 327 | この関数は副作用をもつオプショナルな計算をとり、オプショナルな結果をもつ副作用のある計算を返します。 328 | 329 | ## Applicativeによる検証 330 | 331 | この章のソースコードでは住所録アプリケーションで使われるいろいろなデータ型が定義されています。詳細はここでは割愛しますが、`Data.AddressBook`モジュールからエクスポートされる重要な関数は次のような型を持っています。 332 | 333 | ```haskell 334 | address :: String -> String -> String -> Address 335 | 336 | phoneNumber :: PhoneType -> String -> PhoneNumber 337 | 338 | person :: String -> String -> Address -> Array PhoneNumber -> Person 339 | ``` 340 | 341 | ここで、`PhoneType`は次のような代数的データ型として定義されています。 342 | 343 | ```haskell 344 | data PhoneType = HomePhone | WorkPhone | CellPhone | OtherPhone 345 | ``` 346 | 347 | これらの関数は住所録の項目を表す`Person`を構築するのに使います。例えば、`Data.AddressBook`には次のような値が定義されています。 348 | 349 | ```haskell 350 | examplePerson :: Person 351 | examplePerson = 352 | person "John" "Smith" 353 | (address "123 Fake St." "FakeTown" "CA") 354 | [ phoneNumber HomePhone "555-555-5555" 355 | , phoneNumber CellPhone "555-555-0000" 356 | ] 357 | ``` 358 | 359 | `PSCi`でこれらの値使ってみましょう(結果は整形されています)。 360 | 361 | ```text 362 | > import Data.AddressBook 363 | > examplePerson 364 | Person 365 | { firstName: "John", 366 | , lastName: "Smith", 367 | , address: Address 368 | { street: "123 Fake St." 369 | , city: "FakeTown" 370 | , state: "CA" 371 | }, 372 | , phones: [ PhoneNumber 373 | { type: HomePhone 374 | , number: "555-555-5555" 375 | } 376 | , PhoneNumber 377 | { type: CellPhone 378 | , number: "555-555-0000" 379 | } 380 | ] 381 | } 382 | ``` 383 | 384 | 前の章では型`Person`のデータ構造を検証するのに`Either String`関手をどのように使うかを見ました。例えば、データ構造の2つの名前を検証する関数が与えられたとき、データ構造全体を次のように検証することができます。 385 | 386 | ```haskell 387 | nonEmpty :: String -> Either String Unit 388 | nonEmpty "" = Left "Field cannot be empty" 389 | nonEmpty _ = Right unit 390 | 391 | validatePerson :: Person -> Either String Person 392 | validatePerson (Person o) = 393 | person <$> (nonEmpty o.firstName *> pure o.firstName) 394 | <*> (nonEmpty o.lastName *> pure o.lastName) 395 | <*> pure o.address 396 | <*> pure o.phones 397 | ``` 398 | 399 | 最初の2行では`nonEmpty`関数を使って空文字列でないことを検証しています。もし入力が空なら`nonEMpty`はエラーを返し(`Left`構築子で示されています)、そうでなければ`Right`構築子を使って空の値(`unit`)を正常に返します。2つの検証を実行し、右辺の検証の結果を返すことを示す連鎖演算子`*>`を使っています。ここで、入力を変更せずに返す検証器として右辺では単に`pure`を使っています。 400 | 401 | 最後の2行では何の検証も実行せず、単に`address`フィールドと`phones`フィールドを残りの引数として`person`関数へと提供しています。 402 | 403 | この関数は`PSCi`でうまく動作するように見えますが、以前見たような制限があります。 404 | 405 | ```haskell 406 | > validatePerson $ person "" "" (address "" "" "") [] 407 | (Left "Field cannot be empty") 408 | ``` 409 | 410 | `Either String`Applicative関手は遭遇した最初のエラーだけを返します。でもこの入力では、名前の不足と姓の不足という2つのエラーがわかるようにしたくなるでしょう。 411 | 412 | `purescript-validation`ライブラリは別のApplicative関手も提供されています。これは単に`V`と呼ばれていて、何らかの**半群**(Semigroup)でエラーを返す機能があります。たとえば、`V (Array String)`を使うと、新しいエラーを配列の最後に連結していき、`String`の配列をエラーとして返すことができます。 413 | 414 | `Data.Validation`モジュールは`Data.AddressBook`モジュールのデータ構造を検証するために`V (Array String)`Applicative関手を使っています。 415 | 416 | `Data.AddressBook.Validation`モジュールにある検証の例としては次のようになります。 417 | 418 | ```haskell 419 | type Errors = Array String 420 | 421 | nonEmpty :: String -> String -> V Errors Unit 422 | nonEmpty field "" = invalid ["Field '" <> field <> "' cannot be empty"] 423 | nonEmpty _ _ = pure unit 424 | 425 | lengthIs :: String -> Number -> String -> V Errors Unit 426 | lengthIs field len value | S.length value /= len = 427 | invalid ["Field '" <> field <> "' must have length " <> show len] 428 | lengthIs _ _ _ = 429 | pure unit 430 | 431 | validateAddress :: Address -> V Errors Address 432 | validateAddress (Address o) = 433 | address <$> (nonEmpty "Street" o.street *> pure o.street) 434 | <*> (nonEmpty "City" o.city *> pure o.city) 435 | <*> (lengthIs "State" 2 o.state *> pure o.state) 436 | ``` 437 | 438 | `validateAddress`は`Address`を検証します。`street`と`city`が空でないかどうか、`state`の文字列の長さが2であるかどうかを検証します。 439 | 440 | `nonEmpty`と`lengthIs`の2つの検証関数はいずれも、`Data.Validation`モジュールで提供されている`invalid`関数をエラーを示すために使っていることに注目してください。`Array String`半群を扱っているので、`invalid`は引数として文字列の配列を取ります。 441 | 442 | `PSCi`でこの関数を使ってみましょう。 443 | 444 | ```text 445 | > import Data.AddressBook 446 | > import Data.AddressBook.Validation 447 | 448 | > validateAddress $ address "" "" "" 449 | (Invalid [ "Field 'Street' cannot be empty" 450 | , "Field 'City' cannot be empty" 451 | , "Field 'State' must have length 2" 452 | ]) 453 | 454 | > validateAddress $ address "" "" "CA" 455 | (Invalid [ "Field 'Street' cannot be empty" 456 | , "Field 'City' cannot be empty" 457 | ]) 458 | ``` 459 | 460 | これで、すべての検証エラーの配列を受け取ることができるようになりました。 461 | 462 | ## 正規表現検証器 463 | 464 | `validatePhoneNumber`関数では引数の形式を検証するために正規表現を使っています。重要なのは`matches`検証関数で、この関数は`Data.String.Regex`モジュールのて定義されている`Regex`を使って入力を検証しています。 465 | 466 | ```haskell 467 | matches :: String -> R.Regex -> String -> V Errors Unit 468 | matches _ regex value | R.test regex value = 469 | pure unit 470 | matches field _ _ = 471 | invalid ["Field '" <> field <> "' did not match the required format"] 472 | ``` 473 | 474 | 繰り返しになりますが、`pure`は常に成功する検証を表しており、エラーの配列の伝達には`invalid`が使われています。 475 | 476 | これまでと同じような感じで、`validatePhoneNumber`は`matches`関数から構築されています。 477 | 478 | ```haskell 479 | validatePhoneNumber :: PhoneNumber -> V Errors PhoneNumber 480 | validatePhoneNumber (PhoneNumber o) = 481 | phoneNumber <$> pure o."type" 482 | <*> (matches "Number" phoneNumberRegex o.number *> pure o.number) 483 | ``` 484 | 485 | また、`PSCi`でいろいろな有効な入力や無効な入力に対して、この検証器を実行してみてください。 486 | 487 | ```text 488 | > validatePhoneNumber $ phoneNumber HomePhone "555-555-5555" 489 | Valid (PhoneNumber { type: HomePhone, number: "555-555-5555" }) 490 | 491 | > validatePhoneNumber $ phoneNumber HomePhone "555.555.5555" 492 | Invalid (["Field 'Number' did not match the required format"]) 493 | ``` 494 | 495 | ## 演習 496 | 497 | 1. (簡単) 正規表現の検証器を使って、`Address`型の`state`フィールドが2文字のアルファベットであることを確かめてください。**ヒント**:`phoneNumberRegex`のソースコードを参照してみましょう。 498 | 499 | 1. (やや難しい)`matches`検証器を使って、文字列に全く空白が含まれないことを検証する検証関数を​​書いてください。この関数を使って、適切な場合に`nonEmpty`を置き換えてください。 500 | 501 | ## Traversable関手 502 | 503 | 残った検証器は、これまで見てきた検証器を組み合わせて`Person`全体を検証する`validatePerson`です。 504 | 505 | ```haskell 506 | arrayNonEmpty :: forall a. String -> Array a -> V Errors Unit 507 | arrayNonEmpty field [] = 508 | invalid ["Field '" <> field <> "' must contain at least one value"] 509 | arrayNonEmpty _ _ = 510 | pure unit 511 | 512 | validatePerson :: Person -> V Errors Person 513 | validatePerson (Person o) = 514 | person <$> (nonEmpty "First Name" o.firstName *> 515 | pure o.firstName) 516 | <*> (nonEmpty "Last Name" o.lastName *> 517 | pure o.lastName) 518 | <*> validateAddress o.address 519 | <*> (arrayNonEmpty "Phone Numbers" o.phones *> 520 | traverse validatePhoneNumber o.phones) 521 | ``` 522 | 523 | ここに今まで見たことのない興味深い関数がひとつあります。最後の行で使われている`traverse`です。 524 | 525 | `traverse`は`Data.Traversable`モジュールの`Traversable`型クラスで定義されています。 526 | 527 | ```haskell 528 | class (Functor t, Foldable t) <= Traversable t where 529 | traverse :: forall a b f. Applicative f => (a -> f b) -> t a -> f (t b) 530 | sequence :: forall a f. Applicative f => t (f a) -> f (t a) 531 | ``` 532 | 533 | `Traversable`は**Traversable関手**の型クラスを定義します。これらの関数の型は少し難しそうに見えるかもしれませんが、`validatePerson`は良いきっかけとなる例です。 534 | 535 | すべてのTraversable関手は`Functor`と`Foldable`のどちらでもあります(**Foldable 関手**は構造をひとつの値へとまとめる、畳み込み操作を提供する型構築子であったことを思い出してください)。それ加えて、`Traversable`関手はその構造に依存した副作用のあつまりを連結する機能を提供します。 536 | 537 | 複雑そうに聞こえるかもしれませんが、配列の場合に特殊化して簡単に考えてみましょう。配列型構築子は`Traversable`である、つまり次のような関数が存在するということです。 538 | 539 | ```haskell 540 | traverse :: forall a b f. Applicative f => (a -> f b) -> Array a -> f (Array b) 541 | ``` 542 | 543 | 直感的には、Applicative関手`f`と、型`a`の値をとり型`b`の値を返す(`f`で追跡される副作用を持つ)関数が与えられたとき、型`[a]`の配列の要素それぞれにこの関数を適用し、型`[b]`の(`f`で追跡される副作用を持つ)結果を得ることができます。 544 | 545 | まだよくわからないでしょうか。それでは、更に`f`を`V Errors`Applicative関手に特殊化して考えてみましょう。`traversable`が次のような型の関数だとしましょう。 546 | 547 | ```haskell 548 | traverse :: forall a b. (a -> V Errors b) -> Array a -> V Errors (Array b) 549 | ``` 550 | 551 | この型シグネチャは、型`a`についての検証関数`f`があれば、`traverse f`は型`Array a`の配列についての検証関数であるということを言っています。これはまさに今必要になっている`Person`データ構造体の`phones`フィールドを検証する検証器そのものです!それぞれの要素が成功するかどうかを検証する検証関数を作るために、`validatePhoneNumber`を`traverse`へ渡しています。 552 | 553 | 一般に、`traverse`はデータ構造の要素をひとつづつ辿っていき、副作用のある計算を実行して結果を累積します。 554 | 555 | `Traversable`のもう一つの関数、`sequence`の型シグネチャには見覚えがあるかもしれません。 556 | 557 | ```haskell 558 | sequence :: forall a f. (Applicative m) => t (f a) -> f (t a) 559 | ``` 560 | 561 | 実際、先ほど書いた`combineArray`関数は`Traversable`型の`sequence`関数が特殊化されたものに過ぎません。`t`を配列型構築子として、`combineArray`関数の型をもう一度考えてみましょう。 562 | 563 | ```haskell 564 | combineList :: forall f a. Applicative f => List (f a) -> f (List a) 565 | ``` 566 | 567 | `Traversable`関手は、作用のある計算の集合を集めてその作用を連鎖させるという、データ構造走査の考え方を把握できるようにするものです。実際、`sequence`と`traversable`は`Traversable`を定義するのにどちらも同じくらい重要です。これらはお互いが互いを利用して実装することができます。これについては興味ある読者への演習として残しておきます。 568 | 569 | 配列の`Traversable`インスタンスは`Data.Traversable`モジュールで与えられています。`traverse`の定義は次のようになっています。 570 | 571 | ```haskell 572 | -- traverse :: forall a b f. Applicative f => (a -> f b) -> List a -> f (List b) 573 | traverse _ Nil = pure Nil 574 | traverse f (Cons x xs) = Cons <$> f x <*> traverse f xs 575 | ``` 576 | 577 | 入力が空の配列のときには、単に`pure`を使って空の配列を返すことができます。配列が空でないときは、関数`f`を使うと先頭の要素から型`f b`の計算を作成することができます。また、配列の残りに対して`traverse`を再帰的に呼び出すことができます。最後に、Applicative関手`f`までcons演算子`(:)`を持ち上げて、2つの結果を組み合わせます。 578 | 579 | Traversable関手の例はただの配列以外にもあります。以前に見た`Maybe`型構築子も`Traversable`のインスタンスを持っています。`PSCi`で試してみましょう。 580 | 581 | ```text 582 | > import Data.Maybe 583 | > import Data.Traversable 584 | 585 | > traverse (nonEmpty "Example") Nothing 586 | (Valid Nothing) 587 | 588 | > traverse (nonEmpty "Example") (Just "") 589 | (Invalid ["Field 'Example' cannot be empty"]) 590 | 591 | > traverse (nonEmpty "Example") (Just "Testing") 592 | (Valid (Just unit)) 593 | ``` 594 | 595 | これらの例では、`Nothing`の値の走査は検証なしで`Nothing`の値を返し、`Just x`を走査すると`x`を検証するのにこの検証関数が使われるということを示しています。つまり、`traverse`は型`a`についての検証関数をとり、`Maybe a`についての検証関数を返すのです。 596 | 597 | 他にも、何らかの型`a`についての`Tuple a`や`Either a`や、連結リストの型構築子`List`といったTraversable関手があります。一般的に、「コンテナ」のようなデータの型構築子は大抵`Traversable`インスタンスを持っています。例として、演習では二分木の型の`Traversable`インスタンスを書くようになっています。 598 | 599 | ## 演習 600 | 601 | 1. (やや難しい) 左から右へと副作用を連鎖させる、次のような二分木データ構造についての`Traversable`インスタンスを書いてください。 602 | 603 | ```haskell 604 | data Tree a = Leaf | Branch (Tree a) a (Tree a) 605 | ``` 606 | これは木の走査の順序に対応しています。行きがけ順の走査についてはどうでしょうか。帰りがけ順では? 607 | 608 | 1. (やや難しい)`Data.Maybe`を使って`Person`の`address`フィールドを省略可能になるようにコードを変更してください。**ヒント**:`traverse`を使って型`Maybe a`のフィールドを検証してみましょう。 609 | 610 | 1. (難しい)`traverse`を使って`sequence`を書いてみましょう。また、`sequence`を使って`traverse`を書けるでしょうか? 611 | 612 | ## Applicative関手による並列処理 613 | 614 | これまでの議論では、Applicative関手がどのように「副作用を結合」させるかを説明するときに、「結合」(combine)という単語を選びました。しかしながら、これらのすべての例において、Applicative関手は作用を「連鎖」(sequence)させる、というように言っても同じく妥当です。`Traverse`関手はデータ構造に従って作用を順番に結合させる`sequence`関数を提供する、という直感的理解とこれは一致するでしょう。 615 | 616 | しかし一般には、Applicative関手はこれよりももっと一般的です。Applicative関手の規則は、その計算を実行する副作用にどんな順序付けも強制しません。実際、並列に副作用を実行するためのApplicative関手というものは妥当になりえます。 617 | 618 | たとえば、`V`検証関手はエラーの**配列**を返しますが、その代わりに`Set`半群を選んだとしてもやはり正常に動き、このときどんな順序でそれぞれの検証器を実行しても問題はありません。データ構造に対して並列にこれを実行することさえできるのです! 619 | 620 | 別の例とし、`purescript-parallel`パッケージは、並列計算をサポートする`Parallel`型クラスを与えます。**非同期計算**を表現する型構築子`parallel`は、並列に結果を計算する`Applicative`インスタンスを持つことができます。 621 | 622 | ```haskell 623 | f <$> parallel computation1 624 | <*> parallel computation2 625 | ``` 626 | 627 | この計算は、`computation1`と`computation2`を非同期に使って値を計算を始めるでしょう。そして両方の結果の計算が終わった時に、関数`f`を使ってひとつの結果へと結合するでしょう。 628 | 629 | この考え方の詳細は、本書の後半で**コールバック地獄**の問題に対してApplicative関手を応用するときに見ていきます。 630 | 631 | Applicative関手は並列に結合されうる副作用を捕捉する自然な方法です。 632 | 633 | ## まとめ 634 | 635 | この章では新しい考え方をたくさん扱いました。 636 | 637 | - 関数適用の概念を副作用の考え方を表現する型構築子へと一般化する、**Applicative関手**の概念を導入しました。 638 | - データ構造の検証という課題にApplicative関手がどのような解決策を与えるか、単一のエラーの報告からデータ構造を横断するすべてのエラーの報告へ変換できるApplicative関手を見てきました。 639 | - 要素が副作用を持つ値の結合に使われることのできるコンテナである**Traversable関手**の考え方を表現する、`Traversable`型クラス導入しました。 640 | 641 | Applicative関手は多くの問題に対して優れた解決策を与える興味深い抽象化です。本書を通じて何度も見ることになるでしょう。今回は、**どうやって**検証を行うかではなく、**何を**検証器が検証すべきなのかを定義することを可能にする、宣言的なスタイルで書く手段をApplicative関手は提供しました。一般に、Applicative関手は**領域特化言語**の設計のための便利な道具になります。 642 | 643 | 次の章では、これに関連する**モナド**という型クラスについて見ていきましょう。 644 | 645 | -------------------------------------------------------------------------------- /src/chapter09.md: -------------------------------------------------------------------------------- 1 | # キャンバスグラフィックス 2 | 3 | ## この章の目標 4 | 5 | この章のコード例では、PureScriptでHTML5のCanvas APIを使用して2Dグラフィックスを生成する `purescript-canvas`パッケージに焦点をあててコードを拡張していきます。 6 | 7 | ## プロジェクトの準備 8 | 9 | このモジュールのプロジェクトでは、以下のBowerの依存関係が新しく追加されています。 10 | 11 | - `purescript-canvas`- HTML5のCanvas APIのメソッドの型が定義されています。 12 | - `purescript-refs`- **大域的な変更可能領域への参照**を扱うための副作用を提供しています。 13 | 14 | この章のソースコードは、それぞれに `main`メソッドが定義されている複数のモ​​ジュールへと分割されています。この章の節の内容はそれぞれ異なるファイルで実装されており、それぞれの節で対応するファイルの `main`メソッドを実行できるように、Pulpビルドコマンドを変更することで `Main`モジュールが変更できるようになっています。 15 | 16 | HTMLファイル `html/index.html`には、各例で使用される単一の `canvas`要素、およびコンパイルされたPureScriptコードを読み込む `script`要素が含まれています。各節のコードをテストするには、ブラウザでこのHTMLファイルを開いてください。 17 | 18 | ## 単純な図形 19 | 20 | `Example/Rectangle.purs`ファイルにはキャンバスの中心に青い四角形をひとつ描画するという簡単な例が含まれています。このモジュールは、 `Control.Monad.Eff`モジュールと、Canvas APIを扱うための `Eff`モナドのアクションが定義されている `Graphics.Canvas`モジュールをインポートします。 21 | 22 | 他のモジュールでも同様ですが、 `main`アクションは最初に `getCanvasElementById`アクションを使ってCanvasオブジェクトへの参照を取得しています。また、 `getContext2D`アクションを使ってキャンバスの2Dレンダリングコンテキストを参照しています。 23 | 24 | ```haskell 25 | main = void $ unsafePartial do 26 | Just canvas <- getCanvasElementById "canvas" 27 | ctx <- getContext2D canvas 28 | ``` 29 | 30 | **注意**:この`unsafePartial`の呼び出しは必須です。これは `getCanvasElementById`の結果のパターンマッチングが部分的で、`Just`値構築子だけと照合するためです。ここではこれで問題ありませんが、実際の製品のコードではおそらく`Nothing`値構築子と照合させ、適切なエラーメッセージを提供したほうがよいでしょう。 31 | 32 | これらのアクションの型は、`PSCi`を使うかドキュメントを見ると確認できます。 33 | 34 | ```haskell 35 | getCanvasElementById :: forall eff. String -> 36 | Eff (canvas :: Canvas | eff) (Maybe CanvasElement) 37 | 38 | getContext2D :: forall eff. CanvasElement -> 39 | Eff (canvas :: Canvas | eff) Context2D 40 | ``` 41 | 42 | `CanvasElement`と `Context2D`は `Graphics.Canvas`モジュールで定義されている型です。このモジュールでは、モジュール内のすべてのアクションで使用されている `Canvas`作用も定義されています。 43 | 44 | グラフィックスコンテキスト `ctx`は、キャンバスの状態を管理し、プリミティブな図形を描画したり、スタイルや色を設定したり、座標変換を適用するためのメソッドを提供しています。 45 | 46 | `ctx`の取得に続けて、 `setFillStyle`アクションを使って塗りのスタイルを青一色の塗りつぶしに設定しています。 47 | 48 | ```haskell 49 | setFillStyle "#0000FF" ctx 50 | ``` 51 | 52 | `setFillStyle`アクションがグラフィックスコンテキストを引数として取っていることに注意してください。これは `Graphics.Canvas`で共通のパターンです。 53 | 54 | 最後に、 `fillPath`アクションを使用して矩形を塗りつぶしています。 `fillPath`は次のような型を持っています。 55 | 56 | ```haskell 57 | fillPath :: forall eff a. Context2D -> 58 | Eff (canvas :: Canvas | eff) a -> 59 | Eff (canvas :: Canvas | eff) a 60 | ``` 61 | 62 | `fillPath`はグラフィックスコンテキストとレンダリングするパスを構築する別のアクションを引数にとります。パスは `rect`アクションを使うと構築することができます。 `rect`はグラフィックスコンテキストと矩形の位置及びサイズを格納するレコードを引数にとります。 63 | 64 | ```haskell 65 | fillPath ctx $ rect ctx 66 | { x: 250.0 67 | , y: 250.0 68 | , w: 100.0 69 | , h: 100.0 70 | } 71 | ``` 72 | 73 | mainモジュールの名前として`Example.Rectangle`を指定して、この長方形のコード例をビルドしましょう。 74 | 75 | ```text 76 | $ mkdir dist/ 77 | $ pulp build -O --main Example.Rectangle --to dist/Main.js 78 | ``` 79 | 80 | それでは `html/index.html`ファイルを開き、このコードによってキャンバスの中央に青い四角形が描画されていることを確認してみましょう。 81 | 82 | ## 行多相を利用する 83 | 84 | パスを描画する方法は他にもあります。 `arc`関数は円弧を描画します。 `moveTo`関数、 `lineTo`関数、 `closePath`関数は細かい線分を組み合わせることでパスを描画します。 85 | 86 | `Shapes.purs`ファイルでは長方形と円弧セグメント、三角形の、3つの図形を描画しています。 87 | 88 | `rect`関数は引数としてレコードをとることを見てきました。実際には、長方形のプロパティは型同義語で定義されています。 89 | 90 | ```haskell 91 | type Rectangle = { x :: Number 92 | , y :: Number 93 | , w :: Number 94 | , h :: Number 95 | } 96 | ``` 97 | 98 | `x`と `y`プロパティは左上隅の位置を表しており、 `w`と `h`のプロパティはそれぞれ幅と高さを表しています。 99 | 100 | `arc`関数に以下のような型を持つレコードを渡して呼び出すと、円弧を描画することができます。 101 | 102 | ```haskell 103 | type Arc = { x :: Number 104 | , y :: Number 105 | , r :: Number 106 | , start :: Number 107 | , end :: Number 108 | } 109 | ``` 110 | 111 | ここで、 `x`と `y`プロパティは弧の中心、 `r`は半径、 `start`と `end`は弧の両端の角度を弧度法で表しています。 112 | 113 | たとえば、次のコードは中心 `(300、300)`、半径 `50`の円弧を塗りつぶします。 114 | 115 | ```haskell 116 | fillPath ctx $ arc ctx 117 | { x : 300.0 118 | , y : 300.0 119 | , r : 50.0 120 | , start : Math.pi * 5.0 / 8.0 121 | , end : Math.pi * 2.0 122 | } 123 | ``` 124 | 125 | `Number`型の `x`と `y`というプロパティが `Rectangle`レコード型と `Arc`レコード型の両方に含まれていることに注意してください。どちらの場合でもこの組は点を表しています。これは、いずれのレコード型にも適用できる、行多相な関数を書くことができることを意味します。 126 | 127 | たとえば、 `Shapes`モジュールでは `x`と `y`のプロパティを変更し図形を並行移動する `translate`関数を定義されています。 128 | 129 | ```haskell 130 | translate :: forall r. Number -> Number -> 131 | { x :: Number, y :: Number | r } -> 132 | { x :: Number, y :: Number | r } 133 | translate dx dy shape = shape 134 | { x = shape.x + dx 135 | , y = shape.y + dy 136 | } 137 | ``` 138 | 139 | この行多相型に注目してください。これは `triangle`が `x`と `y`というプロパティと、**それに加えて他の任意のプロパティ**を持ったどんなレコードでも受け入れるということを言っています。 `x`フィールドと `y`フィールドは更新されますが、残りのフィールドは変更されません。 140 | 141 | これは**レコード更新構文**の例です。 `shape { ... }`という式は、 `shape`を元にして、括弧の中で指定されたように値が更新されたフィールドを持つ新たなレコードを作ります。波括弧の中の式はレコードリテラルのようなコロンではなく、等号でラベルと式を区切って書くことに注意してください。 142 | 143 | `Shapes`の例からわかるように、 `translate`関数は `Rectangle`レコードと `Arc`レコード双方に対して使うことができます。 144 | 145 | `Shape`の例で描画される3つめの型は線分ごとのパスです。対応するコードは次のようになります。 146 | 147 | ```haskell 148 | setFillStyle "#FF0000" ctx 149 | 150 | fillPath ctx $ do 151 | moveTo ctx 300.0 260.0 152 | lineTo ctx 260.0 340.0 153 | lineTo ctx 340.0 340.0 154 | closePath ctx 155 | ``` 156 | 157 | ここでは3つの関数が使われています。 158 | 159 | - `moveTo`はパスの現在位置を指定された座標へ移動させます。 160 | - `lineTo`は現在の位置と指定された座標の間に線分を描画し、現在の位置を更新します。 161 | - `closePath`は開始位置と現在位置を結ぶ線分を描画し、パスを閉じます。 162 | 163 | このコード片を実行すると、二等辺三角形を塗りつぶされます。 164 | 165 | mainモジュールとして`Example.Shapes`を指定して、この例をビルドしましょう。 166 | 167 | ```text 168 | $ pulp build -O --main Example.Shapes --to dist/Main.js 169 | ``` 170 | 171 | そしてもう一度 `html/index.html`を開き、結果を確認してください。キャンバスに3つの異なる図形が描画されるはずです。 172 | 173 | ## 演習 174 | 175 | 1. (簡単) これまでの例のそれぞれについて、 `strokePath`関数や `setStrokeStyle`関数を使ってみましょう。 176 | 177 | 1. (簡単) 関数の引数の内部でdo記法ブロックを使うと、 `fillPath`関数と `strokePath`関数で共通のスタイルを持つ複雑なパスを描画することができます。同じ `fillPath`呼び出しで隣り合った2つの矩形を描画するように、 `Rectangle`のコード例を変更してみてください。線分と円弧を組み合わせてを、円の扇形を描画してみてください。 178 | 179 | 1. (やや難しい) 次のような2次元の点を表すレコードが与えられたとします。 180 | 181 | ```haskell 182 | type Point = { x :: Number, y :: Number } 183 | ``` 184 | 185 | 多数の点からなる閉じたパスを描く関数 `renderPath`書いてください。 186 | 187 | ```haskell 188 | renderPath :: forall eff. Context2D -> Array Point -> 189 | Eff (canvas :: Canvas | eff) Unit 190 | ``` 191 | 192 | 次のような関数を考えます。 193 | 194 | ```haskell 195 | f :: Number -> Point 196 | ``` 197 | 198 | この関数は引数として `1`から `0`の間の `Number`をとり、 `Point`を返します。 `renderPath`関数を利用して関数 `f`のグラフを描くアクションを書いてください。そのアクションは有限個の点を `f`からサンプリングすることによって近似しなければなりません。 199 | 200 | 関数 `f`を変更し、異なるパスが描画されることを確かめてください。 201 | 202 | ## 無作為に円を描く 203 | 204 | `Example/Random.purs`ファイルには2種類の異なる副作用が混在した `Eff`モナドを使う例が含まれています。この例では無作為に生成された円をキャンバスに100個描画します。 205 | 206 | `main`アクションはこれまでのようにグラフィックスコンテキストへの参照を取得し、ストロークと塗りつぶしスタイルを設定します。 207 | 208 | ```haskell 209 | setFillStyle "#FF0000" ctx 210 | setStrokeStyle "#000000" ctx 211 | ``` 212 | 213 | 次のコードでは `forE`アクションを使って `0`から `100`までの整数について繰り返しをしています。 214 | 215 | ```haskell 216 | for_ (1 .. 100) \_ -> do 217 | ``` 218 | 219 | これらの数は `0`から `1`の間に無作為に分布しており、それぞれ `x`座標、 `y`座標、半径 `r`を表しています。 220 | 221 | ```haskell 222 | x <- random 223 | y <- random 224 | r <- random 225 | ``` 226 | 227 | 次のコードでこれらの変数に基づいて `Arc`を作成し、最後に現在のスタイルに従って円弧の塗りつぶしと線描が行われます。 228 | 229 | ```haskell 230 | let path = arc ctx 231 | { x : x * 600.0 232 | , y : y * 600.0 233 | , r : r * 50.0 234 | , start : 0.0 235 | , end : Math.pi * 2.0 236 | } 237 | fillPath ctx path 238 | strokePath ctx path 239 | ``` 240 | 241 | `forE`に渡された関数が正しい型を持つようにするため、最後の行は必要であることに注意してください。 242 | 243 | mainモジュールとして`Example.Random`を指定して、この例をビルドしましょう。 244 | 245 | ```text 246 | $ pulp build -O --main Example.Random --to dist/Main.js 247 | ``` 248 | 249 | `html/index.html`を開いて、結果を確認してみましょう。 250 | 251 | ## 座標変換 252 | 253 | キャンバスは簡単な図形を描画するだけのものではありません。キャンバスは変換行列を扱うことができ、図形は描画の前に形状を変形してから描画されます。図形は平行移動、回転、拡大縮小、および斜め変形することができます。 254 | 255 | `purescript-canvas`ライブラリではこれらの変換を以下の関数で提供しています。 256 | 257 | ```haskell 258 | translate :: forall eff. TranslateTransform -> Context2D 259 | -> Eff (canvas :: Canvas | eff) Context2D 260 | rotate :: forall eff. Number -> Context2D 261 | -> Eff (canvas :: Canvas | eff) Context2D 262 | scale :: forall eff. ScaleTransform -> Context2D 263 | -> Eff (canvas :: Canvas | eff) Context2D 264 | transform :: forall eff. Transform -> Context2D 265 | -> Eff (canvas :: Canvas | eff) Context2D 266 | ``` 267 | 268 | `translate`アクションは `TranslateTransform`レコードのプロパティで指定した大きさだけ平行移動を行います。 269 | 270 | `rotate`アクションは最初の引数で指定されたラジアンの値に応じて原点を中心とした回転を行います。 271 | 272 | `scale`アクションは原点を中心として拡大縮小します。 `ScaleTransform`レコードは `X`軸と `y`軸に沿った拡大率を指定するのに使います。 273 | 274 | 最後の `transform`はこの4つのうちで最も一般的なアクションです。このアクションは行列に従ってアフィン変換を行います。 275 | 276 | これらのアクションが呼び出された後に描画される図形は、自動的に適切な座標変換が適用されます。 277 | 278 | 実際には、これらの関数のそれぞれの作用は、コンテキストの現在の変換行列に対して変換行列を**右から乗算**していきます。つまり、もしある作用の変換をしていくと、その作用は実際には逆順に適用されていきます。次のような座標変換のアクションを考えてみましょう。 279 | 280 | ```haskell 281 | transformations ctx = do 282 | translate { translateX: 10.0, translateY: 10.0 } ctx 283 | scale { scaleX: 2.0, scaleY: 2.0 } ctx 284 | rotate (Math.pi / 2.0) ctx 285 | 286 | renderScene 287 | ``` 288 | 289 | このアクションの作用では、まずシーンが回転され、それから拡大縮小され、最後に平行移動されます。      290 | 291 | ## コンテキストの保存 292 | 293 | 一般的な使い方としては、変換を適用してシーンの一部をレンダリングし、それからその変換を元に戻します。 294 | 295 | Canvas APIにはキャンバスの状態の**スタック**を操作する `save`と `restore`メソッドが備わっています。 `purescript-canvas`ではこの機能を次のような関数でラップしています。 296 | 297 | ```haskell 298 | save :: forall eff. Context2D -> Eff (canvas :: Canvas | eff) Context2D 299 | restore :: forall eff. Context2D -> Eff (canvas :: Canvas | eff) Context2D 300 | ``` 301 | 302 | `save`アクションは現在のコンテキストの状態(現在の変換行列や描画スタイル)をスタックにプッシュし、 `restore`アクションはスタックの一番上の状態をポップし、コンテキストの状態を復元します。 303 | 304 | これらのアクションにより、現在の状態を保存し、いろいろなスタイルや変換を適用し、プリミティブを描画し、最後に元の変換と状態を復元することが可能になります。例えば、次の関数はいくつかのキャンバスアクションを実行しますが、その前に回転を適用し、そのあとに変換を復元します。 305 | 306 | ```haskell 307 | rotated ctx render = do 308 | save ctx 309 | rotate Math.pi ctx 310 | render 311 | restore ctx 312 | ``` 313 | 314 | こういったよくある使いかたの高階関数を利用した抽象化として、 `purescript-canvas`ライブラリでは元のコンテキスト状態を維持しながらいくつかのキャンバスアクションを実行する `withContext`関数が提供されています。 315 | 316 | ```haskell 317 | withContext :: forall eff a. Context2D -> 318 | Eff (canvas :: Canvas | eff) a -> 319 | Eff (canvas :: Canvas | eff) a 320 | ``` 321 | 322 | `withContext`を使うと、先ほどの `rotated`関数を次のように書き換えることができます。 323 | 324 | ```haskell 325 | rotated ctx render = withContext ctx do 326 | rotate Math.pi ctx 327 | render 328 | ``` 329 | 330 | ## 大域的な変更可能状態 331 | 332 | この節では `purescript-refs`パッケージを使って `Eff`モナドの別の作用について実演してみます。 333 | 334 | `Control.Monad.Eff.Ref`モジュールでは大域的に変更可能な参照のための型構築子、および関連する作用を提供します。 335 | 336 | ```text 337 | > import Control.Monad.Eff.Ref 338 | 339 | > :kind Ref 340 | Type -> Type 341 | 342 | > :kind REF 343 | Control.Monad.Eff.Effect 344 | ``` 345 | 346 | 型 `RefVal a`の値は型 `a`値を保持する変更可能な領域への参照で、前の章で見た `STRef h a`によく似ています。その違いは、 `ST`作用は `runST`を用いて除去することができますが、 `Ref`作用はハンドラを提供しないということです。 `ST`は安全に局所的な状態変更を追跡するために使用されますが、 `Ref`は大域的な状態変更を追跡するために使用されます。そのため、 `Ref`は慎重に使用する必要があります。 347 | 348 | `Example/Refs.purs`ファイルには `canvas`要素上のマウスクリックを追跡するのに `Ref`作用を使用する例が含まれています。 349 | 350 | このコー​​ドでは最初に `newRef`アクションを使って値 `0`で初期化された領域への新しい参照を作成しています。 351 | 352 | ```haskell 353 | clickCount <- newRef 0 354 | ``` 355 | 356 | クリックイベントハンドラの内部では、 `modifyRef`アクションを使用してクリック数を更新しています。 357 | 358 | ```haskell 359 | modifyRef clickCount (\count -> count + 1) 360 | ``` 361 | 362 | `readRef`アクションは新しいクリック数を読み取るために使われています。 363 | 364 | ```haskell 365 | count <- readRef clickCount 366 | ``` 367 | 368 | `render`関数では、クリック数に応じて変換を矩形に適用しています。 369 | 370 | ```haskell 371 | withContext ctx do 372 | let scaleX = Math.sin (toNumber count * Math.pi / 4.0) + 1.5 373 | let scaleY = Math.sin (toNumber count * Math.pi / 6.0) + 1.5 374 | 375 | translate { translateX: 300.0, translateY: 300.0 } ctx 376 | rotate (toNumber count * Math.pi / 18.0) ctx 377 | scale { scaleX: scaleX, scaleY: scaleY } ctx 378 | translate { translateX: -100.0, translateY: -100.0 } ctx 379 | 380 | fillPath ctx $ rect ctx 381 | { x: 0.0 382 | , y: 0.0 383 | , w: 200.0 384 | , h: 200.0 385 | } 386 | ``` 387 | 388 | このアクションでは元の変換を維持するために `withContext`を使用しており、それから続く変換を順に適用しています(変換が下から上に適用されることを思い出してください)。 389 | 390 | - 中心が原点に来るように、矩形を `(-100, -100)`平行移動します。 391 | - 矩形を原点を中心に拡大縮小します。 392 | - 矩形を原点を中心に `10`度の倍数だけ回転します。 393 | - 中心がキャンバスの中心に位置するように長方形を `(300、300)`だけ平行移動します。 394 | 395 | このコード例をビルドしてみましょう。 396 | 397 | ```text 398 | $ pulp build -O --main Example.Refs --to dist/Main.js 399 | ``` 400 | 401 | `html/index.html`ファイルを開いてみましょう。何度かキャンバスをクリックすると、キャンバスの中心の周りを回転する緑の四角形が表示されるはずです。 402 | 403 | ## 演習 404 | 405 | 1. (簡単) パスの線描と塗りつぶしを同時に行う高階関数を書いてください。その関数を使用して `Random.purs`例を書きなおしてください。 406 | 407 | 1. (やや難しい)`Random`作用と `DOM`作用を使用して、マウスがクリックされたときにキャンバスに無作為な位置、色、半径の円を描画するアプリケーションを作成してください。 408 | 409 | 1. (やや難しい) シーンを指定された座標を中心に回転する関数を書いてください。**ヒント**:最初にシーンを原点まで平行移動しましょう。 410 | 411 | ## L-Systems 412 | 413 | この章の最後の例として、 `purescript-canvas`パッケージを使用して**L-systems**(Lindenmayer systems)を描画する関数を記述します。 414 | 415 | L-Systemsは**アルファベット**、つまり初期状態となるアルファベットの文字列と、**生成規則**の集合で定義されています。各生成規則は、アルファベットの文字をとり、それを置き換える文字の配列を返します。この処理は文字の初期配列から始まり、複数回繰り返されます。 416 | 417 | もしアルファベットの各文字がキャンバス上で実行される命令と対応付けられていれば、その指示に順番に従うことでL-Systemsを描画することができます。 418 | 419 | たとえば、アルファベットが文字 `L`(左回転)、 `R`(右回転)、 `F`(前進)で構成されていたとします。また、次のような生成規則を定義します。 420 | 421 | ```text 422 | L -> L 423 | R -> R 424 | F -> FLFRRFLF 425 | ``` 426 | 427 | 配列 "FRRFRRFRR" から始めて処理を繰り返すと、次のような経過を辿ります。 428 | 429 | ```text 430 | FRRFRRFRR 431 | FLFRRFLFRRFLFRRFLFRRFLFRRFLFRR 432 | FLFRRFLFLFLFRRFLFRRFLFRRFLFLFLFRRFLFRRFLFRRFLF... 433 | ``` 434 | 435 | この命令群に対応する線分パスをプロットすると、**コッホ曲線**と呼ばれる曲線に近似します。反復回数を増やすと、曲線の解像度が増加していきます。 436 | 437 | それでは型と関数の言語へとこれを翻訳してみましょう。 438 | 439 | アルファベットの選択肢は型の選択肢によって表すことができます。今回の例では、以下のような型で定義することができます。 440 | 441 | ```haskell 442 | data Alphabet = L | R | F 443 | ``` 444 | 445 | このデータ型では、アルファベットの文字ごとに1つづつデータ構築子が定義されています。 446 | 447 | 文字の初期配列はどのように表したらいいでしょうか。単なるアルファベットの配列でいいでしょう。これを `Sentence`と呼ぶことにします。 448 | 449 | ```haskell 450 | type Sentence = Array Alphabet 451 | 452 | initial :: Sentence 453 | initial = [F, R, R, F, R, R, F, R, R] 454 | ``` 455 | 456 | 生成規則は `Alphabet`から `Sentence`への関数として表すことができます。 457 | 458 | ```haskell 459 | productions :: Alphabet -> Sentence 460 | productions L = [L] 461 | productions R = [R] 462 | productions F = [F, L, F, R, R, F, L, F] 463 | ``` 464 | 465 | これはまさに上記の仕様をそのまま書き写したものです。 466 | 467 | これで、この形式の仕様を受け取りキャンバスに描画する関数 `lsystem`を実装することができます。 `lsystem`はどのような型を持っているべきでしょうか。この関数は初期状態 `initial`と生成規則 `productions`のような値だけでなく、アルファベットの文字をキャンバスに描画する関数を引数に取る必要があります。 468 | 469 | `lsystem`の型の最初の大まかな設計としては、次のようになるかもしれません。 470 | 471 | ```haskell 472 | forall eff. Sentence 473 | -> (Alphabet -> Sentence) 474 | -> (Alphabet -> Eff (canvas :: Canvas | eff) Unit) 475 | -> Int 476 | -> Eff (canvas :: Canvas | eff) Unit 477 | ``` 478 | 479 | 最初の2つの引数の型は、値 `initial`と `productions`に対応しています。 480 | 481 | 3番目の引数は、アルファベットの文字を取り、キャンバス上のいくつかのアクションを実行することによって**翻訳**する関数を表します。この例では、文字 `L`は左回転、文字 `R`で右回転、文字 `F`は前進を意味します。 482 | 483 | 最後の引数は、実行したい生成規則の繰り返し回数を表す数です。 484 | 485 | 最初に気づくことは、現在の `lsystem`関数は `Alphabet`型だけで機能しますが、どんなアルファベットについても機能すべきですから、この型はもっと一般化されるべきです。それでは、量子化された型変数 `a`について、 `Alphabet`と `Sentence`を `a`と `Array a`で置き換えましょう。 486 | 487 | ```haskell 488 | forall a eff. Array a 489 | -> (a -> Array a) 490 | -> (a -> Eff (canvas :: Canvas | eff) Unit) 491 | -> Int 492 | -> Eff (canvas :: Canvas | eff) Unit 493 | ``` 494 | 495 | 次に気付くこととしては、「左回転」と「右回転」のような命令を実装するためには、いくつかの状態を管理する必要があります。具体的に言えば、その時点でパスが向いている方向を状態として持たなければなりません。計算を通じて状態を関数に渡すように変更する必要があります。ここでも `lsystem`関数は状態がどんな型でも動作しなければなりませんから、型変数 `s`を使用してそれを表しています。 496 | 497 | 型 `s`を追加する必要があるのは3箇所で、次のようになります。 498 | 499 | ```haskell 500 | forall a s eff. Array a 501 | -> (a -> Array a) 502 | -> (s -> a -> Eff (canvas :: Canvas | eff) s) 503 | -> Int 504 | -> s 505 | -> Eff (canvas :: Canvas | eff) s 506 | ``` 507 | 508 | まず追加の引数の型として `lsystem`に型 `s`が追加されています。この引数はL-Systemの初期状態を表しています。 509 | 510 | 型 `s`は引数にも現れますが、翻訳関数(`lsystem`の第3引数)の返り値の型としても現れます。翻訳関数は今のところ、引数としてL-Systemの現在の状態を受け取り、返り値として更新された新しい状態を返します。 511 | 512 | この例の場合では、次のような型を使って状態を表す型を定義することができます。 513 | 514 | ```haskell 515 | type State = 516 | { x :: Number 517 | , y :: Number 518 | , theta :: Number 519 | } 520 | ``` 521 | 522 | プロパティ `x`と `y`はパスの現在の位置を表しており、プロパティ `theta`は現在の向きを表しており、ラジアンで表された水平線に対するパスの角度です。 523 | 524 | システムの初期状態としては次のようなものが考えられます。 525 | 526 | ```haskell 527 | initialState :: State 528 | initialState = { x: 120.0, y: 200.0, theta: 0.0 } 529 | ``` 530 | 531 | それでは、 `lsystem`関数を実装してみます。定義はとても単純であることがわかるでしょう。 532 | 533 | `lsystem`は第4引数の値(型 `Number`)に応じて再帰するのが良さそうです。再帰の各ステップでは、生成規則に従って状態が更新され、現在の文が変化していきます。このことを念頭に置きつつ、まずは関数の引数の名前を導入して、補助関数に処理を移譲することから始めましょう。 534 | 535 | ```haskell 536 | lsystem :: forall a s eff. Array a 537 | -> (a -> Array a) 538 | -> (s -> a -> Eff (canvas :: Canvas | eff) s) 539 | -> Int 540 | -> s 541 | -> Eff (canvas :: Canvas | eff) s 542 | lsystem init prod interpret n state = go init n 543 | where 544 | ``` 545 | 546 | `go`関数は第2引数に応じて再帰することで動きます。 `n`がゼロであるときと `n`がゼロでないときの2つの場合で分岐します。 547 | 548 | `n`がゼロの場合では再帰は完了し、解釈関数に応じて現在の文を解釈します。ここでは引数として与えられている、 549 | 550 | * 型 `Array a`の文 551 | * 型 `s`の状態 552 | * 型 `s -> a -> Eff (canvas :: Canvas | eff) s`の関数 553 | 554 | を参照することができます。これらの引数の型を考えると、以前定義した `foldM`の呼び出しにちょうど対応していることがわかります。 `foldM`は `purescript-control`パッケージでも定義されています。 555 | 556 | ```haskell 557 | go s 0 = foldM interpret state s 558 | ``` 559 | 560 | ゼロでない場合ではどうでしょうか。その場合は、単に生成規則を現在の文のそれぞれの文字に適用して、その結果を連結し、そしてこの処理を再帰します。 561 | 562 | ```haskell 563 | go s n = go (concatMap prod s) (n - 1) 564 | ``` 565 | 566 | これだけです!`foldM`や `concatMap`のような高階関数を使うと、このようにアイデアを簡潔に表現することができるのです。 567 | 568 | しかし、まだ完全に終わったわけではありません。ここで与えた型は、実際はまだ特殊化されすぎています。この定義ではキャンバスの操作が実装のどこにも使われていないことに注目してください。それに、まったく `Eff`モナドの構造を利用していません。実際には、この関数は**どんな**モナド `m`についても動作するのです! 569 | 570 | この章に添付されたソースコードで定義されている、 `lsystem`のもっと一般的な型は次のようになっています。 571 | 572 | ```haskell 573 | lsystem :: forall a m s . Monad m => 574 | Array a 575 | -> (a -> Array a) 576 | -> (s -> a -> m s) 577 | -> Int 578 | -> s 579 | -> m s 580 | ``` 581 | 582 | この型が言っているのは、この翻訳関数はモナド `m`で追跡される任意の副作用をまったく自由に持つことができる、ということだと理解することができます。キャンバスに描画したり、またはコンソールに情報を出力するかもしれませんし、失敗や複数の戻り値に対応しているかもしれません。こういった様々な型の副作用を使ったL-Systemを記述してみることを読者にお勧めします。 583 | 584 | この関数は実装からデータを分離することの威力を示す良い例となっています。この手法の利点は、複数の異なる方法でデータを解釈する自由が得られることです。 `lsystem`は2つの小さな関数へと分解することができるかもしれません。ひとつめは `concatMap`の適用の繰り返しを使って文を構築するもので、ふたつめは `foldM`を使って文を翻訳するものです。これは読者の演習として残しておきます。 585 | 586 | それでは翻訳関数を実装して、この章の例を完成させましょう​​。 `lsystem`の型は型シグネチャが言っているのは、翻訳関数の型は、何らかの型 `a`と `s`、型構築子 `m`について、 `s -> a -> m s`でなければならないということです。今回は `a`を `Alphabet`、 `s`を `State`、モナド `m`を `Eff (canvas :: Canvas)`というように選びたいということがわかっています。これにより次のような型になります。 587 | 588 | ```haskell 589 | interpret :: State -> Alphabet -> Eff (canvas :: Canvas) State 590 | ``` 591 | 592 | この関数を実装するには、 `Alphabet`型の3つのデータ構築子それぞれについて処理する必要があります。文字 `L`(左回転)と `R`(右回転)の解釈では、 `theta`を適切な角度へ変更するように状態を更新するだけです。 593 | 594 | ```haskell 595 | interpret state L = pure $ state { theta = state.theta - Math.pi / 3 } 596 | interpret state R = pure $ state { theta = state.theta + Math.pi / 3 } 597 | ``` 598 | 599 | 文字 `F`(前進)を解釈するには、パスの新しい位置を計算し、線分を描画し、状態を次のように更新します。 600 | 601 | ```haskell 602 | interpret state F = do 603 | let x = state.x + Math.cos state.theta * 1.5 604 | y = state.y + Math.sin state.theta * 1.5 605 | moveTo ctx state.x state.y 606 | lineTo ctx x y 607 | pure { x, y, theta: state.theta } 608 | ``` 609 | 610 | この章のソースコードでは、名前 `ctx`を参照できるようにするために、 `interpret`関数は `main`関数内で `let`束縛を使用して定義されていることに注意してください。 `State`型がコンテキストを持つように変更することは可能でしょうが、それはこのシステムの状態の変化部分ではないので不適切でしょう。 611 | 612 | このL-Systemsを描画するには、次のような `strokePath`アクションを使用するだけです。 613 | 614 | ```haskell 615 | strokePath ctx $ lsystem initial productions interpret 5 initialState 616 | ``` 617 | 618 | L-Systemをコンパイルし、 619 | 620 | ```text 621 | $ pulp build -O --main Example.LSystem --to dist/Main.js 622 | ``` 623 | 624 | `html/index.html`を開いてみましょう。キャンバスにコッホ曲線が描画されるのがわかると思います。 625 | 626 | ## 演習 627 | 628 | 1. (簡単)`strokePath`の代わりに `fillPath`を使用するように、上のL-Systemsの例を変更してください。**ヒント**:`closePath`の呼び出しを含め、 `moveTo`の呼び出しを `interpret`関数の外側に移動する必要があります。 629 | 630 | 1. (簡単) 描画システムへの影響を理解するために、コード中の様々な数値の定数を変更してみてください。 631 | 632 | 1. (やや難しい)`lsystem`関数を2つの小さな関数に分割してください。ひとつめは `concatMap`の適用の繰り返しを使用して最終的な結果を構築するもので、ふたつめは `foldM`を使用して結果を解釈するものでなくてはなりません。 633 | 634 | 1. (やや難しい)`setShadowOffsetX`アクション、 `setShadowOffsetY`アクション、 `setShadowBlur`アクション、 `setShadowColor`アクションを使い、塗りつぶされた図形にドロップシャドウを追加してください。**ヒント**:`PSCi`を使って、これらの関数の型を調べてみましょう。 635 | 636 | 1. (やや難しい) 向きを変えるときの角度の大きさは今のところ一定(`pi/3`)です。その代わりに、 `Alphabet`データ型の中に角度の大きさを追加して、生成規則によって角度を変更できるようにしてください。 637 | 638 | ```haskell 639 | type Angle = Number 640 | 641 | data Alphabet = L Angle | R Angle | F Angle 642 | ``` 643 | 644 | 生成規則でこの新しい情報を使うと、どんな面白い図形を作ることができるでしょうか。 645 | 646 | 1. (難しい)`L`(60度左回転 )、 `R`(60度右回転)、 `F`(前進)、 `M`(これも前進)という4つの文字からなるアルファベットでL-Systemが与えられたとします。 647 | 648 | このシステムの文の初期状態は、単一の文字 `M`です。 649 | 650 | このシステムの生成規則は次のように指定されています。 651 | 652 | ```text 653 | L -> L 654 | R -> R 655 | F -> FLMLFRMRFRMRFLMLF 656 | M -> MRFRMLFLMLFLMRFRM 657 | ``` 658 | 659 | このL-Systemを描画してください。**注意**:最後の文のサイズは反復回数に従って指数関数的に増大するので、生成規則の繰り返しの回数を削減することが必要になります。 660 | 661 | ここで、生成規則における `L`と `M`の間の対称性に注目してください。ふたつの「前進」命令は、次のようなアルファベット型を使用すると、 `Boolean`値を使って区別することができます。 662 | 663 | ```haskell 664 | data Alphabet = L | R | F Boolean 665 | ``` 666 | 667 | このアルファベットの表現を使用して、もう一度このL-Systemを実装してください。 668 | 669 | 1. (難しい) 翻訳関数で別のモナド `m`を使ってみましょう。 `Trace`作用を利用してコンソール上にL-Systemを出力したり、 `Random`作用を利用して状態の型に無作為の突然変異を適用したりしてみてください。 670 | 671 | ## まとめ 672 | 673 | この章では、 `purescript-canvas`ライブラリを使用することにより、PureScriptからHTML5 Canvas APIを使う方法について学びました。マップや畳み込み、レコードと行多型、副作用を扱うための `Eff`モナドなど、これまで学んできた手法を利用した実用的な例について多く見ました。 674 | 675 | この章の例では、高階関数の威力を示すとともに、**実装からのデータの分離**も実演してみせました。これは例えば、代数データ型を使用してこれらの概念を次のように拡張し、描画関数からシーンの表現を完全に分離できるようになります。 676 | 677 | ```haskell 678 | data Scene = Rect Rectangle 679 | | Arc Arc 680 | | PiecewiseLinear (Array Point) 681 | | Transformed Transform Scene 682 | | Clipped Rectangle Scene 683 | | ... 684 | ``` 685 | 686 | この手法は `purescript-drawing`パッケージでも採用されており、描画前にさまざまな方法でデータとしてシーンを操作することができるという柔軟性をもたらしています。 687 | 688 | 次の章では、PureScriptの**外部関数インタフェース**(foreign function interface)を使って、既存のJavaScriptの関数をラップした `purescript-canvas`のようなライブラリを実装する方法について説明します。 689 | 690 | 691 | -------------------------------------------------------------------------------- /src/chapter12.md: -------------------------------------------------------------------------------- 1 | # コールバック地獄 2 | 3 | ## この章の目標 4 | 5 | この章では、これまでに見てきたモナド変換子やApplicative関手といった道具が、現実世界の問題解決にどのように役立つかを見ていきましょう。ここでは特に、**コールバック地獄**(callback hell)の問題を解決について見ていきます。 6 | 7 | ## プロジェクトの準備 8 | 9 | この章のソースコードは、 `pulp run`を使ってコンパイルして実行することができます。 また、 `request`モジュールをNPMを使ってインストールする必要があります。 10 | 11 | ```text 12 | npm install 13 | ``` 14 | 15 | ## 問題 16 | 17 | 通常、JavaScriptの非同期処理コードでは、プログラムの流れを構造化するために**コールバック**(callbacks)を使用します。たとえば、ファイルからテキストを読み取るのに好ましいアプローチとしては、 `readFile`関数を使用し、コールバック、つまりテキストが利用可能になったときに呼び出される関数を渡すことです。 18 | 19 | ```javascript 20 | function readText(onSuccess, onFailure) { 21 | var fs = require('fs'); 22 | fs.readFile('file1.txt', { encoding: 'utf-8' }, function (error, data) { 23 | if (error) { 24 | onFailure(error.code); 25 | } else { 26 | onSuccess(data); 27 | } 28 | }); 29 | } 30 | ``` 31 | 32 | しかしながら、複数の非同期操作が関与している場合には入れ子になったコールバックを生じることになり、すぐに読めないコードになってしまいます。 33 | 34 | ```javascript 35 | function copyFile(onSuccess, onFailure) { 36 | var fs = require('fs'); 37 | fs.readFile('file1.txt', { encoding: 'utf-8' }, function (error, data1) { 38 | if (error) { 39 | onFailure(error.code); 40 | } else { 41 | fs.writeFile('file2.txt', data, { encoding: 'utf-8' }, function (error) { 42 | if (error) { 43 | onFailure(error.code); 44 | } else { 45 | onSuccess(); 46 | } 47 | }); 48 | } 49 | }); 50 | } 51 | ``` 52 | 53 | この問題に対する解決策のひとつとしては、独自の関数に個々の非同期呼び出しを分割することです。 54 | 55 | ```javascript 56 | function writeCopy(data, onSuccess, onFailure) { 57 | var fs = require('fs'); 58 | fs.writeFile('file2.txt', data, { encoding: 'utf-8' }, function (error) { 59 | if (error) { 60 | onFailure(error.code); 61 | } else { 62 | onSuccess(); 63 | } 64 | }); 65 | } 66 | 67 | function copyFile(onSuccess, onFailure) { 68 | var fs = require('fs'); 69 | fs.readFile('file1.txt', { encoding: 'utf-8' }, function (error, data) { 70 | if (error) { 71 | onFailure(error.code); 72 | } else { 73 | writeCopy(data, onSuccess, onFailure); 74 | } 75 | }); 76 | } 77 | ``` 78 | 79 | この解決策は一応は機能しますが、いくつか問題があります。 80 | 81 | - 上で `writeCopy`へ `data`を渡したのと同じ方法で、非同期関数に関数の引数として途中の結果を渡さなければなりません。これは小さな関数についてはうまくいきますが、多くのコールバック関係する場合はデータの依存関係は複雑になることがあり、関数の引数が大量に追加される結果になります。 82 | - どんな非同期関数でもコールバック `onSuccess`と `onFailure`が引数として定義されるという共通のパターンがありますが、このパターンはソースコードに付随したモジュールのドキュメントに記述することで実施しなければなりません。このパターンを管理するには型システムのほうがよいですし、型システムで使い方を強制しておくほうがいいでしょう。 83 | 84 | 次に、これらの問題を解決するために、これまでに学んだ手法を使用する方法について説明していきます。 85 | 86 | ## 継続モナド 87 | 88 | `copyFile`の例をFFIを使ってPureScriptへと翻訳していきましょう。PureScriptで書いていくにつれ、計算の構造はわかりやすくなり、 `purescript-transformers`パッケージで定義されている継続モナド変換子 `ContT`が自然に導入されることになるでしょう。 89 | 90 | まず、FFIを使って `readFile`と `writeFile`に型を与えなくてはなりません。型同義語をいくつかと、ファイル入出力のための作用を定義することから始めましょう。 91 | 92 | ```haskell 93 | foreign import data FS :: Effect 94 | 95 | type ErrorCode = String 96 | type FilePath = String 97 | ``` 98 | 99 | `readFile`はファイル名と2引数のコールバックを引数に取ります。ファイルが正常に読み込まれた場合は、2番目の引数にはファイルの内容が含まれますが、そうでない場合は、最初の引数がエラーを示すために使われます。 100 | 101 | 今回は `readFile`を2つのコールバックを引数としてとる関数としてラップすることにします。先ほどの `copyFile`や `writeCopy`とまったく同じように、エラーコールバック(`onFailure`)と結果コールバック(`onSuccess`)の2つです。簡単のために `Data.Function`の多引数関数の機能を使うと、このラップされた関数 `readFileImpl`は次のようになるでしょう。 102 | 103 | ```haskell 104 | foreign import readFileImpl 105 | :: forall eff 106 | . Fn3 FilePath 107 | (String -> Eff (fs :: FS | eff) Unit) 108 | (ErrorCode -> Eff (fs :: FS | eff) Unit) 109 | (Eff (fs :: FS | eff) Unit) 110 | ``` 111 | 112 | 外部JavaScriptモジュールでは、`readFileImpl`は次のように定義されます。 113 | 114 | ```javascript 115 | exports.readFileImpl = function(path, onSuccess, onFailure) { 116 | return function() { 117 | require('fs').readFile(path, { 118 | encoding: 'utf-8' 119 | }, function(error, data) { 120 | if (error) { 121 | onFailure(error.code)(); 122 | } else { 123 | onSuccess(data)(); 124 | } 125 | }); 126 | }; 127 | }; 128 | ``` 129 | 130 | `readFileImpl`はファイルパス、成功時のコールバック、失敗時のコールバックという3つの引数を取り、空(`Unit`)の結果を返す副作用のある計算を返す、ということをこの型は言っています。コー​​ルバック自身にも、その作用を追跡するために `Eff`モナドを使うような型が与えられていることに注意してください。 131 | 132 | この `readFileImpl`の実装がその型の正しい実行時表現を持っている理由を、よく理解しておくようにしてください。 133 | 134 | `writeFileImpl`もよく似ています。違いはファイルがコールバックではなく関数自身に渡されるということだけです。実装は次のようになります。 135 | 136 | ```haskell 137 | foreign import writeFileImpl 138 | :: forall eff 139 | . Fn4 FilePath 140 | String 141 | (Eff (fs :: FS | eff) Unit) 142 | (ErrorCode -> Eff (fs :: FS | eff) Unit) 143 | (Eff (fs :: FS | eff) Unit) 144 | ``` 145 | 146 | ```javascript 147 | exports.writeFileImpl = function(path, data, onSuccess, onFailure) { 148 | return function() { 149 | require('fs').writeFile(path, data, { 150 | encoding: 'utf-8' 151 | }, function(error) { 152 | if (error) { 153 | onFailure(error.code)(); 154 | } else { 155 | onSuccess(); 156 | } 157 | }); 158 | }; 159 | }; 160 | ``` 161 | 162 | これらのFFIの宣言が与えられれば、 `readFile`と `writeFile`の実装を書くことができます。 `Data.Function`ライブラリを使って、多引数のFFIバインディングを通常の(カリー化された)PureScript関数へと変換するので、もう少し読みやすい型になるでしょう。 163 | 164 | さらに、成功時と失敗時の2つの必須のコールバックに代わって、成功か失敗の**どちらか**(Either) に対応した単一のコールバックを要求するようにします。つまり、新しいコールバックは引数として `Either ErrorCode`モナドの値をとります。 165 | 166 | ```haskell 167 | readFile :: forall eff . FilePath 168 | -> (Either ErrorCode String -> Eff (fs :: FS | eff) Unit) 169 | -> Eff (fs :: FS | eff) Unit 170 | readFile path k = 171 | runFn3 readFileImpl 172 | path 173 | (k <<< Right) 174 | (k <<< Left) 175 | 176 | writeFile :: forall eff . FilePath 177 | -> String 178 | -> (Either ErrorCode Unit -> Eff (fs :: FS | eff) Unit) 179 | -> Eff (fs :: FS | eff) Unit 180 | writeFile path text k = 181 | runFn4 writeFileImpl 182 | path 183 | text 184 | (k $ Right unit) 185 | (k <<< Left) 186 | ``` 187 | 188 | ここで、重要なパターンを見つけることができます。これらの関数は何らかのモナド(この場合は `Eff (fs :: FS | eff)`)で値を返すコールバックをとり、**同一のモナド**で値を返します。これは、最初のコールバックが結果を返したときに、そのモナドは次の非同期関数の入力に結合するためにその結果を使用することができることを意味しています。実際、 `copyFile`の例で手作業でやったことがまさにそれです。 189 | 190 | これは `purescript-transformers`の `Control.Monad.Cont.Trans`モジュールで定義されている**継続モナド変換子**(continuation monad transformer)の基礎となっています。 191 | 192 | `ContT`は次のようなnewtypeとして定義されます。 193 | 194 | ```haskell 195 | newtype ContT r m a = ContT ((a -> m r) -> m r) 196 | ``` 197 | 198 | **継続**(continuation)はコールバックの別名です。継続は計算の**残余**(remainder)を捕捉します。ここで「残余」とは、非同期呼び出しが行われ、結果が提供された後に起こることを指しています。 199 | 200 | `ContT`データ構築子の引数は `readFile`と `writeFile`の型ととてもよく似ています。実際、もし型`a`を`ErrorCode String`型、`r`を`Unit`、`m`をモナド`Eff(fs :: FS | eff)`というように選ぶと、`readFile`の型の右辺を復元することができます。 201 | 202 | `readFile`や`writeFile`のような非同期のアクションを組み立てるために使う`Async`モナドを定義するため、次のような型同義語を導入します。 203 | 204 | ```haskell 205 | type Async eff = ContT Unit (Eff eff) 206 | ``` 207 | 208 | 今回の目的では `Eff`モナドを変換するために常に `ContT`を使い、型 `r`は常に `Unit`になりますが、必ずそうしなければならないというわけではありません。 209 | 210 | `ContT`データ構築子を適用するだけで、 `readFile`と `writeFile`を `Async`モナドの計算として扱うことができます。 211 | 212 | ```haskell 213 | readFileCont 214 | :: forall eff 215 | . FilePath 216 | -> Async (fs :: FS | eff) (Either ErrorCode String) 217 | readFileCont path = ContT $ readFile path 218 | 219 | writeFileCont 220 | :: forall eff 221 | . FilePath 222 | -> String 223 | -> Async (fs :: FS | eff) (Either ErrorCode Unit) 224 | writeFileCont path text = ContT $ writeFile path text 225 | ``` 226 | 227 | ここで `ContT`モナド変換子に対してdo記法を使うだけで、ファイル複製処理を書くことができます。 228 | 229 | ```haskell 230 | copyFileCont 231 | :: forall eff 232 | . FilePath 233 | -> FilePath 234 | -> Async (fs :: FS | eff) (Either ErrorCode Unit) 235 | copyFileCont src dest = do 236 | e <- readFileCont src 237 | case e of 238 | Left err -> pure $ Left err 239 | Right content -> writeFileCont dest content 240 | ``` 241 | 242 | `readFileCont`の非同期性がdo記法によってモナドの束縛に隠されていることに注目してください。これはまさに同期的なコードのように見えますが、 `ContT`モナド変換子は非同期関数を書くのを手助けしているのです。 243 | 244 | 継続を与えて `runContT`ハンドラを使うと、この計算を実行することができます。この継続は**次に何をするか**、例えば非同期なファイル複製処理が完了した時に何をするか、を表しています。この簡単な例では、型 `Either ErrorCode Unit`の結果をコンソールに出力する `logShow`関数を単に継続として選んでいます。 245 | 246 | ```haskell 247 | import Prelude 248 | 249 | import Control.Monad.Eff.Console (logShow) 250 | import Control.Monad.Cont.Trans (runContT) 251 | 252 | main = 253 | runContT 254 | (copyFileCont "/tmp/1.txt" "/tmp/2.txt") 255 | logShow 256 | ``` 257 | 258 | ## 演習 259 | 260 | 1. (簡単)`readFileCont`と `writeFileCont`を使って、2つのテキストフ​​ァイルを連結する関数を書いてください。 261 | 262 | 1. (やや難しい) FFIを使って、 `setTimeout`関数に適切な型を与えてください。また、 `Async`モナドを使った次のようなラッパー関数を書いてください。 263 | 264 | ```haskell 265 | type Milliseconds = Int 266 | 267 | foreign import data TIMEOUT :: Effect 268 | 269 | setTimeoutCont 270 | :: forall eff 271 | . Milliseconds 272 | -> Async (timeout :: TIMEOUT | eff) Unit 273 | ``` 274 | 275 | ## ExceptTを機能させる 276 | 277 | この方法はうまく動きますが、まだ改良の余地があります。 278 | 279 | `copyFileCont`の実装において、次に何をするかを決定するためには、パターン照合を使って(型 `Either ErrorCode String`の)`readFileCont`計算の結果を解析しなければなりません。しかしながら、 `Either`モナドは対応するモナド変換子 `ExceptT`を持っていることがわかっているので、 `ContT`を持つ `ExceptT`を使って非同期計算とエラー処理の2つの作用を結合できると期待するのは理にかなっています。 280 | 281 | 実際にそれは可能で、 `ExceptT`の定義を見ればそれがなぜかがわかります。 282 | 283 | ```haskell 284 | newtype ExceptT e m a = ExceptT (m (Either e a)) 285 | ``` 286 | 287 | `ExceptT`は基礎のモナドの結果を単純に `a`から `Either e a`に変更します。現在のモナドスタックを `ExceptT ErrorCode`変換子で変換するように、 `copyFileCont`を書き換えることができることを意味します。それは現在の方法に `ExceptT`データ構築子を適用するだけなので簡単です。型同義語を与えると、ここでも型シグネチャを整理することができます。 288 | 289 | ```haskell 290 | readFileContEx 291 | :: forall eff 292 | . FilePath 293 | -> ExceptT ErrorCode (Async (fs :: FS | eff)) String 294 | readFileContEx path = ExceptT $ readFileCont path 295 | 296 | writeFileContEx 297 | :: forall eff 298 | . FilePath 299 | -> String 300 | -> ExceptT ErrorCode (Async (fs :: FS | eff)) Unit 301 | writeFileContEx path text = ExceptT $ writeFileCont path text 302 | ``` 303 | 304 | 非同期エラー処理が `ExceptT`モナド変換子の内部に隠されているので、このファイル複製処理ははるかに単純になります。 305 | 306 | ```haskell 307 | copyFileContEx 308 | :: forall eff 309 | . FilePath 310 | -> FilePath 311 | -> ExceptT ErrorCode (Async (fs :: FS | eff)) Unit 312 | copyFileContEx src dest = do 313 | content <- readFileContEx src 314 | writeFileContEx dest content 315 | ``` 316 | 317 | ## 演習 318 | 319 | 1. (やや難しい) 任意のエラーを処理するために、 `ExceptT`を使用して2つのファイルを連結しする先ほどの解決策を書きなおしてください。 320 | 321 | 1. (やや難しい) 入力ファイル名の配列を与えて複数のテキストファイルを連結する関数 `concatenateMany`を書く。 **ヒント**:`traverse`を使用します。 322 | 323 | ## HTTPクライアント 324 | 325 | `ContT`を使って非同期機能を処理する例として、この章のソースコードの `Network.HTTP.Client`モジュールについても見ていきましょう。このモジュールでは `Async`モナドを使用して、NodeJSの非同期を `request`モジュールを使っています。 326 | 327 | `request`モジュールは、URLとコールバックを受け取り、応答が利用可能なとき、またはエラーが発生したときにHTTP(S)リクエストを生成してコールバックを呼び出す関数を提供します。 リクエストの例を次に示します。 328 | 329 | ```javascript 330 | require('request')('http://purescript.org'), function(err, _, body) { 331 | if (err) { 332 | console.error(err); 333 | } else { 334 | console.log(body); 335 | } 336 | }); 337 | ``` 338 | 339 | `Async`モナドを使うと、この簡単な例をPureScriptで書きなおすことができます。 340 | 341 | `Network.HTTP.Client`モジュールでは、 `request`メソッドは以下のようなAPIを持つ関数 `getImpl`としてラップされています。 342 | 343 | ```haskell 344 | foreign import data HTTP :: Effect 345 | 346 | type URI = String 347 | 348 | foreign import getImpl 349 | :: forall eff 350 | . Fn3 URI 351 | (String -> Eff (http :: HTTP | eff) Unit) 352 | (String -> Eff (http :: HTTP | eff) Unit) 353 | (Eff (http :: HTTP | eff) Unit) 354 | ``` 355 | 356 | ```javascript 357 | exports.getImpl = function(uri, done, fail) { 358 | return function() { 359 | require('request')(uri, function(err, _, body) { 360 | if (err) { 361 | fail(err)(); 362 | } else { 363 | done(body)(); 364 | } 365 | }); 366 | }; 367 | }; 368 | ``` 369 | 370 | 再び`Data.Function.Uncurried`モジュールを使って、これを通常のカリー化されたPureScript関数に変換します。先ほどと同じように、2つのコールバックを`Maybe Chunk`型の値を受け入れるひとつのコールバックに変換しています。`Either String String`型の値を受け取り、`ContT`データ構築子を適用して`Async`モナドのアクションを構築します。 371 | 372 | ```haskell 373 | get :: forall eff. 374 | URI -> 375 | Async (http :: HTTP | eff) (Either String String) 376 | get req = ContT \k -> 377 | runFn3 getImpl req (k <<< Right) (k <<< Left) 378 | ``` 379 | 380 | ## 演習 381 | 382 | 1. (やや難しい)`runContT`を使ってHTTP応答の各チャンクをコンソールへ出力することで、 `get`を試してみてください。 383 | 384 | 1. (やや難しい)`readFileCont`と `writeFileCont`に対して以前に行ったように、 `ExceptT`を使い `get`をラップする関数 `getEx`を書いてください。 385 | 386 | 1.(難しい) `getEx`と `writeFileContEx`を使って、ディスク上のファイルからの内容をを保存する関数を書いてください。 387 | 388 | ## 並列計算 389 | 390 | `ContT`モナドとdo記法を使って、非同期計算を順番に実行されるように合成する方法を見てきました。非同期計算を**並列に**合成することもできたら便利でしょう。 391 | 392 | もし`ContT`を使って`Eff`モナドを変換しているなら、単に2つの計算のうち一方を開始した後に他方の計算を開始すれば、並列に計算することができます。 393 | 394 | `purescript-parallel`パッケージは型クラス`Parallel`を定義します。この型クラスはモナドのために並列計算を提供する`Async`のようなものです。以前に本書でApplicative関手を導入したとき、並列計算を合成するときにApplicative関手がどのように便利なのかを観察しました。実は`Parallel`のインスタンスは、(`Async`のような)モナド`m`と、並列に計算を合成するために使われるApplicative関手`f`との対応関係を定義しているのです。 395 | 396 | ```haskell 397 | class (Monad m, Applicative f) <= Parallel f m | m -> f, f -> m where 398 | sequential :: forall a. f a -> m a 399 | parallel :: forall a. m a -> f a 400 | ``` 401 | 402 | このクラスは2つの関数を定義しています。 403 | 404 | - `parallel`:モナド `m`を計算し、それを応用ファンクタ `f`の計算に変換します。 405 | - `sequential`:反対方向の変換を行います。 406 | 407 | `purescript-parallel`ライブラリは `Async`モナドの `Parallel`インスタンスを提供します。 これは、2つの継続(continuation)のどちらが呼び出されたかを追跡することによって、変更可能な参照を使用して並列に `Async`アクションを組み合わせます。 両方の結果が返されたら、最終結果を計算してメインの継続に渡すことができます。 408 | 409 | `parallel`関数を使うと`readFileCont`アクションの別のバージョンを作成することもできます。これは並列に組み合わせることができます。2つのテキストファイルを並列に読み取り、連結してその結果を出力する簡単な例は次のようになります。 410 | 411 | ```haskell 412 | import Prelude 413 | import Control.Apply (lift2) 414 | import Control.Monad.Cont.Trans (runContT) 415 | import Control.Monad.Eff.Console (logShow) 416 | import Control.Monad.Parallel (parallel, sequential) 417 | 418 | main = flip runContT logShow do 419 | sequential $ 420 | lift2 append 421 | <$> parallel (readFileCont "/tmp/1.txt") 422 | <*> parallel (readFileCont "/tmp/2.txt") 423 | ``` 424 | 425 | `readFileCont`は `Either ErrorCode String`型の値を返すので、 `lift2`を使って `Either`型構築子より `append`関数を持ち上げて結合関数を形成する必要があることに注意してください。 426 | 427 | Applicative関手では任意個引数の関数の持ち上げができるので、このApplicativeコンビネータを使ってより多くの計算を並列に実行することができます。 `traverse`と `sequence`のようなApplicative関手を扱うすべての標準ライブラリ関数から恩恵を受けることもできます。 428 | 429 | 必要に応じて `Parralel`と `runParallel`を使って型構築子を変更することで、do記法ブロックのApplicativeコンビネータを使って、直列的なコードの一部で並列計算を結合したり、またはその逆を行ったりすることができます。 430 | 431 | ## 演習 432 | 433 | 1. (簡単)`parallel`と ` sequential`を使って2つのHTTPリクエストを作成し、それらのレスポンス内容を並行して収集します。あなたの結合関数は2つのレスポンス内容を連結しなければならず、続けて `print`を使って結果をコンソールに出力してください。 434 | 435 | 1. (やや難しい)`Async`に対応するapplicative関手は ` Alternative`のインスタンスです。このインスタンスによって定義される `<|>`演算子は2つの計算を並列に実行し、最初に完了する計算結果を返します。 436 | 437 | この `Alternative`インスタンスを `setTimeoutCont`関数と共に使用して関数を定義してください。 438 | 439 | ```haskell 440 | timeout :: forall a eff 441 | . Milliseconds 442 | -> Async (timeout :: TIMEOUT | eff) a 443 | -> Async (timeout :: TIMEOUT | eff) (Maybe a) 444 | ``` 445 | 446 | 指定された計算が指定されたミリ秒数以内に結果を提供しない場合、 `Nothing`を返します。 447 | 448 | 1. (やや難しい)`purescript-parallel`は `ExceptT`を含むいくつかのモナド変換子のための `Parallel`クラスのインスタンスも提供します。 449 | 450 | `lift2`で `append`を持ち上げる代わりに、 `ExceptT`を使ってエラー処理を行うように、並列ファイル入出力の例を書きなおしてください。解決策は `Async`モナドを変換するために `ExceptT`変換子を使うとよいでしょう。 451 | 452 | 同様の手法で複数の入力ファイルを並列に読み込むために `concatenateMany`関数を書き換えてください。 453 | 454 | 1. (難しい、拡張) ディスク上のJSON文書の配列が与えられ、それぞれの文書はディスク上の他のファイルへの参照の配列を含んでいるとします。 455 | 456 | ```javascript 457 | { references: ['/tmp/1.json', '/tmp/2.json'] } 458 | ``` 459 | 入力として単一のファイル名をとり、そのファイルから参照されているディスク上のすべてのJSONファイルをたどって、参照されたすべてのファイルの一覧を収集するユーティリティを書いてください。 460 | 461 | そのユーティリティは、JSON文書を解析するために `purescript-foreign`ライブラリを使用する必要があり、単一のファイルが参照するファイルは並列に取得しなければなりません! 462 | 463 | ## まとめ 464 | 465 | この章ではモナド変換子の実用的なデモンストレーションを見てきました。 466 | 467 | - コールバック渡しの一般的なJavaScriptのイディオムを `ContT`モナド変換子によって捉えることができる方法を説明しました。 468 | - どのようにコールバック地獄の問題を解決するかを説明しました。 直列の非同期計算を表現するdo記法を使用して、かつ並列性を表現するためにApplicative関手によって解決することができる方法を説明しました。 469 | - **非同期エラー**を表現するために `ExceptT`を使いました。 470 | 471 | 472 | -------------------------------------------------------------------------------- /src/chapter13.md: -------------------------------------------------------------------------------- 1 | # テストの自動生成 2 | 3 | ## この章の目標 4 | 5 | この章では、テスティングの問題に対する、型クラスの特に洗練された応用について示します。**どのようにテストするのかを**コンパイラに教えるのではなく、コードが**どのような性質を持っているべきか**を教えることでテストします。型クラスを使って無作為データ生成のための定型コードを隠し、テストケースを仕様から無作為に生成することができます。これは**生成的テスティング**(generative testing、またはproperty-based testing)と呼ばれ、Haskellの[QuickCheck](http://www.haskell.org/haskellwiki/Introduction_to_QuickCheck1)ライブラリによって知られるようになった手法です。 6 | 7 | `purescript-quickcheck`パッケージはHaskellのQuickCheckライブラリをPureScriptにポーティングしたもので、型や構文はもとのライブラリとほとんど同じようになっています。 `purescript-quickcheck`を使って簡単なライブラリをテストし、Pulpでテストスイートを自動化されたビルドに統合する方法を見ていきます。 8 | 9 | ## プロジェクトの準備 10 | 11 | この章のプロジェクトにはBower依存関係として `purescript-quickcheck`が追加されます。 12 | 13 | Pulpプロジェクトでは、テストソースは `test`ディレクトリに置かれ、テストスイートのメインモジュールは `Test.Main`と名づけられます。 テストスイートは、 `pulp test`コマンドを使用して実行できます。 14 | 15 | ## プロパティの書き込み 16 | 17 | `Merge`モジュールでは `purescript-quickcheck`ライブラリの機能を実演するために使う簡単な関数 `merge`が実装されています。 18 | 19 | ```haskell 20 | merge :: Array Int -> Array Int -> Array Int 21 | ``` 22 | 23 | `merge`は2つのソートされた数の配列をとって、その要素を統合し、ソートされた結果を返します。例えば次のようになります。 24 | 25 | ```text 26 | > import Merge 27 | > merge [1, 3, 5] [2, 4, 6] 28 | 29 | [1, 2, 3, 4, 5, 6] 30 | ``` 31 | 32 | 典型的なテストスイートでは、手作業でこのような小さなテストケースをいくつも作成し、結果が正しい値と等しいことを確認することでテスト実施します。しかし、 `merge`関数について知る必要があるものはすべて、2つの性質に要約することができます。 33 | 34 | - (既ソート性)`xs`と `ys`がソート済みなら、 `merge xs ys`もソート済みになります。 35 | - (部分配列) `xs`と `ys`ははどちらも `merge xs ys`の部分配列で、要素は元の配列と同じ順序で現れます。 36 | 37 | `purescript-quickcheck`では、無作為なテストケースを生成することで、直接これらの性質をテストすることができます。コードが持つべき性質を、次のような関数として述べるだけです。 38 | 39 | ```haskell 40 | main = do 41 | quickCheck \xs ys -> 42 | isSorted $ merge (sort xs) (sort ys) 43 | quickCheck \xs ys -> 44 | xs `isSubarrayOf` merge xs ys 45 | ``` 46 | 47 | ここで、 `isSorted`と `isSubarrayOf`は次のような型を持つ補助関数として実装されています。 48 | 49 | ```haskell 50 | isSorted :: forall a. Ord a => Array a -> Boolean 51 | isSubarrayOf :: forall a. Eq a => Array a -> Array a -> Boolean 52 | ``` 53 | 54 | このコードを実行すると、 `purescript-quickcheck`は無作為な入力 `xs`と `ys`を生成してこの関数に渡すことで、主張しようとしている性質を反証しようとします。何らかの入力に対して関数が `false`を返した場合、性質は正しくないことが示され、ライブラリはエラーを発生させます。幸いなことに、次のように100個の無作為なテストケースを生成しても、ライブラリはこの性質を反証することができません。 55 | 56 | ```text 57 | $ pulp test 58 | 59 | * Build successful. Running tests... 60 | 61 | 100/100 test(s) passed. 62 | 100/100 test(s) passed. 63 | 64 | * Tests OK. 65 | ``` 66 | 67 | もし `merge`関数に意図的にバグを混入した場合(例えば、大なりのチェックを小なりのチェックへと変更するなど)、最初に失敗したテストケースの後で例外が実行時に投げられます。 68 | 69 | ```text 70 | Error: Test 1 failed: 71 | Test returned false 72 | ``` 73 | 74 | このエラーメッセージではあまり役に立ちませんが、これから見ていくように、少しの作業で改良することができます。 75 | 76 | ## エラーメッセージの改善 77 | 78 | テストケースが失敗した時に同時にエラーメッセージを提供するには、 `purescript-quickcheck`の ``演算子を使います。次のように性質の定義に続けて ``で区切ってエラーメッセージを書くだけです。 79 | 80 | ```haskell 81 | quickCheck \xs ys -> 82 | let 83 | result = merge (sort xs) (sort ys) 84 | in 85 | xs `isSubarrayOf` result show xs <> " not a subarray of " <> show result 86 | ``` 87 | 88 | このとき、もしバグを混入するようにコードを変更すると、最初のテストケースが失敗したときに改良されたエラーメッセージが表示されます。 89 | 90 | ```text 91 | Error: Test 6 failed: 92 | [79168] not a subarray of [-752832,686016] 93 | ``` 94 | 95 | 入力 `xs`が無作為に選ばれた数の配列として生成されていることに注目してください。 96 | 97 | ## 演習 98 | 99 | 1. (簡単) 空の配列を持つ配列を統合しても元の配列は変更されない、と主張する性質を書いてください。 100 | 101 | 1. (簡単) `merge`の残りの性質に対して、適切なエラーメッセージを追加してください。 102 | 103 | ## 多相的なコードのテスト 104 | 105 | `Merge`モジュールでは、数の配列だけでなく、 `Ord`型クラスに属するどんな型の配列に対しても動作する、 `merge`関数を一般化した `mergePoly`という関数が定義されています。 106 | 107 | ```haskell 108 | mergePoly :: forall a. Ord a => Array a -> Array a -> Array a 109 | ``` 110 | 111 | `merge`の代わりに `mergePoly`を使うように元のテストを変更すると、次のようなエラーメッセージが表示されます。 112 | 113 | ```text 114 | No type class instance was found for 115 | 116 | Test.QuickCheck.Arbitrary.Arbitrary t0 117 | 118 | The instance head contains unknown type variables. 119 | Consider adding a type annotation. 120 | ``` 121 | 122 | このエラーメッセージは、配列に持たせたい要素の型が何なのかわからないので、コンパイラが無作為なテストケースを生成できなかったということを示しています。このような場合、補助関数を使と、コンパイラが特定の型を推論すること強制できます。例えば、恒等関数の同義語として `ints`という関数を定義します。 123 | 124 | ```haskell 125 | ints :: Array Int -> Array Int 126 | ints = id 127 | ``` 128 | 129 | それから、コンパイラが引数の2つの配列の型 `Array Int`を推論するように、テストを変更します。 130 | 131 | ```haskell 132 | quickCheck \xs ys -> 133 | isSorted $ ints $ mergePoly (sort xs) (sort ys) 134 | quickCheck \xs ys -> 135 | ints xs `isSubarrayOf` mergePoly xs ys 136 | ``` 137 | 138 | ここで、 `numbers`関数が不明な型を解消するために使われるので、 `xs`と `ys`はどちらも型 `Array Int`を持っています。 139 | 140 | ## 演習 141 | 142 | 1. (簡単)`xs`と `ys`の型を `Array Boolean`に強制する関数 `bools`を書き、 `mergePoly`をその型でテストする性質を追加してください。 143 | 144 | 1. (やや難しい) 標準関数から(例えば `purescript-arrays`パッケージから)ひとつ関数を選び、適切なエラーメッセージを含めてQuickCheckの性質を書いてください。その性質は、補助関数を使って多相型引数を `Int`か `Boolean`のどちらかに固定しなければいけません。 145 | 146 | ## 任意のデータの生成 147 | 148 | `purescript-quickcheck`ライブラリを使って性質についてのテストケースを無作為に生成する方法について説明します。 149 | 150 | 無作為に値を生成することができるような型は、次のような型クラス `Arbitary`のインスタンスを持っています。 151 | 152 | ```haskell 153 | class Arbitrary t where 154 | arbitrary :: Gen t 155 | ``` 156 | 157 | `Gen`型構築子は**決定的無作為データ生成**の副作用を表しています。 決定的無作為データ生成は、擬似乱数生成器を使って、シード値から決定的無作為関数の引数を生成します。 `Test.QuickCheck.Gen`モジュールは、ジェネレータを構築するためのいくつかの有用なコンビネータを定義します。 158 | 159 | `Gen`はモナドでもApplicative関手でもあるので、 `Arbitary`型クラスの新しいインスタンスを作成するのに、いつも使っているようなコンビネータを自由に使うことができます。 160 | 161 | 例えば、 `purescript-quickcheck`ライブラリで提供されている `Int`型の `Arbitrary`インスタンスは、関数を整数から任意の整数値のバイトまでマップするための `Functor`インスタンスを `Gen`に使用することで、バイト値の分布した値を生成します。 162 | 163 | ```haskell 164 | newtype Byte = Byte Int 165 | 166 | instance arbitraryByte :: Arbitrary Byte where 167 | arbitrary = map intToByte arbitrary 168 | where 169 | intToByte n | n >= 0 = Byte (n `mod` 256) 170 | | otherwise = intToByte (-n) 171 | ``` 172 | 173 | ここでは、0から255までの間の整数値であるような型 `Byte`を定義しています。 `Arbitrary`インスタンスの `<$>`演算子を使って、 `uniformToByte`関数を `arbitrary`アクションまで持ち上げています。この型の `arbitrary`アクションの型は `Gen Number`だと推論されますが、これは0から1の間に均一に分布する数を生成することを意味しています。 174 | 175 | 176 | 177 | この考え方を `merge`に対しての既ソート性テストを改良するのに使うこともできます。 178 | 179 | ```haskell 180 | quickCheck \xs ys -> 181 | isSorted $ numbers $ mergePoly (sort xs) (sort ys) 182 | ``` 183 | 184 | このテストでは、任意の配列 `xs`と `ys`を生成しますが、 `merge`はソート済みの入力を期待しているので、 `xs`と `ys`をソートしておかなければなりません。一方で、ソートされた配列を表すnewtypeを作成し、ソートされたデータを生成する `Arbitrary`インスタンスを書くこともできます。 185 | 186 | ```haskell 187 | newtype Sorted a = Sorted (Array a) 188 | 189 | sorted :: forall a. Sorted a -> Array a 190 | sorted (Sorted xs) = xs 191 | 192 | instance arbSorted :: (Arbitrary a, Ord a) => Arbitrary (Sorted a) where 193 | arbitrary = map (Sorted <<< sort) arbitrary 194 | ``` 195 | 196 | この型構築子を使うと、テストを次のように変更することができます。 197 | 198 | ```haskell 199 | quickCheck \xs ys -> 200 | isSorted $ ints $ mergePoly (sorted xs) (sorted ys) 201 | ``` 202 | 203 | これは些細な変更に見えるかもしれませんが、 `xs`と `ys`の型はただの `Array Int`から `Sorted Int`へと変更されています。これにより、 `mergePoly`関数はソート済みの入力を取る、という**意図**を、わかりやすく示すことができます。理想的には、 `mergePoly`関数自体の型が `Sorted`型構築子を使うようにするといいでしょう。 204 | 205 | より興味深い例として、 `Tree`モジュールでは枝の値でソートされた二分木の型が定義されています。 206 | 207 | ```haskell 208 | data Tree a 209 | = Leaf 210 | | Branch (Tree a) a (Tree a) 211 | ``` 212 | 213 | `Tree`モジュールでは次のAPIが定義されています。 214 | 215 | ```haskell 216 | insert :: forall a. Ord a => a -> Tree a -> Tree a 217 | member :: forall a. Ord a => a -> Tree a -> Boolean 218 | fromArray :: forall a. Ord a => Array a -> Tree a 219 | toArray :: forall a. Tree a -> Array a 220 | ``` 221 | 222 | `insert`関数は新しい要素をソート済みの二分木に挿入するのに使われ、 `member`関数は特定の値の有無を木に問い合わせるのに使われます。例えば次のようになります。 223 | 224 | ```text 225 | > import Tree 226 | 227 | > member 2 $ insert 1 $ insert 2 Leaf 228 | true 229 | 230 | > member 1 Leaf 231 | false 232 | ``` 233 | 234 | `toArray`関数と `fromArray`関数は、ソートされた木とソートされた配列を相互に変換するために使われます。 `fromArray`を使うと、木についての `Arbitrary`インスタンスを書くことができます。 235 | 236 | ```haskell 237 | instance arbTree :: (Arbitrary a, Ord a) => Arbitrary (Tree a) where 238 | arbitrary = map fromArray arbitrary 239 | ``` 240 | 241 | 型 `a`についての有効な `Arbitary`インスタンスが存在していれば、テストする性質の引数の型として `Tree a`を使うことができます。例えば、 `member`テストは値を挿入した後は常に `true`を返すことをテストできます。 242 | 243 | ```haskell 244 | quickCheck \t a -> 245 | member a $ insert a $ treeOfInt t 246 | ``` 247 | 248 | ここでは、引数 `t`は `Tree Number`型の無作為に生成された木です。型引数は、識別関数 `treeOfInt`によって明確化されています。 249 | 250 | ## 演習 251 | 252 | 1. (やや難しい) `a-z`の範囲から無作為に選ばれた文字の集まりを生成する `Arbitrary`インスタンスを持った、 `String`のnewtypeを作ってください。**ヒント**:`Test.QuickCheck.Gen`モジュールから `elements`と `arrayOf`関数を使います。 253 | 254 | 1. (難しい) 木に挿入された値は、任意に多くの挿入があった後も、その木の構成要素であることを主張する性質を書いてください。 255 | 256 | ## 高階関数のテスト 257 | 258 | `Merge`モジュールは `merge`関数についての他の生成も定義します。 `mergeAith`関数は、統合される要素の順序を決定するのに使われる、追加の関数を引数としてとります。つまり `mergeWith`は高階関数です。 259 | 260 | 例えば、すでに長さの昇順になっている2つの配列を統合するのに、 `length`関数を最初の引数として渡します。このとき、結果も長さの昇順になっていなければなりません。 261 | 262 | ```haskell 263 | > import Data.String 264 | 265 | > mergeWith length 266 | ["", "ab", "abcd"] 267 | ["x", "xyz"] 268 | 269 | ["","x","ab","xyz","abcd"] 270 | ``` 271 | 272 | このような関数をテストするにはどうしたらいいでしょうか。理想的には、関数であるような最初の引数を含めた、3つの引数すべてについて、値を生成したいと思うでしょう。 273 | 274 | 関数を無作為に生成せきるようにする、もうひとつの型クラスがあります。この型クラスは `Coarbitrary`と呼ばれており、次のように定義されています。 275 | 276 | ```haskell 277 | class Coarbitrary t where 278 | coarbitrary :: forall r. t -> Gen r -> Gen r 279 | ``` 280 | 281 | `coarbitrary`関数は、型 `t`と、関数の結果の型 `r`についての無作為な生成器を関数の引数としてとり、無作為な生成器を**かき乱す**のにこの引数を使います。つまり、この引数を使って、乱数生成器の無作為な出力を変更しているのです。 282 | 283 | また、もし関数の定義域が `Coarbitrary`で、値域が `Arbitrary`なら、 `Arbitrary`の関数を与える型クラスインスタンスが存在しています。 284 | 285 | ```haskell 286 | instance arbFunction :: (Coarbitrary a, Arbitrary b) => Arbitrary (a -> b) 287 | ``` 288 | 289 | 実は、これが意味しているのは、引数として関数を取るような性質を記述できるということです。 `mergeWith`関数の場合では、新しい引数を考慮するようにテストを修正すると、最初の引数を無作為に生成することができます。 290 | 291 | 既ソート性の性質については、必ずしも `Ord`インスタンスを持っているとは限らないので、結果がソートされているということを保証することができませんが、引数として渡す関数 `f`にしたがって結果がソートされている期待することはできます。さらに、2つの入力配列が `f`に従ってソートされている必要がありますので、 `sortBy`関数を使って関数 `f`が適用されたあとの比較に基づいて `xs`と `ys`をソートします。 292 | 293 | ```haskell 294 | quickCheck \xs ys f -> 295 | isSorted $ 296 | map f $ 297 | mergeWith (intToBool f) 298 | (sortBy (compare `on` f) xs) 299 | (sortBy (compare `on` f) ys) 300 | ``` 301 | 302 | ここでは、関数 `f`の型を明確にするために、関数 `intToBool`を使用しています。 303 | 304 | ```haskell 305 | intToBool :: (Int -> Boolean) -> Int -> Boolean 306 | intToBool = id 307 | ``` 308 | 309 | 部分配列性については、単に関数の名前を `mergeWith`に変えるだけです。引き続き入力配列は結果の部分配列になっていると期待できます。 310 | 311 | ```haskell 312 | quickCheck \xs ys f -> 313 | xs `isSubarrayOf` mergeWith (numberToBool f) xs ys 314 | ``` 315 | 316 | 関数は `Arbitrary`であるだけでなく `Coarbitrary`でもあります。 317 | 318 | ```haskell 319 | instance coarbFunction :: (Arbitrary a, Coarbitrary b) => Coarbitrary (a -> b) 320 | ``` 321 | 322 | これは値の生成が単純な関数だけに限定されるものではないことを意味しています。つまり、**高階関数**や、引数が高階関数であるような関数すら無作為に生成することができるのです。 323 | 324 | ## Coarbitraryのインスタンスを書く 325 | 326 | `Gen`の `Monad`や `Applicative`インスタンスを使って独自のデータ型に対して `Arbitrary`インスタンスを書くことができるのとちょうど同じように、独自の `Coarbitrary`インスタンスを書くこともできます。これにより、無作為に生成される関数の定義域として、独自のデータ型を使うことができるようになります。 327 | 328 | `Tree`型の `Coarbitrary`インスタンスを書いてみましょう。枝に格納されている要素の型に `Coarbitrary`インスタンスが必要になります。 329 | 330 | ```haskell 331 | instance coarbTree :: Coarbitrary a => Coarbitrary (Tree a) where 332 | ``` 333 | 334 | 型 `Tree a`の値を与えられた乱数発生器をかき乱す関数を記述する必要があります。入力値が `Leaf`であれば、そのままの生成器を返します。 335 | 336 | ```haskell 337 | coarbitrary Leaf = id 338 | ``` 339 | 340 | もし木が `Branch`なら、 341 | 関数合成で独自のかき乱し関数を作ることにより、 342 | 左の部分木、値、右の部分木を使って生成器をかき乱します。 343 | 344 | ```haskell 345 | coarbitrary (Branch l a r) = 346 | coarbitrary l <<< 347 | coarbitrary a <<< 348 | coarbitrary r 349 | ``` 350 | 351 | これで、木を引数にとるような関数を含む性質を自由に書くことができるようになりました。たとえば、 `Tree`モジュールでは述語が引数のどんな部分木についても成り立っているかを調べる関数 `anywhere`が定義されています。 352 | 353 | ```haskell 354 | anywhere :: forall a. (Tree a -> Boolean) -> Tree a -> Boolean 355 | ``` 356 | 357 | これで、無作為にこの述語関数 `anywhere`を生成することができるようになりました。例えば、 `anywhere`関数が次のような**ある命題のもとで不変**であることを期待します。 358 | 359 | ```haskell 360 | quickCheck \f g t -> 361 | anywhere (\s -> f s || g s) t == 362 | anywhere f (treeOfInt t) || anywhere g t 363 | ``` 364 | 365 | ここで、 `treeOfInt`関数は木に含まれる値の型を型 `Int`に固定するために使われています。 366 | 367 | ```haskell 368 | treeOfInt :: Tree Int -> Tree Int 369 | treeOfInt = id 370 | ``` 371 | 372 | ## 副作用のないテスト 373 | 374 | テストの目的では通常、テストスイートの `main`アクションには `quickCheck`関数の呼び出しが含まれています。しかし、副作用を使わない `quickCheckPure`と呼ばれる `quickCheck`関数の亜種もあります。 `quickCheckPure`は、入力として乱数の種をとり、テスト結果の配列を返す純粋な関数です。 375 | 376 | `PSCi`を使用して `quickCheckPure`を使ってみましょう。ここでは `merge`操作が結合法則を満たすことをテストしてみます。 377 | 378 | ```text 379 | > import Prelude 380 | > import Merge 381 | > import Test.QuickCheck 382 | > import Test.QuickCheck.LCG (mkSeed) 383 | 384 | > :paste 385 | … quickCheckPure (mkSeed 12345) 10 \xs ys zs -> 386 | … ((xs `merge` ys) `merge` zs) == 387 | … (xs `merge` (ys `merge` zs)) 388 | … ^D 389 | 390 | Success : Success : ... 391 | ``` 392 | 393 | `quickCheckPure`は乱数の種、生成するテストケースの数、テストする性質の3つの引数をとります。もしすべてのテストケースに成功したら、 `Success`データ構築子の配列がコンソールに出力されます。 394 | 395 | `quickCheckPure`は、性能ベンチマークの入力データ生成や、ウェブアプリケーションのフォームデータ例を無作為に生成するというような状況で便利かもしれません。 396 | 397 | ## 演習 398 | 399 | 1. (簡単) `Byte`と `Sorted`型構築子についての `Coarbitrary`インスタンスを書いてください。 400 | 401 | 1. (やや難しい)任意の関数 `f`について、 `mergeWith f`関数の結合性を主張する(高階)性質を書いてください。 `quickCheckPure`を使って `PSCi`でその性質をテストしてください。 402 | 403 | 1. (やや難しい)次のデータ型の `Coarbitrary`インスタンスを書いてください。 404 | 405 | ```haskell 406 | data OneTwoThree a = One a | Two a a | Three a a a 407 | ``` 408 | 409 | **ヒント**:`Test.QuickCheck.Gen`で定義された `oneOf`関数を使って `Arbitrary`インスタンスを定義します。 410 | 1. (やや難しい)`all`関数を使って `quickCheckPure`関数の結果を単純化してください。その関数はもしどんなテストもパスするなら `true`を返し、そうでなければ `false`を返さなくてはいけません。 `purescript-monoids`で定義されている `First`モノイドを、失敗時の最初のエラーを保存するために `foldMap`関数と一緒に使ってみてください。 411 | 412 | ## まとめ 413 | 414 | この章では、生成的テスティングのパラダイムを使って宣言的な方法でテストを書くための、 `purescript-quickcheck`パッケージを導入しました。 415 | 416 | - `pulp test`使ってQuickCheckをテストを自動化する方法を説明しました。 417 | - エラーメッセージを改良する ``演算子の使い方と、性質を関数として書く方法を説明しました。 418 | - `Arbitrary`と `Coarbitrary`型クラスは、定型的なテストコードの自動生成を可能にし、高階性質関数を可能にすることも説明しました。 419 | - 独自のデータ型に対して `Arbitrary`と `Coarbitrary`インスタンスを実装する方法を説明しました。 420 | 421 | -------------------------------------------------------------------------------- /src/chapter14.md: -------------------------------------------------------------------------------- 1 | # 領域特化言語 2 | 3 | ## この章の目標 4 | 5 | この章では、多数の標準的な手法を使ったPureScriptにおける**領域特化言語**(domain-specific language, DSL) の実装について探求していきます。 6 | 7 | 領域特化言語とは、特定の問題領域での開発に適した言語のことです。領域特化言語の構文および機能は、その領域内の考え方を表現するコードの読みやすさを最大限に発揮すべく選択されます。本書の中では、すでに領域特化言語の例を幾つか見てきています。 8 | 9 | - 第11章で開発された `Game`モナドと関連するアクションは、**テキストアドベンチャーゲーム開発**という領域に対しての領域特化言語を構成しています。 10 | - 第12章で `ContT`と `Parallel`関手のために書いたコンビネータのライブラリは、**非同期プログラミング**の領域に対する領域特化言語の例と考えることができます。 11 | - 第13章で扱った `purescript-quickcheck`パッケージは、**生成的テスティング**の領域の領域特化言語です。このコンビネータはテストの性質対して特に表現力の高い記法を可能にします。 12 | 13 | この章では、領域特化言語の実装において、いくつかの標準的な手法による構造的なアプローチを取ります。これがこの話題の完全な説明だということでは決してありませんが、独自の目的に対する具体的なDSLを構築するには十分な知識を与えてくれるでしょう。 14 | 15 | この章で実行している例は、HTML文書を作成するための領域特化言語になります。正しいHTML文書を記述するための型安全な言語を開発することが目的で、少しづつ実装を改善することによって作業していきます。 16 | 17 | ## プロジェクトの準備 18 | 19 | この章で使うプロジェクトには新しいBower依存性が追加されます。これから使う道具のひとつである**Freeモナド**が定義されている `purescript-free`ライブラリです。 20 | 21 | このプロジェクトのソースコードは、PSCiを使ってビルドすることができます。 22 | 23 | ## HTMLデータ型 24 | 25 | このHTMLライブラリの最も基本的なバージョンは `Data.DOM.Simple`モジュールで定義されています。このモジュールには次の型定義が含まれています。 26 | 27 | ```haskell 28 | newtype Element = Element 29 | { name :: String 30 | , attribs :: Array Attribute 31 | , content :: Maybe (Array Content) 32 | } 33 | 34 | data Content 35 | = TextContent String 36 | | ElementContent Element 37 | 38 | newtype Attribute = Attribute 39 | { key :: String 40 | , value :: String 41 | } 42 | ``` 43 | 44 | `Element`型はHTMLの要素を表しており、各要素は要素名、属性のペア​​の配列と、要素の内容でで構成されています。 `content`プロパティでは、 `Maybe`タイプを使って要素が開いている(他の要素やテキストを含む)か閉じているかを示しています。 45 | 46 | このライブラリの鍵となる機能は次の関数です。 47 | 48 | ```haskell 49 | render :: Element -> String 50 | ``` 51 | 52 | この関数はHTML要素をHTML文字列として出力します。 `PSCi`で明示的に適当な型の値を構築し、ライブラリのこのバージョンを試してみましょう。 53 | 54 | ```text 55 | $ pulp repl 56 | 57 | > import Prelude 58 | > import Data.DOM.Simple 59 | > import Data.Maybe 60 | > import Control.Monad.Eff.Console 61 | 62 | > :paste 63 | … log $ render $ Element 64 | … { name: "p" 65 | … , attribs: [ 66 | … Attribute 67 | … { key: "class" 68 | … , value: "main" 69 | … } 70 | … ] 71 | … , content: Just [ 72 | … TextContent "Hello World!" 73 | … ] 74 | … } 75 | … ^D 76 | 77 |

Hello World!

78 | unit 79 | ``` 80 | 81 | 現状のライブラリにはいくつかの問題があります。 82 | 83 | - HTML文書の作成に手がかかります。すべての新しい要素が少なくとも1つのレコードと1つのデータ構築子が必要です。 84 | - 無効な文書を表現できてしまいます。 85 | - 要素名の入力を間違えるかもしれません 86 | - 要素に間違った型の属性を関連付けることができてしまいます 87 | - 開いた要素が正しい場合でも、閉じた要素を使用することができてしまいます 88 | 89 | この章では、さまざまな手法を用いてこれらの問題を解決し、このライブラリーをHTML文書を作成するために使える領域特化言語にしていきます。 90 | 91 | ## スマート構築子 92 | 93 | 最初に導入する手法は方法は単純なものですが、とても効果的です。モジュールの使用者にデータの表現を露出する代わりに、モジュールエクスポートリスト(module exports list)を使ってデータ構築子 `Element`、 `Content`、 `Attribute`を隠蔽し、正しいことが明らかなデータだけ構築する、いわゆる**スマート構築子**(smart constructors)だけをエクスポートします。 94 | 95 | 例を示しましょう。まず、HTML要素を作成するための便利な関数を提供します。 96 | 97 | ```haskell 98 | element :: String -> Array Attribute -> Maybe (Array Content) -> Element 99 | element name attribs content = Element 100 | { name: name 101 | , attribs: attribs 102 | , content: content 103 | } 104 | ``` 105 | 106 | 次に、 `element`関数を適用することによってHTML要素を作成する、スマート構築子を作成します。 107 | 108 | ```haskell 109 | a :: Array Attribute -> Array Content -> Element 110 | a attribs content = element "a" attribs (Just content) 111 | 112 | p :: Array Attribute -> Array Content -> Element 113 | p attribs content = element "p" attribs (Just content) 114 | 115 | img :: Array Attribute -> Element 116 | img attribs = element "img" attribs Nothing 117 | ``` 118 | 119 | 最後に、正しいデータ構造だけを構築することがわかっているこれらの関数をエクスポートするように、モジュールエクスポートリストを更新します。 120 | 121 | ```haskell 122 | module Data.DOM.Smart 123 | ( Element 124 | , Attribute(..) 125 | , Content(..) 126 | 127 | , a 128 | , p 129 | , img 130 | 131 | , render 132 | ) where 133 | ``` 134 | 135 | モジュールエクスポートリストはモジュール名の直後の括弧内に書きます。各モジュールのエクスポートは次の3種類のいずれかです。 136 | 137 | - 値の名前で示された、値(または関数) 138 | - クラスの名で示された、型クラス 139 | - 型の名前で示された型構築子、およびそれに続けて括弧で囲まれた関連するデータ構築子のリスト 140 | 141 | ここでは、 `Element`の**型**をエクスポートしていますが、データ構築子はエクスポートしていません。もしデータ構築子をエクスポートすると、モジュールの使用者が不正なHTML要素を構築できてしまいます。 142 | 143 | `Attribute`と `Content`型についてはデータ構築子をすべてエクスポートしています(エクスポートリストの記号 `..`で示されています)。これから、これらの型にスマート構築子の手法を適用していきます。 144 | 145 | すでにライブラリにいくつかの大きな改良を加わっていることに注意してください。 146 | 147 | - 不正な名前を持つHTML要素を表現することは不可能です(もちろん、ライブラリが提供する要素名に制限されています)。 148 | - 閉じた要素は、構築するときに内容を含めることはできません。 149 | 150 | `Content`型にもとても簡単にこの手法を適用することができます。単にエクスポートリストから `Content`型のデータ構築子を取り除き、次のスマート構築子を提供します。 151 | 152 | ```haskell 153 | text :: String -> Content 154 | text = TextContent 155 | 156 | elem :: Element -> Content 157 | elem = ElementContent 158 | ``` 159 | 160 | `Attribute`型にも同じ手法を適用してみましょう。まず、属性のための汎用のスマート構築子を用意します。最初の試みとしては、次のようなものになるかもしれません。 161 | 162 | ```haskell 163 | attribute :: String -> String -> Attribute 164 | attribute key value = Attribute 165 | { key: key 166 | , value: value 167 | } 168 | 169 | infix 4 attribute as := 170 | ``` 171 | 172 | この定義では元の `Element`型と同じ問題に悩まされています。存在しなかったり、名前が間違っているような属性を表現することが可能です。この問題を解決するために、属性名を表すnewtypeを作成します。 173 | 174 | ```haskell 175 | newtype AttributeKey = AttributeKey String 176 | ``` 177 | 178 | それから、この演算子を次のように変更します。 179 | 180 | ```haskell 181 | attribute :: AttributeKey -> String -> Attribute 182 | attribute (AttributeKey key) value = Attribute 183 | { key: key 184 | , value: value 185 | } 186 | ``` 187 | 188 | `AttributeKey`データ構築子をエクスポートしなければ、明示的にエクスポートされた次のような関数を使う以外に、使用者が型 `AttributeKey`の値を構築する方法はありません。いくつかの例を示します。 189 | 190 | ```haskell 191 | href :: AttributeKey 192 | href = AttributeKey "href" 193 | 194 | _class :: AttributeKey 195 | _class = AttributeKey "class" 196 | 197 | src :: AttributeKey 198 | src = AttributeKey "src" 199 | 200 | width :: AttributeKey 201 | width = AttributeKey "width" 202 | 203 | height :: AttributeKey 204 | height = AttributeKey "height" 205 | ``` 206 | 207 | 新しいモジュールの最終的なエクスポートリストは次のようになります。もうどんなデータ構築子も直接エクスポートしていないことに注意してください。 208 | 209 | ```haskell 210 | module Data.DOM.Smart 211 | ( Element 212 | , Attribute 213 | , Content 214 | , AttributeKey 215 | 216 | , a 217 | , p 218 | , img 219 | 220 | , href 221 | , _class 222 | , src 223 | , width 224 | , height 225 | 226 | , attribute, (:=) 227 | , text 228 | , elem 229 | 230 | , render 231 | ) where 232 | ``` 233 | 234 | `PSCi`でこの新しいモジュールを試してみると、コードが大幅に簡潔になり、改良されていることがわかります。 235 | 236 | ```text 237 | $ pulp repl 238 | 239 | > import Prelude 240 | > import Data.DOM.Smart 241 | > import Control.Monad.Eff.Console 242 | > log $ render $ p [ _class := "main" ] [ text "Hello World!" ] 243 | 244 |

Hello World!

245 | unit 246 | ``` 247 | 248 | しかし、基礎のデータ表現が変更されていないので、 `render`関数を変更する必要はなかったことにも注目してください。これはスマート構築子による手法の利点のひとつです。外部APIの使用者によって認識される表現から、モジュールの内部データ表現を分離することができるのです。 249 | 250 | ## 演習 251 | 252 | 1. (簡単)`Data.DOM.Smart`モジュールで `render`を使った新しいHTML文書の作成を試してみましょう。 253 | 254 | 1. (やや難しい) `checked`と `disabled`など、値を要求しないHTML属性がありますが、これらは次のような**空の属性**として表示されるかもしれません。 255 | 256 | ```html 257 | <input disabled> 258 | ``` 259 | 260 | 空の属性を扱えるように `Attribute`の表現を変更してください。要素に空の属性を追加するために、 `attribute`または `:=`の代わりに使える関数を記述してください。 261 | 262 | ## 幻影型 263 | 264 | 次に適用する手法についての動機を与えるために、次のコードを考えてみます。 265 | 266 | ```text 267 | > log $ render $ img 268 | [ src := "cat.jpg" 269 | , width := "foo" 270 | , height := "bar" 271 | ] 272 | 273 | <img src="cat.jpg" width="foo" height="bar" /> 274 | unit 275 | ``` 276 | 277 | ここでの問題は、 `width`と `height`についての文字列値を提供しているということで、ここで与えることができるのはピクセルやパーセントの単位の数値だけであるべきです。 278 | 279 | `AttributeKey`型にいわゆる**幻影型**(phantom type)引数を導入すると、この問題を解決できます。 280 | 281 | ```haskell 282 | newtype AttributeKey a = AttributeKey String 283 | ``` 284 | 285 | 定義の右辺に対応する型 `a`の値が存在しないので、この型変数 `a`は**幻影型**と呼ばれています。この型 `a`はコンパイル時により多くの情報を提供するためだけに存在しています。任意の型 `AttributeKey a`の値は実行時には単なる文字列ですが、そのキーに関連付けられた値に期待されている型を教えてくれます。 286 | 287 | `AttributeKey`の新しい形式で受け取るように、 `attribute`関数の型を次のように変更します。 288 | 289 | ```haskell 290 | attribute :: forall a. IsValue a => AttributeKey a -> a -> Attribute 291 | attribute (AttributeKey key) value = Attribute 292 | { key: key 293 | , value: toValue value 294 | } 295 | ``` 296 | 297 | ここで、幻影型の引数 `a`は、属性キーと属性値が互換性のある型を持っていることを確認するために使われます。使用者は `AttributeKey a`を型の値を直接作成できないので(ライブラリで提供されている定数を介してのみ得ることができます)、すべての属性が正しくなります。 298 | 299 | `IsValue`制約は、キーに関連付けられた値がなんであれ、その値を文字列に変換し、生成したHTML内に出力できることを保証します。 `IsValue`型クラスは次のように定義されています。   300 | 301 | ```haskell 302 | class IsValue a where 303 | toValue :: a -> String 304 | ``` 305 | 306 | `String`と `Int`型についての型クラスインスタンスも提供しておきます。 307 | 308 | ```haskell 309 | instance stringIsValue :: IsValue String where 310 | toValue = id 311 | 312 | instance intIsValue :: IsValue Int where 313 | toValue = show 314 | ``` 315 | 316 | また、これらの型が新しい型変数を反映するように、 `AttributeKey`定数を更新しなければいけません。 317 | 318 | ```haskell 319 | href :: AttributeKey String 320 | href = AttributeKey "href" 321 | 322 | _class :: AttributeKey String 323 | _class = AttributeKey "class" 324 | 325 | src :: AttributeKey String 326 | src = AttributeKey "src" 327 | 328 | width :: AttributeKey Int 329 | width = AttributeKey "width" 330 | 331 | height :: AttributeKey Int 332 | height = AttributeKey "height" 333 | ``` 334 | 335 | これで、不正なHTML文書を表現することが不可能で、 `width`と `height`属性を表現するのに数を使うことが強制されていることがわかります。 336 | 337 | ```text 338 | > import Prelude 339 | > import Data.DOM.Phantom 340 | > import Control.Monad.Eff.Console 341 | 342 | > :paste 343 | … log $ render $ img 344 | … [ src := "cat.jpg" 345 | … , width := 100 346 | … , height := 200 347 | … ] 348 | … ^D 349 | 350 | <img src="cat.jpg" width="100" height="200" /> 351 | unit 352 | ``` 353 | 354 | ## 演習 355 | 356 | 1. (簡単) ピクセルまたはパーセントの長さのいずれかを表すデータ型を作成してください。その型について `IsValue`のインスタンスを書いてください。この型を使うように `width`と `height`属性を変更してください。 357 | 358 | 1. (難しい) 幻影型を使って真偽値 `true`、 `false`についての表現を最上位で定義することで、 `AttributeKey`が `disabled`や `chacked`のような**空の属性**を表現しているかどうかを符号化することができます。 359 | 360 | 361 | ```haskell 362 | data True 363 | data False 364 | ``` 365 | 366 | 幻影型を使って、使用者が `attribute`演算子を空の属性に対して使うことを防ぐように、前の演習の解答を変更してください。 367 | 368 | ## Freeモナド 369 | 370 | APIに施す最後の変更は、 `Content`型をモナドにしてdo記法を使えるようにするために、**Freeモナド**と呼ばれる構造を使うことです。Freeモナドは、入れ子になった要素をわかりやすくなるよう、HTML文書の構造化を可能にします。次のようなコードを考えます。 371 | 372 | ```haskell 373 | p [ _class := "main" ] 374 | [ elem $ img 375 | [ src := "cat.jpg" 376 | , width := 100 377 | , height := 200 378 | ] 379 | , text "A cat" 380 | ] 381 | ``` 382 | 383 | これを次のように書くことができるようになります。 384 | 385 | ```haskell 386 | p [ _class := "main" ] $ do 387 | elem $ img 388 | [ src := "cat.jpg" 389 | , width := 100 390 | , height := 200 391 | ] 392 | text "A cat" 393 | ``` 394 | 395 | しかし、do記法だけがFreeモナドの恩恵だというわけではありません。モナドのアクションの**表現**をその**解釈**から分離し、同じアクションに**複数の解釈**を持たせることをFreeモナドは可能にします。 396 | 397 | `Free`モナドは `purescript-free`ライブラリの `Control.Monad.Free`モジュールで定義されています。 `PSCi`を使うと、次のようにFreeモナドについての基本的な情報を見ることができます。 398 | 399 | ```text 400 | > import Control.Monad.Free 401 | 402 | > :kind Free 403 | (Type -> Type) -> Type -> Type 404 | ``` 405 | 406 | `Free`の種は、引数として型構築子を取り、別の型構築子を返すことを示しています。実は、 `Free`モナドは任意の `Functor`を `Monad`にするために使うことができます! 407 | 408 | モナドのアクションの**表現**を定義することから始めます。これを行うには、サポートする各モナドアクションそれぞれについて、ひとつのデータ構築子を持つ `Functor`を作成する必要があります。今回の場合、2つのモナドのアクションは `elem`と `text`になります。実際には、 `Content`型を次のように変更するだけです。 409 | 410 | ```haskell 411 | data ContentF a 412 | = TextContent String a 413 | | ElementContent Element a 414 | 415 | instance functorContentF :: Functor ContentF where 416 | map f (TextContent s x) = TextContent s (f x) 417 | map f (ElementContent e x) = ElementContent e (f x) 418 | ``` 419 | 420 | ここで、この `ContentF`型構築子は以前の `Content`データ型とよく似ています。 `Functor`インスタンスでは、単に各データ構築子で型 `a`の構成要素に関数 `f`を適用します。 421 | 422 | これにより、最初の型引数として `ContentF`型構築子を使うことで構築された、新しい `Content`型構築子を `Free`モナドを包むnewtypeとして定義することができます。 423 | 424 | ```haskell 425 | type Content = Free ContentF 426 | ``` 427 | 428 | 型のシノニムの代わりにnewtypeを使用して、使用者に対してライブラリの内部表現を露出することを避ける事ができます。 `Content`データ構築子を隠すことで、提供しているモナドのアクションだけを使うことを仕様者に制限しています。 429 | 430 | `ContentF`は `Functor`なので、 `Free ContentF`に対する `Monad`インスタンスが自動的に手に入り、このインスタンスを `Content`上の `Monad`インスタンスへと持ち上げることができます。 431 | 432 | `Content`の新しい型引数を考慮するように、少し `Element`データ型を変更する必要があります。モナドの計算の戻り値の型が `Unit`であることだけが要求されます。 433 | 434 | ```haskell 435 | newtype Element = Element 436 | { name :: String 437 | , attribs :: Array Attribute 438 | , content :: Maybe (Content Unit) 439 | } 440 | ``` 441 | 442 | また、 `Content`モナドについての新しいモナドのアクションになる `elem`と `text`関数を変更する必要があります。これを行うには、 `Control.Monad.Free`モジュールで提供されている `liftF`関数を使います。この関数の(簡略化された)型は次のようになっています。 443 | 444 | ```haskell 445 | liftF :: forall f a. (Functor f) => f a -> Free f a 446 | ``` 447 | 448 | `liftF`は、何らかの型 `a`について、型 `f a`の値からFreeモナドのアクションを構築できるようにします。今回の場合、 `ContentF`型構築子のデータ構築子を次のようにそのまま使うだけです。 449 | 450 | ```haskell 451 | text :: String -> Content Unit 452 | text s = liftF $ TextContent s unit 453 | 454 | elem :: Element -> Content Unit 455 | elem e = liftF $ ElementContent e unit 456 | ``` 457 | 458 | 他にもコードの変更はありますが、興味深い変更は `render`関数に対してのものです。ここでは、このFreeモナドを**解釈**しなければいけません。 459 | 460 | ## モナドの解釈 461 | 462 | `Control.Monad.Free`モジュールでは、Freeモナドで計算を解釈するための多数の関数が提供されています。 463 | 464 | ```haskell 465 | runFree 466 | :: forall f a 467 | . Functor f 468 | => (f (Free f a) -> Free f a) 469 | -> Free f a 470 | -> a 471 | 472 | runFreeM 473 | :: forall f m a 474 | . (Functor f, MonadRec m) 475 | => (f (Free f a) -> m (Free f a)) 476 | -> Free f a 477 | -> m a 478 | ``` 479 | 480 | `runFree`関数は、**純粋な**結果を計算するために使用されます。 `runFreeM`関数は、フリーモナドの動作を解釈するためにモナドを使用することを可能にします 481 | 482 | 厳密には、 `MonadRec`のより強い制約を満たすモナド `m`を使用する制限がされています。これはスタックオーバーフローを心配する必要がないことを意味します。なぜなら `m`は安全な**末尾再帰モナド**(monadic tail recursion)をサポートするからです。 483 | 484 | まず、アクションを解釈することができるモナドを選ばなければなりません。 `Writer String`モナドを使って、結果のHTML文字列を累積することにします。 485 | 486 | 新しい `render`メソッドは補助関数 `renderElement`に移譲して開始し、 `Writer`モナドで計算を実行するため `execWriter`を使用します。 487 | 488 | ```haskell 489 | render :: Element -> String 490 | render = execWriter <<< renderElement 491 | ``` 492 | 493 | `renderElement`はwhereブロックで定義されています。 494 | 495 | ```haskell 496 | where 497 | renderElement :: Element -> Writer String Unit 498 | renderElement (Element e) = do 499 | ``` 500 | 501 | `renderElement`の定義は簡単で、いくつかの小さな文字列を累積するために `Writer`モナドの `tell`アクションを使っています。 502 | 503 | ```haskell 504 | tell "<" 505 | tell e.name 506 | for_ e.attribs $ \x -> do 507 | tell " " 508 | renderAttribute x 509 | renderContent e.content 510 | ``` 511 | 512 | 次に、同じように簡単な `renderAttribute`関数を定義します。 513 | 514 | ```haskell 515 | where 516 | renderAttribute :: Attribute -> Writer String Unit 517 | renderAttribute (Attribute x) = do 518 | tell x.key 519 | tell "=\"" 520 | tell x.value 521 | tell "\"" 522 | ``` 523 | 524 | `renderContent`関数は、もっと興味深いものです。ここでは、 `runFreeM`関数を使って、Freeモナドの内部で補助関数 `renderContentItem`に移譲する計算を解釈しています。 525 | 526 | ```haskell 527 | renderContent :: Maybe (Content Unit) -> Writer String Unit 528 | renderContent Nothing = tell " />" 529 | renderContent (Just content) = do 530 | tell ">" 531 | runFreeM renderContentItem content 532 | tell "" 535 | ``` 536 | 537 | `renderContentItem`の型は `runFreeM`の型シグネチャから推測することができます。関手 `f`は型構築子 `ContentF`で、モナド `m`は解釈している計算のモナド、つまり `Writer String`です。これにより `renderContentItem`について次の型シグネチャがわかります。 538 | 539 | ```haskell 540 | renderContentItem :: ContentF (Content Unit) -> Writer String (Content Unit) 541 | ``` 542 | 543 | `ContentF`の二つのデータ構築子でパターン照合するだけで、この関数を実装することができます。 544 | 545 | ```haskell 546 | renderContentItem (TextContent s rest) = do 547 | tell s 548 | pure rest 549 | renderContentItem (ElementContent e rest) = do 550 | renderElement e 551 | pure rest 552 | ``` 553 | 554 | それぞれの場合において、式 `rest`は型 `Writer String`を持っており、解釈計算の残りを表しています。 `rest`アクションを呼び出すことによって、それぞれの場合を完了することができます。 555 | 556 | これで完了です!`PSCi`で、次のように新しいモナドのAPIを試してみましょう。 557 | 558 | ```text 559 | > import Prelude 560 | > import Data.DOM.Free 561 | > import Control.Monad.Eff.Console 562 | 563 | > :paste 564 | … log $ render $ p [] $ do 565 | … elem $ img [ src := "cat.jpg" ] 566 | … text "A cat" 567 | … ^D 568 | 569 | <p><img src="cat.jpg" />A cat</p> 570 | unit 571 | ``` 572 | 573 | ## 演習 574 | 575 | 1. (やや難しい)`ContentF`型に新しいデータ構築子を追加して、生成されたHTMLにコメントを出力する新しいアクション `comment`に対応してください。 `liftF`を使ってこの新しいアクションを実装してください。新しい構築子を適切に解釈するように、解釈 `renderContentItem`を更新してください。 576 | 577 | ## 言語の拡張 578 | 579 | すべてのアクションが型 `Unit`の何かを返すようなモナドは、さほど興味深いものではありません。実際のところ、概ね良くなったと思われる構文は別として、このモナドは `Monoid`以上の機能は何の追加していません。 580 | 581 | 意味のある結果を返す新しいモナドアクションでこの言語を拡張することで、Freeモナド構造の威力を説明しましょう​​。 582 | 583 | **アンカー**を使用して文書のさまざまな節へのハイパーリンクが含まれているHTML文書を生成するとします。手作業でアンカーの名前を生成すればいいので、これは既に実現できています。文書中で少なくとも2回、ひとつはアンカーの定義自身に、もうひとつはハイパーリンクに、アンカーが含まれています。しかし、この方法には根本的な問題がいくつかあります。 584 | 585 | - 開発者は一意なアンカー名を生成するために失敗することがあります。 586 | - 開発者は、アンカー名のひとつまたは複数のインスタンスを誤って入力するかもしれません。 587 | 588 | 自分の間違いから開発者を保護するために、アンカー名を表す新しい型を導入し、新しい一意な名前を生成するためのモナドアクションを提供することができます。 589 | 590 | 最初の手順は、名前の型を新しく追加することです。 591 | 592 | ```haskell 593 | newtype Name = Name String 594 | 595 | runName :: Name -> String 596 | runName (Name n) = n 597 | ``` 598 | 599 | 繰り返しになりますが、 `Name`は `String`のnewtypeとして定義しており、モジュールのエクスポートリスト内でデータ構築子をエクスポートしないように注意する必要があります。 600 | 601 | 次に、属性値として `Name`を使うことができるように、新しい型 `IsValue`型クラスのインスタンスを定義します。 602 | 603 | ```haskell 604 | instance nameIsValue :: IsValue Name where 605 | toValue (Name n) = n 606 | ``` 607 | 608 | また、次のように `a`要素に現れるハイパーリンクの新しいデータ型を定義します。 609 | 610 | ```haskell 611 | data Href 612 | = URLHref String 613 | | AnchorHref Name 614 | 615 | instance hrefIsValue :: IsValue Href where 616 | toValue (URLHref url) = url 617 | toValue (AnchorHref (Name nm)) = "#" <> nm 618 | ``` 619 | 620 | `href`属性の型の値を変更して、この新しい `Href`型の使用を強制します。また、要素をアンカーに変換するのに使う新しい `name`属性を作成します。 621 | 622 | ```haskell 623 | href :: AttributeKey Href 624 | href = AttributeKey "href" 625 | 626 | name :: AttributeKey Name 627 | name = AttributeKey "name" 628 | ``` 629 | 630 | 残りの問題は、現在モジュールの使用者が新しい名前を生成する方法がないということです。 `Content`モナドでこの機能を提供することができます。まず、 `ContentF`型構築子に新しいデータ構築子を追加する必要があります。 631 | 632 | ```haskell 633 | data ContentF a 634 | = TextContent String a 635 | | ElementContent Element a 636 | | NewName (Name -> a) 637 | ``` 638 | 639 | `NewName`データ構築子は型 `Name`の値を返すアクションに対応しています。データ構築子の引数として `Name`を要求するのではなく、型 `Name -> a`の**関数**を提供するように使用者に要求していることに注意してください。型 `a`は**計算の残り**を表していることを思い出すと、この関数は、型 `Name`の値が返されたあとで、計算を継続する方法を提供するというように直感的に理解することができます。 640 | 641 | 新しいデータ構築子を考慮するように、 `ContentF`についての `Functor`インスタンスを更新する必要があります。 642 | 643 | ```haskell 644 | instance functorContentF :: Functor ContentF where 645 | map f (TextContent s x) = TextContent s (f x) 646 | map f (ElementContent e x) = ElementContent e (f x) 647 | map f (NewName k) = NewName (f <<< k) 648 | ``` 649 | 650 | そして、先ほど述べたように、 `liftF`関数を使うと新しいアクションを構築することができます。 651 | 652 | ```haskell 653 | newName :: Content Name 654 | newName = liftF $ NewName id 655 | ``` 656 | 657 | `id`関数を継続として提供していることに注意してください。型 `Name`の結果を変更せずに返すということを意味しています。 658 | 659 | 最後に、新しいアクションを解釈するために、解釈関数を更新する必要があります。以前は計算を解釈するために `Writer String`モナドを使っていましたが、このモナドは新しい名前を生成する能力を持っていないので、何か他のものに切り替えなければなりません。`WriterT`モナド変換子を`State`モナドと一緒に使うと、必要な作用を組み合わせることができます。型注釈を短く保てるように、この解釈モナドを型同義語として定義しておきます。 660 | 661 | ```haskell 662 | type Interp = WriterT String (State Int) 663 | ``` 664 | 665 | Int型の引数は状態の型で、この場合は増加していくカウンタとして振る舞う数であり、一意な名前を生成するのに使われます。 666 | 667 | `Writer`と `WriterT`モナドはそれらのアクションを抽象化するのに同じ型クラスメンバを使うので、どのアクションも変更する必要がありません。必要なのは、 `Writer String`への参照すべてを `Interp`で置き換えることだけです。しかし、この計算を実行するために使われるハンドラを変更しなければいけません。 `execWriter`の代わりに、 `evalState`を使います。 668 | 669 | ```haskell 670 | render :: Element -> String 671 | render e = evalState (execWriterT (renderElement e)) 0 672 | ``` 673 | 674 | 新しい `NewName`データ構築子を解釈するために、 `renderContentItem`に新しい場合分けを追加しなければいけません。 675 | 676 | ```haskell 677 | renderContentItem (NewName k) = do 678 | n <- get 679 | let fresh = Name $ "name" <> show n 680 | put $ n + 1 681 | pure (k fresh) 682 | ``` 683 | 684 | ここで、型 `Name -> Interp a`の継続 `k`が与えられているので、型 `Interp a`の解釈を構築しなければいけません。この解釈は単純です。 `get`を使って状態を読み、その状態を使って一意な名前を生成し、それから `put`で状態をインクリメントしています。最後に、継続にこの新しい名前を渡して、計算を完了します。 685 | 686 | これにより、 `PSCi`で、 `Content`モナドの内部で一意な名前を生成し、要素の名前とハイパーリンクのリンク先の両方を使って、この新しい機能を試してみましょう。 687 | 688 | ```text 689 | > import Prelude 690 | > import Data.DOM.Name 691 | > import Control.Monad.Eff.Console 692 | 693 | > :paste 694 | … render $ p [ ] $ do 695 | … top <- newName 696 | … elem $ a [ name := top ] $ 697 | … text "Top" 698 | … elem $ a [ href := AnchorHref top ] $ 699 | … text "Back to top" 700 | … ^D 701 | 702 |

TopBack to top

703 | unit 704 | ``` 705 | 706 | 複数回の `newName`呼び出しの結果が、実際に一意な名前になっていることを確かめてみてください。 707 | 708 | ## 演習 709 | 710 | 1. (やや難しい) 使用者から `Element`型を隠蔽すると、さらにAPIを簡素化することができます。次の手順に従って、これらの変更を行ってください。 711 | 712 | - `p`や `img`のような(返り値が `Element`の)関数を `elem`アクションと結合して、型 `Content Unit`を返す新しいアクションを作ってください。 713 | - 型 `Content a`の引数を許容し、結果の型 `Tuple String`を返すように、 `render`関数を変更してください。 714 | 715 | 1. (やや難しい) 型同義語の代わりに `newtype`を使って `Content`モナドの実装を隠し、 `newtype`のためにデータ構築子をエクスポートしないでください。 716 | 717 | 1. (難しい) `ContentF`型を変更して、次の新しいアクションをサポートするようにしてください。 718 | 719 | ```haskell 720 | isMobile :: Content Boolean 721 | ``` 722 | 723 |   このアクションは、この文書がモバイルデバイス上での表示のためにレンダリングされているかどうかを示す真偽値を返します。 724 |   725 |   **ヒント**:`ask`アクションと`ReaderT`型変換子を使って、このアクションを解釈してみてください。あるいは、`RWS`モナドを使うほうが好みの人もいるかもしれません。 726 | 727 | ## まとめ 728 | 729 | この章では、いくつかの標準的な技術を使って、単純な実装を段階的に改善することにより、HTML文書を作成するための領域特化言語を開発しました。 730 | 731 | - データ表現の詳細を隠蔽し、**構築方法により正しい**文書を作ることだけを許可するために、**スマート構築子**を使いました。 732 | - 言語の構文を改善するために、**ユーザ定義の中置2項演算子**を使用しました。 733 | - 使用者が間違った型の属性値を提供するのを防ぐために、データの型に追加の情報を符号化する**幻影型**を使用しました。 734 | - **Freeモナド**を使って、内容の集まりの配列的な表現を、do表記を提供するモナド的な表現に変換しました。この表現を拡張してモナドの新しいアクションを提供し、標準のモナド型変換子でモナドの計算を解釈しました。 735 | 736 | 使用者が間違いを犯すのを防ぎ、領域特化言語の構文を改良するために、これらの手法はすべてPureScriptのモジュールと型システムを活用しています。 737 | 738 | 関数型プログラミング言語による領域特化言語の実装は活発に研究されている分野ですが、いくつかの簡単なテクニックに対して役に立つ導入を提供し、表現力豊かな型を持つ言語で作業すること威力を示すことができていれば幸いです。 739 | 740 | -------------------------------------------------------------------------------- /src/index.md: -------------------------------------------------------------------------------- 1 | ## 本書の内容についての注意事項 2 | 3 | **この翻訳の原著である ["PureScript by Example"](https://leanpub.com/purescript/read) は現在更新が途絶えており、この翻訳も古い内容のままになっています。そのため、本書の内容は最新の言語仕様に沿っておらず、サンプルプログラムの多くは最新の処理系ではコンパイルすることができません。変更のない部分については本書の内容も参考になりますが、もし本書を読み進める場合には十分ご注意ください。最新の情報については、[purescript/documentation](https://github.com/purescript/documentation) (英語) などを参考にしてください。** 4 | 5 | ## 目次 6 | 7 | ## そのほかのフォーマット 8 | 9 | - [原書(英語)](https://leanpub.com/purescript/read) 10 | - [原書 GitHub リポジトリ](https://github.com/paf31/purescript-book) 11 | - [単一 HTML ページ版](purescript-book-ja.html) 12 | - [EPUB 版](purescript-book-ja.epub) 13 | - [PDF 版](purescript-book-ja.pdf) 14 | 15 | ## ライセンス 16 | 17 | This book is licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License](http://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US). 18 | 19 | 本書は[クリエイティブコモンズ 表示 - 非営利 - 継承 3.0 非移植ライセンス](http://creativecommons.org/licenses/by-nc-sa/3.0/deed.ja)でライセンスされています。 20 | 21 | ## 更新履歴 22 | 23 | - 2018/04/28 バージョン 0.11 での変更に合わせ修正 24 | - 2015/07/01 バージョン 0.7 での変更についての注意書きの追加 25 | - 2015/04/09 公開 26 | 27 | --- 28 | 29 | Japanese translation by Hiruberuto and contributors 30 | -------------------------------------------------------------------------------- /templates/default.epub: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $for(author-meta)$ 8 | 9 | $endfor$ 10 | $if(date-meta)$ 11 | 12 | $endif$ 13 | $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ 14 | 15 | $if(quotes)$ 16 | 17 | $endif$ 18 | $if(highlighting-css)$ 19 | 22 | $endif$ 23 | 24 | $for(css)$ 25 | 26 | $endfor$ 27 | 28 | 29 | 30 | 57 | 58 | 59 | 60 | $if(math)$ 61 | $math$ 62 | $endif$ 63 | $for(header-includes)$ 64 | $header-includes$ 65 | $endfor$ 66 | 67 | 68 |
69 | $for(include-before)$ 70 | $include-before$ 71 | $endfor$ 72 | 73 | $if(toc)$ 74 |
75 | $toc$ 76 |
77 | $endif$ 78 | $body$ 79 | $for(include-after)$ 80 | $include-after$ 81 | $endfor$ 82 | 83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /templates/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 実例によるPureScript 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | 32 |
33 |

実例によるPureScript

34 |

ウェブのための関数型プログラミング

35 |

36 | 37 | Phil Freeman, "PureScript by Example - Functional Programming for the Web" 38 | 39 |

40 |
41 |
42 | 43 |
44 |
45 |
46 | 47 | 48 | --------------------------------------------------------------------------------