├── exercises ├── chapter9 │ ├── test │ │ ├── data │ │ │ ├── tree │ │ │ │ ├── b │ │ │ │ │ ├── a.txt │ │ │ │ │ ├── c │ │ │ │ │ │ └── a.txt │ │ │ │ │ └── b.txt │ │ │ │ ├── c │ │ │ │ │ ├── a │ │ │ │ │ │ └── a.txt │ │ │ │ │ └── unused.txt │ │ │ │ ├── a.txt │ │ │ │ ├── unused.txt │ │ │ │ ├── root.txt │ │ │ │ └── expected.txt │ │ │ ├── many │ │ │ │ ├── file1.txt │ │ │ │ ├── file2.txt │ │ │ │ ├── file3.txt │ │ │ │ ├── file4.txt │ │ │ │ ├── file5.txt │ │ │ │ ├── file6.txt │ │ │ │ ├── file7.txt │ │ │ │ ├── file8.txt │ │ │ │ └── file9.txt │ │ │ ├── foo.txt │ │ │ ├── nbChars.txt │ │ │ ├── bar.txt │ │ │ ├── manyConcat.txt │ │ │ └── user.txt │ │ ├── data-out │ │ │ └── foo.txt │ │ ├── MySolutions.purs │ │ ├── HTTP.purs │ │ ├── ParallelFetch.purs │ │ ├── ParallelDelay.purs │ │ ├── Copy.purs │ │ ├── no-peeking │ │ │ └── Solutions.purs │ │ └── Main.purs │ ├── src │ │ └── Main.purs │ ├── .gitignore │ ├── packages.dhall │ ├── package.json │ └── spago.dhall ├── chapter4 │ ├── test │ │ ├── MySolutions.purs │ │ ├── no-peeking │ │ │ └── Solutions.purs │ │ └── Main.purs │ ├── src │ │ ├── Data │ │ │ ├── Person.purs │ │ │ └── Picture.purs │ │ ├── Main.purs │ │ └── ChapterExamples.purs │ ├── .gitignore │ ├── packages.dhall │ └── spago.dhall ├── chapter2 │ ├── test │ │ ├── MySolutions.purs │ │ ├── no-peeking │ │ │ └── Solutions.purs │ │ └── Main.purs │ ├── .gitignore │ ├── src │ │ ├── Main.purs │ │ └── Euler.purs │ ├── packages.dhall │ └── spago.dhall ├── chapter10 │ ├── src │ │ ├── index.js │ │ ├── Effect │ │ │ ├── Alert.js │ │ │ ├── Alert.purs │ │ │ ├── Storage.js │ │ │ └── Storage.purs │ │ ├── index.html │ │ ├── Data │ │ │ ├── AddressBook.purs │ │ │ └── AddressBook │ │ │ │ └── Validation.purs │ │ └── Main.purs │ ├── test │ │ ├── URI.js │ │ ├── MySolutions.js │ │ ├── URI.purs │ │ ├── MySolutions.purs │ │ ├── no-peeking │ │ │ ├── Solutions.js │ │ │ └── Solutions.purs │ │ ├── Examples.js │ │ └── Examples.purs │ ├── .gitignore │ ├── packages.dhall │ ├── html │ │ └── index.html │ ├── package.json │ └── spago.dhall ├── chapter8 │ ├── src │ │ ├── index.js │ │ ├── index.html │ │ ├── Data │ │ │ ├── AddressBook.purs │ │ │ └── AddressBook │ │ │ │ └── Validation.purs │ │ └── Main.purs │ ├── test │ │ ├── MySolutions.purs │ │ ├── Random.purs │ │ ├── Examples.purs │ │ ├── no-peeking │ │ │ └── Solutions.purs │ │ └── Main.purs │ ├── .gitignore │ ├── packages.dhall │ ├── package.json │ └── spago.dhall ├── chapter3 │ ├── test │ │ ├── MySolutions.purs │ │ ├── no-peeking │ │ │ └── Solutions.purs │ │ └── Main.purs │ ├── .gitignore │ ├── src │ │ ├── Main.purs │ │ └── Data │ │ │ └── AddressBook.purs │ ├── packages.dhall │ └── spago.dhall ├── chapter5 │ ├── test │ │ ├── MySolutions.purs │ │ ├── Examples.purs │ │ └── no-peeking │ │ │ └── Solutions.purs │ ├── .gitignore │ ├── src │ │ ├── Main.purs │ │ └── Data │ │ │ └── Path.purs │ ├── packages.dhall │ └── spago.dhall ├── chapter6 │ ├── test │ │ └── MySolutions.purs │ ├── .gitignore │ ├── packages.dhall │ ├── src │ │ ├── Main.purs │ │ └── Data │ │ │ └── Hashable.purs │ └── spago.dhall ├── chapter7 │ ├── test │ │ ├── MySolutions.purs │ │ └── no-peeking │ │ │ └── Solutions.purs │ ├── .gitignore │ ├── src │ │ ├── Main.purs │ │ └── Data │ │ │ ├── AddressBook.purs │ │ │ └── AddressBook │ │ │ └── Validation.purs │ ├── packages.dhall │ └── spago.dhall ├── chapter11 │ ├── test │ │ ├── MySolutions.purs │ │ ├── no-peeking │ │ │ └── Solutions.purs │ │ └── Main.purs │ ├── .gitignore │ ├── packages.dhall │ ├── src │ │ ├── Data │ │ │ ├── GameEnvironment.purs │ │ │ ├── GameItem.purs │ │ │ ├── Coords.purs │ │ │ └── GameState.purs │ │ ├── Split.purs │ │ ├── Main.purs │ │ └── Game.purs │ └── spago.dhall ├── chapter12 │ ├── .gitignore │ ├── src │ │ ├── Main.purs │ │ └── Example │ │ │ ├── Rectangle.purs │ │ │ ├── Random.purs │ │ │ ├── Shapes.purs │ │ │ ├── Refs.purs │ │ │ └── LSystem.purs │ ├── packages.dhall │ ├── test │ │ └── Main.purs │ ├── html │ │ └── index.html │ └── spago.dhall ├── chapter13 │ ├── .gitignore │ ├── src │ │ ├── Main.purs │ │ ├── Sorted.purs │ │ ├── Merge.purs │ │ └── Tree.purs │ ├── packages.dhall │ ├── spago.dhall │ └── test │ │ └── Main.purs ├── chapter14 │ ├── .gitignore │ ├── src │ │ ├── Main.purs │ │ └── Data │ │ │ └── DOM │ │ │ ├── Simple.purs │ │ │ ├── Smart.purs │ │ │ ├── Phantom.purs │ │ │ ├── Free.purs │ │ │ └── Name.purs │ ├── packages.dhall │ ├── test │ │ └── Main.purs │ └── spago.dhall └── LICENSE ├── deploy_key.enc ├── .gitignore ├── .markdownlint.json ├── text ├── LICENSE ├── SUMMARY.md └── chapter2.md ├── scripts ├── packages.dhall ├── testAll.sh ├── buildAll.sh ├── updatePackages.sh └── prepareExercises.sh ├── book.toml ├── CONTRIBUTING.md ├── .github └── workflows │ ├── mdbook.yml │ └── tests.yml ├── purescriptbook.code-workspace └── README.md /exercises/chapter9/test/data/tree/b/a.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/tree/b/c/a.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/tree/c/a/a.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/tree/a.txt: -------------------------------------------------------------------------------- 1 | b/b.txt -------------------------------------------------------------------------------- /exercises/chapter9/test/data/tree/b/b.txt: -------------------------------------------------------------------------------- 1 | c/a.txt -------------------------------------------------------------------------------- /exercises/chapter9/test/data/tree/c/unused.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/tree/unused.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file1.txt: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file2.txt: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file3.txt: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file4.txt: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file5.txt: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file6.txt: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file7.txt: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file8.txt: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/many/file9.txt: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/tree/root.txt: -------------------------------------------------------------------------------- 1 | a.txt 2 | b/a.txt 3 | c/a/a.txt -------------------------------------------------------------------------------- /exercises/chapter4/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/foo.txt: -------------------------------------------------------------------------------- 1 | This is some file data. 2 | And another line. -------------------------------------------------------------------------------- /exercises/chapter9/test/data/nbChars.txt: -------------------------------------------------------------------------------- 1 | A bunch of letters to count with a script. -------------------------------------------------------------------------------- /exercises/chapter9/test/data-out/foo.txt: -------------------------------------------------------------------------------- 1 | This is some file data. 2 | And another line. -------------------------------------------------------------------------------- /exercises/chapter9/test/data/bar.txt: -------------------------------------------------------------------------------- 1 | This is some more data. 2 | And a few 3 | more lines. -------------------------------------------------------------------------------- /deploy_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescript-contrib/purescript-book/HEAD/deploy_key.enc -------------------------------------------------------------------------------- /exercises/chapter2/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | -------------------------------------------------------------------------------- /exercises/chapter10/src/index.js: -------------------------------------------------------------------------------- 1 | import { main } from "../output/Main/index.js"; 2 | 3 | main(); 4 | -------------------------------------------------------------------------------- /exercises/chapter8/src/index.js: -------------------------------------------------------------------------------- 1 | import { main } from "../output/Main/index.js"; 2 | 3 | main(); 4 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/manyConcat.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 4 | 4 5 | 5 6 | 6 7 | 7 8 | 8 9 | 9 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | deploy_key 2 | package-lock.json 3 | dist 4 | .parcel-cache 5 | .cache 6 | /index.html 7 | /book 8 | -------------------------------------------------------------------------------- /exercises/chapter10/test/URI.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export const _encodeURIComponent = encodeURIComponent; 4 | -------------------------------------------------------------------------------- /exercises/chapter10/test/MySolutions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Note to reader: Add your solutions to this file 4 | -------------------------------------------------------------------------------- /exercises/chapter10/test/URI.purs: -------------------------------------------------------------------------------- 1 | module Test.URI where 2 | 3 | foreign import _encodeURIComponent :: String -> String 4 | -------------------------------------------------------------------------------- /exercises/chapter10/src/Effect/Alert.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export const alert = msg => () => 4 | window.alert(msg); 5 | -------------------------------------------------------------------------------- /exercises/chapter9/test/data/tree/expected.txt: -------------------------------------------------------------------------------- 1 | ./root.txt 2 | ./a.txt 3 | ./b/a.txt 4 | ./b/b.txt 5 | ./b/c/a.txt 6 | ./c/a/a.txt -------------------------------------------------------------------------------- /exercises/chapter9/test/data/user.txt: -------------------------------------------------------------------------------- 1 | { 2 | "userId": 1, 3 | "id": 1, 4 | "title": "delectus aut autem", 5 | "completed": false 6 | } -------------------------------------------------------------------------------- /exercises/chapter3/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | 5 | -- Note to reader: Add your solutions to this file 6 | -------------------------------------------------------------------------------- /exercises/chapter5/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | 5 | -- Note to reader: Add your solutions to this file 6 | -------------------------------------------------------------------------------- /exercises/chapter6/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | 5 | -- Note to reader: Add your solutions to this file 6 | -------------------------------------------------------------------------------- /exercises/chapter7/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | 5 | -- Note to reader: Add your solutions to this file 6 | -------------------------------------------------------------------------------- /exercises/chapter8/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | 5 | -- Note to reader: Add your solutions to this file 6 | -------------------------------------------------------------------------------- /exercises/chapter9/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | 5 | -- Note to reader: Add your solutions to this file 6 | -------------------------------------------------------------------------------- /exercises/chapter10/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | 5 | -- Note to reader: Add your solutions to this file 6 | -------------------------------------------------------------------------------- /exercises/chapter11/test/MySolutions.purs: -------------------------------------------------------------------------------- 1 | module Test.MySolutions where 2 | 3 | import Prelude 4 | 5 | -- Note to reader : Add your solutions to this file 6 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "no-duplicate-heading": false, 4 | "no-trailing-punctuation": false, 5 | "commands-show-output": false 6 | } 7 | -------------------------------------------------------------------------------- /exercises/chapter10/src/Effect/Alert.purs: -------------------------------------------------------------------------------- 1 | module Effect.Alert where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | 7 | foreign import alert :: String -> Effect Unit 8 | -------------------------------------------------------------------------------- /exercises/chapter4/src/Data/Person.purs: -------------------------------------------------------------------------------- 1 | module Data.Person where 2 | 3 | type Address = { street :: String, city :: String } 4 | 5 | type Person = { name :: String, address :: Address } 6 | -------------------------------------------------------------------------------- /exercises/chapter9/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | import Effect (Effect) 5 | import Effect.Class.Console (log) 6 | 7 | main :: Effect Unit 8 | main = log "hello" 9 | -------------------------------------------------------------------------------- /exercises/chapter10/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter12/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter13/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter14/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter2/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter3/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter4/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter5/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter6/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter7/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | -------------------------------------------------------------------------------- /exercises/chapter2/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | import Euler (answer) 5 | import Effect.Console (log) 6 | 7 | main = do 8 | log ("The answer is " <> show (answer 1000)) 9 | -------------------------------------------------------------------------------- /exercises/chapter11/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | /index.js -------------------------------------------------------------------------------- /exercises/chapter12/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | -------------------------------------------------------------------------------- /exercises/chapter13/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | -------------------------------------------------------------------------------- /exercises/chapter14/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | -------------------------------------------------------------------------------- /exercises/chapter3/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | -------------------------------------------------------------------------------- /exercises/chapter4/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | -------------------------------------------------------------------------------- /exercises/chapter5/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | -------------------------------------------------------------------------------- /exercises/chapter7/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | -------------------------------------------------------------------------------- /text/LICENSE: -------------------------------------------------------------------------------- 1 | The text of this book is licensed under the Creative Commons 2 | Attribution-NonCommercial-ShareAlike 3.0 Unported License: 3 | 4 | https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US 5 | -------------------------------------------------------------------------------- /exercises/chapter9/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | /test/data-out/* 12 | -------------------------------------------------------------------------------- /exercises/chapter8/.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | /.cache/ 12 | /dist/ 13 | -------------------------------------------------------------------------------- /exercises/chapter10/src/Effect/Storage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export const setItem = key => value => () => 4 | window.localStorage.setItem(key, value); 5 | 6 | export const getItem = key => () => 7 | window.localStorage.getItem(key); 8 | -------------------------------------------------------------------------------- /scripts/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter10/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter11/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter12/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter12/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Class.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | log "You should add some tests." 12 | -------------------------------------------------------------------------------- /exercises/chapter13/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter14/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter14/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Class.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | log "You should add some tests." 12 | -------------------------------------------------------------------------------- /exercises/chapter2/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter3/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter4/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter5/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter6/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter7/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter8/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter9/packages.dhall: -------------------------------------------------------------------------------- 1 | let upstream = 2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.2-20220615/packages.dhall 3 | sha256:6b62a899c22125a2735a7c354bbb66a2fe24ff45cec0a8b8b890769a01a99210 4 | 5 | in upstream 6 | -------------------------------------------------------------------------------- /exercises/chapter8/test/Random.purs: -------------------------------------------------------------------------------- 1 | module Test.Random where 2 | 3 | import Prelude 4 | import Effect (Effect) 5 | import Effect.Random (random) 6 | import Effect.Console (logShow) 7 | 8 | main :: Effect Unit 9 | main = do 10 | n <- random 11 | logShow n 12 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Phil Freeman"] 3 | language = "en" 4 | multilingual = false 5 | src = "text" 6 | title = "PureScript by Example" 7 | [output.html] 8 | git-repository-url = "https://github.com/purescript-contrib/purescript-book" 9 | mathjax-support = true 10 | -------------------------------------------------------------------------------- /exercises/chapter10/src/Effect/Storage.purs: -------------------------------------------------------------------------------- 1 | module Effect.Storage where 2 | 3 | import Prelude 4 | import Data.Argonaut (Json) 5 | import Effect (Effect) 6 | 7 | foreign import setItem :: String -> String -> Effect Unit 8 | 9 | foreign import getItem :: String -> Effect Json 10 | -------------------------------------------------------------------------------- /exercises/chapter2/src/Euler.purs: -------------------------------------------------------------------------------- 1 | module Euler where 2 | 3 | import Prelude 4 | import Data.List (range, filter) 5 | import Data.Foldable (sum) 6 | 7 | ns n = range 0 (n - 1) 8 | 9 | multiples n = filter (\n -> mod n 3 == 0 || mod n 5 == 0) (ns n) 10 | 11 | answer n = sum (multiples n) 12 | -------------------------------------------------------------------------------- /exercises/chapter2/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | import Data.Int (rem) 5 | import Data.Number (pi, sqrt) 6 | 7 | -- ANCHOR: diagonal 8 | diagonal w h = sqrt (w * w + h * h) 9 | -- ANCHOR_END: diagonal 10 | 11 | circleArea r = pi * r * r 12 | 13 | leftoverCents n = rem n 100 14 | -------------------------------------------------------------------------------- /exercises/chapter10/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Address Book 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /exercises/chapter9/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-book-chapter12", 3 | "version": "0.1.0", 4 | "description": "Asynchronous Effects", 5 | "main": "", 6 | "private": true, 7 | "author": "Phil Freeman", 8 | "license": "Creative,Commons,Attribution-NonCommercial-ShareAlike,4.0,International,License", 9 | "dependencies": { 10 | "xhr2": "^0.2.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /exercises/chapter3/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "console" 8 | , "control" 9 | , "effect" 10 | , "lists" 11 | , "maybe" 12 | , "prelude" 13 | , "test-unit" 14 | ] 15 | , packages = ./packages.dhall 16 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/testAll.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Echo commands to shell 4 | set -x 5 | # Exit on first failure 6 | set -e 7 | 8 | # For all chapters 9 | for d in exercises/*; do 10 | # if directory (excludes LICENSE file) 11 | if [ -d $d ]; then 12 | # enter directory 13 | pushd $d 14 | # build 15 | spago test 16 | # exit directory 17 | popd 18 | fi 19 | done 20 | -------------------------------------------------------------------------------- /exercises/chapter12/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2D Graphics Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /exercises/chapter8/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Address Book 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /exercises/chapter10/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Address Book 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /exercises/chapter13/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "console" 9 | , "effect" 10 | , "foldable-traversable" 11 | , "functions" 12 | , "lists" 13 | , "prelude" 14 | , "quickcheck" 15 | ] 16 | , packages = ./packages.dhall 17 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 18 | } 19 | -------------------------------------------------------------------------------- /exercises/chapter2/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "console" 8 | , "effect" 9 | , "foldable-traversable" 10 | , "integers" 11 | , "lists" 12 | , "numbers" 13 | , "prelude" 14 | , "test-unit" 15 | ] 16 | , packages = ./packages.dhall 17 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 18 | } 19 | -------------------------------------------------------------------------------- /exercises/chapter14/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "console" 9 | , "effect" 10 | , "foldable-traversable" 11 | , "free" 12 | , "maybe" 13 | , "prelude" 14 | , "strings" 15 | , "transformers" 16 | ] 17 | , packages = ./packages.dhall 18 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 19 | } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please open PRs or Issues if you find anything to improve in the book. 4 | 5 | We appreciate your feedback. 6 | 7 | ## Editing chapter text 8 | 9 | To test changes to the text locally, install [mdbook](https://github.com/rust-lang/mdBook), then run this command from repo root: 10 | 11 | ```sh 12 | mdbook serve -o 13 | ``` 14 | 15 | The rendered webpage will automatically refresh with any changes to readme files. 16 | -------------------------------------------------------------------------------- /exercises/chapter4/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "console" 9 | , "effect" 10 | , "foldable-traversable" 11 | , "integers" 12 | , "maybe" 13 | , "numbers" 14 | , "partial" 15 | , "prelude" 16 | , "test-unit" 17 | ] 18 | , packages = ./packages.dhall 19 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 20 | } 21 | -------------------------------------------------------------------------------- /exercises/chapter6/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Class.Console (logShow) 7 | import Data.Hashable (hash, hashEqual) 8 | 9 | main :: Effect Unit 10 | main = do 11 | logShow $ hash 123 12 | logShow (hash true) 13 | logShow (hash [1, 2, 3]) 14 | logShow (hash "testing") 15 | logShow (hash 'a') 16 | logShow ("foo" `hashEqual` "foo") 17 | logShow ("foo" `hashEqual` "bar") 18 | -------------------------------------------------------------------------------- /exercises/chapter5/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "console" 9 | , "control" 10 | , "effect" 11 | , "foldable-traversable" 12 | , "integers" 13 | , "maybe" 14 | , "prelude" 15 | , "strings" 16 | , "test-unit" 17 | , "tuples" 18 | ] 19 | , packages = ./packages.dhall 20 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 21 | } 22 | -------------------------------------------------------------------------------- /exercises/chapter11/src/Data/GameEnvironment.purs: -------------------------------------------------------------------------------- 1 | module Data.GameEnvironment where 2 | 3 | -- ANCHOR: env 4 | type PlayerName = String 5 | 6 | newtype GameEnvironment = GameEnvironment 7 | { playerName :: PlayerName 8 | , debugMode :: Boolean 9 | } 10 | -- ANCHOR_END: env 11 | 12 | gameEnvironment :: PlayerName -> Boolean -> GameEnvironment 13 | gameEnvironment playerName debugMode = GameEnvironment 14 | { playerName : playerName 15 | , debugMode : debugMode 16 | } 17 | -------------------------------------------------------------------------------- /scripts/buildAll.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Echo commands to shell 4 | set -x 5 | # Exit on first failure 6 | set -e 7 | 8 | # For all chapters 9 | for d in exercises/*; do 10 | # if directory (excludes LICENSE file) 11 | if [ -d $d ]; then 12 | # enter directory 13 | pushd $d 14 | # build 15 | # if node project 16 | if [ -f package.json ]; then 17 | npm install 18 | fi 19 | spago build 20 | # exit directory 21 | popd 22 | fi 23 | done 24 | -------------------------------------------------------------------------------- /exercises/chapter13/src/Sorted.purs: -------------------------------------------------------------------------------- 1 | module Sorted where 2 | 3 | import Prelude 4 | 5 | import Data.Array (sort) 6 | import Test.QuickCheck.Arbitrary (class Arbitrary, arbitrary) 7 | 8 | newtype Sorted a = Sorted (Array a) 9 | 10 | sorted :: forall a. Sorted a -> Array a 11 | sorted (Sorted xs) = xs 12 | 13 | instance Show a => Show (Sorted a) where 14 | show = show <<< sorted 15 | 16 | instance (Arbitrary a, Ord a) => Arbitrary (Sorted a) where 17 | arbitrary = map (Sorted <<< sort) arbitrary 18 | -------------------------------------------------------------------------------- /exercises/chapter6/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "console" 9 | , "effect" 10 | , "either" 11 | , "foldable-traversable" 12 | , "lists" 13 | , "maybe" 14 | , "newtype" 15 | , "partial" 16 | , "prelude" 17 | , "strings" 18 | , "test-unit" 19 | , "tuples" 20 | ] 21 | , packages = ./packages.dhall 22 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 23 | } 24 | -------------------------------------------------------------------------------- /scripts/updatePackages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run this script from purescript book repo root 4 | 5 | # Echo commands to shell 6 | set -x 7 | # Exit on first failure 8 | set -e 9 | 10 | # Update package set 11 | pushd scripts 12 | spago upgrade-set 13 | popd 14 | 15 | # For all chapters 16 | for d in exercises/*; do 17 | # if directory (excludes LICENSE file) 18 | if [ -d $d ]; then 19 | # copy updated packages.dhall to chapter 20 | cp scripts/packages.dhall $d/packages.dhall 21 | fi 22 | done 23 | -------------------------------------------------------------------------------- /exercises/chapter9/test/HTTP.purs: -------------------------------------------------------------------------------- 1 | module Test.HTTP where 2 | 3 | -- ANCHOR: getUrl 4 | import Prelude 5 | import Affjax.Node as AN 6 | import Affjax.ResponseFormat as ResponseFormat 7 | import Data.Either (Either(..)) 8 | import Effect.Aff (Aff) 9 | 10 | getUrl :: String -> Aff String 11 | getUrl url = do 12 | result <- AN.get ResponseFormat.string url 13 | pure case result of 14 | Left err -> "GET /api response failed to decode: " <> AN.printError err 15 | Right response -> response.body 16 | -- ANCHOR_END: getUrl 17 | -------------------------------------------------------------------------------- /exercises/chapter7/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "console" 9 | , "effect" 10 | , "either" 11 | , "foldable-traversable" 12 | , "integers" 13 | , "lists" 14 | , "maybe" 15 | , "prelude" 16 | , "strings" 17 | , "test-unit" 18 | , "transformers" 19 | , "tuples" 20 | , "validation" 21 | ] 22 | , packages = ./packages.dhall 23 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 24 | } 25 | -------------------------------------------------------------------------------- /exercises/chapter12/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "canvas" 9 | , "console" 10 | , "effect" 11 | , "foldable-traversable" 12 | , "integers" 13 | , "maybe" 14 | , "numbers" 15 | , "partial" 16 | , "prelude" 17 | , "random" 18 | , "refs" 19 | , "web-dom" 20 | , "web-events" 21 | , "web-html" 22 | ] 23 | , packages = ./packages.dhall 24 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 25 | } 26 | -------------------------------------------------------------------------------- /exercises/chapter9/test/ParallelFetch.purs: -------------------------------------------------------------------------------- 1 | module Test.ParallelFetch where 2 | 3 | -- ANCHOR: fetchPar 4 | import Prelude 5 | 6 | import Control.Parallel (parTraverse) 7 | import Effect (Effect) 8 | import Effect.Aff (launchAff_) 9 | import Effect.Class.Console (logShow) 10 | import Test.HTTP (getUrl) 11 | 12 | fetchPar :: Effect Unit 13 | fetchPar = 14 | launchAff_ do 15 | let 16 | urls = map (\n -> "https://reqres.in/api/users/" <> show n) [ 1, 2 ] 17 | res <- parTraverse getUrl urls 18 | logShow res 19 | -- ANCHOR_END: fetchPar 20 | -------------------------------------------------------------------------------- /exercises/chapter8/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-book-chapter8", 3 | "version": "0.1.0", 4 | "description": "The Effect Monad", 5 | "main": "output/Main/index.js", 6 | "private": true, 7 | "author": "Phil Freeman", 8 | "license": "Creative,Commons,Attribution-NonCommercial-ShareAlike,4.0,International,License", 9 | "dependencies": { 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1" 12 | }, 13 | "devDependencies": { 14 | "process": "^0.11.10" 15 | }, 16 | "alias": { 17 | "process": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /exercises/chapter11/src/Data/GameItem.purs: -------------------------------------------------------------------------------- 1 | module Data.GameItem where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | 7 | -- ANCHOR: GameItem 8 | data GameItem = Candle | Matches 9 | -- ANCHOR_END: GameItem 10 | 11 | instance Show GameItem where 12 | show Candle = "Candle" 13 | show Matches = "Matches" 14 | 15 | derive instance Eq GameItem 16 | derive instance Ord GameItem 17 | 18 | readItem :: String -> Maybe GameItem 19 | readItem "Candle" = Just Candle 20 | readItem "Matches" = Just Matches 21 | readItem _ = Nothing 22 | -------------------------------------------------------------------------------- /exercises/chapter10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-book-chapter10", 3 | "version": "0.1.0", 4 | "description": "The Foreign Function Interface", 5 | "main": "output/Main/index.js", 6 | "private": true, 7 | "author": "Phil Freeman", 8 | "license": "Creative,Commons,Attribution-NonCommercial-ShareAlike,4.0,International,License", 9 | "dependencies": { 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1" 12 | }, 13 | "devDependencies": { 14 | "process": "^0.11.10" 15 | }, 16 | "alias": { 17 | "process": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /exercises/chapter9/test/ParallelDelay.purs: -------------------------------------------------------------------------------- 1 | module Test.ParallelDelay where 2 | 3 | -- ANCHOR: delays 4 | import Prelude 5 | 6 | import Control.Parallel (parSequence_) 7 | import Data.Array (replicate) 8 | import Data.Foldable (sequence_) 9 | import Effect (Effect) 10 | import Effect.Aff (Aff, Milliseconds(..), delay, launchAff_) 11 | 12 | delayArray :: Array (Aff Unit) 13 | delayArray = replicate 100 $ delay $ Milliseconds 10.0 14 | 15 | seqDelay :: Effect Unit 16 | seqDelay = launchAff_ $ sequence_ delayArray 17 | 18 | parDelay :: Effect Unit 19 | parDelay = launchAff_ $ parSequence_ delayArray 20 | -- ANCHOR_END: delays 21 | -------------------------------------------------------------------------------- /exercises/chapter11/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "console" 9 | , "control" 10 | , "effect" 11 | , "either" 12 | , "foldable-traversable" 13 | , "identity" 14 | , "lists" 15 | , "maybe" 16 | , "newtype" 17 | , "node-readline" 18 | , "optparse" 19 | , "ordered-collections" 20 | , "prelude" 21 | , "strings" 22 | , "test-unit" 23 | , "transformers" 24 | , "tuples" 25 | ] 26 | , packages = ./packages.dhall 27 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 28 | } 29 | -------------------------------------------------------------------------------- /exercises/chapter11/src/Data/Coords.purs: -------------------------------------------------------------------------------- 1 | module Data.Coords where 2 | 3 | import Prelude 4 | 5 | newtype Coords = Coords 6 | { x :: Int 7 | , y :: Int 8 | } 9 | 10 | instance Show Coords where 11 | show (Coords p) = "Coords " <> 12 | "{ x: " <> show p.x <> 13 | ", y: " <> show p.y <> 14 | " }" 15 | 16 | derive instance Eq Coords 17 | derive instance Ord Coords 18 | 19 | coords :: Int -> Int -> Coords 20 | coords x y = Coords { x: x, y: y } 21 | 22 | prettyPrintCoords :: Coords -> String 23 | prettyPrintCoords (Coords p) = "(" <> show p.x <> ", " <> show p.y <> ")" 24 | -------------------------------------------------------------------------------- /text/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Foreword](../README.md) 4 | 5 | * [Introduction](chapter1.md) 6 | * [Getting Started](chapter2.md) 7 | * [Functions and Records](chapter3.md) 8 | * [Pattern Matching](chapter4.md) 9 | * [Recursion, Maps And Folds](chapter5.md) 10 | * [Type Classes](chapter6.md) 11 | * [Applicative Validation](chapter7.md) 12 | * [The Effect Monad](chapter8.md) 13 | * [Asynchronous Effects](chapter9.md) 14 | * [The Foreign Function Interface](chapter10.md) 15 | * [Monadic Adventures](chapter11.md) 16 | * [Canvas Graphics](chapter12.md) 17 | * [Generative Testing](chapter13.md) 18 | * [Domain-Specific Languages](chapter14.md) 19 | -------------------------------------------------------------------------------- /exercises/chapter9/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "aff" 8 | , "affjax" 9 | , "affjax-node" 10 | , "arrays" 11 | , "bifunctors" 12 | , "console" 13 | , "effect" 14 | , "either" 15 | , "exceptions" 16 | , "foldable-traversable" 17 | , "functions" 18 | , "maybe" 19 | , "node-buffer" 20 | , "node-fs-aff" 21 | , "node-path" 22 | , "ordered-collections" 23 | , "parallel" 24 | , "prelude" 25 | , "strings" 26 | , "test-unit" 27 | ] 28 | , packages = ./packages.dhall 29 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 30 | } 31 | -------------------------------------------------------------------------------- /exercises/chapter8/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "arrays" 8 | , "console" 9 | , "control" 10 | , "effect" 11 | , "either" 12 | , "exceptions" 13 | , "foldable-traversable" 14 | , "integers" 15 | , "lists" 16 | , "numbers" 17 | , "maybe" 18 | , "prelude" 19 | , "random" 20 | , "react-basic" 21 | , "react-basic-dom" 22 | , "react-basic-hooks" 23 | , "st" 24 | , "strings" 25 | , "test-unit" 26 | , "tuples" 27 | , "validation" 28 | , "web-dom" 29 | , "web-html" 30 | ] 31 | , packages = ./packages.dhall 32 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 33 | } 34 | -------------------------------------------------------------------------------- /exercises/chapter13/src/Merge.purs: -------------------------------------------------------------------------------- 1 | module Merge where 2 | 3 | import Prelude 4 | 5 | import Data.List (List(..), fromFoldable, toUnfoldable, reverse) 6 | 7 | merge :: Array Int -> Array Int -> Array Int 8 | merge = mergePoly 9 | 10 | mergePoly :: forall a. Ord a => Array a -> Array a -> Array a 11 | mergePoly = mergeWith identity 12 | 13 | mergeWith :: forall a b. Ord b => (a -> b) -> Array a -> Array a -> Array a 14 | mergeWith f = \xs ys -> 15 | toUnfoldable (go Nil (fromFoldable xs) (fromFoldable ys)) 16 | where 17 | go acc Nil ys = reverse acc <> ys 18 | go acc xs Nil = reverse acc <> xs 19 | go acc xs@(Cons x xs') ys@(Cons y ys') = 20 | case compare (f x) (f y) of 21 | LT -> go (Cons x acc) xs' ys 22 | _ -> go (Cons y acc) xs ys' 23 | -------------------------------------------------------------------------------- /exercises/chapter12/src/Example/Rectangle.purs: -------------------------------------------------------------------------------- 1 | module Example.Rectangle where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Data.Maybe (Maybe(..)) 7 | import Graphics.Canvas (rect, fillPath, setFillStyle, getContext2D, 8 | getCanvasElementById) 9 | import Partial.Unsafe (unsafePartial) 10 | 11 | -- ANCHOR: main 12 | main :: Effect Unit 13 | main = void $ unsafePartial do 14 | Just canvas <- getCanvasElementById "canvas" 15 | ctx <- getContext2D canvas 16 | -- ANCHOR_END: main 17 | 18 | -- ANCHOR: setFillStyle 19 | setFillStyle ctx "#00F" 20 | -- ANCHOR_END: setFillStyle 21 | 22 | -- ANCHOR: fillPath 23 | fillPath ctx $ rect ctx 24 | { x: 250.0 25 | , y: 250.0 26 | , width: 100.0 27 | , height: 100.0 28 | } 29 | -- ANCHOR_END: fillPath 30 | -------------------------------------------------------------------------------- /exercises/chapter10/spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "aff" 8 | , "aff-promise" 9 | , "argonaut" 10 | , "argonaut-generic" 11 | , "arrays" 12 | , "bifunctors" 13 | , "console" 14 | , "control" 15 | , "effect" 16 | , "either" 17 | , "exceptions" 18 | , "foldable-traversable" 19 | , "free" 20 | , "functions" 21 | , "maybe" 22 | , "ordered-collections" 23 | , "pairs" 24 | , "prelude" 25 | , "react-basic" 26 | , "react-basic-dom" 27 | , "react-basic-hooks" 28 | , "strings" 29 | , "test-unit" 30 | , "tuples" 31 | , "validation" 32 | , "web-dom" 33 | , "web-html" 34 | ] 35 | , packages = ./packages.dhall 36 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 37 | } 38 | -------------------------------------------------------------------------------- /scripts/prepareExercises.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script removes meta information that is not intended for readers of the book 4 | 5 | # Echo commands to shell 6 | set -x 7 | # Exit on first failure 8 | set -e 9 | 10 | # For all chapters 11 | for d in exercises/chapter*; do 12 | # All .purs & .js files of chapter exercises 13 | FILES=$(find $d/src $d/test -name '*.purs' -o -name '*.js') 14 | 15 | for f in $FILES; do 16 | # Delete lines starting with an 'ANCHOR' comment 17 | perl -ni -e 'print if !/^\s*(--|\/\/) ANCHOR/' $f 18 | 19 | # Delete lines with a note to delete them 20 | perl -ni -e 'print if !/This line should have been automatically deleted/' $f 21 | done 22 | 23 | # If there's a no-peeking directory 24 | if [ -d $d/test/no-peeking ]; then 25 | # Move 'no-peeking' sources out of the compilation path 26 | mv $d/test/no-peeking $d 27 | fi 28 | done 29 | -------------------------------------------------------------------------------- /exercises/chapter8/test/Examples.purs: -------------------------------------------------------------------------------- 1 | module Test.Examples where 2 | 3 | import Prelude 4 | import Control.Plus (empty) 5 | import Data.List (List(..), (:)) 6 | import Data.Maybe (Maybe(..)) 7 | import Data.Array ((..)) 8 | 9 | {-| Monads and Do Notation -} 10 | -- ANCHOR: countThrows 11 | countThrows :: Int -> Array (Array Int) 12 | countThrows n = do 13 | x <- 1 .. 6 14 | y <- 1 .. 6 15 | if x + y == n 16 | then pure [ x, y ] 17 | else empty 18 | -- ANCHOR_END: countThrows 19 | 20 | {-| Folding With Monads -} 21 | foldM :: forall m a b 22 | . Monad m 23 | => (a -> b -> m a) 24 | -> a 25 | -> List b 26 | -> m a 27 | foldM _ a Nil = pure a 28 | foldM f a (b : bs) = do 29 | a' <- f a b 30 | foldM f a' bs 31 | 32 | -- ANCHOR: safeDivide 33 | safeDivide :: Int -> Int -> Maybe Int 34 | safeDivide _ 0 = Nothing 35 | safeDivide a b = Just (a / b) 36 | -- ANCHOR_END: safeDivide 37 | -------------------------------------------------------------------------------- /exercises/chapter9/test/Copy.purs: -------------------------------------------------------------------------------- 1 | module Test.Copy where 2 | 3 | -- ANCHOR: copyFile 4 | import Prelude 5 | import Data.Either (Either(..)) 6 | import Effect.Aff (Aff, attempt, message, launchAff_) 7 | import Effect (Effect) 8 | import Effect.Class.Console (log) 9 | import Node.Encoding (Encoding(..)) 10 | import Node.FS.Aff (readTextFile, writeTextFile) 11 | import Node.Path (FilePath) 12 | 13 | main :: Effect Unit 14 | main = launchAff_ program 15 | 16 | program :: Aff Unit 17 | program = do 18 | result <- attempt $ copyFile "file1.txt" "file2.txt" 19 | case result of 20 | Left e -> log $ "There was a problem with copyFile: " <> message e 21 | _ -> pure unit 22 | 23 | copyFile :: FilePath -> FilePath -> Aff Unit 24 | copyFile file1 file2 = do 25 | my_data <- readTextFile UTF8 file1 26 | writeTextFile UTF8 file2 my_data 27 | -- ANCHOR_END: copyFile 28 | 29 | -- Main is unused, and is only linked to in text 30 | -------------------------------------------------------------------------------- /.github/workflows/mdbook.yml: -------------------------------------------------------------------------------- 1 | name: Build Book 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup mdBook 15 | uses: peaceiris/actions-mdbook@v1 16 | with: 17 | mdbook-version: 'latest' 18 | 19 | - name: Set up PureScript toolchain 20 | uses: purescript-contrib/setup-purescript@main 21 | 22 | - name: Add version section 23 | run: | 24 | version=$(purs --version) 25 | today=$(date -I) 26 | echo -e "\n## Release\n" >> README.md 27 | echo -e "PureScript v$version\n" >> README.md 28 | echo -e "Published on $today" >> README.md 29 | 30 | - run: mdbook build 31 | 32 | - name: Deploy 33 | uses: peaceiris/actions-gh-pages@v3 34 | if: github.ref == 'refs/heads/master' 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | publish_dir: ./book 38 | cname: book.purescript.org 39 | -------------------------------------------------------------------------------- /exercises/chapter11/src/Data/GameState.purs: -------------------------------------------------------------------------------- 1 | module Data.GameState where 2 | 3 | import Prelude 4 | 5 | import Data.Coords (Coords(..), coords) 6 | import Data.GameItem (GameItem(..)) 7 | -- ANCHOR: imports 8 | import Data.Map as M 9 | import Data.Set as S 10 | -- ANCHOR_END: imports 11 | import Data.Tuple (Tuple(..)) 12 | 13 | -- ANCHOR: GameState 14 | newtype GameState = GameState 15 | { items :: M.Map Coords (S.Set GameItem) 16 | , player :: Coords 17 | , inventory :: S.Set GameItem 18 | } 19 | -- ANCHOR_END: GameState 20 | 21 | instance Show GameState where 22 | show (GameState o) = 23 | "GameState " <> 24 | "{ items: " <> show o.items <> 25 | ", player: " <> show o.player <> 26 | ", inventory: " <> show o.inventory <> 27 | " }" 28 | 29 | initialGameState :: GameState 30 | initialGameState = GameState 31 | { items : M.fromFoldable [ Tuple (coords 0 1) (S.singleton Candle) 32 | , Tuple (coords 0 0) (S.singleton Matches) 33 | ] 34 | , player : Coords { x: 0, y: 0 } 35 | , inventory : S.empty 36 | } 37 | -------------------------------------------------------------------------------- /exercises/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-16 Phil Freeman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /exercises/chapter14/src/Data/DOM/Simple.purs: -------------------------------------------------------------------------------- 1 | module Data.DOM.Simple where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.String (joinWith) 7 | 8 | newtype Element = Element 9 | { name :: String 10 | , attribs :: Array Attribute 11 | , content :: Maybe (Array Content) 12 | } 13 | 14 | data Content 15 | = TextContent String 16 | | ElementContent Element 17 | 18 | newtype Attribute = Attribute 19 | { key :: String 20 | , value :: String 21 | } 22 | 23 | render :: Element -> String 24 | render (Element e) = 25 | "<" <> e.name <> 26 | " " <> joinWith " " (map renderAttribute e.attribs) <> 27 | renderContent e.content 28 | where 29 | renderAttribute :: Attribute -> String 30 | renderAttribute (Attribute x) = x.key <> "=\"" <> x.value <> "\"" 31 | 32 | renderContent :: Maybe (Array Content) -> String 33 | renderContent Nothing = " />" 34 | renderContent (Just content) = 35 | ">" <> joinWith "" (map renderContentItem content) <> 36 | " e.name <> ">" 37 | where 38 | renderContentItem :: Content -> String 39 | renderContentItem (TextContent s) = s 40 | renderContentItem (ElementContent e') = render e' 41 | -------------------------------------------------------------------------------- /purescriptbook.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "exercises/chapter2" 8 | }, 9 | { 10 | "path": "exercises/chapter3" 11 | }, 12 | { 13 | "path": "exercises/chapter4" 14 | }, 15 | { 16 | "path": "exercises/chapter5" 17 | }, 18 | { 19 | "path": "exercises/chapter6" 20 | }, 21 | { 22 | "path": "exercises/chapter7" 23 | }, 24 | { 25 | "path": "exercises/chapter8" 26 | }, 27 | { 28 | "path": "exercises/chapter9" 29 | }, 30 | { 31 | "path": "exercises/chapter10" 32 | }, 33 | { 34 | "path": "exercises/chapter11" 35 | }, 36 | { 37 | "path": "exercises/chapter12" 38 | }, 39 | { 40 | "path": "exercises/chapter13" 41 | }, 42 | { 43 | "path": "exercises/chapter14" 44 | } 45 | ], 46 | "settings": { 47 | "files.exclude": { 48 | "**/.git": true, 49 | "**/.svn": true, 50 | "**/.hg": true, 51 | "**/CVS": true, 52 | "**/.DS_Store": true, 53 | "exercises": true 54 | } 55 | }, 56 | "extensions": { 57 | "recommendations": [ 58 | "nwolverson.ide-purescript", 59 | "nwolverson.language-purescript", 60 | "mvakula.vscode-purty" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /exercises/chapter12/src/Example/Random.purs: -------------------------------------------------------------------------------- 1 | module Example.Random where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Random (random) 7 | import Data.Array ((..)) 8 | import Data.Foldable (for_) 9 | import Data.Maybe (Maybe(..)) 10 | import Graphics.Canvas (strokePath, fillPath, arc, setStrokeStyle, 11 | setFillStyle, getContext2D, getCanvasElementById) 12 | import Data.Number as Number 13 | import Partial.Unsafe (unsafePartial) 14 | 15 | main :: Effect Unit 16 | main = void $ unsafePartial do 17 | Just canvas <- getCanvasElementById "canvas" 18 | ctx <- getContext2D canvas 19 | 20 | -- ANCHOR: style 21 | setFillStyle ctx "#F00" 22 | setStrokeStyle ctx "#000" 23 | -- ANCHOR_END: style 24 | 25 | -- ANCHOR: for 26 | for_ (1 .. 100) \_ -> do 27 | -- ANCHOR_END: for 28 | -- ANCHOR: random 29 | x <- random 30 | y <- random 31 | r <- random 32 | -- ANCHOR_END: random 33 | 34 | -- ANCHOR: path 35 | let path = arc ctx 36 | { x : x * 600.0 37 | , y : y * 600.0 38 | , radius : r * 50.0 39 | , start : 0.0 40 | , end : Number.tau 41 | , useCounterClockwise: false 42 | } 43 | 44 | fillPath ctx path 45 | strokePath ctx path 46 | -- ANCHOR_END: path 47 | -------------------------------------------------------------------------------- /exercises/chapter8/src/Data/AddressBook.purs: -------------------------------------------------------------------------------- 1 | module Data.AddressBook where 2 | 3 | import Prelude 4 | import Data.Generic.Rep (class Generic) 5 | import Data.Show.Generic (genericShow) 6 | 7 | type Address 8 | = { street :: String 9 | , city :: String 10 | , state :: String 11 | } 12 | 13 | address :: String -> String -> String -> Address 14 | address street city state = { street, city, state } 15 | 16 | data PhoneType 17 | = HomePhone 18 | | WorkPhone 19 | | CellPhone 20 | | OtherPhone 21 | 22 | derive instance Generic PhoneType _ 23 | 24 | instance Show PhoneType where 25 | show = genericShow 26 | 27 | type PhoneNumber 28 | = { "type" :: PhoneType 29 | , number :: String 30 | } 31 | 32 | phoneNumber :: PhoneType -> String -> PhoneNumber 33 | phoneNumber ty number = 34 | { "type": ty 35 | , number: number 36 | } 37 | 38 | type Person 39 | = { firstName :: String 40 | , lastName :: String 41 | , homeAddress :: Address 42 | , phones :: Array PhoneNumber 43 | } 44 | 45 | person :: String -> String -> Address -> Array PhoneNumber -> Person 46 | person firstName lastName homeAddress phones = { firstName, lastName, homeAddress, phones } 47 | 48 | examplePerson :: Person 49 | examplePerson = 50 | person "John" "Smith" 51 | (address "123 Fake St." "FakeTown" "CA") 52 | [ phoneNumber HomePhone "555-555-5555" 53 | , phoneNumber CellPhone "555-555-0000" 54 | ] 55 | -------------------------------------------------------------------------------- /exercises/chapter13/src/Tree.purs: -------------------------------------------------------------------------------- 1 | module Tree where 2 | 3 | import Prelude 4 | 5 | import Data.Foldable (foldr) 6 | import Test.QuickCheck.Arbitrary (class Coarbitrary, class Arbitrary, coarbitrary, arbitrary) 7 | 8 | data Tree a 9 | = Leaf 10 | | Branch (Tree a) a (Tree a) 11 | 12 | instance (Arbitrary a, Ord a) => Arbitrary (Tree a) where 13 | arbitrary = map fromArray arbitrary 14 | 15 | instance (Coarbitrary a) => Coarbitrary (Tree a) where 16 | coarbitrary Leaf = identity 17 | coarbitrary (Branch l a r) = 18 | coarbitrary l <<< 19 | coarbitrary a <<< 20 | coarbitrary r 21 | 22 | insert :: forall a. (Ord a) => a -> Tree a -> Tree a 23 | insert a Leaf = Branch Leaf a Leaf 24 | insert a (Branch l a1 r) | a < a1 = Branch (insert a l) a1 r 25 | insert a (Branch l a1 r) = Branch l a1 (insert a r) 26 | 27 | member :: forall a. (Ord a) => a -> Tree a -> Boolean 28 | member _ Leaf = false 29 | member a (Branch _ a1 _) | a == a1 = true 30 | member a (Branch l a1 _) | a < a1 = a `member` l 31 | member a (Branch _ _ r) = a `member` r 32 | 33 | toArray :: forall a. Tree a -> Array a 34 | toArray Leaf = [] 35 | toArray (Branch l a r) = toArray l <> [a] <> toArray r 36 | 37 | fromArray :: forall a. (Ord a) => Array a -> Tree a 38 | fromArray = foldr insert Leaf 39 | 40 | anywhere :: forall a. (Tree a -> Boolean) -> Tree a -> Boolean 41 | anywhere f Leaf = f Leaf 42 | anywhere f t@(Branch l _ r) = anywhere f l || f t || anywhere f r 43 | -------------------------------------------------------------------------------- /exercises/chapter12/src/Example/Shapes.purs: -------------------------------------------------------------------------------- 1 | module Example.Shapes where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Data.Maybe (Maybe(..)) 7 | import Graphics.Canvas (closePath, lineTo, moveTo, fillPath, 8 | setFillStyle, arc, rect, getContext2D, 9 | getCanvasElementById) 10 | import Data.Number as Number 11 | import Partial.Unsafe (unsafePartial) 12 | 13 | -- ANCHOR: translate 14 | translate 15 | :: forall r 16 | . Number 17 | -> Number 18 | -> { x :: Number, y :: Number | r } 19 | -> { x :: Number, y :: Number | r } 20 | translate dx dy shape = shape 21 | { x = shape.x + dx 22 | , y = shape.y + dy 23 | } 24 | -- ANCHOR_END: translate 25 | 26 | main :: Effect Unit 27 | main = void $ unsafePartial do 28 | Just canvas <- getCanvasElementById "canvas" 29 | ctx <- getContext2D canvas 30 | 31 | setFillStyle ctx "#00F" 32 | 33 | fillPath ctx $ rect ctx $ translate (-200.0) (-200.0) 34 | { x: 250.0 35 | , y: 250.0 36 | , width: 100.0 37 | , height: 100.0 38 | } 39 | 40 | setFillStyle ctx "#0F0" 41 | 42 | fillPath ctx $ arc ctx $ translate 200.0 200.0 43 | { x: 300.0 44 | , y: 300.0 45 | , radius: 50.0 46 | , start: 0.0 47 | , end: Number.tau * 2.0 / 3.0 48 | , useCounterClockwise: false 49 | } 50 | 51 | -- ANCHOR: path 52 | setFillStyle ctx "#F00" 53 | 54 | fillPath ctx $ do 55 | moveTo ctx 300.0 260.0 56 | lineTo ctx 260.0 340.0 57 | lineTo ctx 340.0 340.0 58 | closePath ctx 59 | -- ANCHOR_END: path 60 | -------------------------------------------------------------------------------- /exercises/chapter5/src/Data/Path.purs: -------------------------------------------------------------------------------- 1 | module Data.Path 2 | ( Path(..) 3 | , root 4 | , ls 5 | , filename 6 | , isDirectory 7 | , size 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Data.Maybe (Maybe(..)) 13 | 14 | data Path 15 | = Directory String (Array Path) 16 | | File String Int 17 | 18 | instance Show Path where 19 | show = filename 20 | 21 | root :: Path 22 | root = 23 | Directory "/" 24 | [ Directory "/bin/" 25 | [ File "/bin/cp" 24800 26 | , File "/bin/ls" 34700 27 | , File "/bin/mv" 20200 28 | ] 29 | , Directory "/etc/" 30 | [ File "/etc/hosts" 300 31 | ] 32 | , Directory "/home/" 33 | [ Directory "/home/user/" 34 | [ File "/home/user/todo.txt" 1020 35 | , Directory "/home/user/code/" 36 | [ Directory "/home/user/code/js/" 37 | [ File "/home/user/code/js/test.js" 40000 38 | ] 39 | , Directory "/home/user/code/haskell/" 40 | [ File "/home/user/code/haskell/test.hs" 5000 41 | ] 42 | ] 43 | ] 44 | ] 45 | ] 46 | 47 | filename :: Path -> String 48 | filename (File name _) = name 49 | filename (Directory name _) = name 50 | 51 | isDirectory :: Path -> Boolean 52 | isDirectory (Directory _ _) = true 53 | isDirectory _ = false 54 | 55 | ls :: Path -> Array Path 56 | ls (Directory _ xs) = xs 57 | ls _ = [] 58 | 59 | size :: Path -> Maybe Int 60 | size (File _ bytes) = Just bytes 61 | size _ = Nothing 62 | -------------------------------------------------------------------------------- /exercises/chapter11/src/Split.purs: -------------------------------------------------------------------------------- 1 | module Split where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Except (runExcept) 6 | import Control.Monad.Except.Trans (ExceptT, throwError) 7 | import Control.Monad.State.Trans (StateT, runStateT, get, put) 8 | import Control.Monad.Writer.Trans (WriterT, runWriterT, tell) 9 | import Control.MonadPlus (guard) 10 | import Data.Either (Either) 11 | import Data.Identity (Identity) 12 | import Data.String (take, drop, toUpper, toLower) 13 | import Data.Tuple (Tuple) 14 | 15 | type Errors = Array String 16 | 17 | type Log = Array String 18 | 19 | type Parser = StateT String (WriterT Log (ExceptT Errors Identity)) 20 | 21 | -- ANCHOR: split 22 | split :: Parser String 23 | split = do 24 | s <- get 25 | tell ["The state is " <> show s] 26 | case s of 27 | "" -> throwError ["Empty string"] 28 | _ -> do 29 | put (drop 1 s) 30 | pure (take 1 s) 31 | -- ANCHOR_END: split 32 | 33 | eof :: Parser Unit 34 | eof = do 35 | s <- get 36 | tell ["The state is " <> show s] 37 | case s of 38 | "" -> pure unit 39 | _ -> throwError ["Expected end-of-file"] 40 | 41 | -- ANCHOR: upper 42 | upper :: Parser String 43 | upper = do 44 | s <- split 45 | guard $ toUpper s == s 46 | pure s 47 | -- ANCHOR_END: upper 48 | 49 | -- ANCHOR: lower 50 | lower :: Parser String 51 | lower = do 52 | s <- split 53 | guard $ toLower s == s 54 | pure s 55 | -- ANCHOR_END: lower 56 | 57 | runParser :: forall a. Parser a -> String -> Either Errors (Tuple (Tuple a String) Log) 58 | runParser p = runExcept <<< runWriterT <<< runStateT p 59 | -------------------------------------------------------------------------------- /exercises/chapter3/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | import Data.AddressBook (AddressBook, Entry) 5 | import Data.List (filter, head, nubByEq, null) 6 | import Data.Maybe (Maybe) 7 | 8 | findEntryByStreet :: String -> AddressBook -> Maybe Entry 9 | -- Equivalent: findEntryByStreet streetName book = head (filter filterEntry book) 10 | -- Equivalent: findEntryByStreet streetName book = head $ filter filterEntry book 11 | findEntryByStreet streetName = head <<< filter filterEntry 12 | where 13 | filterEntry :: Entry -> Boolean 14 | filterEntry e = e.address.street == streetName 15 | 16 | -- Example alternative implementation using property accessor 17 | findEntryByStreet' :: String -> AddressBook -> Maybe Entry 18 | findEntryByStreet' streetName = head <<< filter (_.address.street >>> eq streetName) 19 | 20 | isInBook :: String -> String -> AddressBook -> Boolean 21 | -- Equivalent: isInBook firstName lastName book = not null $ filter filterEntry book 22 | isInBook firstName lastName = not null <<< filter filterEntry 23 | where 24 | filterEntry :: Entry -> Boolean 25 | filterEntry entry = 26 | entry.firstName == firstName && 27 | entry.lastName == lastName 28 | 29 | removeDuplicates :: AddressBook -> AddressBook 30 | -- Equivalent: removeDuplicates book = nubByEq entriesAreDuplicated book 31 | removeDuplicates = nubByEq entriesAreDuplicated 32 | where 33 | entriesAreDuplicated :: Entry -> Entry -> Boolean 34 | entriesAreDuplicated e1 e2 = 35 | e1.firstName == e2.firstName && 36 | e1.lastName == e2.lastName 37 | -------------------------------------------------------------------------------- /exercises/chapter2/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Test.MySolutions 5 | import Test.NoPeeking.Solutions -- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 6 | import Effect (Effect) 7 | import Euler (answer) 8 | import Test.Unit (suite, test) 9 | import Test.Unit.Assert as Assert 10 | import Test.Unit.Main (runTest) 11 | 12 | main :: Effect Unit 13 | main = do 14 | runTest do 15 | suite "Euler - Sum of Multiples" do 16 | test "below 10" do 17 | Assert.equal 23 (answer 10) 18 | test "below 1000" do 19 | Assert.equal 233168 (answer 1000) 20 | {- Move this block comment starting point to enable more tests 21 | This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. -} 22 | -- ANCHOR: diagonalTests 23 | suite "diagonal" do 24 | test "3 4 5" do 25 | Assert.equal 5.0 (diagonal 3.0 4.0) 26 | test "5 12 13" do 27 | Assert.equal 13.0 (diagonal 5.0 12.0) 28 | -- ANCHOR_END: diagonalTests 29 | suite "circleArea" do 30 | test "radius 1" do 31 | Assert.equal 3.141592653589793 (circleArea 1.0) 32 | test "radius 3" do 33 | Assert.equal 28.274333882308138 (circleArea 3.0) 34 | suite "leftoverCents" do 35 | test "23" do 36 | Assert.equal 23 (leftoverCents 23) 37 | test "456" do 38 | Assert.equal 56 (leftoverCents 456) 39 | test "-789" do 40 | Assert.equal (-89) (leftoverCents (-789)) 41 | 42 | {- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 43 | -} 44 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run PR Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up PureScript toolchain 16 | uses: purescript-contrib/setup-purescript@main 17 | 18 | - name: Cache PureScript dependencies 19 | uses: actions/cache@v2 20 | with: 21 | key: ${{ runner.os }}-spago-${{ hashFiles('**/*.dhall') }} 22 | path: | 23 | exercises/*/.spago 24 | exercises/*/output 25 | 26 | - name: Set up Node toolchain 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: "14.x" 30 | 31 | - name: Cache NPM dependencies 32 | uses: actions/cache@v2 33 | env: 34 | cache-name: cache-node-modules 35 | with: 36 | path: ~/.npm 37 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} 38 | restore-keys: | 39 | ${{ runner.os }}-build-${{ env.cache-name }}- 40 | ${{ runner.os }}-build- 41 | ${{ runner.os }}- 42 | 43 | - name: Install NPM dependencies 44 | run: npm install purescript spago markdownlint-cli@0.37.0 45 | 46 | - name: Run Build 47 | run: ./scripts/buildAll.sh 48 | 49 | - name: Run tests 50 | run: ./scripts/testAll.sh 51 | 52 | - name: Prepare exercises 53 | run: ./scripts/prepareExercises.sh 54 | 55 | - name: Run tests again 56 | run: ./scripts/testAll.sh 57 | 58 | - name: Run Markdown lint 59 | run: npx markdownlint **/*.md 60 | -------------------------------------------------------------------------------- /exercises/chapter8/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | import Control.Monad.ST.Ref (modify, new, read, write) 5 | import Control.Monad.ST (ST, for, run) 6 | import Data.Array (foldM, head, nub, sort, tail) 7 | import Data.Int (even, toNumber) 8 | import Data.List (List(..), (:)) 9 | import Data.Maybe (Maybe) 10 | import Effect (Effect) 11 | import Effect.Exception (error, throwException) 12 | 13 | third :: forall a. Array a -> Maybe a 14 | third arr = do 15 | skip1st <- tail arr 16 | skip2nd <- tail skip1st 17 | head skip2nd 18 | 19 | possibleSums :: Array Int -> Array Int 20 | possibleSums xs = nub $ sort $ foldM (\acc i -> [ acc, acc + i ]) 0 xs 21 | 22 | filterM :: forall a m. Monad m => (a -> m Boolean) -> List a -> m (List a) 23 | filterM _ Nil = pure Nil 24 | 25 | filterM f (x : xs) = do 26 | b <- f x 27 | xs' <- filterM f xs 28 | pure if b then x : xs' else xs' 29 | 30 | exceptionDivide :: Int -> Int -> Effect Int 31 | exceptionDivide _ 0 = throwException $ error "div zero" 32 | exceptionDivide a b = pure $ a / b 33 | 34 | estimatePi :: Int -> Number 35 | estimatePi n = 36 | run do 37 | accRef <- new 0.0 38 | for 1 (n + 1) \k -> 39 | let 40 | sign = if even k then -1.0 else 1.0 41 | in 42 | modify (\acc -> acc + sign / (2.0 * toNumber k - 1.0)) accRef 43 | final <- read accRef 44 | pure $ final * 4.0 45 | 46 | fibonacci :: Int -> Int 47 | fibonacci 0 = 0 48 | fibonacci 1 = 1 49 | fibonacci n = 50 | run 51 | ( do 52 | x <- new 0 53 | y <- new 1 54 | for 2 n \k -> do 55 | x' <- read x 56 | y' <- read y 57 | _ <- write (x' + y') y 58 | write y' x 59 | x' <- read x 60 | y' <- read y 61 | pure $ x' + y' 62 | ) 63 | -------------------------------------------------------------------------------- /exercises/chapter10/src/Data/AddressBook.purs: -------------------------------------------------------------------------------- 1 | module Data.AddressBook where 2 | 3 | import Prelude 4 | 5 | -- ANCHOR: import 6 | import Data.Argonaut (class DecodeJson, class EncodeJson) 7 | import Data.Argonaut.Decode.Generic (genericDecodeJson) 8 | import Data.Argonaut.Encode.Generic (genericEncodeJson) 9 | import Data.Generic.Rep (class Generic) 10 | -- ANCHOR_END: import 11 | import Data.Show.Generic (genericShow) 12 | 13 | type Address 14 | = { street :: String 15 | , city :: String 16 | , state :: String 17 | } 18 | 19 | address :: String -> String -> String -> Address 20 | address street city state = { street, city, state } 21 | 22 | data PhoneType 23 | = HomePhone 24 | | WorkPhone 25 | | CellPhone 26 | | OtherPhone 27 | 28 | -- ANCHOR: PhoneType_generic 29 | derive instance Generic PhoneType _ 30 | 31 | instance EncodeJson PhoneType where encodeJson = genericEncodeJson 32 | instance DecodeJson PhoneType where decodeJson = genericDecodeJson 33 | -- ANCHOR_END: PhoneType_generic 34 | instance Show PhoneType where show = genericShow 35 | 36 | type PhoneNumber 37 | = { "type" :: PhoneType 38 | , number :: String 39 | } 40 | 41 | phoneNumber :: PhoneType -> String -> PhoneNumber 42 | phoneNumber ty number = 43 | { "type": ty 44 | , number: number 45 | } 46 | 47 | type Person 48 | = { firstName :: String 49 | , lastName :: String 50 | , homeAddress :: Address 51 | , phones :: Array PhoneNumber 52 | } 53 | 54 | person :: String -> String -> Address -> Array PhoneNumber -> Person 55 | person firstName lastName homeAddress phones = { firstName, lastName, homeAddress, phones } 56 | 57 | examplePerson :: Person 58 | examplePerson = 59 | person "John" "Smith" 60 | (address "123 Fake St." "FakeTown" "CA") 61 | [ phoneNumber HomePhone "555-555-5555" 62 | , phoneNumber CellPhone "555-555-0000" 63 | ] 64 | -------------------------------------------------------------------------------- /exercises/chapter13/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Data.Array (sort, sortBy) 6 | import Data.Foldable (foldr) 7 | import Data.Function (on) 8 | import Data.List (List(..), fromFoldable) 9 | import Effect (Effect) 10 | import Merge (mergeWith, mergePoly, merge) 11 | import Sorted (sorted) 12 | import Test.QuickCheck (quickCheck, ()) 13 | import Tree (Tree, member, insert, toArray, anywhere) 14 | 15 | isSorted :: forall a. (Ord a) => Array a -> Boolean 16 | isSorted = go <<< fromFoldable 17 | where 18 | go (Cons x1 t@(Cons x2 _)) = x1 <= x2 && go t 19 | go _ = true 20 | 21 | ints :: Array Int -> Array Int 22 | ints = identity 23 | 24 | intToBool :: (Int -> Boolean) -> Int -> Boolean 25 | intToBool = identity 26 | 27 | treeOfInt :: Tree Int -> Tree Int 28 | treeOfInt = identity 29 | 30 | main :: Effect Unit 31 | main = do 32 | -- Tests for module 'Merge' 33 | 34 | quickCheck \xs ys -> 35 | let 36 | result = merge (sort xs) (sort ys) 37 | expected = sort $ xs <> ys 38 | in 39 | eq result expected "Result:\n" <> show result <> "\nnot equal to expected:\n" <> show expected 40 | 41 | quickCheck \xs ys -> 42 | eq (merge (sorted xs) (sorted ys)) (sort $ sorted xs <> sorted ys) 43 | 44 | quickCheck \xs ys -> 45 | eq (ints $ mergePoly (sorted xs) (sorted ys)) (sort $ sorted xs <> sorted ys) 46 | 47 | quickCheck \xs ys f -> 48 | let 49 | result = map f $ mergeWith (intToBool f) (sortBy (compare `on` f) xs) (sortBy (compare `on` f) ys) 50 | expected = map f $ sortBy (compare `on` f) $ xs <> ys 51 | in 52 | eq result expected 53 | 54 | -- Tests for module 'Tree' 55 | 56 | quickCheck \t a -> member a $ insert a $ treeOfInt t 57 | quickCheck \t xs -> isSorted $ toArray $ foldr insert t $ ints xs 58 | 59 | quickCheck \f g t -> 60 | anywhere (\s -> f s || g s) t == 61 | anywhere f (treeOfInt t) || anywhere g t -------------------------------------------------------------------------------- /exercises/chapter7/src/Data/AddressBook.purs: -------------------------------------------------------------------------------- 1 | module Data.AddressBook where 2 | 3 | import Prelude 4 | import Data.Generic.Rep (class Generic) 5 | import Data.Show.Generic (genericShow) 6 | 7 | type Address 8 | = { street :: String 9 | , city :: String 10 | , state :: String 11 | } 12 | 13 | -- ANCHOR: address_anno 14 | address :: String -> String -> String -> Address 15 | -- ANCHOR_END: address_anno 16 | address street city state = { street, city, state } 17 | 18 | -- ANCHOR: PhoneType 19 | data PhoneType 20 | = HomePhone 21 | | WorkPhone 22 | | CellPhone 23 | | OtherPhone 24 | -- ANCHOR_END: PhoneType 25 | 26 | {- 27 | Eq and Show instances are needed by unit tests to 28 | compare and report differences between PhoneType values 29 | (HomePhone, WorkPhone, etc). 30 | -} 31 | derive instance Eq PhoneType 32 | 33 | -- Generic Show instance 34 | derive instance Generic PhoneType _ 35 | 36 | instance Show PhoneType where 37 | show = genericShow 38 | {- 39 | -- Manually-written Show instance 40 | instance Show PhoneType where 41 | show HomePhone = "HomePhone" 42 | show WorkPhone = "WorkPhone" 43 | show CellPhone = "CellPhone" 44 | show OtherPhone = "OtherPhone" 45 | -} 46 | 47 | type PhoneNumber 48 | = { "type" :: PhoneType 49 | , number :: String 50 | } 51 | 52 | -- ANCHOR: phoneNumber_anno 53 | phoneNumber :: PhoneType -> String -> PhoneNumber 54 | -- ANCHOR_END: phoneNumber_anno 55 | phoneNumber ty number = 56 | { "type": ty 57 | , number: number 58 | } 59 | 60 | type Person 61 | = { firstName :: String 62 | , lastName :: String 63 | , homeAddress :: Address 64 | , phones :: Array PhoneNumber 65 | } 66 | 67 | -- ANCHOR: person_anno 68 | person :: String -> String -> Address -> Array PhoneNumber -> Person 69 | -- ANCHOR_END: person_anno 70 | person firstName lastName homeAddress phones = { firstName, lastName, homeAddress, phones } 71 | 72 | -- ANCHOR: examplePerson 73 | examplePerson :: Person 74 | examplePerson = 75 | person "John" "Smith" 76 | (address "123 Fake St." "FakeTown" "CA") 77 | [ phoneNumber HomePhone "555-555-5555" 78 | , phoneNumber CellPhone "555-555-0000" 79 | ] 80 | -- ANCHOR_END: examplePerson 81 | -------------------------------------------------------------------------------- /exercises/chapter5/test/Examples.purs: -------------------------------------------------------------------------------- 1 | module Test.Examples where 2 | 3 | import Prelude 4 | import Control.Alternative (guard) 5 | import Data.Array (concatMap, filter, null, tail, (..), (:)) 6 | import Data.Foldable (product) 7 | import Data.Maybe (fromMaybe) 8 | import Data.Path (Path, ls) 9 | 10 | -- ANCHOR: factorial 11 | factorial :: Int -> Int 12 | factorial 0 = 1 13 | factorial n = n * factorial (n - 1) 14 | -- ANCHOR_END: factorial 15 | 16 | -- ANCHOR: fib 17 | fib :: Int -> Int 18 | fib 0 = 0 19 | fib 1 = 1 20 | fib n = fib (n - 1) + fib (n - 2) 21 | -- ANCHOR_END: fib 22 | 23 | -- ANCHOR: length 24 | length :: forall a. Array a -> Int 25 | length [] = 0 26 | length arr = 1 + (length $ fromMaybe [] $ tail arr) 27 | -- ANCHOR_END: length 28 | 29 | -- ANCHOR: factors 30 | factors :: Int -> Array (Array Int) 31 | factors n = filter (\xs -> product xs == n) do 32 | i <- 1 .. n 33 | j <- i .. n 34 | pure [ i, j ] 35 | -- ANCHOR_END: factors 36 | 37 | -- ANCHOR: factorsV2 38 | factorsV2 :: Int -> Array (Array Int) 39 | factorsV2 n = filter (\xs -> product xs == n) do 40 | i <- 1 .. n 41 | j <- i .. n 42 | [ [ i, j ] ] 43 | -- ANCHOR_END: factorsV2 44 | 45 | -- ANCHOR: factorsV3 46 | factorsV3 :: Int -> Array (Array Int) 47 | factorsV3 n = do 48 | i <- 1 .. n 49 | j <- i .. n 50 | guard $ i * j == n 51 | pure [ i, j ] 52 | -- ANCHOR_END: factorsV3 53 | 54 | -- ANCHOR: factorialTailRec 55 | factorialTailRec :: Int -> Int -> Int 56 | factorialTailRec 0 acc = acc 57 | factorialTailRec n acc = factorialTailRec (n - 1) (acc * n) 58 | -- ANCHOR_END: factorialTailRec 59 | 60 | -- ANCHOR: lengthTailRec 61 | lengthTailRec :: forall a. Array a -> Int 62 | lengthTailRec arr = length' arr 0 63 | where 64 | length' :: Array a -> Int -> Int 65 | length' [] acc = acc 66 | length' arr' acc = length' (fromMaybe [] $ tail arr') (acc + 1) 67 | -- ANCHOR_END: lengthTailRec 68 | 69 | -- ANCHOR: allFiles_signature 70 | allFiles :: Path -> Array Path 71 | -- ANCHOR_END: allFiles_signature 72 | -- ANCHOR: allFiles_implementation 73 | allFiles file = file : concatMap allFiles (ls file) 74 | -- ANCHOR_END: allFiles_implementation 75 | 76 | -- ANCHOR: allFiles_2 77 | allFiles' :: Path -> Array Path 78 | allFiles' file = file : do 79 | child <- ls file 80 | allFiles' child 81 | -- ANCHOR_END: allFiles_2 82 | -------------------------------------------------------------------------------- /exercises/chapter6/src/Data/Hashable.purs: -------------------------------------------------------------------------------- 1 | module Data.Hashable 2 | ( HashCode 3 | , hashCode 4 | , class Hashable 5 | , hash 6 | , hashEqual 7 | , combineHashes 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Data.Char (toCharCode) 13 | import Data.Either (Either(..)) 14 | import Data.Foldable (foldl) 15 | import Data.Function (on) 16 | import Data.Maybe (Maybe(..)) 17 | import Data.String.CodeUnits (toCharArray) 18 | import Data.Tuple (Tuple(..)) 19 | 20 | -- ANCHOR: Hashable 21 | newtype HashCode = HashCode Int 22 | 23 | instance Eq HashCode where 24 | eq (HashCode a) (HashCode b) = a == b 25 | 26 | hashCode :: Int -> HashCode 27 | hashCode h = HashCode (h `mod` 65535) 28 | 29 | class Eq a <= Hashable a where 30 | hash :: a -> HashCode 31 | -- ANCHOR_END: Hashable 32 | 33 | instance Show HashCode where 34 | show (HashCode h) = "(HashCode " <> show h <> ")" 35 | 36 | -- ANCHOR: combineHashes 37 | combineHashes :: HashCode -> HashCode -> HashCode 38 | combineHashes (HashCode h1) (HashCode h2) = hashCode (73 * h1 + 51 * h2) 39 | -- ANCHOR_END: combineHashes 40 | 41 | -- ANCHOR: hashEqual 42 | hashEqual :: forall a. Hashable a => a -> a -> Boolean 43 | hashEqual = eq `on` hash 44 | -- ANCHOR_END: hashEqual 45 | 46 | -- ANCHOR: hashChar 47 | instance Hashable Char where 48 | hash = hash <<< toCharCode 49 | -- ANCHOR_END: hashChar 50 | 51 | -- ANCHOR: hashString 52 | instance Hashable String where 53 | hash = hash <<< toCharArray 54 | -- ANCHOR_END: hashString 55 | 56 | -- ANCHOR: hashInt 57 | instance Hashable Int where 58 | hash = hashCode 59 | -- ANCHOR_END: hashInt 60 | 61 | -- ANCHOR: hashBoolean 62 | instance Hashable Boolean where 63 | hash false = hashCode 0 64 | hash true = hashCode 1 65 | -- ANCHOR_END: hashBoolean 66 | 67 | -- ANCHOR: hashArray 68 | instance Hashable a => Hashable (Array a) where 69 | hash = foldl combineHashes (hashCode 0) <<< map hash 70 | -- ANCHOR_END: hashArray 71 | 72 | instance Hashable a => Hashable (Maybe a) where 73 | hash Nothing = hashCode 0 74 | hash (Just a) = hashCode 1 `combineHashes` hash a 75 | 76 | instance (Hashable a, Hashable b) => Hashable (Tuple a b) where 77 | hash (Tuple a b) = hash a `combineHashes` hash b 78 | 79 | instance (Hashable a, Hashable b) => Hashable (Either a b) where 80 | hash (Left a) = hashCode 0 `combineHashes` hash a 81 | hash (Right b) = hashCode 1 `combineHashes` hash b 82 | -------------------------------------------------------------------------------- /exercises/chapter10/src/Data/AddressBook/Validation.purs: -------------------------------------------------------------------------------- 1 | module Data.AddressBook.Validation where 2 | 3 | import Prelude 4 | 5 | import Data.AddressBook (Address, Person, PhoneNumber, address, person, phoneNumber) 6 | import Data.Either (Either) 7 | import Data.String (length) 8 | import Data.String.Regex (Regex, test) 9 | import Data.String.Regex.Flags (noFlags) 10 | import Data.String.Regex.Unsafe (unsafeRegex) 11 | import Data.Traversable (traverse) 12 | import Data.Validation.Semigroup (V, invalid, toEither) 13 | 14 | type Errors 15 | = Array String 16 | 17 | nonEmpty :: String -> String -> V Errors String 18 | nonEmpty field "" = invalid [ "Field '" <> field <> "' cannot be empty" ] 19 | nonEmpty _ value = pure value 20 | 21 | validatePhoneNumbers :: String -> Array PhoneNumber -> V Errors (Array PhoneNumber) 22 | validatePhoneNumbers field [] = 23 | invalid [ "Field '" <> field <> "' must contain at least one value" ] 24 | validatePhoneNumbers _ phones = 25 | traverse validatePhoneNumber phones 26 | 27 | lengthIs :: String -> Int -> String -> V Errors String 28 | lengthIs field len value | length value /= len = 29 | invalid [ "Field '" <> field <> "' must have length " <> show len ] 30 | lengthIs _ _ value = pure value 31 | 32 | phoneNumberRegex :: Regex 33 | phoneNumberRegex = unsafeRegex "^\\d{3}-\\d{3}-\\d{4}$" noFlags 34 | 35 | matches :: String -> Regex -> String -> V Errors String 36 | matches _ regex value | test regex value 37 | = pure value 38 | matches field _ _ = invalid [ "Field '" <> field <> "' did not match the required format" ] 39 | 40 | validateAddress :: Address -> V Errors Address 41 | validateAddress a = 42 | address <$> nonEmpty "Street" a.street 43 | <*> nonEmpty "City" a.city 44 | <*> lengthIs "State" 2 a.state 45 | 46 | validatePhoneNumber :: PhoneNumber -> V Errors PhoneNumber 47 | validatePhoneNumber pn = 48 | phoneNumber <$> pure pn."type" 49 | <*> matches "Number" phoneNumberRegex pn.number 50 | 51 | validatePerson :: Person -> V Errors Person 52 | validatePerson p = 53 | person <$> nonEmpty "First Name" p.firstName 54 | <*> nonEmpty "Last Name" p.lastName 55 | <*> validateAddress p.homeAddress 56 | <*> validatePhoneNumbers "Phone Numbers" p.phones 57 | 58 | validatePerson' :: Person -> Either Errors Person 59 | validatePerson' p = toEither $ validatePerson p 60 | -------------------------------------------------------------------------------- /exercises/chapter8/src/Data/AddressBook/Validation.purs: -------------------------------------------------------------------------------- 1 | module Data.AddressBook.Validation where 2 | 3 | import Prelude 4 | 5 | import Data.AddressBook (Address, Person, PhoneNumber, address, person, phoneNumber) 6 | import Data.Either (Either) 7 | import Data.String (length) 8 | import Data.String.Regex (Regex, test) 9 | import Data.String.Regex.Flags (noFlags) 10 | import Data.String.Regex.Unsafe (unsafeRegex) 11 | import Data.Traversable (traverse) 12 | import Data.Validation.Semigroup (V, invalid, toEither) 13 | 14 | type Errors 15 | = Array String 16 | 17 | nonEmpty :: String -> String -> V Errors String 18 | nonEmpty field "" = invalid [ "Field '" <> field <> "' cannot be empty" ] 19 | nonEmpty _ value = pure value 20 | 21 | validatePhoneNumbers :: String -> Array PhoneNumber -> V Errors (Array PhoneNumber) 22 | validatePhoneNumbers field [] = 23 | invalid [ "Field '" <> field <> "' must contain at least one value" ] 24 | validatePhoneNumbers _ phones = 25 | traverse validatePhoneNumber phones 26 | 27 | lengthIs :: String -> Int -> String -> V Errors String 28 | lengthIs field len value | length value /= len = 29 | invalid [ "Field '" <> field <> "' must have length " <> show len ] 30 | lengthIs _ _ value = pure value 31 | 32 | phoneNumberRegex :: Regex 33 | phoneNumberRegex = unsafeRegex "^\\d{3}-\\d{3}-\\d{4}$" noFlags 34 | 35 | matches :: String -> Regex -> String -> V Errors String 36 | matches _ regex value | test regex value 37 | = pure value 38 | matches field _ _ = invalid [ "Field '" <> field <> "' did not match the required format" ] 39 | 40 | validateAddress :: Address -> V Errors Address 41 | validateAddress a = 42 | address <$> nonEmpty "Street" a.street 43 | <*> nonEmpty "City" a.city 44 | <*> lengthIs "State" 2 a.state 45 | 46 | validatePhoneNumber :: PhoneNumber -> V Errors PhoneNumber 47 | validatePhoneNumber pn = 48 | phoneNumber <$> pure pn."type" 49 | <*> matches "Number" phoneNumberRegex pn.number 50 | 51 | validatePerson :: Person -> V Errors Person 52 | validatePerson p = 53 | person <$> nonEmpty "First Name" p.firstName 54 | <*> nonEmpty "Last Name" p.lastName 55 | <*> validateAddress p.homeAddress 56 | <*> validatePhoneNumbers "Phone Numbers" p.phones 57 | 58 | validatePerson' :: Person -> Either Errors Person 59 | validatePerson' p = toEither $ validatePerson p 60 | -------------------------------------------------------------------------------- /exercises/chapter10/test/no-peeking/Solutions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export const volumeFn = function (x, y, z) { 4 | return x * y * z; 5 | } 6 | 7 | export const volumeArrow = x => y => z => 8 | x * y * z; 9 | 10 | export const cumulativeSumsComplex = arr => { 11 | let sum = { real: 0, imag: 0 } 12 | let sums = [] 13 | arr.forEach(x => { 14 | sum = { 15 | real: sum.real + x.real, 16 | imag: sum.imag + x.imag 17 | }; 18 | sums.push(sum); 19 | }); 20 | return sums; 21 | }; 22 | 23 | export const quadraticRootsImpl = mkPair => poly => { 24 | let { a, b, c } = poly; 25 | let radicand = b * b - 4 * a * c; 26 | if (radicand >= 0) { 27 | let rt = Math.sqrt(radicand); 28 | return mkPair 29 | ({ real: (-b + rt) / (2 * a), imag: 0 }) 30 | ({ real: (-b - rt) / (2 * a), imag: 0 }); 31 | } else { 32 | let rt = Math.sqrt(-radicand); 33 | return mkPair 34 | ({ real: -b / (2 * a), imag: rt / (2 * a) }) 35 | ({ real: -b / (2 * a), imag: -rt / (2 * a) }); 36 | } 37 | }; 38 | 39 | export const toMaybeImpl = just => nothing => undefined$ => { 40 | if (undefined$ === undefined) { 41 | return nothing 42 | } else { 43 | return just(undefined$) 44 | } 45 | } 46 | 47 | export const valuesOfMapJson = j => { 48 | let m = new Map(j); 49 | let s = new Set(m.values()) 50 | return Array.from(s); 51 | }; 52 | 53 | export const quadraticRootsSetJson = poly => { 54 | let { a, b, c } = poly; 55 | let radicand = b * b - 4 * a * c; 56 | if (radicand >= 0) { 57 | let rt = Math.sqrt(radicand); 58 | return Array.from(new Set([ 59 | { real: (-b + rt) / (2 * a), imag: 0 }, 60 | { real: (-b - rt) / (2 * a), imag: 0 }])); 61 | } else { 62 | let rt = Math.sqrt(-radicand); 63 | return Array.from(new Set([ 64 | { real: -b / (2 * a), imag: rt / (2 * a) }, 65 | { real: -b / (2 * a), imag: -rt / (2 * a) }])); 66 | } 67 | }; 68 | 69 | export const quadraticRootsSafeJson = poly => { 70 | let { a, b, c } = poly; 71 | let radicand = b * b - 4 * a * c; 72 | if (radicand >= 0) { 73 | let rt = Math.sqrt(radicand); 74 | return [ 75 | { real: (-b + rt) / (2 * a), imag: 0 }, 76 | { real: (-b - rt) / (2 * a), imag: 0 }]; 77 | } else { 78 | let rt = Math.sqrt(-radicand); 79 | return [ 80 | { real: -b / (2 * a), imag: rt / (2 * a) }, 81 | { real: -b / (2 * a), imag: -rt / (2 * a) }]; 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /exercises/chapter12/src/Example/Refs.purs: -------------------------------------------------------------------------------- 1 | module Example.Refs where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Console (logShow) 7 | import Effect.Ref as Ref 8 | import Data.Int (toNumber) 9 | import Data.Maybe (Maybe(..)) 10 | import Graphics.Canvas (Context2D, getContext2D, getCanvasElementById, 11 | rect, fillPath, translate, scale, rotate, withContext, 12 | setFillStyle) 13 | import Data.Number as Number 14 | import Partial.Unsafe (unsafePartial) 15 | import Web.DOM.Document (toParentNode) 16 | import Web.DOM.Element (toEventTarget) 17 | import Web.DOM.ParentNode (QuerySelector(..), querySelector) 18 | import Web.Event.Event (EventType(..)) 19 | import Web.Event.EventTarget (addEventListener, eventListener) 20 | import Web.HTML (window) 21 | import Web.HTML.HTMLDocument (toDocument) 22 | import Web.HTML.Window (document) 23 | 24 | render :: Context2D -> Int -> Effect Unit 25 | render ctx count = void do 26 | setFillStyle ctx "#FFF" 27 | 28 | fillPath ctx $ rect ctx 29 | { x: 0.0 30 | , y: 0.0 31 | , width: 600.0 32 | , height: 600.0 33 | } 34 | 35 | setFillStyle ctx "#0F0" 36 | 37 | -- ANCHOR: withContext 38 | withContext ctx do 39 | let scaleX = Number.sin (toNumber count * Number.tau / 8.0) + 1.5 40 | let scaleY = Number.sin (toNumber count * Number.tau / 12.0) + 1.5 41 | 42 | translate ctx { translateX: 300.0, translateY: 300.0 } 43 | rotate ctx (toNumber count * Number.tau / 36.0) 44 | scale ctx { scaleX: scaleX, scaleY: scaleY } 45 | translate ctx { translateX: -100.0, translateY: -100.0 } 46 | 47 | fillPath ctx $ rect ctx 48 | { x: 0.0 49 | , y: 0.0 50 | , width: 200.0 51 | , height: 200.0 52 | } 53 | -- ANCHOR_END: withContext 54 | 55 | main :: Effect Unit 56 | main = void $ unsafePartial do 57 | Just canvas <- getCanvasElementById "canvas" 58 | ctx <- getContext2D canvas 59 | 60 | -- ANCHOR: clickCount 61 | clickCount <- Ref.new 0 62 | -- ANCHOR_END: clickCount 63 | 64 | render ctx 0 65 | doc <- map (toParentNode <<< toDocument) (document =<< window) 66 | Just node <- querySelector (QuerySelector "#canvas") doc 67 | 68 | clickListener <- eventListener $ \_ -> do 69 | logShow "Mouse clicked!" 70 | -- ANCHOR: count 71 | count <- Ref.modify (\count -> count + 1) clickCount 72 | -- ANCHOR_END: count 73 | render ctx count 74 | 75 | addEventListener (EventType "click") clickListener true (toEventTarget node) 76 | -------------------------------------------------------------------------------- /exercises/chapter3/src/Data/AddressBook.purs: -------------------------------------------------------------------------------- 1 | -- ANCHOR: imports 2 | module Data.AddressBook where 3 | 4 | import Prelude 5 | 6 | import Control.Plus (empty) 7 | import Data.List (List(..), filter, head) 8 | import Data.Maybe (Maybe) 9 | -- ANCHOR_END: imports 10 | 11 | -- ANCHOR: Address 12 | type Address = 13 | { street :: String 14 | , city :: String 15 | , state :: String 16 | } 17 | -- ANCHOR_END: Address 18 | 19 | -- ANCHOR: Entry 20 | type Entry = 21 | { firstName :: String 22 | , lastName :: String 23 | , address :: Address 24 | } 25 | -- ANCHOR_END: Entry 26 | 27 | -- ANCHOR: AddressBook 28 | type AddressBook = List Entry 29 | -- ANCHOR_END: AddressBook 30 | 31 | -- ANCHOR: showAddress 32 | showAddress :: Address -> String 33 | showAddress addr = addr.street <> ", " <> 34 | addr.city <> ", " <> 35 | addr.state 36 | -- ANCHOR_END: showAddress 37 | 38 | -- ANCHOR: showEntry_signature 39 | showEntry :: Entry -> String 40 | -- ANCHOR_END: showEntry_signature 41 | -- ANCHOR: showEntry_implementation 42 | showEntry entry = entry.lastName <> ", " <> 43 | entry.firstName <> ": " <> 44 | showAddress entry.address 45 | -- ANCHOR_END: showEntry_implementation 46 | 47 | -- ANCHOR: emptyBook 48 | emptyBook :: AddressBook 49 | emptyBook = empty 50 | -- ANCHOR_END: emptyBook 51 | 52 | -- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. NOTE TO MAINTAINER: If editing `insertEntry`, remember to also update the non-anchored (and unsimplified) version of this function that is hardcoded in the book text. 53 | -- ANCHOR: insertEntry 54 | -- ANCHOR: insertEntry_signature 55 | insertEntry :: Entry -> AddressBook -> AddressBook 56 | -- ANCHOR_END: insertEntry_signature 57 | insertEntry = Cons 58 | -- ANCHOR_END: insertEntry 59 | 60 | -- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. NOTE TO MAINTAINER: If editing `findEntry`, remember to also update the non-anchored (and unsimplified) version of this function that is hardcoded in the book text. 61 | -- ANCHOR: findEntry_signature 62 | findEntry :: String -> String -> AddressBook -> Maybe Entry 63 | -- ANCHOR_END: findEntry_signature 64 | -- ANCHOR: findEntry_implementation 65 | findEntry firstName lastName = head <<< filter filterEntry 66 | where 67 | -- ANCHOR_END: findEntry_implementation 68 | filterEntry :: Entry -> Boolean 69 | filterEntry entry = entry.firstName == firstName && entry.lastName == lastName 70 | 71 | -------------------------------------------------------------------------------- /exercises/chapter9/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | import Affjax as AX 5 | import Affjax.ResponseFormat as AXRF 6 | import Affjax.Node as AN 7 | import Control.Parallel (parOneOf, parTraverse) 8 | import Data.Array (concat, (:)) 9 | import Data.Either (Either(..), hush) 10 | import Data.Maybe (Maybe(..)) 11 | import Data.String (Pattern(..), length, split) 12 | import Data.Traversable (fold, traverse) 13 | import Effect.Aff (Aff, Error, Milliseconds(..), attempt, delay) 14 | import Node.Encoding (Encoding(..)) 15 | import Node.FS.Aff (readTextFile, writeTextFile) 16 | import Node.Path (FilePath) 17 | import Node.Path as Path 18 | 19 | -- Group: Async PS 20 | concatenateFiles :: FilePath -> FilePath -> FilePath -> Aff Unit 21 | concatenateFiles f1 f2 fConcat = do 22 | f1Data <- readTextFile UTF8 f1 23 | f2Data <- readTextFile UTF8 f2 24 | writeTextFile UTF8 fConcat $ f1Data <> f2Data 25 | 26 | concatenateMany :: Array FilePath -> FilePath -> Aff Unit 27 | concatenateMany arr out = do 28 | arrContents <- traverse (readTextFile UTF8) arr 29 | writeTextFile UTF8 out $ fold arrContents 30 | 31 | countCharacters :: FilePath -> Aff (Either Error Int) 32 | countCharacters file = 33 | attempt do 34 | contents <- readTextFile UTF8 file 35 | pure $ length contents 36 | 37 | -- Group: HTTP 38 | writeGet :: String -> FilePath -> Aff Unit 39 | writeGet url out = do 40 | result <- AN.get AXRF.string url 41 | let 42 | str = case result of 43 | Left err -> "GET /api response failed to decode: " <> AX.printError err 44 | Right response -> response.body 45 | writeTextFile UTF8 out str 46 | 47 | -- Group: Parallel 48 | concatenateManyParallel :: Array FilePath -> FilePath -> Aff Unit 49 | concatenateManyParallel arr out = do 50 | arrContents <- parTraverse (readTextFile UTF8) arr 51 | writeTextFile UTF8 out $ fold arrContents 52 | 53 | getWithTimeout :: Number -> String -> Aff (Maybe String) 54 | getWithTimeout ms url = 55 | parOneOf 56 | [ AN.get AXRF.string url <#> hush <#> map _.body 57 | , delay (Milliseconds ms) $> Nothing 58 | ] 59 | 60 | recurseFiles :: FilePath -> Aff (Array FilePath) 61 | recurseFiles file = do 62 | contents <- readTextFile UTF8 file 63 | case contents of 64 | "" -> pure [ file ] 65 | c -> do 66 | let 67 | dir = Path.dirname file 68 | 69 | files = split (Pattern "\n") contents 70 | 71 | filesFromRoot = map (\f -> Path.concat [ dir, f ]) files 72 | arrarr <- parTraverse recurseFiles filesFromRoot 73 | pure $ file : concat arrarr 74 | -------------------------------------------------------------------------------- /exercises/chapter11/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.RWS (RWSResult(..), runRWS) 6 | import Data.Foldable (fold, for_) 7 | import Data.GameEnvironment (GameEnvironment, gameEnvironment) 8 | import Data.GameState (GameState, initialGameState) 9 | import Data.Newtype (wrap) 10 | import Data.String (split) 11 | import Effect (Effect) 12 | import Effect.Console (log) 13 | import Game (game) 14 | -- ANCHOR: import_RL 15 | import Node.ReadLine as RL 16 | -- ANCHOR_END: import_RL 17 | import Options.Applicative ((<**>)) 18 | import Options.Applicative as OP 19 | 20 | -- ANCHOR: runGame_sig 21 | runGame :: GameEnvironment -> Effect Unit 22 | -- ANCHOR_END: runGame_sig 23 | -- ANCHOR: runGame_interface 24 | runGame env = do 25 | interface <- RL.createConsoleInterface RL.noCompletion 26 | -- ANCHOR_END: runGame_interface 27 | -- ANCHOR: runGame_prompt 28 | RL.setPrompt "> " interface 29 | -- ANCHOR_END: runGame_prompt 30 | 31 | let 32 | -- ANCHOR: runGame_lineHandler 33 | lineHandler :: GameState -> String -> Effect Unit 34 | lineHandler currentState input = do 35 | case runRWS (game (split (wrap " ") input)) env currentState of 36 | RWSResult state _ written -> do 37 | for_ written log 38 | RL.setLineHandler (lineHandler state) $ interface 39 | RL.prompt interface 40 | pure unit 41 | -- ANCHOR_END: runGame_lineHandler 42 | 43 | -- ANCHOR: runGame_attach_handler 44 | RL.setLineHandler (lineHandler initialGameState) interface 45 | RL.prompt interface 46 | -- ANCHOR_END: runGame_attach_handler 47 | 48 | pure unit 49 | 50 | main :: Effect Unit 51 | -- ANCHOR: main 52 | main = OP.customExecParser prefs argParser >>= runGame 53 | -- ANCHOR_END: main 54 | where 55 | 56 | -- ANCHOR: argParser 57 | argParser :: OP.ParserInfo GameEnvironment 58 | argParser = OP.info (env <**> OP.helper) parserOptions 59 | -- ANCHOR_END: argParser 60 | 61 | -- ANCHOR: env 62 | env :: OP.Parser GameEnvironment 63 | env = gameEnvironment <$> player <*> debug 64 | 65 | player :: OP.Parser String 66 | player = OP.strOption $ fold 67 | [ OP.long "player" 68 | , OP.short 'p' 69 | , OP.metavar "" 70 | , OP.help "The player's name " 71 | ] 72 | 73 | debug :: OP.Parser Boolean 74 | debug = OP.switch $ fold 75 | [ OP.long "debug" 76 | , OP.short 'd' 77 | , OP.help "Use debug mode" 78 | ] 79 | -- ANCHOR_END: env 80 | 81 | prefs = OP.prefs OP.showHelpOnEmpty 82 | -- ANCHOR: parserOptions 83 | parserOptions = fold 84 | [ OP.fullDesc 85 | , OP.progDesc "Play the game as " 86 | , OP.header "Monadic Adventures! A game to learn monad transformers" 87 | ] 88 | -- ANCHOR_END: parserOptions 89 | -------------------------------------------------------------------------------- /exercises/chapter12/src/Example/LSystem.purs: -------------------------------------------------------------------------------- 1 | module Example.LSystem where 2 | 3 | import Prelude 4 | 5 | import Data.Maybe (Maybe(..)) 6 | import Data.Array (concatMap, foldM) 7 | import Effect (Effect) 8 | import Graphics.Canvas (strokePath, setStrokeStyle, lineTo, moveTo, 9 | getContext2D, getCanvasElementById) 10 | import Data.Number as Number 11 | import Partial.Unsafe (unsafePartial) 12 | 13 | -- ANCHOR: lsystem_anno 14 | lsystem :: forall a m s 15 | . Monad m 16 | => Array a 17 | -> (a -> Array a) 18 | -> (s -> a -> m s) 19 | -> Int 20 | -> s 21 | -> m s 22 | -- ANCHOR_END: lsystem_anno 23 | -- ANCHOR: lsystem_impl 24 | lsystem init prod interpret n state = go init n 25 | where 26 | -- ANCHOR_END: lsystem_impl 27 | -- ANCHOR: lsystem_go_s_0 28 | go s 0 = foldM interpret state s 29 | -- ANCHOR_END: lsystem_go_s_0 30 | -- ANCHOR: lsystem_go_s_i 31 | go s i = go (concatMap prod s) (i - 1) 32 | -- ANCHOR_END: lsystem_go_s_i 33 | 34 | -- ANCHOR: letter 35 | data Letter = L | R | F 36 | -- ANCHOR_END: letter 37 | 38 | -- ANCHOR: sentence 39 | type Sentence = Array Letter 40 | -- ANCHOR_END: sentence 41 | 42 | -- ANCHOR: state 43 | type State = 44 | { x :: Number 45 | , y :: Number 46 | , theta :: Number 47 | } 48 | -- ANCHOR_END: state 49 | 50 | -- ANCHOR: initial 51 | initial :: Sentence 52 | initial = [F, R, R, F, R, R, F, R, R] 53 | -- ANCHOR_END: initial 54 | 55 | -- ANCHOR: productions 56 | productions :: Letter -> Sentence 57 | productions L = [L] 58 | productions R = [R] 59 | productions F = [F, L, F, R, R, F, L, F] 60 | -- ANCHOR_END: productions 61 | 62 | -- ANCHOR: initialState 63 | initialState :: State 64 | initialState = { x: 120.0, y: 200.0, theta: 0.0 } 65 | -- ANCHOR_END: initialState 66 | 67 | main :: Effect Unit 68 | main = void $ unsafePartial do 69 | Just canvas <- getCanvasElementById "canvas" 70 | ctx <- getContext2D canvas 71 | 72 | let 73 | -- ANCHOR: interpret_anno 74 | interpret :: State -> Letter -> Effect State 75 | -- ANCHOR_END: interpret_anno 76 | -- ANCHOR: interpretLR 77 | interpret state L = pure $ state { theta = state.theta - Number.tau / 6.0 } 78 | interpret state R = pure $ state { theta = state.theta + Number.tau / 6.0 } 79 | -- ANCHOR_END: interpretLR 80 | -- ANCHOR: interpretF 81 | interpret state F = do 82 | let x = state.x + Number.cos state.theta * 1.5 83 | y = state.y + Number.sin state.theta * 1.5 84 | moveTo ctx state.x state.y 85 | lineTo ctx x y 86 | pure { x, y, theta: state.theta } 87 | -- ANCHOR_END: interpretF 88 | 89 | setStrokeStyle ctx "#000" 90 | 91 | -- ANCHOR: strokePath 92 | strokePath ctx $ lsystem initial productions interpret 5 initialState 93 | -- ANCHOR_END: strokePath 94 | -------------------------------------------------------------------------------- /exercises/chapter14/src/Data/DOM/Smart.purs: -------------------------------------------------------------------------------- 1 | module Data.DOM.Smart 2 | ( Element 3 | , Attribute 4 | , Content 5 | , AttributeKey 6 | 7 | , a 8 | , p 9 | , img 10 | 11 | , href 12 | , _class 13 | , src 14 | , width 15 | , height 16 | 17 | , attribute, (:=) 18 | , text 19 | , elem 20 | 21 | , render 22 | ) where 23 | 24 | import Prelude 25 | 26 | import Data.Maybe (Maybe(..)) 27 | import Data.String (joinWith) 28 | 29 | newtype Element = Element 30 | { name :: String 31 | , attribs :: Array Attribute 32 | , content :: Maybe (Array Content) 33 | } 34 | 35 | data Content 36 | = TextContent String 37 | | ElementContent Element 38 | 39 | newtype Attribute = Attribute 40 | { key :: String 41 | , value :: String 42 | } 43 | 44 | element :: String -> Array Attribute -> Maybe (Array Content) -> Element 45 | element name attribs content = Element 46 | { name: name 47 | , attribs: attribs 48 | , content: content 49 | } 50 | 51 | text :: String -> Content 52 | text = TextContent 53 | 54 | elem :: Element -> Content 55 | elem = ElementContent 56 | 57 | newtype AttributeKey = AttributeKey String 58 | 59 | attribute :: AttributeKey -> String -> Attribute 60 | attribute (AttributeKey key) value = Attribute 61 | { key: key 62 | , value: value 63 | } 64 | 65 | infix 4 attribute as := 66 | 67 | a :: Array Attribute -> Array Content -> Element 68 | a attribs content = element "a" attribs (Just content) 69 | 70 | p :: Array Attribute -> Array Content -> Element 71 | p attribs content = element "p" attribs (Just content) 72 | 73 | img :: Array Attribute -> Element 74 | img attribs = element "img" attribs Nothing 75 | 76 | href :: AttributeKey 77 | href = AttributeKey "href" 78 | 79 | _class :: AttributeKey 80 | _class = AttributeKey "class" 81 | 82 | src :: AttributeKey 83 | src = AttributeKey "src" 84 | 85 | width :: AttributeKey 86 | width = AttributeKey "width" 87 | 88 | height :: AttributeKey 89 | height = AttributeKey "height" 90 | 91 | render :: Element -> String 92 | render (Element e) = 93 | "<" <> e.name <> 94 | " " <> joinWith " " (map renderAttribute e.attribs) <> 95 | renderContent e.content 96 | where 97 | renderAttribute :: Attribute -> String 98 | renderAttribute (Attribute x) = x.key <> "=\"" <> x.value <> "\"" 99 | 100 | renderContent :: Maybe (Array Content) -> String 101 | renderContent Nothing = " />" 102 | renderContent (Just content) = 103 | ">" <> joinWith "" (map renderContentItem content) <> 104 | " e.name <> ">" 105 | where 106 | renderContentItem :: Content -> String 107 | renderContentItem (TextContent s) = s 108 | renderContentItem (ElementContent e') = render e' 109 | -------------------------------------------------------------------------------- /exercises/chapter3/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Test.MySolutions 5 | import Test.NoPeeking.Solutions -- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 6 | import Data.AddressBook (AddressBook, Entry, emptyBook, findEntry, insertEntry) 7 | import Data.Maybe (Maybe(..)) 8 | import Effect (Effect) 9 | import Test.Unit (TestSuite, suite, test) 10 | import Test.Unit.Assert as Assert 11 | import Test.Unit.Main (runTest) 12 | 13 | john :: Entry 14 | john = 15 | { firstName: "John" 16 | , lastName: "Smith" 17 | , address: 18 | { street: "123 Fake St.", city: "Faketown", state: "CA" } 19 | } 20 | 21 | peggy :: Entry 22 | peggy = 23 | { firstName: "Peggy" 24 | , lastName: "Hill" 25 | , address: 26 | { street: "84 Rainey St.", city: "Arlen", state: "TX" } 27 | } 28 | 29 | ned :: Entry 30 | ned = 31 | { firstName: "Ned" 32 | , lastName: "Flanders" 33 | , address: 34 | { street: "740 Evergreen Terrace", city: "Springfield", state: "USA" } 35 | } 36 | 37 | book :: AddressBook 38 | book = 39 | insertEntry john 40 | $ insertEntry peggy 41 | $ insertEntry ned 42 | emptyBook 43 | 44 | otherJohn :: Entry 45 | otherJohn = 46 | { firstName: "John" 47 | , lastName: "Smith" 48 | , address: 49 | { street: "678 Fake Rd.", city: "Fakeville", state: "NY" } 50 | } 51 | 52 | 53 | bookWithDuplicate :: AddressBook 54 | bookWithDuplicate = 55 | insertEntry john 56 | $ insertEntry otherJohn 57 | book 58 | 59 | main :: Effect Unit 60 | main = 61 | runTest do 62 | runChapterExamples 63 | {- Move this block comment starting point to enable more tests 64 | This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. -} 65 | suite "Exercise - findEntryByStreet" do 66 | test "Lookup existing" do 67 | Assert.equal (Just john) 68 | $ findEntryByStreet john.address.street book 69 | test "Lookup missing" do 70 | Assert.equal Nothing 71 | $ findEntryByStreet "456 Nothing St." book 72 | suite "Exercise - isInBook" do 73 | test "Check existing" do 74 | Assert.equal true 75 | $ isInBook ned.firstName ned.lastName book 76 | test "Check missing" do 77 | Assert.equal false 78 | $ isInBook "unknown" "person" book 79 | test "Exercise - removeDuplicates" do 80 | Assert.equal book 81 | $ removeDuplicates bookWithDuplicate 82 | 83 | {- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 84 | -} 85 | runChapterExamples :: TestSuite 86 | runChapterExamples = do 87 | test "Todo for book maintainers - Add tests for chapter examples" do 88 | Assert.equal true true 89 | suite "findEntry" do 90 | test "Lookup existing" 91 | $ Assert.equal (Just ned) 92 | $ findEntry ned.firstName ned.lastName book 93 | test "Lookup missing" 94 | $ Assert.equal Nothing 95 | $ findEntry "unknown" "person" book 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PureScript by Example 2 | 3 | This repository contains a [community fork](https://github.com/purescript-contrib/purescript-book/) of _PureScript by Example_ by Phil Freeman, also known as "the PureScript book". This version differs from the original in that it has been updated so that the code and exercises work with up-to-date versions of the compiler, libraries, and tools. Some chapters have also been rewritten to showcase the latest features of the PureScript ecosystem. 4 | 5 | If you enjoyed the book or found it useful, please consider buying a copy of [the original on Leanpub](https://leanpub.com/purescript). 6 | 7 | Translations: [日本語 (Japanese)](https://gemmaro.github.io/purescript-book/) 8 | 9 | ## Status 10 | 11 | This book is being continuously updated as the language evolves, so please report any [issues](https://github.com/purescript-contrib/purescript-book/issues) you discover with the material. We appreciate any feedback you have to share, even if it's as simple as pointing out a confusing section that we could make more beginner-friendly. 12 | 13 | Unit tests are also being added to each chapter so you can check if your answers to the exercises are correct. See [#79](https://github.com/purescript-contrib/purescript-book/issues/79) for the latest status on tests. 14 | 15 | ## About the Book 16 | 17 | PureScript is a small, strongly, statically typed programming language with expressive types, written in and inspired by Haskell, and compiling to Javascript. 18 | 19 | Functional programming in JavaScript has seen quite a lot of popularity recently, but large-scale application development is hindered by the lack of a disciplined environment in which to write code. PureScript aims to solve that problem by bringing the power of strongly-typed functional programming to the world of JavaScript development. 20 | 21 | This book will show you how to get started with the PureScript programming language, from the basics (setting up a development environment) to the advanced. 22 | 23 | Each chapter will be motivated by a particular problem, and in the course of solving that problem, new functional programming tools and techniques will be introduced. Here are some examples of problems that will be solved in this book: 24 | 25 | - Transforming data structures with maps and folds 26 | - Form field validation using applicative functors 27 | - Testing code with QuickCheck 28 | - Using the canvas 29 | - Domain specific language implementation 30 | - Working with the DOM 31 | - JavaScript interoperability 32 | - Parallel asynchronous execution 33 | 34 | ## License 35 | 36 | Copyright (c) 2014-2017 Phil Freeman. 37 | 38 | The text of this book is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License: . 39 | 40 | Some text is derived from the [PureScript Documentation Repo](https://github.com/purescript/documentation), which uses the same license, and is copyright [various contributors](https://github.com/purescript/documentation/blob/master/CONTRIBUTORS.md). 41 | 42 | The exercises are licensed under the MIT license. 43 | -------------------------------------------------------------------------------- /exercises/chapter14/src/Data/DOM/Phantom.purs: -------------------------------------------------------------------------------- 1 | module Data.DOM.Phantom 2 | ( Element 3 | , Attribute 4 | , Content 5 | , AttributeKey 6 | , class IsValue 7 | , toValue 8 | 9 | , a 10 | , p 11 | , img 12 | 13 | , href 14 | , _class 15 | , src 16 | , width 17 | , height 18 | 19 | , attribute, (:=) 20 | , text 21 | , elem 22 | 23 | , render 24 | ) where 25 | 26 | import Prelude 27 | 28 | import Data.Maybe (Maybe(..)) 29 | import Data.String (joinWith) 30 | 31 | newtype Element = Element 32 | { name :: String 33 | , attribs :: Array Attribute 34 | , content :: Maybe (Array Content) 35 | } 36 | 37 | data Content 38 | = TextContent String 39 | | ElementContent Element 40 | 41 | newtype Attribute = Attribute 42 | { key :: String 43 | , value :: String 44 | } 45 | 46 | newtype AttributeKey a = AttributeKey String 47 | 48 | element :: String -> Array Attribute -> Maybe (Array Content) -> Element 49 | element name attribs content = Element 50 | { name: name 51 | , attribs: attribs 52 | , content: content 53 | } 54 | 55 | text :: String -> Content 56 | text = TextContent 57 | 58 | elem :: Element -> Content 59 | elem = ElementContent 60 | 61 | class IsValue a where 62 | toValue :: a -> String 63 | 64 | instance IsValue String where 65 | toValue = identity 66 | 67 | instance IsValue Int where 68 | toValue = show 69 | 70 | attribute :: forall a. IsValue a => AttributeKey a -> a -> Attribute 71 | attribute (AttributeKey key) value = Attribute 72 | { key: key 73 | , value: toValue value 74 | } 75 | 76 | infix 4 attribute as := 77 | 78 | a :: Array Attribute -> Array Content -> Element 79 | a attribs content = element "a" attribs (Just content) 80 | 81 | p :: Array Attribute -> Array Content -> Element 82 | p attribs content = element "p" attribs (Just content) 83 | 84 | img :: Array Attribute -> Element 85 | img attribs = element "img" attribs Nothing 86 | 87 | href :: AttributeKey String 88 | href = AttributeKey "href" 89 | 90 | _class :: AttributeKey String 91 | _class = AttributeKey "class" 92 | 93 | src :: AttributeKey String 94 | src = AttributeKey "src" 95 | 96 | width :: AttributeKey Int 97 | width = AttributeKey "width" 98 | 99 | height :: AttributeKey Int 100 | height = AttributeKey "height" 101 | 102 | render :: Element -> String 103 | render (Element e) = 104 | "<" <> e.name <> 105 | " " <> joinWith " " (map renderAttribute e.attribs) <> 106 | renderContent e.content 107 | where 108 | renderAttribute :: Attribute -> String 109 | renderAttribute (Attribute x) = x.key <> "=\"" <> x.value <> "\"" 110 | 111 | renderContent :: Maybe (Array Content) -> String 112 | renderContent Nothing = " />" 113 | renderContent (Just content) = 114 | ">" <> joinWith "" (map renderContentItem content) <> 115 | " e.name <> ">" 116 | where 117 | renderContentItem :: Content -> String 118 | renderContentItem (TextContent s) = s 119 | renderContentItem (ElementContent e') = render e' 120 | -------------------------------------------------------------------------------- /exercises/chapter4/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | 5 | import ChapterExamples (Amp(..), Volt(..), Coulomb(..)) 6 | import Data.Maybe (Maybe(Just, Nothing)) 7 | import Data.Person (Person) 8 | import Data.Picture 9 | ( Bounds 10 | , Picture 11 | , Point 12 | , Shape(Circle, Rectangle, Line, Text) 13 | , bounds 14 | , getCenter 15 | , intersect 16 | , origin 17 | ) 18 | import Data.Picture as DataP 19 | import Data.Number as Number 20 | 21 | factorial :: Int -> Int 22 | factorial 0 = 1 23 | factorial n = n * factorial (n - 1) 24 | 25 | binomial :: Int -> Int -> Int 26 | binomial _ 0 = 1 27 | binomial 0 _ = 0 28 | binomial n k | n < k = 0 29 | | otherwise = factorial n / (factorial k * (factorial (n - k))) 30 | 31 | pascal :: Int -> Int -> Int 32 | pascal _ 0 = 1 33 | pascal 0 _ = 0 34 | pascal n k 35 | = pascal (n-1) k + pascal (n - 1) (k - 1) 36 | {- 37 | Most general type for sameCity and livesInLA functions taking into account row polymorphism: 38 | 39 | sameCity 40 | :: forall r1 s1. { address :: { city :: String | s1 } | r1 } 41 | -> forall r2 s2. { address :: { city :: String | s2 } | r2 } 42 | -> Boolean 43 | 44 | livesInLA 45 | :: forall r s. { address :: { city :: String | s } | r } 46 | -> Boolean 47 | -} 48 | 49 | sameCity :: Person -> Person -> Boolean 50 | sameCity { address: { city: c1 } } { address: { city: c2 } } 51 | | c1 == c2 = true 52 | | otherwise = false 53 | 54 | fromSingleton :: forall a. a -> Array a -> a 55 | fromSingleton a [b] = b 56 | fromSingleton a _ = a 57 | 58 | circleAtOrigin :: Shape 59 | circleAtOrigin = Circle origin 10.0 60 | 61 | centerShape :: Shape -> Shape 62 | centerShape (Circle c r) = Circle origin r 63 | centerShape (Rectangle c w h) = Rectangle origin w h 64 | centerShape line@(Line s e) = Line (s - delta) (e - delta) 65 | where 66 | delta = getCenter line 67 | centerShape (Text loc text) = Text origin text 68 | 69 | scaleShape :: Number -> Shape -> Shape 70 | scaleShape i (Circle c r) = Circle c (r * i) 71 | scaleShape i (Rectangle c w h) = Rectangle c (w * i) (h * i) 72 | scaleShape i (Line s e) = Line (s * scale) (e * scale) 73 | where 74 | scale = {x: i, y: i} 75 | scaleShape i text = text 76 | 77 | doubleScaleAndCenter :: Shape -> Shape 78 | doubleScaleAndCenter = centerShape <<< scaleShape 2.0 79 | 80 | shapeText :: Shape -> Maybe String 81 | shapeText (Text _ text) = Just text 82 | shapeText _ = Nothing 83 | 84 | area :: Shape -> Number 85 | area (Circle _ r) = Number.pi * r * r 86 | area (Rectangle _ h w) = h * w 87 | area _ = 0.0 88 | 89 | {- 90 | The real solution for this exercise just requires adding the 91 | `Clipped` constructor to `Shape` directly in `Picture.purs`. 92 | But we're using `ShapeExt` here as a workaround so we don't need to edit 93 | code outside of this file. 94 | -} 95 | data ShapeExt 96 | = Clipped Picture Point Number Number 97 | | Shape Shape 98 | 99 | {- 100 | Your solution should edit `shapeBounds` in `Picture.purs`. 101 | -} 102 | shapeBounds :: ShapeExt -> Bounds 103 | shapeBounds (Clipped pic pt w h) = intersect (bounds pic) (DataP.shapeBounds (Rectangle pt w h)) 104 | shapeBounds (Shape shape) = DataP.shapeBounds shape 105 | 106 | newtype Watt = Watt Number 107 | 108 | calculateWattage :: Amp -> Volt -> Watt 109 | calculateWattage (Amp i) (Volt v) = Watt $ i * v 110 | -------------------------------------------------------------------------------- /exercises/chapter4/src/ChapterExamples.purs: -------------------------------------------------------------------------------- 1 | module ChapterExamples where 2 | 3 | import Prelude hiding (gcd) 4 | -- ANCHOR: lzsImport 5 | import Data.Array (tail) 6 | import Data.Foldable (sum) 7 | import Data.Maybe (fromMaybe) 8 | -- ANCHOR_END: lzsImport 9 | -- ANCHOR: unsafePartialImport 10 | import Partial.Unsafe (unsafePartial) 11 | -- ANCHOR_END: unsafePartialImport 12 | 13 | -- ANCHOR: gcd 14 | gcd :: Int -> Int -> Int 15 | gcd n 0 = n 16 | gcd 0 m = m 17 | gcd n m = if n > m 18 | then gcd (n - m) m 19 | else gcd n (m - n) 20 | -- ANCHOR_END: gcd 21 | 22 | -- ANCHOR: fromString 23 | fromString :: String -> Boolean 24 | fromString "true" = true 25 | fromString _ = false 26 | -- ANCHOR_END: fromString 27 | 28 | -- ANCHOR: toString 29 | toString :: Boolean -> String 30 | toString true = "true" 31 | toString false = "false" 32 | -- ANCHOR_END: toString 33 | 34 | -- ANCHOR: gcdV2 35 | gcdV2 :: Int -> Int -> Int 36 | gcdV2 n 0 = n 37 | gcdV2 0 n = n 38 | gcdV2 n m | n > m = gcdV2 (n - m) m 39 | | otherwise = gcdV2 n (m - n) 40 | -- ANCHOR_END: gcdV2 41 | 42 | -- ANCHOR: isEmpty 43 | isEmpty :: forall a. Array a -> Boolean 44 | isEmpty [] = true 45 | isEmpty _ = false 46 | -- ANCHOR_END: isEmpty 47 | 48 | -- ANCHOR: takeFive 49 | takeFive :: Array Int -> Int 50 | takeFive [0, 1, a, b, _] = a * b 51 | takeFive _ = 0 52 | -- ANCHOR_END: takeFive 53 | 54 | -- ANCHOR: showPerson 55 | showPerson :: { first :: String, last :: String } -> String 56 | showPerson { first: x, last: y } = y <> ", " <> x 57 | -- ANCHOR_END: showPerson 58 | 59 | -- ANCHOR: showPersonV2 60 | showPersonV2 :: { first :: String, last :: String } -> String 61 | showPersonV2 { first, last } = last <> ", " <> first 62 | -- ANCHOR_END: showPersonV2 63 | 64 | -- ANCHOR: unknownPerson 65 | unknownPerson :: { first :: String, last :: String } 66 | unknownPerson = { first, last } 67 | where 68 | first = "Jane" 69 | last = "Doe" 70 | -- ANCHOR_END: unknownPerson 71 | 72 | -- ANCHOR: livesInLA 73 | type Address = { street :: String, city :: String } 74 | 75 | type Person = { name :: String, address :: Address } 76 | 77 | livesInLA :: Person -> Boolean 78 | livesInLA { address: { city: "Los Angeles" } } = true 79 | livesInLA _ = false 80 | -- ANCHOR_END: livesInLA 81 | 82 | -- ANCHOR: sortPair 83 | sortPair :: Array Int -> Array Int 84 | sortPair arr@[x, y] 85 | | x <= y = arr 86 | | otherwise = [y, x] 87 | sortPair arr = arr 88 | -- ANCHOR_END: sortPair 89 | 90 | -- ANCHOR: lzs 91 | lzs :: Array Int -> Array Int 92 | lzs [] = [] 93 | lzs xs = case sum xs of 94 | 0 -> xs 95 | _ -> lzs (fromMaybe [] $ tail xs) 96 | -- ANCHOR_END: lzs 97 | 98 | -- ANCHOR: partialFunction 99 | partialFunction :: Boolean -> Boolean 100 | partialFunction = unsafePartial \true -> true 101 | -- ANCHOR_END: partialFunction 102 | 103 | -- ANCHOR: electricalUnits 104 | newtype Volt = Volt Number 105 | newtype Ohm = Ohm Number 106 | newtype Amp = Amp Number 107 | -- ANCHOR_END: electricalUnits 108 | 109 | -- ANCHOR: calculateCurrent 110 | calculateCurrent :: Volt -> Ohm -> Amp 111 | calculateCurrent (Volt v) (Ohm r) = Amp (v / r) 112 | 113 | battery :: Volt 114 | battery = Volt 1.5 115 | 116 | lightbulb :: Ohm 117 | lightbulb = Ohm 500.0 118 | 119 | current :: Amp 120 | current = calculateCurrent battery lightbulb 121 | -- ANCHOR_END: calculateCurrent 122 | 123 | -- ANCHOR: Coulomb 124 | newtype Coulomb = MakeCoulomb Number 125 | -- ANCHOR_END: Coulomb 126 | 127 | -- These are to enable testing. Will be explained in Ch6. 128 | derive newtype instance Eq Amp 129 | derive newtype instance Show Amp 130 | -------------------------------------------------------------------------------- /exercises/chapter10/test/Examples.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // ANCHOR: square 4 | export const square = function (n) { 5 | return n * n; 6 | }; 7 | // ANCHOR_END: square 8 | 9 | // ANCHOR: diagonal 10 | export const diagonal = function (w) { 11 | return function (h) { 12 | return Math.sqrt(w * w + h * h); 13 | }; 14 | }; 15 | // ANCHOR_END: diagonal 16 | 17 | // ANCHOR: diagonal_arrow 18 | export const diagonalArrow = w => h => 19 | Math.sqrt(w * w + h * h); 20 | // ANCHOR_END: diagonal_arrow 21 | 22 | // ANCHOR: diagonal_uncurried 23 | export const diagonalUncurried = function (w, h) { 24 | return Math.sqrt(w * w + h * h); 25 | }; 26 | // ANCHOR_END: diagonal_uncurried 27 | 28 | export const cumulativeSums = arr => { 29 | 30 | let sum = 0 31 | let sums = [] 32 | arr.forEach(x => { 33 | sum += x; 34 | sums.push(sum); 35 | }); 36 | return sums; 37 | }; 38 | 39 | 40 | export const addComplex = a => b => { 41 | return { 42 | real: a.real + b.real, 43 | imag: a.imag + b.imag 44 | } 45 | }; 46 | 47 | export const maybeHeadImpl = just => nothing => arr => { 48 | if (arr.length) { 49 | return just(arr[0]); 50 | } else { 51 | return nothing; 52 | } 53 | }; 54 | 55 | export const undefinedHead = arr => 56 | arr[0]; 57 | 58 | export const isUndefined = value => 59 | value === undefined; 60 | 61 | export const unsafeHead = arr => { 62 | if (arr.length) { 63 | return arr[0]; 64 | } else { 65 | throw new Error('unsafeHead: empty array'); 66 | } 67 | }; 68 | 69 | 70 | export const boldImpl = show => x => 71 | show(x).toUpperCase() + "!!!"; 72 | 73 | export const showEqualityImpl = eq => show => a => b => { 74 | if (eq(a)(b)) { 75 | return "Equivalent"; 76 | } else { 77 | return show(a) + " is not equal to " + show(b); 78 | } 79 | } 80 | 81 | export const yellImpl = show => x => () => 82 | console.log(show(x).toUpperCase() + "!!!"); 83 | 84 | export const diagonalLog = function (w, h) { 85 | let result = Math.sqrt(w * w + h * h); 86 | console.log("Diagonal is " + result); 87 | return result; 88 | }; 89 | 90 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); 91 | 92 | export const sleepImpl = ms => () => 93 | wait(ms); 94 | 95 | async function diagonalWait(delay, w, h) { 96 | await wait(delay); 97 | return Math.sqrt(w * w + h * h); 98 | } 99 | 100 | export const diagonalAsyncImpl = delay => w => h => () => 101 | diagonalWait(delay, w, h); 102 | 103 | 104 | export const cumulativeSumsBroken = arr => { 105 | let sum = 0 106 | let sums = [] 107 | arr.forEach(x => { 108 | sum += x; 109 | sums.push(sum); 110 | }); 111 | sums.push("Broken"); // Bug 112 | return sums; 113 | }; 114 | 115 | 116 | export const addComplexBroken = a => b => { 117 | return { 118 | real: a.real + b.real, 119 | broken: a.imag + b.imag // Bug 120 | } 121 | }; 122 | 123 | export const cumulativeSumsJson = cumulativeSumsBroken 124 | // Try the non-broken version too 125 | //export const cumulativeSumsJson = cumulativeSums 126 | 127 | export const addComplexJson = addComplexBroken 128 | // Try the non-broken version too 129 | //export const addComplexJson = addComplex 130 | 131 | 132 | export const mapSetFooJson = j => { 133 | let m = new Map(j); 134 | m.set("Foo", 42); 135 | return Array.from(m); 136 | }; 137 | 138 | /* 139 | These versions always point to either the working or broken versions 140 | to enable automated testing. 141 | The examples accompanying the text are meant to be swapped 142 | between versions by the reader. 143 | */ 144 | export const cumulativeSumsJsonBroken = cumulativeSumsBroken 145 | export const addComplexJsonBroken = addComplexBroken 146 | export const cumulativeSumsJsonWorking = cumulativeSums 147 | export const addComplexJsonWorking = addComplex 148 | -------------------------------------------------------------------------------- /exercises/chapter14/src/Data/DOM/Free.purs: -------------------------------------------------------------------------------- 1 | module Data.DOM.Free 2 | ( Element 3 | , Attribute 4 | , Content 5 | , ContentF 6 | , AttributeKey 7 | , class IsValue 8 | , toValue 9 | 10 | , a 11 | , p 12 | , img 13 | 14 | , href 15 | , _class 16 | , src 17 | , width 18 | , height 19 | 20 | , attribute, (:=) 21 | , text 22 | , elem 23 | 24 | , render 25 | ) where 26 | 27 | import Prelude 28 | 29 | import Control.Monad.Free (Free, runFreeM, liftF) 30 | import Control.Monad.Writer (Writer, execWriter) 31 | import Control.Monad.Writer.Class (tell) 32 | import Data.Foldable (for_) 33 | import Data.Maybe (Maybe(..)) 34 | 35 | newtype Element = Element 36 | { name :: String 37 | , attribs :: Array Attribute 38 | , content :: Maybe (Content Unit) 39 | } 40 | 41 | data ContentF a 42 | = TextContent String a 43 | | ElementContent Element a 44 | 45 | instance Functor ContentF where 46 | map f (TextContent s x) = TextContent s (f x) 47 | map f (ElementContent e x) = ElementContent e (f x) 48 | 49 | type Content = Free ContentF 50 | 51 | newtype Attribute = Attribute 52 | { key :: String 53 | , value :: String 54 | } 55 | 56 | newtype AttributeKey a = AttributeKey String 57 | 58 | element :: String -> Array Attribute -> Maybe (Content Unit) -> Element 59 | element name attribs content = Element 60 | { name: name 61 | , attribs: attribs 62 | , content: content 63 | } 64 | 65 | text :: String -> Content Unit 66 | text s = liftF $ TextContent s unit 67 | 68 | elem :: Element -> Content Unit 69 | elem e = liftF $ ElementContent e unit 70 | 71 | class IsValue a where 72 | toValue :: a -> String 73 | 74 | instance IsValue String where 75 | toValue = identity 76 | 77 | instance IsValue Int where 78 | toValue = show 79 | 80 | attribute :: forall a. IsValue a => AttributeKey a -> a -> Attribute 81 | attribute (AttributeKey key) value = Attribute 82 | { key: key 83 | , value: toValue value 84 | } 85 | 86 | infix 4 attribute as := 87 | 88 | a :: Array Attribute -> Content Unit -> Element 89 | a attribs content = element "a" attribs (Just content) 90 | 91 | p :: Array Attribute -> Content Unit -> Element 92 | p attribs content = element "p" attribs (Just content) 93 | 94 | img :: Array Attribute -> Element 95 | img attribs = element "img" attribs Nothing 96 | 97 | href :: AttributeKey String 98 | href = AttributeKey "href" 99 | 100 | _class :: AttributeKey String 101 | _class = AttributeKey "class" 102 | 103 | src :: AttributeKey String 104 | src = AttributeKey "src" 105 | 106 | width :: AttributeKey Int 107 | width = AttributeKey "width" 108 | 109 | height :: AttributeKey Int 110 | height = AttributeKey "height" 111 | 112 | render :: Element -> String 113 | render = execWriter <<< renderElement 114 | where 115 | renderElement :: Element -> Writer String Unit 116 | renderElement (Element e) = do 117 | tell "<" 118 | tell e.name 119 | for_ e.attribs $ \x -> do 120 | tell " " 121 | renderAttribute x 122 | renderContent e.content 123 | where 124 | renderAttribute :: Attribute -> Writer String Unit 125 | renderAttribute (Attribute x) = do 126 | tell x.key 127 | tell "=\"" 128 | tell x.value 129 | tell "\"" 130 | 131 | renderContent :: Maybe (Content Unit) -> Writer String Unit 132 | renderContent Nothing = tell " />" 133 | renderContent (Just content) = do 134 | tell ">" 135 | runFreeM renderContentItem content 136 | tell "" 139 | 140 | renderContentItem :: forall a. ContentF (Content a) -> Writer String (Content a) 141 | renderContentItem (TextContent s rest) = do 142 | tell s 143 | pure rest 144 | renderContentItem (ElementContent e' rest) = do 145 | renderElement e' 146 | pure rest 147 | -------------------------------------------------------------------------------- /exercises/chapter5/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | import Control.Alternative (guard) 5 | import Data.Array (cons, filter, head, length, null, tail, (..), (:)) 6 | import Data.Foldable (foldl) 7 | import Data.Maybe (Maybe(..), fromMaybe) 8 | import Data.Path (Path, filename, isDirectory, ls, size) 9 | import Data.Tuple (Tuple(..)) 10 | import Test.Examples 11 | 12 | isEven :: Int -> Boolean 13 | isEven n = 14 | if n < 0 15 | then isEven (-n) 16 | else if n == 0 17 | then true 18 | else not (isEven (n - 1)) 19 | 20 | oneIfEven :: Int -> Int 21 | oneIfEven n = if isEven n then 1 else 0 22 | 23 | countEven :: Array Int -> Int 24 | countEven xs = 25 | if null xs then 0 26 | else oneIfEven (fromMaybe 1 $ head xs ) + countEven (fromMaybe [] $ tail xs) 27 | 28 | squared :: Array Number -> Array Number 29 | squared arr = map (\n -> n * n) arr 30 | 31 | keepNonNegative :: Array Number -> Array Number 32 | keepNonNegative arr = filter (\n -> n >= 0.0) arr 33 | 34 | infix 4 filter as <$?> 35 | 36 | keepNonNegativeRewrite :: Array Number -> Array Number 37 | keepNonNegativeRewrite arr = (\n -> n >= 0.0) <$?> arr 38 | 39 | isPrime :: Int -> Boolean 40 | isPrime n = n > 1 && length (factors n) == 1 41 | 42 | cartesianProduct :: ∀ a. Array a -> Array a -> Array (Array a) 43 | cartesianProduct left right = do 44 | a_ <- left 45 | b_ <- right 46 | [ [ a_, b_ ] ] 47 | 48 | triples :: Int -> Array (Array Int) 49 | triples n = do 50 | i <- 1 .. n 51 | j <- i .. n 52 | k <- j .. n 53 | guard $ i * i + j * j == k * k 54 | pure [ i, j, k ] 55 | 56 | -- | Provide the prime numbers that, multiplied together, make the argument. 57 | primeFactors :: Int -> Array Int 58 | primeFactors n = factorize 2 n 59 | where 60 | factorize :: Int -> Int -> Array Int 61 | factorize _ 1 = [] 62 | factorize divisor dividend = 63 | if dividend `mod` divisor == 0 then 64 | cons divisor $ factorize (divisor) (dividend / divisor) 65 | else 66 | factorize (divisor + 1) dividend 67 | 68 | allTrue :: Array Boolean -> Boolean 69 | allTrue bools = foldl (\acc bool -> acc && bool) true bools 70 | 71 | {- 72 | Answer to array characterization question: 73 | `foldl (==) false xs` returns true when `xs` contains ... 74 | ... an odd number of `false` elements. 75 | -} 76 | fibTailRec :: Int -> Int 77 | fibTailRec 0 = 0 78 | fibTailRec 1 = 1 79 | fibTailRec n = fib' n 2 0 1 80 | where 81 | fib' :: Int -> Int -> Int -> Int -> Int 82 | fib' limit count n1 n2 = 83 | if limit == count then 84 | n1 + n2 85 | else 86 | fib' limit (count + 1) n2 (n1 + n2) 87 | 88 | reverse :: ∀ a. Array a -> Array a 89 | reverse = foldl (\xs x -> [ x ] <> xs) [] 90 | 91 | onlyFiles :: Path -> Array Path 92 | onlyFiles path = filter (not isDirectory) (allFiles path) 93 | 94 | allSizes :: Array Path -> Array (Tuple String Int) 95 | allSizes paths = 96 | map 97 | ( \p -> case size p of 98 | Just n -> Tuple (filename p) n 99 | Nothing -> Tuple (filename p) 0 100 | ) 101 | paths 102 | 103 | whereIs :: Path -> String -> Maybe Path 104 | whereIs path fileName = head $ do 105 | path' <- allFiles path 106 | child <- ls path' 107 | guard $ filename child == filename path' <> fileName 108 | pure path' 109 | 110 | largestSmallest :: Path -> Array Path 111 | largestSmallest path = foldl loop [] (onlyFiles path) where 112 | loop :: Array Path -> Path -> Array Path 113 | loop [largest, smallest] current | size current < size smallest = [largest, current] 114 | | size current > size largest = [current, smallest] 115 | | otherwise = [largest, smallest] 116 | loop [last] current | size current < size last = [current, last] 117 | | otherwise = [last, current] 118 | loop arr current = current : arr 119 | -------------------------------------------------------------------------------- /exercises/chapter10/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | 5 | import Control.Alt (alt) 6 | import Control.Apply (lift2) 7 | import Data.Argonaut (class DecodeJson, class EncodeJson, Json, JsonDecodeError(..), decodeJson, encodeJson, jsonParser, printJsonDecodeError) 8 | import Data.Argonaut.Decode.Generic (genericDecodeJson) 9 | import Data.Argonaut.Encode.Generic (genericEncodeJson) 10 | import Data.Bifunctor (lmap) 11 | import Data.Either (Either(..)) 12 | import Data.Eq.Generic (genericEq) 13 | import Data.Foldable (foldr) 14 | import Data.Function.Uncurried (Fn3) 15 | import Data.Generic.Rep (class Generic) 16 | import Data.Map (Map) 17 | import Data.Maybe (Maybe(..)) 18 | import Data.Pair (Pair(..)) 19 | import Data.Set (Set) 20 | import Data.Show.Generic (genericShow) 21 | import Test.Examples (Complex, Quadratic, Undefined) 22 | 23 | foreign import volumeFn :: Fn3 Number Number Number Number 24 | 25 | foreign import volumeArrow :: Number -> Number -> Number -> Number 26 | 27 | foreign import cumulativeSumsComplex :: Array Complex -> Array Complex 28 | 29 | foreign import quadraticRootsImpl :: (forall a. a -> a -> Pair a) -> Quadratic -> Pair Complex 30 | 31 | quadraticRoots :: Quadratic -> Pair Complex 32 | quadraticRoots poly = quadraticRootsImpl Pair poly 33 | 34 | foreign import toMaybeImpl :: forall a. (forall x. x -> Maybe x) -> (forall x. Maybe x) -> Undefined a -> Maybe a 35 | 36 | toMaybe :: forall a. Undefined a -> Maybe a 37 | toMaybe = toMaybeImpl Just Nothing 38 | 39 | foreign import valuesOfMapJson :: Json -> Json 40 | 41 | valuesOfMap :: Map String Int -> Either JsonDecodeError (Set Int) 42 | valuesOfMap = encodeJson >>> valuesOfMapJson >>> decodeJson 43 | 44 | valuesOfMapGeneric :: 45 | forall k v. 46 | EncodeJson k => 47 | EncodeJson v => 48 | DecodeJson v => 49 | Ord k => 50 | Ord v => 51 | Map k v -> 52 | Either JsonDecodeError (Set v) 53 | valuesOfMapGeneric = encodeJson >>> valuesOfMapJson >>> decodeJson 54 | 55 | foreign import quadraticRootsSetJson :: Json -> Json 56 | 57 | quadraticRootsSet :: Quadratic -> Either JsonDecodeError (Set Complex) 58 | quadraticRootsSet = encodeJson >>> quadraticRootsSetJson >>> decodeJson 59 | 60 | foreign import quadraticRootsSafeJson :: Json -> Json 61 | 62 | newtype WrapPair a 63 | = WrapPair (Pair a) 64 | 65 | instance DecodeJson a => DecodeJson (WrapPair a) where 66 | decodeJson j = do 67 | decoded <- decodeJson j 68 | case decoded of 69 | [ a, b ] -> map WrapPair $ lift2 Pair (decodeJson a) (decodeJson b) 70 | [ ] -> Left $ AtIndex 0 MissingValue 71 | [ a ] -> Left $ AtIndex 1 MissingValue 72 | _ -> Left $ AtIndex 2 $ UnexpectedValue j 73 | 74 | quadraticRootsSafeWrap :: Quadratic -> Either JsonDecodeError (WrapPair Complex) 75 | quadraticRootsSafeWrap = encodeJson >>> quadraticRootsSafeJson >>> decodeJson 76 | 77 | quadraticRootsSafe :: Quadratic -> Either JsonDecodeError (Pair Complex) 78 | quadraticRootsSafe = quadraticRootsSafeWrap >>> map (\(WrapPair p) -> p) 79 | 80 | parseAndDecodeArray2D :: String -> Either String (Array (Array Int)) 81 | parseAndDecodeArray2D str = do 82 | j <- jsonParser str 83 | lmap printJsonDecodeError $ decodeJson j 84 | 85 | data Tree a 86 | = Leaf a 87 | | Branch (Tree a) (Tree a) 88 | 89 | derive instance Generic (Tree a) _ 90 | 91 | instance EncodeJson a => EncodeJson (Tree a) where 92 | encodeJson t = genericEncodeJson t 93 | 94 | instance DecodeJson a => DecodeJson (Tree a) where 95 | decodeJson t = genericDecodeJson t 96 | 97 | instance Eq a => Eq (Tree a) where 98 | eq t = genericEq t 99 | 100 | instance Show a => Show (Tree a) where 101 | show t = genericShow t 102 | 103 | data IntOrString 104 | = IntOrString_Int Int 105 | | IntOrString_String String 106 | 107 | instance EncodeJson IntOrString where 108 | encodeJson (IntOrString_Int i) = encodeJson i 109 | encodeJson (IntOrString_String s) = encodeJson s 110 | 111 | instance DecodeJson IntOrString where 112 | decodeJson j = 113 | foldr alt (Left $ TypeMismatch "Not Int or String") 114 | [ map IntOrString_Int $ decodeJson j 115 | , map IntOrString_String $ decodeJson j 116 | ] 117 | 118 | derive instance Generic IntOrString _ 119 | 120 | instance Eq IntOrString where 121 | eq = genericEq 122 | 123 | instance Show IntOrString where 124 | show = genericShow 125 | -------------------------------------------------------------------------------- /exercises/chapter11/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | 5 | import Control.Alt ((<|>)) 6 | import Control.Monad.Except (ExceptT, throwError) 7 | import Control.Monad.Reader (Reader, ReaderT, ask, lift, local, runReader, runReaderT) 8 | import Control.Monad.State (State, StateT, get, put, execState, modify_) 9 | import Control.Monad.Writer (Writer, WriterT, tell, runWriter, execWriterT) 10 | import Data.Array (some) 11 | import Data.Foldable (fold, foldl) 12 | import Data.GameState (GameState(..)) 13 | import Data.Identity (Identity) 14 | import Data.List ((:)) 15 | import Data.List as L 16 | import Data.Map as M 17 | import Data.Maybe (Maybe(..)) 18 | import Data.Monoid (power) 19 | import Data.Monoid.Additive (Additive(..)) 20 | import Data.Newtype (unwrap) 21 | import Data.Set as S 22 | import Data.String (joinWith) 23 | import Data.String.CodeUnits (stripPrefix, toCharArray) 24 | import Data.String.Pattern (Pattern(..)) 25 | import Data.Traversable (sequence, traverse_) 26 | import Data.Tuple (Tuple) 27 | import Game (Game) 28 | 29 | -- 30 | 31 | testParens :: String -> Boolean 32 | testParens str = 33 | let 34 | openTally :: Char -> Int -> Int 35 | -- Open parens only considered if not already in deficit. 36 | -- No recovery from too-many closed parens. 37 | openTally '(' tally | tally >= 0 = tally + 1 38 | openTally ')' tally = tally - 1 39 | -- Non-parens has no effect 40 | openTally _ tally = tally 41 | 42 | sumParens :: Array Char -> State Int Unit 43 | sumParens = traverse_ \c -> modify_ $ openTally c 44 | 45 | finalTally :: Int 46 | finalTally = execState (sumParens $ toCharArray str) 0 47 | in 48 | finalTally == 0 49 | 50 | -- 51 | 52 | type Level = Int 53 | type Doc = (Reader Level) String 54 | 55 | line :: String -> Doc 56 | line str = do 57 | level <- ask 58 | pure $ (power " " level) <> str 59 | 60 | indent :: Doc -> Doc 61 | indent = local $ (+) 1 62 | 63 | cat :: Array Doc -> Doc 64 | cat = sequence >=> joinWith "\n" >>> pure 65 | 66 | render :: Doc -> String 67 | render doc = runReader doc 0 68 | 69 | -- 70 | 71 | sumArrayWriter :: Array Int -> Writer (Additive Int) Unit 72 | sumArrayWriter = traverse_ \n -> do 73 | tell $ Additive n 74 | pure unit 75 | 76 | -- 77 | 78 | collatz :: Int -> Tuple Int (Array Int) 79 | collatz c = runWriter $ cltz 0 c 80 | where 81 | cltz :: Int -> Int -> Writer (Array Int) Int 82 | cltz i 1 = do 83 | tell [ 1 ] 84 | pure i 85 | cltz i n = do 86 | tell [ n ] 87 | if mod n 2 == 0 88 | then cltz (i + 1) (n / 2) 89 | else cltz (i + 1) ((3 * n) + 1) 90 | 91 | -- 92 | 93 | safeDivide :: Int -> Int -> ExceptT String Identity Int 94 | safeDivide _ 0 = throwError "Divide by zero!" 95 | safeDivide a b = pure $ a / b 96 | 97 | -- 98 | 99 | type Errors = Array String 100 | type Log = Array String 101 | type Parser = StateT String (WriterT Log (ExceptT Errors Identity)) 102 | 103 | string :: String -> Parser String 104 | string prefix = do 105 | st <- get 106 | lift $ tell ["The state is " <> st] 107 | case stripPrefix (Pattern prefix) st of 108 | Just rest -> do 109 | put rest 110 | pure prefix 111 | _ -> do 112 | lift $ lift $ throwError ["Could not parse"] 113 | 114 | -- 115 | 116 | type Level' = Int 117 | type Doc' = (WriterT (Array String) (ReaderT Level' Identity)) Unit 118 | 119 | line' :: String -> Doc' 120 | line' s = do 121 | level <- lift $ ask 122 | tell [ (power " " level) <> s ] 123 | pure unit 124 | 125 | indent' :: Doc' -> Doc' 126 | indent' = local $ (+) 1 127 | 128 | render' :: Doc' -> String 129 | render' doct = joinWith "\n" $ unwrap $ runReaderT (execWriterT doct) 0 130 | 131 | asFollowedByBs :: Parser String 132 | asFollowedByBs = do 133 | as <- some $ string "a" 134 | bs <- some $ string "b" 135 | pure $ fold $ as <> bs 136 | 137 | asOrBs :: Parser String 138 | asOrBs = fold <$> some (string "a" <|> string "b") 139 | 140 | -- Note, that this function should be defined in Game.purs to avoid creating a circular dependency. 141 | cheat :: Game Unit 142 | cheat = do 143 | GameState state <- get 144 | let newInventory = foldl S.union state.inventory state.items 145 | tell $ foldl (\acc x -> ("You now have the " <> show x) : acc) L.Nil $ S.unions state.items 146 | put $ GameState state { items = M.empty, inventory = newInventory } 147 | -------------------------------------------------------------------------------- /exercises/chapter11/src/Game.purs: -------------------------------------------------------------------------------- 1 | module Game where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.RWS (RWS) 6 | import Control.Monad.Reader (ask) 7 | import Control.Monad.State (get, modify_, put) 8 | import Control.Monad.Writer (tell) 9 | import Data.Coords (Coords(..), prettyPrintCoords, coords) 10 | import Data.Foldable (for_) 11 | import Data.GameEnvironment (GameEnvironment(..)) 12 | import Data.GameItem (GameItem(..), readItem) 13 | import Data.GameState (GameState(..)) 14 | import Data.List as L 15 | import Data.Map as M 16 | import Data.Maybe (Maybe(..)) 17 | import Data.Set as S 18 | 19 | -- ANCHOR: Game 20 | type Log = L.List String 21 | 22 | type Game = RWS GameEnvironment Log GameState 23 | -- ANCHOR_END: Game 24 | 25 | describeRoom :: Game Unit 26 | describeRoom = do 27 | GameState state <- get 28 | case state.player of 29 | Coords { x: 0, y: 0 } -> tell (L.singleton "You are in a dark forest. You see a path to the north.") 30 | Coords { x: 0, y: 1 } -> tell (L.singleton "You are in a clearing.") 31 | _ -> tell (L.singleton "You are deep in the forest.") 32 | 33 | -- ANCHOR: pickup_start 34 | pickUp :: GameItem -> Game Unit 35 | pickUp item = do 36 | GameState state <- get 37 | -- ANCHOR_END: pickup_start 38 | -- ANCHOR: pickup_case 39 | case state.player `M.lookup` state.items of 40 | -- ANCHOR_END: pickup_case 41 | -- ANCHOR: pickup_Just 42 | Just items | item `S.member` items -> do 43 | -- ANCHOR_END: pickup_Just 44 | -- ANCHOR: pickup_body 45 | let newItems = M.update (Just <<< S.delete item) state.player state.items 46 | newInventory = S.insert item state.inventory 47 | put $ GameState state { items = newItems 48 | , inventory = newInventory 49 | } 50 | tell (L.singleton ("You now have the " <> show item)) 51 | -- ANCHOR_END: pickup_body 52 | -- ANCHOR: pickup_err 53 | _ -> tell (L.singleton "I don't see that item here.") 54 | -- ANCHOR_END: pickup_err 55 | 56 | move :: Int -> Int -> Game Unit 57 | move dx dy = modify_ (\(GameState state) -> GameState (state { player = updateCoords state.player })) 58 | where 59 | updateCoords :: Coords -> Coords 60 | updateCoords (Coords p) = coords (p.x + dx) (p.y + dy) 61 | 62 | -- ANCHOR: has 63 | has :: GameItem -> Game Boolean 64 | has item = do 65 | GameState state <- get 66 | pure $ item `S.member` state.inventory 67 | -- ANCHOR_END: has 68 | 69 | use :: GameItem -> Game Unit 70 | use Candle = tell (L.singleton "I don't know what you want me to do with that.") 71 | use Matches = do 72 | hasCandle <- has Candle 73 | if hasCandle 74 | then do 75 | GameEnvironment env <- ask 76 | tell (L.fromFoldable [ "You light the candle." 77 | , "Congratulations, " <> env.playerName <> "!" 78 | , "You win!" 79 | ]) 80 | else tell (L.singleton "You don't have anything to light.") 81 | 82 | -- ANCHOR: game_sig 83 | game :: Array String -> Game Unit 84 | -- ANCHOR_END: game_sig 85 | game ["look"] = do 86 | GameState state <- get 87 | tell (L.singleton ("You are at " <> prettyPrintCoords state.player)) 88 | describeRoom 89 | for_ (M.lookup state.player state.items) $ \items -> 90 | tell (map (\item -> "You can see the " <> show item <> ".") (S.toUnfoldable items :: L.List GameItem)) 91 | game ["inventory"] = do 92 | GameState state <- get 93 | tell (map (\item -> "You have the " <> show item <> ".") (S.toUnfoldable state.inventory :: L.List GameItem)) 94 | game ["north"] = move 0 1 95 | game ["south"] = move 0 (-1) 96 | game ["west"] = move (-1) 0 97 | game ["east"] = move 1 0 98 | game ["take", item] = 99 | case readItem item of 100 | Nothing -> tell (L.singleton "I don't know what item you are referring to.") 101 | Just gameItem -> pickUp gameItem 102 | game ["use", item] = 103 | case readItem item of 104 | Nothing -> tell (L.singleton "I don't know what item you are referring to.") 105 | Just gameItem -> do 106 | hasItem <- has gameItem 107 | if hasItem 108 | then use gameItem 109 | else tell (L.singleton "You don't have that item.") 110 | game ["debug"] = do 111 | -- ANCHOR: debug 112 | GameEnvironment env <- ask 113 | if env.debugMode 114 | then do 115 | state :: GameState <- get 116 | tell (L.singleton (show state)) 117 | else tell (L.singleton "Not running in debug mode.") 118 | -- ANCHOR_END: debug 119 | game [] = pure unit 120 | game _ = tell (L.singleton "I don't understand.") 121 | -------------------------------------------------------------------------------- /exercises/chapter4/src/Data/Picture.purs: -------------------------------------------------------------------------------- 1 | -- ANCHOR: module_picture 2 | module Data.Picture where 3 | 4 | import Prelude 5 | import Data.Foldable (foldl) 6 | import Data.Number (infinity) 7 | -- ANCHOR_END: module_picture 8 | -- ANCHOR: picture_import_as 9 | import Data.Number as Number 10 | -- ANCHOR_END: picture_import_as 11 | 12 | -- ANCHOR: Point 13 | type Point = 14 | { x :: Number 15 | , y :: Number 16 | } 17 | -- ANCHOR_END: Point 18 | 19 | -- ANCHOR: showPoint 20 | showPoint :: Point -> String 21 | showPoint { x, y } = 22 | "(" <> show x <> ", " <> show y <> ")" 23 | -- ANCHOR_END: showPoint 24 | 25 | -- ANCHOR: Shape 26 | data Shape 27 | = Circle Point Number 28 | | Rectangle Point Number Number 29 | | Line Point Point 30 | | Text Point String 31 | -- ANCHOR_END: Shape 32 | 33 | -- ANCHOR: showShape 34 | showShape :: Shape -> String 35 | showShape (Circle c r) = 36 | "Circle [center: " <> showPoint c <> ", radius: " <> show r <> "]" 37 | showShape (Rectangle c w h) = 38 | "Rectangle [center: " <> showPoint c <> ", width: " <> show w <> ", height: " <> show h <> "]" 39 | showShape (Line start end) = 40 | "Line [start: " <> showPoint start <> ", end: " <> showPoint end <> "]" 41 | showShape (Text loc text) = 42 | "Text [location: " <> showPoint loc <> ", text: " <> show text <> "]" 43 | -- ANCHOR_END: showShape 44 | 45 | -- ANCHOR: exampleLine 46 | exampleLine :: Shape 47 | exampleLine = Line p1 p2 48 | where 49 | p1 :: Point 50 | p1 = { x: 0.0, y: 0.0 } 51 | 52 | p2 :: Point 53 | p2 = { x: 100.0, y: 50.0 } 54 | -- ANCHOR_END: exampleLine 55 | 56 | -- ANCHOR: origin 57 | origin :: Point 58 | origin = { x, y } 59 | where 60 | x = 0.0 61 | y = 0.0 62 | -- ANCHOR_END: origin 63 | -- Would generally write it like this instead: 64 | -- origin = { x: 0.0, y: 0.0 } 65 | 66 | getCenter :: Shape -> Point 67 | getCenter (Circle c r) = c 68 | getCenter (Rectangle c w h) = c 69 | getCenter (Line s e) = (s + e) * {x: 0.5, y: 0.5} 70 | getCenter (Text loc text) = loc 71 | 72 | -- ANCHOR: Picture 73 | type Picture = Array Shape 74 | -- ANCHOR_END: Picture 75 | 76 | -- ANCHOR: showPicture 77 | showPicture :: Picture -> Array String 78 | showPicture = map showShape 79 | -- ANCHOR_END: showPicture 80 | 81 | -- ANCHOR: Bounds 82 | type Bounds = 83 | { top :: Number 84 | , left :: Number 85 | , bottom :: Number 86 | , right :: Number 87 | } 88 | -- ANCHOR_END: Bounds 89 | 90 | showBounds :: Bounds -> String 91 | showBounds b = 92 | "Bounds [top: " <> show b.top <> 93 | ", left: " <> show b.left <> 94 | ", bottom: " <> show b.bottom <> 95 | ", right: " <> show b.right <> 96 | "]" 97 | 98 | shapeBounds :: Shape -> Bounds 99 | shapeBounds (Circle { x, y } r) = 100 | { top: y - r 101 | , left: x - r 102 | , bottom: y + r 103 | , right: x + r 104 | } 105 | shapeBounds (Rectangle { x, y } w h) = 106 | { top: y - h / 2.0 107 | , left: x - w / 2.0 108 | , bottom: y + h / 2.0 109 | , right: x + w / 2.0 110 | } 111 | shapeBounds (Line p1 p2) = 112 | { top: Number.min p1.y p2.y 113 | , left: Number.min p1.x p2.x 114 | , bottom: Number.max p1.y p2.y 115 | , right: Number.max p1.x p2.x 116 | } 117 | shapeBounds (Text { x, y } _) = 118 | { top: y 119 | , left: x 120 | , bottom: y 121 | , right: x 122 | } 123 | 124 | union :: Bounds -> Bounds -> Bounds 125 | union b1 b2 = 126 | { top: Number.min b1.top b2.top 127 | , left: Number.min b1.left b2.left 128 | , bottom: Number.max b1.bottom b2.bottom 129 | , right: Number.max b1.right b2.right 130 | } 131 | 132 | intersect :: Bounds -> Bounds -> Bounds 133 | intersect b1 b2 = 134 | { top: Number.max b1.top b2.top 135 | , left: Number.max b1.left b2.left 136 | , bottom: Number.min b1.bottom b2.bottom 137 | , right: Number.min b1.right b2.right 138 | } 139 | 140 | emptyBounds :: Bounds 141 | emptyBounds = 142 | { top: infinity 143 | , left: infinity 144 | , bottom: -infinity 145 | , right: -infinity 146 | } 147 | 148 | infiniteBounds :: Bounds 149 | infiniteBounds = 150 | { top: -infinity 151 | , left: -infinity 152 | , bottom: infinity 153 | , right: infinity 154 | } 155 | 156 | -- ANCHOR: bounds 157 | bounds :: Picture -> Bounds 158 | bounds = foldl combine emptyBounds 159 | where 160 | combine :: Bounds -> Shape -> Bounds 161 | combine b shape = union (shapeBounds shape) b 162 | -- ANCHOR_END: bounds 163 | 164 | {- 165 | These `instance`s are to enable testing. 166 | Feel free to ignore these. 167 | They'll make more sense in the next chapter. 168 | -} 169 | derive instance Eq Shape 170 | 171 | instance Show Shape where 172 | show shape = showShape shape 173 | -------------------------------------------------------------------------------- /exercises/chapter14/src/Data/DOM/Name.purs: -------------------------------------------------------------------------------- 1 | module Data.DOM.Name 2 | ( Element 3 | , Attribute 4 | , Name 5 | , Content 6 | , ContentF 7 | , AttributeKey 8 | , class IsValue 9 | , toValue 10 | , Href(..) 11 | 12 | , a 13 | , p 14 | , img 15 | 16 | , href 17 | , _class 18 | , src 19 | , width 20 | , height 21 | , name 22 | 23 | , attribute, (:=) 24 | , text 25 | , elem 26 | , newName 27 | 28 | , render 29 | ) where 30 | 31 | import Prelude 32 | 33 | import Control.Monad.Free (Free, runFreeM, liftF) 34 | import Control.Monad.State (State, evalState) 35 | import Control.Monad.State.Trans (put, get) 36 | import Control.Monad.Writer.Trans (WriterT, execWriterT, tell) 37 | import Data.Foldable (for_) 38 | import Data.Maybe (Maybe(..)) 39 | 40 | newtype Element = Element 41 | { name :: String 42 | , attribs :: Array Attribute 43 | , content :: Maybe (Content Unit) 44 | } 45 | 46 | newtype Name = Name String 47 | 48 | data ContentF a 49 | = TextContent String a 50 | | ElementContent Element a 51 | | NewName (Name -> a) 52 | 53 | instance Functor ContentF where 54 | map f (TextContent s x) = TextContent s (f x) 55 | map f (ElementContent e x) = ElementContent e (f x) 56 | map f (NewName k) = NewName (f <<< k) 57 | 58 | type Content = Free ContentF 59 | 60 | newtype Attribute = Attribute 61 | { key :: String 62 | , value :: String 63 | } 64 | 65 | newtype AttributeKey a = AttributeKey String 66 | 67 | element :: String -> Array Attribute -> Maybe (Content Unit) -> Element 68 | element name_ attribs content = Element { name: name_, attribs, content } 69 | 70 | text :: String -> Content Unit 71 | text s = liftF $ TextContent s unit 72 | 73 | elem :: Element -> Content Unit 74 | elem e = liftF $ ElementContent e unit 75 | 76 | newName :: Content Name 77 | newName = liftF $ NewName identity 78 | 79 | class IsValue a where 80 | toValue :: a -> String 81 | 82 | instance IsValue String where 83 | toValue = identity 84 | 85 | instance IsValue Int where 86 | toValue = show 87 | 88 | instance IsValue Name where 89 | toValue (Name n) = n 90 | 91 | attribute :: forall a. IsValue a => AttributeKey a -> a -> Attribute 92 | attribute (AttributeKey key) value = Attribute 93 | { key: key 94 | , value: toValue value 95 | } 96 | 97 | infix 4 attribute as := 98 | 99 | a :: Array Attribute -> Content Unit -> Element 100 | a attribs content = element "a" attribs (Just content) 101 | 102 | p :: Array Attribute -> Content Unit -> Element 103 | p attribs content = element "p" attribs (Just content) 104 | 105 | img :: Array Attribute -> Element 106 | img attribs = element "img" attribs Nothing 107 | 108 | data Href 109 | = URLHref String 110 | | AnchorHref Name 111 | 112 | instance IsValue Href where 113 | toValue (URLHref url) = url 114 | toValue (AnchorHref (Name nm)) = "#" <> nm 115 | 116 | href :: AttributeKey Href 117 | href = AttributeKey "href" 118 | 119 | name :: AttributeKey Name 120 | name = AttributeKey "name" 121 | 122 | _class :: AttributeKey String 123 | _class = AttributeKey "class" 124 | 125 | src :: AttributeKey String 126 | src = AttributeKey "src" 127 | 128 | width :: AttributeKey Int 129 | width = AttributeKey "width" 130 | 131 | height :: AttributeKey Int 132 | height = AttributeKey "height" 133 | 134 | type Interp = WriterT String (State Int) 135 | 136 | render :: Element -> String 137 | render = \e -> evalState (execWriterT (renderElement e)) 0 138 | where 139 | renderElement :: Element -> Interp Unit 140 | renderElement (Element e) = do 141 | tell "<" 142 | tell e.name 143 | for_ e.attribs $ \x -> do 144 | tell " " 145 | renderAttribute x 146 | renderContent e.content 147 | where 148 | renderAttribute :: Attribute -> Interp Unit 149 | renderAttribute (Attribute x) = do 150 | tell x.key 151 | tell "=\"" 152 | tell x.value 153 | tell "\"" 154 | 155 | renderContent :: Maybe (Content Unit) -> Interp Unit 156 | renderContent Nothing = tell " />" 157 | renderContent (Just content) = do 158 | tell ">" 159 | runFreeM renderContentItem content 160 | tell "" 163 | 164 | renderContentItem :: forall a. ContentF (Content a) -> Interp (Content a) 165 | renderContentItem (TextContent s rest) = do 166 | tell s 167 | pure rest 168 | renderContentItem (ElementContent e' rest) = do 169 | renderElement e' 170 | pure rest 171 | renderContentItem (NewName k) = do 172 | n <- get 173 | let fresh = Name $ "name" <> show n 174 | put $ n + 1 175 | pure (k fresh) 176 | -------------------------------------------------------------------------------- /exercises/chapter8/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Test.MySolutions 5 | import Test.NoPeeking.Solutions -- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 6 | 7 | import Data.Either (Either(..), fromLeft, fromRight) 8 | import Data.List (List(..), foldM, (:)) 9 | import Data.Maybe (Maybe(..)) 10 | import Effect (Effect) 11 | import Effect.Exception (error, message, try) 12 | import Effect.Unsafe (unsafePerformEffect) 13 | import Data.Number (abs, pi) 14 | import Test.Examples (countThrows, safeDivide) 15 | import Test.Unit (TestSuite, suite, test) 16 | import Test.Unit.Assert as Assert 17 | import Test.Unit.Main (runTest) 18 | 19 | main :: Effect Unit 20 | main = 21 | runTest do 22 | runChapterExamples 23 | {- Move this block comment starting point to enable more tests 24 | This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. -} 25 | suite "Exercises Group - Monads and Applicatives" do 26 | suite "third" do 27 | test "No elements" 28 | $ Assert.equal Nothing 29 | $ third ([] :: Array Int) 30 | test "1 element" 31 | $ Assert.equal Nothing 32 | $ third [ 1 ] 33 | test "2 elements" 34 | $ Assert.equal Nothing 35 | $ third [ 1, 2 ] 36 | test "3 elements" 37 | $ Assert.equal (Just 3) 38 | $ third [ 1, 2, 3 ] 39 | test "4 elements" 40 | $ Assert.equal (Just 4) 41 | $ third [ 1, 2, 4, 3 ] 42 | suite "possibleSums" do 43 | test "[]" 44 | $ Assert.equal [ 0 ] 45 | $ possibleSums [] 46 | test "[1, 2, 10]" 47 | $ Assert.equal [ 0, 1, 2, 3, 10, 11, 12, 13 ] 48 | $ possibleSums [ 1, 2, 10 ] 49 | suite "filterM" do 50 | suite "Array Monad" do 51 | let 52 | onlyPositives :: Int -> Array Boolean 53 | onlyPositives i = [ i >= 0 ] 54 | test "Empty" 55 | $ Assert.equal [ Nil ] 56 | $ filterM 57 | onlyPositives 58 | Nil 59 | test "Not Empty" 60 | $ Assert.equal [ (2 : 4 : Nil) ] 61 | $ filterM 62 | onlyPositives 63 | (2 : (-1) : 4 : Nil) 64 | suite "Maybe Monad" do 65 | let 66 | -- This is an impractical filtering function, 67 | -- and could be simplified, but it's fine for 68 | -- testing purposes. 69 | onlyPositiveEvenIntegers :: Int -> Maybe Boolean 70 | onlyPositiveEvenIntegers i = if i < 0 then Nothing else Just $ 0 == i `mod` 2 71 | test "Nothing" 72 | $ Assert.equal Nothing 73 | $ filterM 74 | onlyPositiveEvenIntegers 75 | (2 : 3 : (-1) : 4 : Nil) 76 | test "Just positive even integers" 77 | $ Assert.equal (Just (2 : 4 : Nil)) 78 | $ filterM 79 | onlyPositiveEvenIntegers 80 | (2 : 3 : 4 : Nil) 81 | suite "exceptionDivide" do 82 | test "6 / 3" 83 | $ Assert.equal 2 84 | $ fromRight 0 85 | $ unsafePerformEffect 86 | $ try $ exceptionDivide 6 3 87 | test "6 / 0" 88 | $ Assert.equal "div zero" 89 | $ message 90 | $ fromLeft (error "") 91 | $ unsafePerformEffect 92 | $ try $ exceptionDivide 6 0 93 | suite "ST" do 94 | suite "estimatePi" do 95 | test "1000 terms of Gregory Series" 96 | $ Assert.assert "Estimated value of pi not within threshold" 97 | (abs (estimatePi 1000 - pi) < 0.002) 98 | test "1000000 terms of Gregory Series" 99 | $ Assert.assert "Estimated value of pi not within threshold" 100 | (abs (estimatePi 1000000 - pi) < 0.000002) 101 | suite "fibonacci" do 102 | test "40th Fibonacci number" 103 | $ Assert.equal 102334155 (fibonacci 40) 104 | test "45th Fibonacci number" 105 | $ Assert.equal 1134903170 (fibonacci 45) 106 | 107 | {- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 108 | -} 109 | runChapterExamples :: TestSuite 110 | runChapterExamples = 111 | -- Testing chapter examples in book - for reader reference only 112 | suite "Chapter Examples" do 113 | suite "countThrows" do 114 | test "10" do 115 | Assert.equal [ [ 4, 6 ], [ 5, 5 ], [ 6, 4 ] ] 116 | $ countThrows 10 117 | test "12" do 118 | Assert.equal [ [ 6, 6 ] ] 119 | $ countThrows 12 120 | suite "safeDivide" do 121 | test "Just" do 122 | Assert.equal (Just 5) 123 | $ safeDivide 10 2 124 | test "Nothing" do 125 | Assert.equal Nothing 126 | $ safeDivide 10 0 127 | suite "foldM with safeDivide" do 128 | test "[5, 2, 2] has a Just answer" do 129 | Assert.equal (Just 5) 130 | $ foldM safeDivide 100 (5 : 2 : 2 : Nil) 131 | test "[5, 0, 2] has a Nothing answer" do 132 | Assert.equal (Nothing) 133 | $ foldM safeDivide 100 (5 : 0 : 2 : Nil) 134 | -------------------------------------------------------------------------------- /exercises/chapter10/test/Examples.purs: -------------------------------------------------------------------------------- 1 | module Test.Examples where 2 | 3 | import Prelude 4 | 5 | import Control.Promise (Promise, toAffE) 6 | import Data.Argonaut (Json, JsonDecodeError, decodeJson, encodeJson) 7 | import Data.Either (Either) 8 | import Data.Function.Uncurried (Fn2, mkFn2, runFn2) 9 | import Data.Map (Map) 10 | import Data.Maybe (Maybe(..)) 11 | import Effect (Effect) 12 | import Effect.Aff (Aff) 13 | import Effect.Uncurried (EffectFn2) 14 | 15 | foreign import square :: Number -> Number 16 | 17 | -- ANCHOR: diagonal 18 | foreign import diagonal :: Number -> Number -> Number 19 | -- ANCHOR_END: diagonal 20 | 21 | -- ANCHOR: diagonal_arrow 22 | foreign import diagonalArrow :: Number -> Number -> Number 23 | -- ANCHOR_END: diagonal_arrow 24 | 25 | -- ANCHOR: diagonal_uncurried 26 | foreign import diagonalUncurried :: Fn2 Number Number Number 27 | -- ANCHOR_END: diagonal_uncurried 28 | 29 | -- ANCHOR: uncurried_add 30 | uncurriedAdd :: Fn2 Int Int Int 31 | uncurriedAdd = mkFn2 \n m -> m + n 32 | -- ANCHOR_END: uncurried_add 33 | 34 | -- ANCHOR: uncurried_sum 35 | uncurriedSum :: Int 36 | uncurriedSum = runFn2 uncurriedAdd 3 10 37 | -- ANCHOR_END: uncurried_sum 38 | 39 | -- ANCHOR: curried_add 40 | curriedAdd :: Int -> Int -> Int 41 | curriedAdd n m = m + n 42 | 43 | curriedSum :: Int 44 | curriedSum = curriedAdd 3 10 45 | -- ANCHOR_END: curried_add 46 | 47 | foreign import cumulativeSums :: Array Int -> Array Int 48 | 49 | type Complex 50 | = { real :: Number 51 | , imag :: Number 52 | } 53 | 54 | foreign import addComplex :: Complex -> Complex -> Complex 55 | 56 | foreign import maybeHeadImpl :: forall a. (forall x. x -> Maybe x) -> (forall x. Maybe x) -> Array a -> Maybe a 57 | 58 | maybeHead :: forall a. Array a -> Maybe a 59 | maybeHead arr = maybeHeadImpl Just Nothing arr 60 | 61 | foreign import data Undefined :: Type -> Type 62 | 63 | foreign import undefinedHead :: forall a. Array a -> Undefined a 64 | 65 | foreign import isUndefined :: forall a. Undefined a -> Boolean 66 | 67 | isEmpty :: forall a. Array a -> Boolean 68 | isEmpty = isUndefined <<< undefinedHead 69 | 70 | foreign import unsafeHead :: forall a. Array a -> a 71 | 72 | type Quadratic 73 | = { a :: Number 74 | , b :: Number 75 | , c :: Number 76 | } 77 | 78 | foreign import boldImpl :: forall a. (a -> String) -> a -> String 79 | 80 | bold :: forall a. Show a => a -> String 81 | bold = boldImpl show 82 | 83 | foreign import showEqualityImpl :: forall a. (a -> a -> Boolean) -> (a -> String) -> a -> a -> String 84 | 85 | showEquality :: forall a. Eq a => Show a => a -> a -> String 86 | showEquality = showEqualityImpl eq show 87 | 88 | foreign import yellImpl :: forall a. (a -> String) -> a -> Effect Unit 89 | 90 | yell :: forall a. Show a => a -> Effect Unit 91 | yell = yellImpl show 92 | 93 | foreign import diagonalLog :: EffectFn2 Number Number Number 94 | 95 | foreign import sleepImpl :: Int -> Effect (Promise Unit) 96 | 97 | sleep :: Int -> Aff Unit 98 | sleep = sleepImpl >>> toAffE 99 | 100 | foreign import diagonalAsyncImpl :: Int -> Number -> Number -> Effect (Promise Number) 101 | 102 | diagonalAsync :: Int -> Number -> Number -> Aff Number 103 | diagonalAsync i x y = toAffE $ diagonalAsyncImpl i x y 104 | 105 | foreign import cumulativeSumsBroken :: Array Int -> Array Int 106 | 107 | foreign import addComplexBroken :: Complex -> Complex -> Complex 108 | 109 | foreign import cumulativeSumsJson :: Array Int -> Json 110 | 111 | -- ANCHOR: cumulativeSumsDecoded 112 | cumulativeSumsDecoded :: Array Int -> Either JsonDecodeError (Array Int) 113 | cumulativeSumsDecoded arr = decodeJson $ cumulativeSumsJson arr 114 | -- ANCHOR_END: cumulativeSumsDecoded 115 | 116 | foreign import addComplexJson :: Complex -> Complex -> Json 117 | 118 | -- ANCHOR: addComplexDecoded 119 | addComplexDecoded :: Complex -> Complex -> Either JsonDecodeError Complex 120 | addComplexDecoded a b = decodeJson $ addComplexJson a b 121 | -- ANCHOR_END: addComplexDecoded 122 | 123 | -- ANCHOR: mapSetFooJson 124 | foreign import mapSetFooJson :: Json -> Json 125 | 126 | mapSetFoo :: Map String Int -> Either JsonDecodeError (Map String Int) 127 | mapSetFoo json = decodeJson $ mapSetFooJson $ encodeJson json 128 | -- ANCHOR_END: mapSetFooJson 129 | 130 | {- 131 | These versions always point to either the working or broken versions 132 | to enable automated testing. 133 | The examples accompanying the text are meant to be swapped 134 | between versions by the reader. 135 | -} 136 | foreign import cumulativeSumsJsonBroken :: Array Int -> Json 137 | 138 | cumulativeSumsDecodedBroken :: Array Int -> Either JsonDecodeError (Array Int) 139 | cumulativeSumsDecodedBroken = cumulativeSumsJsonBroken >>> decodeJson 140 | 141 | foreign import addComplexJsonBroken :: Complex -> Complex -> Json 142 | 143 | addComplexDecodedBroken :: Complex -> Complex -> Either JsonDecodeError Complex 144 | addComplexDecodedBroken a b = decodeJson $ addComplexJsonBroken a b 145 | 146 | foreign import cumulativeSumsJsonWorking :: Array Int -> Json 147 | 148 | cumulativeSumsDecodedWorking :: Array Int -> Either JsonDecodeError (Array Int) 149 | cumulativeSumsDecodedWorking = cumulativeSumsJsonWorking >>> decodeJson 150 | 151 | foreign import addComplexJsonWorking :: Complex -> Complex -> Json 152 | 153 | addComplexDecodedWorking :: Complex -> Complex -> Either JsonDecodeError Complex 154 | addComplexDecodedWorking a b = decodeJson $ addComplexJsonWorking a b 155 | -------------------------------------------------------------------------------- /exercises/chapter7/test/no-peeking/Solutions.purs: -------------------------------------------------------------------------------- 1 | module Test.NoPeeking.Solutions where 2 | 3 | import Prelude 4 | 5 | import Control.Apply (lift2) 6 | import Data.AddressBook (Address, PhoneNumber, address) 7 | import Data.AddressBook.Validation (Errors, matches, nonEmpty, validateAddress, validatePhoneNumbers) 8 | import Data.Generic.Rep (class Generic) 9 | import Data.Show.Generic (genericShow) 10 | import Data.Maybe (Maybe(..)) 11 | import Data.String.Regex (Regex) 12 | import Data.String.Regex.Flags (noFlags) 13 | import Data.String.Regex.Unsafe (unsafeRegex) 14 | import Data.Traversable (class Foldable, class Traversable, foldMap, foldl, foldr, sequence, traverse) 15 | import Data.Validation.Semigroup (V) 16 | 17 | {-| Exercise Group 1 -} 18 | -- Exercise 1 19 | addMaybe :: forall a. Semiring a => Maybe a -> Maybe a -> Maybe a 20 | addMaybe = lift2 add 21 | 22 | subMaybe :: forall a. Ring a => Maybe a -> Maybe a -> Maybe a 23 | subMaybe = lift2 sub 24 | 25 | mulMaybe :: forall a. Semiring a => Maybe a -> Maybe a -> Maybe a 26 | mulMaybe = lift2 mul 27 | 28 | divMaybe :: forall a. EuclideanRing a => Maybe a -> Maybe a -> Maybe a 29 | divMaybe = lift2 div 30 | 31 | -- Exercise 2 32 | addApply :: forall f a. Apply f => Semiring a => f a -> f a -> f a 33 | addApply = lift2 add 34 | 35 | mulApply :: forall f a. Apply f => Semiring a => f a -> f a -> f a 36 | mulApply = lift2 mul 37 | 38 | subApply :: forall f a. Apply f => Ring a => f a -> f a -> f a 39 | subApply = lift2 sub 40 | 41 | divApply :: forall f a. Apply f => EuclideanRing a => f a -> f a -> f a 42 | divApply = lift2 div 43 | 44 | -- Exercise 3 45 | combineMaybe :: forall a f. Applicative f => Maybe (f a) -> f (Maybe a) 46 | combineMaybe (Just x) = map Just x 47 | 48 | combineMaybe _ = pure Nothing 49 | 50 | {-| Exercise Group 2 -} 51 | -- Exercise 1 52 | stateRegex :: Regex 53 | stateRegex = unsafeRegex "^[a-zA-Z]{2}$" noFlags 54 | 55 | -- Exercise 2 56 | nonEmptyRegex :: Regex 57 | nonEmptyRegex = unsafeRegex "\\S" noFlags 58 | 59 | -- Exercise 3 60 | validateAddressImproved :: Address -> V Errors Address 61 | validateAddressImproved a = 62 | address 63 | <$> matches "Street" nonEmptyRegex a.street 64 | <*> matches "City" nonEmptyRegex a.city 65 | <*> matches "State" stateRegex a.state 66 | 67 | {-| Exercise Group 3 -} 68 | -- Exercise 1 69 | data Tree a 70 | = Leaf 71 | | Branch (Tree a) a (Tree a) 72 | 73 | -- Solution using derived instances: 74 | 75 | derive instance Eq a => Eq (Tree a) 76 | 77 | derive instance Generic (Tree a) _ 78 | 79 | instance Show a => Show (Tree a) where 80 | show t = genericShow t 81 | 82 | {- 83 | -- Solution using manually-written instances: 84 | 85 | instance Eq a => Eq (Tree a) where 86 | eq Leaf Leaf = true 87 | eq (Branch t1a va t2a) (Branch t1b vb t2b) 88 | = t1a == t1b 89 | && va == vb 90 | && t2a == t2b 91 | eq _ _ = false 92 | 93 | instance Show a => Show (Tree a) where 94 | show Leaf = "Leaf" 95 | show (Branch t1 v t2) = 96 | "(Branch " <> show t1 <> " " <> show v <> " " <> show t2 <> ")" 97 | -} 98 | 99 | -- Exercise 2 100 | instance Functor Tree where 101 | map _ Leaf = Leaf 102 | map f (Branch t1 v t2) = Branch (map f t1) (f v) (map f t2) 103 | 104 | instance Foldable Tree where 105 | foldl _ acc Leaf = acc 106 | foldl f acc (Branch t1 v t2) = foldl f (f (foldl f acc t1) v) t2 107 | foldr _ acc Leaf = acc 108 | foldr f acc (Branch t1 v t2) = foldr f (f v (foldr f acc t2)) t1 109 | foldMap _ Leaf = mempty 110 | foldMap f (Branch t1 v t2) = foldMap f t1 <> f v <> foldMap f t2 111 | 112 | instance Traversable Tree where 113 | traverse _ Leaf = pure Leaf 114 | traverse f (Branch t1 v t2) = ado 115 | mt1 <- traverse f t1 116 | mv <- f v 117 | mt2 <- traverse f t2 118 | in Branch mt1 mv mt2 119 | -- Equivalent 120 | --traverse f (Branch t1 v t2) = Branch <$> traverse f t1 <*> f v <*> traverse f t2 121 | sequence Leaf = pure Leaf 122 | sequence (Branch t1 v t2) = ado 123 | mt1 <- sequence t1 124 | mv <- v 125 | mt2 <- sequence t2 126 | in Branch mt1 mv mt2 127 | 128 | -- Exercise 3 129 | traversePreOrder :: forall a m b. Applicative m => (a -> m b) -> Tree a -> m (Tree b) 130 | traversePreOrder _ Leaf = pure Leaf 131 | 132 | traversePreOrder f (Branch t1 v t2) = ado 133 | mv <- f v 134 | mt1 <- traversePreOrder f t1 135 | mt2 <- traversePreOrder f t2 136 | in Branch mt1 mv mt2 137 | 138 | -- Exercise 4 139 | traversePostOrder :: forall a m b. Applicative m => (a -> m b) -> Tree a -> m (Tree b) 140 | traversePostOrder _ Leaf = pure Leaf 141 | 142 | traversePostOrder f (Branch t1 v t2) = ado 143 | mt1 <- traversePostOrder f t1 144 | mt2 <- traversePostOrder f t2 145 | mv <- f v 146 | in Branch mt1 mv mt2 147 | 148 | -- Exercise 5 149 | type PersonOptionalAddress 150 | = { firstName :: String 151 | , lastName :: String 152 | , homeAddress :: Maybe Address 153 | , phones :: Array PhoneNumber 154 | } 155 | 156 | personOptionalAddress :: String -> String -> Maybe Address -> Array PhoneNumber -> PersonOptionalAddress 157 | personOptionalAddress firstName lastName homeAddress phones = { firstName, lastName, homeAddress, phones } 158 | 159 | validatePersonOptionalAddress :: PersonOptionalAddress -> V Errors PersonOptionalAddress 160 | validatePersonOptionalAddress p = 161 | personOptionalAddress 162 | <$> nonEmpty "First Name" p.firstName 163 | <*> nonEmpty "Last Name" p.lastName 164 | <*> traverse validateAddress p.homeAddress 165 | <*> validatePhoneNumbers "Phone Numbers" p.phones 166 | 167 | -- Exercise 6 168 | sequenceUsingTraverse :: forall a m t. Traversable t => Applicative m => t (m a) -> m (t a) 169 | sequenceUsingTraverse t = traverse identity t 170 | 171 | -- Exercise 7 172 | traverseUsingSequence :: forall a b m t. Traversable t => Applicative m => (a -> m b) -> t a -> m (t b) 173 | traverseUsingSequence f t = sequence $ map f t 174 | -------------------------------------------------------------------------------- /exercises/chapter7/src/Data/AddressBook/Validation.purs: -------------------------------------------------------------------------------- 1 | module Data.AddressBook.Validation where 2 | 3 | import Prelude 4 | 5 | import Data.AddressBook (Address, Person, PhoneNumber, address, person, phoneNumber) 6 | import Data.Either (Either(..)) 7 | import Data.String (length) 8 | import Data.String.Regex (Regex, test) 9 | import Data.String.Regex.Flags (noFlags) 10 | import Data.String.Regex.Unsafe (unsafeRegex) 11 | import Data.Traversable (traverse) 12 | import Data.Validation.Semigroup (V, invalid) 13 | 14 | ----------------- 15 | -- Some simple early examples returning `Either` instead of `V`: 16 | 17 | -- ANCHOR: nonEmpty1 18 | nonEmpty1 :: String -> Either String String 19 | nonEmpty1 "" = Left "Field cannot be empty" 20 | nonEmpty1 value = Right value 21 | -- ANCHOR_END: nonEmpty1 22 | 23 | -- ANCHOR: validatePerson1 24 | validatePerson1 :: Person -> Either String Person 25 | validatePerson1 p = 26 | person <$> nonEmpty1 p.firstName 27 | <*> nonEmpty1 p.lastName 28 | <*> pure p.homeAddress 29 | <*> pure p.phones 30 | -- ANCHOR_END: validatePerson1 31 | 32 | -- ANCHOR: validatePerson1Ado 33 | validatePerson1Ado :: Person -> Either String Person 34 | validatePerson1Ado p = ado 35 | f <- nonEmpty1 p.firstName 36 | l <- nonEmpty1 p.lastName 37 | in person f l p.homeAddress p.phones 38 | -- ANCHOR_END: validatePerson1Ado 39 | 40 | ----------------- 41 | 42 | -- ANCHOR: Errors 43 | type Errors 44 | = Array String 45 | -- ANCHOR_END: Errors 46 | 47 | -- ANCHOR: nonEmpty 48 | nonEmpty :: String -> String -> V Errors String 49 | nonEmpty field "" = invalid [ "Field '" <> field <> "' cannot be empty" ] 50 | nonEmpty _ value = pure value 51 | -- ANCHOR_END: nonEmpty 52 | 53 | -- ANCHOR: validatePhoneNumbers 54 | validatePhoneNumbers :: String -> Array PhoneNumber -> V Errors (Array PhoneNumber) 55 | validatePhoneNumbers field [] = 56 | invalid [ "Field '" <> field <> "' must contain at least one value" ] 57 | validatePhoneNumbers _ phones = 58 | traverse validatePhoneNumber phones 59 | -- ANCHOR_END: validatePhoneNumbers 60 | 61 | -- ANCHOR: lengthIs 62 | lengthIs :: String -> Int -> String -> V Errors String 63 | lengthIs field len value | length value /= len = 64 | invalid [ "Field '" <> field <> "' must have length " <> show len ] 65 | lengthIs _ _ value = pure value 66 | -- ANCHOR_END: lengthIs 67 | 68 | -- ANCHOR: phoneNumberRegex 69 | -- | We use `Data.String.Regex.Unsafe.unsafeRegex` here instead of `Data.String.Regex.regex` 70 | -- | in order to simplify the code. 71 | -- | 72 | -- | The safe function has this signature: 73 | -- | 74 | -- | ```purescript 75 | -- | regex :: String -> RegexFlags -> Either String Regex 76 | -- | ``` 77 | -- | 78 | -- | which can fail if passed an invalid regex `String`. This potential failure is worth 79 | -- | checking for at runtime when working with a user-provided regex `String`. 80 | -- | But in our case, we hardcode a literal regex `String`, so it's not as problematic 81 | -- | to use this more convenient "unsafe" version that may throw an exception: 82 | -- | 83 | -- | ```purescript 84 | -- | unsafeRegex :: String -> RegexFlags -> Regex 85 | -- | ``` 86 | -- | 87 | -- | We can achieve a bit more safety by binding our `Regex` values at the top level, 88 | -- | so any potential runtime exceptions are thrown as soon as our application starts. 89 | -- | This is better than defining these values in a local context, where the error may 90 | -- | not be encountered until later on during application execution. 91 | phoneNumberRegex :: Regex 92 | phoneNumberRegex = unsafeRegex "^\\d{3}-\\d{3}-\\d{4}$" noFlags 93 | -- ANCHOR_END: phoneNumberRegex 94 | 95 | -- ANCHOR: matches 96 | matches :: String -> Regex -> String -> V Errors String 97 | matches _ regex value | test regex value 98 | = pure value 99 | matches field _ _ = invalid [ "Field '" <> field <> "' did not match the required format" ] 100 | -- ANCHOR_END: matches 101 | 102 | -- ANCHOR: validateAddress 103 | validateAddress :: Address -> V Errors Address 104 | validateAddress a = 105 | address <$> nonEmpty "Street" a.street 106 | <*> nonEmpty "City" a.city 107 | <*> lengthIs "State" 2 a.state 108 | -- ANCHOR_END: validateAddress 109 | 110 | -- ANCHOR: validateAddressAdo 111 | validateAddressAdo :: Address -> V Errors Address 112 | validateAddressAdo a = ado 113 | street <- nonEmpty "Street" a.street 114 | city <- nonEmpty "City" a.city 115 | state <- lengthIs "State" 2 a.state 116 | in address street city state 117 | -- ANCHOR_END: validateAddressAdo 118 | 119 | -- ANCHOR: validatePhoneNumber 120 | validatePhoneNumber :: PhoneNumber -> V Errors PhoneNumber 121 | validatePhoneNumber pn = 122 | phoneNumber <$> pure pn."type" 123 | <*> matches "Number" phoneNumberRegex pn.number 124 | -- ANCHOR_END: validatePhoneNumber 125 | 126 | -- ANCHOR: validatePhoneNumberAdo 127 | validatePhoneNumberAdo :: PhoneNumber -> V Errors PhoneNumber 128 | validatePhoneNumberAdo pn = ado 129 | tpe <- pure pn."type" 130 | number <- matches "Number" phoneNumberRegex pn.number 131 | in phoneNumber tpe number 132 | -- ANCHOR_END: validatePhoneNumberAdo 133 | 134 | -- ANCHOR: validatePerson 135 | validatePerson :: Person -> V Errors Person 136 | validatePerson p = 137 | person <$> nonEmpty "First Name" p.firstName 138 | <*> nonEmpty "Last Name" p.lastName 139 | <*> validateAddress p.homeAddress 140 | <*> validatePhoneNumbers "Phone Numbers" p.phones 141 | -- ANCHOR_END: validatePerson 142 | 143 | -- ANCHOR: validatePersonAdo 144 | validatePersonAdo :: Person -> V Errors Person 145 | validatePersonAdo p = ado 146 | firstName <- nonEmpty "First Name" p.firstName 147 | lastName <- nonEmpty "Last Name" p.lastName 148 | address <- validateAddress p.homeAddress 149 | numbers <- validatePhoneNumbers "Phone Numbers" p.phones 150 | in person firstName lastName address numbers 151 | -- ANCHOR_END: validatePersonAdo 152 | -------------------------------------------------------------------------------- /exercises/chapter8/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Data.AddressBook (PhoneNumber, examplePerson) 6 | import Data.AddressBook.Validation (Errors, validatePerson') 7 | import Data.Array (mapWithIndex, updateAt) 8 | import Data.Either (Either(..)) 9 | import Data.Maybe (Maybe(..), fromMaybe) 10 | import Data.Tuple (Tuple(..)) 11 | import Effect (Effect) 12 | import Effect.Console (log) 13 | import Effect.Exception (throw) 14 | import React.Basic.DOM as D 15 | import React.Basic.DOM.Events (targetValue) 16 | import React.Basic.Events (handler) 17 | import React.Basic.Hooks (ReactComponent, element, reactComponent, useState) 18 | import React.Basic.Hooks as R 19 | import Web.DOM.NonElementParentNode (getElementById) 20 | import Web.HTML (window) 21 | import Web.HTML.HTMLDocument (toNonElementParentNode) 22 | import Web.HTML.Window (document) 23 | 24 | -- Note that there's a Purty formatting bug that 25 | -- adds an unwanted blank line 26 | -- https://gitlab.com/joneshf/purty/issues/77 27 | -- ANCHOR: renderValidationErrors 28 | renderValidationErrors :: Errors -> Array R.JSX 29 | renderValidationErrors [] = [] 30 | renderValidationErrors xs = 31 | let 32 | renderError :: String -> R.JSX 33 | renderError err = D.li_ [ D.text err ] 34 | in 35 | [ D.div 36 | { className: "alert alert-danger row" 37 | , children: [ D.ul_ (map renderError xs) ] 38 | } 39 | ] 40 | -- ANCHOR_END: renderValidationErrors 41 | 42 | -- Helper function to render a single form field with an 43 | -- event handler to update 44 | -- ANCHOR: formField 45 | formField :: String -> String -> String -> (String -> Effect Unit) -> R.JSX 46 | formField name placeholder value setValue = 47 | D.div 48 | { className: "form-group row" 49 | , children: 50 | [ D.label 51 | { className: "col-sm col-form-label" 52 | , htmlFor: name 53 | , children: [ D.text name ] 54 | } 55 | , D.div 56 | { className: "col-sm" 57 | , children: 58 | [ D.input 59 | { className: "form-control" 60 | , id: name 61 | , placeholder 62 | , value 63 | , onChange: 64 | let 65 | handleValue :: Maybe String -> Effect Unit 66 | handleValue (Just v) = setValue v 67 | handleValue Nothing = pure unit 68 | in 69 | handler targetValue handleValue 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | -- ANCHOR_END: formField 76 | 77 | mkAddressBookApp :: Effect (ReactComponent {}) 78 | mkAddressBookApp = 79 | -- incoming \props are unused 80 | reactComponent "AddressBookApp" \props -> R.do 81 | -- `useState` takes a default initial value and returns the 82 | -- current value and a way to update the value. 83 | -- Consult react-hooks docs for a more detailed explanation of `useState`. 84 | Tuple person setPerson <- useState examplePerson 85 | let 86 | errors = case validatePerson' person of 87 | Left e -> e 88 | Right _ -> [] 89 | 90 | -- helper-function to return array unchanged instead of Nothing if index is out of bounds 91 | updateAt' :: forall a. Int -> a -> Array a -> Array a 92 | updateAt' i x xs = fromMaybe xs (updateAt i x xs) 93 | 94 | -- helper-function to render a single phone number at a given index 95 | renderPhoneNumber :: Int -> PhoneNumber -> R.JSX 96 | renderPhoneNumber index phone = 97 | formField 98 | (show phone."type") 99 | "XXX-XXX-XXXX" 100 | phone.number 101 | (\s -> setPerson _ { phones = updateAt' index phone { number = s } person.phones }) 102 | 103 | -- helper-function to render all phone numbers 104 | renderPhoneNumbers :: Array R.JSX 105 | renderPhoneNumbers = mapWithIndex renderPhoneNumber person.phones 106 | -- ANCHOR: mkAddressBookApp_pure 107 | pure 108 | $ D.div 109 | { className: "container" 110 | , children: 111 | renderValidationErrors errors 112 | <> [ D.div 113 | { className: "row" 114 | , children: 115 | [ D.form_ 116 | $ [ D.h3_ [ D.text "Basic Information" ] 117 | , formField "First Name" "First Name" person.firstName \s -> 118 | setPerson _ { firstName = s } 119 | , formField "Last Name" "Last Name" person.lastName \s -> 120 | setPerson _ { lastName = s } 121 | , D.h3_ [ D.text "Address" ] 122 | , formField "Street" "Street" person.homeAddress.street \s -> 123 | setPerson _ { homeAddress { street = s } } 124 | , formField "City" "City" person.homeAddress.city \s -> 125 | setPerson _ { homeAddress { city = s } } 126 | , formField "State" "State" person.homeAddress.state \s -> 127 | setPerson _ { homeAddress { state = s } } 128 | , D.h3_ [ D.text "Contact Information" ] 129 | ] 130 | <> renderPhoneNumbers 131 | ] 132 | , key: "person-form" 133 | } 134 | ] 135 | } 136 | -- ANCHOR_END: mkAddressBookApp_pure 137 | 138 | -- ANCHOR: main 139 | main :: Effect Unit 140 | main = do 141 | log "Rendering address book component" 142 | -- Get window object 143 | w <- window 144 | -- Get window's HTML document 145 | doc <- document w 146 | -- Get "container" element in HTML 147 | ctr <- getElementById "container" $ toNonElementParentNode doc 148 | case ctr of 149 | Nothing -> throw "Container element not found." 150 | Just c -> do 151 | -- Create AddressBook react component 152 | addressBookApp <- mkAddressBookApp 153 | let 154 | -- Create JSX node from react component. Pass-in empty props 155 | app = element addressBookApp {} 156 | -- Render AddressBook JSX node in DOM "container" element 157 | D.render app c 158 | -- ANCHOR_END: main 159 | -------------------------------------------------------------------------------- /exercises/chapter9/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Test.MySolutions 5 | import Test.NoPeeking.Solutions -- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 6 | 7 | import Data.Array ((..)) 8 | import Data.Bifunctor (lmap) 9 | import Data.Either (Either(..)) 10 | import Data.Foldable (for_) 11 | import Data.Maybe (Maybe(..)) 12 | import Data.Set as Set 13 | import Data.String (Pattern(..), split) 14 | import Effect (Effect) 15 | import Effect.Exception (message) 16 | import Node.Encoding (Encoding(..)) 17 | import Node.FS.Aff (readTextFile, readdir, realpath, unlink) 18 | import Node.Path (FilePath) 19 | import Node.Path as Path 20 | import Test.Copy (copyFile) 21 | import Test.HTTP (getUrl) 22 | import Test.Unit (TestSuite, suite, test) 23 | import Test.Unit.Assert as Assert 24 | import Test.Unit.Main (runTest) 25 | 26 | inDir :: FilePath 27 | inDir = Path.concat [ "test", "data" ] 28 | 29 | outDir :: FilePath 30 | outDir = Path.concat [ "test", "data-out" ] 31 | 32 | -- If, for any reason, you want or need to run this test offline, or without 33 | -- full internet access, you can create this API endpoint locally by installing 34 | -- http-server (npm i -g http-server) and running it in the "/test/data" 35 | -- directory (http-server -p 42524) 36 | reqUrl :: String 37 | reqUrl = 38 | -- Both http and https work for this API endpoint. 39 | "https://jsonplaceholder.typicode.com/todos/1" 40 | -- If you want or need to use the http version (not the https), just 41 | -- remove the 's' from `https://`: 42 | --"http://jsonplaceholder.typicode.com/todos/1" 43 | -- Use this url for the http-server solution: 44 | --"http://localhost:42524/user.txt" 45 | 46 | main :: Effect Unit 47 | main = 48 | runTest do 49 | test "setup" do 50 | -- Clear test output directory 51 | files <- readdir outDir 52 | for_ files \f -> unlink $ Path.concat [ outDir, f ] 53 | runChapterExamples 54 | {- Move this block comment starting point to enable more tests 55 | This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. -} 56 | test "concatenateFiles" do 57 | let 58 | inFoo = Path.concat [ inDir, "foo.txt" ] 59 | 60 | inBar = Path.concat [ inDir, "bar.txt" ] 61 | 62 | outFooBar = Path.concat [ outDir, "foobar.txt" ] 63 | concatenateFiles inFoo inBar outFooBar 64 | -- Check for valid concat 65 | inFooTxt <- readTextFile UTF8 inFoo 66 | inBarTxt <- readTextFile UTF8 inBar 67 | outFooBarTxt <- readTextFile UTF8 outFooBar 68 | Assert.equal (inFooTxt <> inBarTxt) outFooBarTxt 69 | test "concatenateMany" do 70 | let 71 | inFiles = map (\i -> Path.concat [ inDir, "many", "file" <> show i <> ".txt" ]) $ 1 .. 9 72 | 73 | outFile = Path.concat [ outDir, "manyConcat.txt" ] 74 | 75 | expectedOutFile = Path.concat [ inDir, "manyConcat.txt" ] 76 | concatenateMany inFiles outFile 77 | -- Check for valid concat 78 | actualOutTxt <- readTextFile UTF8 outFile 79 | expectedOutTxt <- readTextFile UTF8 expectedOutFile 80 | Assert.equal expectedOutTxt actualOutTxt 81 | suite "countCharacters" do 82 | test "exists" do 83 | chars <- countCharacters $ Path.concat [ inDir, "nbChars.txt" ] 84 | Assert.equal (Right 42) $ lmap message chars 85 | test "missing" do 86 | absolutePath <- realpath $ Path.concat [ inDir ] 87 | chars <- countCharacters $ Path.concat [ absolutePath, "foof.txt" ] 88 | Assert.equal (Left ("ENOENT: no such file or directory, open '" <> absolutePath <> Path.sep <> "foof.txt'")) $ lmap message chars 89 | test "writeGet" do 90 | let 91 | outFile = Path.concat [ outDir, "user.txt" ] 92 | 93 | expectedOutFile = Path.concat [ inDir, "user.txt" ] 94 | writeGet reqUrl outFile 95 | -- Check for valid write 96 | actualOutTxt <- readTextFile UTF8 outFile 97 | expectedOutTxt <- readTextFile UTF8 expectedOutFile 98 | Assert.equal expectedOutTxt actualOutTxt 99 | test "concatenateManyParallel" do 100 | let 101 | inFiles = map (\i -> Path.concat [ inDir, "many", "file" <> show i <> ".txt" ]) $ 1 .. 9 102 | 103 | outFile = Path.concat [ outDir, "manyConcatParallel.txt" ] 104 | 105 | expectedOutFile = Path.concat [ inDir, "manyConcat.txt" ] 106 | concatenateManyParallel inFiles outFile 107 | -- Check for valid concat 108 | actualOutTxt <- readTextFile UTF8 outFile 109 | expectedOutTxt <- readTextFile UTF8 expectedOutFile 110 | Assert.equal expectedOutTxt actualOutTxt 111 | suite "getWithTimeout" do 112 | test "valid site" do 113 | let 114 | expectedOutFile = Path.concat [ inDir, "user.txt" ] 115 | actual <- getWithTimeout 10000.0 reqUrl 116 | expected <- Just <$> readTextFile UTF8 expectedOutFile 117 | Assert.equal expected actual 118 | test "no response" do 119 | actual <- getWithTimeout 10.0 "https://example.com:81" 120 | Assert.equal Nothing actual 121 | suite "recurseFiles" do 122 | let 123 | recurseDir = Path.concat [ inDir, "tree" ] 124 | test "many files" do 125 | expectedTxt <- readTextFile UTF8 $ Path.concat [ recurseDir, "expected.txt" ] 126 | let 127 | expected = Path.normalize <$> split (Pattern "\n") expectedTxt 128 | actual <- recurseFiles $ Path.concat [ recurseDir, "root.txt" ] 129 | let 130 | actualRelative = map (\f -> Path.relative recurseDir f) actual 131 | Assert.equal (Set.fromFoldable expected) $ Set.fromFoldable actualRelative 132 | test "one file" do 133 | let 134 | file = Path.concat [ recurseDir, "c", "unused.txt" ] 135 | 136 | expected = [ file ] 137 | actual <- recurseFiles file 138 | Assert.equal (Set.fromFoldable expected) $ Set.fromFoldable actual 139 | 140 | {- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 141 | -} 142 | runChapterExamples :: TestSuite 143 | runChapterExamples = do 144 | test "copyFile" do 145 | let 146 | inFoo = Path.concat [ inDir, "foo.txt" ] 147 | 148 | outFoo = Path.concat [ outDir, "foo.txt" ] 149 | copyFile inFoo outFoo 150 | -- Check for valid copy 151 | inFooTxt <- readTextFile UTF8 inFoo 152 | outFooTxt <- readTextFile UTF8 outFoo 153 | Assert.equal inFooTxt outFooTxt 154 | test "getUrl" do 155 | let 156 | expectedOutFile = Path.concat [ inDir, "user.txt" ] 157 | str <- getUrl reqUrl 158 | -- Check for valid read 159 | expectedOutTxt <- readTextFile UTF8 expectedOutFile 160 | Assert.equal expectedOutTxt str 161 | -------------------------------------------------------------------------------- /text/chapter2.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Chapter Goals 4 | 5 | In this chapter, we'll set up a working PureScript development environment, solve some exercises, and use the tests provided with this book to check our answers. You may also find a [video walkthrough of this chapter](https://www.youtube.com/watch?v=GPjPwb6d-70) helpful if that better suits your learning style. 6 | 7 | ## Environment Setup 8 | 9 | First, work through this [Getting Started Guide](https://github.com/purescript/documentation/blob/master/guides/Getting-Started.md) in the Documentation Repo to setup your environment and learn a few basics about the language. Don't worry if the code in the example solution to the [Project Euler](http://projecteuler.net/problem=1) problem is confusing or contains unfamiliar syntax. We'll cover all of this in great detail in the upcoming chapters. 10 | 11 | ### Editor support 12 | 13 | You can use your preferred editor to write PureScript (for example, to solve the book exercises). See [Editor Support Documentation](https://github.com/purescript/documentation/blob/master/ecosystem/Editor-and-tool-support.md#editor-support). 14 | 15 | > Note that some editors expect a `spago.dhall` file in the root of the opened project for full IDE support. For example, you should open the `chapter2` directory to work on the exercises in this chapter. 16 | > 17 | > If you use VS Code, you can use the provided workspace to open all the chapters simultaneously. 18 | 19 | ## Solving Exercises 20 | 21 | Now that you've installed the necessary development tools, clone this book's repo. 22 | 23 | ```sh 24 | git clone https://github.com/purescript-contrib/purescript-book.git 25 | ``` 26 | 27 | The book repo contains PureScript example code and unit tests for the exercises that accompany each chapter. There's some initial setup required to reset the exercise solutions so they are ready to be solved by you. Use the `prepareExercises.sh` script to simplify this process: 28 | 29 | ```sh 30 | cd purescript-book 31 | ./scripts/prepareExercises.sh 32 | git add . 33 | git commit --all --message "Exercises ready to be solved" 34 | ``` 35 | 36 | Now run the tests for this chapter: 37 | 38 | ```sh 39 | cd exercises/chapter2 40 | spago test 41 | ``` 42 | 43 | You should see the following successful test output: 44 | 45 | ```sh 46 | → Suite: Euler - Sum of Multiples 47 | ✓ Passed: below 10 48 | ✓ Passed: below 1000 49 | 50 | All 2 tests passed! 🎉 51 | ``` 52 | 53 | Note that the `answer` function (found in `src/Euler.purs`) has been modified to find the multiples of 3 and 5 below any integer. The test suite (located in `test/Main.purs`) for this `answer` function is more comprehensive than the test in the earlier getting-started guide. Don't worry about understanding how this test framework code works while reading these early chapters. 54 | 55 | The remainder of the book contains lots of exercises. If you write your solutions in the `Test.MySolutions` module (`test/MySolutions.purs`), you can check your work against the provided test suite. 56 | 57 | Let's work through this next exercise together in a test-driven-development style. 58 | 59 | ## Exercise 60 | 61 | 1. (Medium) Write a `diagonal` function to compute the length of the diagonal (or hypotenuse) of a right-angled triangle when given the lengths of the two other sides. 62 | 63 | ## Solution 64 | 65 | We'll start by enabling the tests for this exercise. Move the start of the block-comment down a few lines, as shown below. Block comments start with `{-` and end with `-}`: 66 | 67 | ```hs 68 | {{#include ../exercises/chapter2/test/Main.purs:diagonalTests}} 69 | {- Move this block comment starting point to enable more tests 70 | ``` 71 | 72 | If we attempt to run the test now, we'll encounter a compilation error because we have not yet implemented our `diagonal` function. 73 | 74 | ```sh 75 | $ spago test 76 | 77 | Error found: 78 | in module Test.Main 79 | at test/Main.purs:21:27 - 21:35 (line 21, column 27 - line 21, column 35) 80 | 81 | Unknown value diagonal 82 | ``` 83 | 84 | Let's first look at what happens with a faulty version of this function. Add the following code to `test/MySolutions.purs`: 85 | 86 | ```hs 87 | import Data.Number (sqrt) 88 | 89 | diagonal w h = sqrt (w * w + h) 90 | ``` 91 | 92 | And check our work by running `spago test`: 93 | 94 | ```hs 95 | → Suite: diagonal 96 | ☠ Failed: 3 4 5 because expected 5.0, got 3.605551275463989 97 | ☠ Failed: 5 12 13 because expected 13.0, got 6.082762530298219 98 | 99 | 2 tests failed: 100 | ``` 101 | 102 | Uh-oh, that's not quite right. Let's fix this with the correct application of the Pythagorean formula by changing the function to: 103 | 104 | ```hs 105 | {{#include ../exercises/chapter2/test/no-peeking/Solutions.purs:diagonal}} 106 | ``` 107 | 108 | Trying `spago test` again now shows all tests are passing: 109 | 110 | ```hs 111 | → Suite: Euler - Sum of Multiples 112 | ✓ Passed: below 10 113 | ✓ Passed: below 1000 114 | → Suite: diagonal 115 | ✓ Passed: 3 4 5 116 | ✓ Passed: 5 12 13 117 | 118 | All 4 tests passed! 🎉 119 | ``` 120 | 121 | Success! Now you're ready to try these next exercises on your own. 122 | 123 | ## Exercises 124 | 125 | 1. (Easy) Write a function `circleArea` which computes the area of a circle with a given radius. Use the `pi` constant, which is defined in the `Numbers` module. _Hint_: don't forget to import `pi` by modifying the `import Data.Number` statement. 126 | 1. (Medium) Write a function `leftoverCents` which takes an `Int` and returns what's leftover after dividing by `100`. Use the `rem` function. Search [Pursuit](https://pursuit.purescript.org/) for this function to learn about usage and which module to import it from. _Note:_ Your IDE may support auto-importing of this function if you accept the auto-completion suggestion. 127 | 128 | ## Conclusion 129 | 130 | In this chapter, we installed the PureScript compiler and the Spago tool. We also learned how to write solutions to exercises and check these for correctness. 131 | 132 | There will be many more exercises in the chapters ahead, and working through those helps with learning the material. If any of the exercises stumps you, please reach out to any of the community resources listed in the [Getting Help](https://book.purescript.org/chapter1.html#getting-help) section of this book, or even file an issue in this [book's repo](https://github.com/purescript-contrib/purescript-book/issues). This reader feedback on which exercises could be made more approachable helps us improve the book. 133 | 134 | Once you solve all the exercises in a chapter, you may compare your answers against those in the `no-peeking/Solutions.purs`. No peeking, please, without putting in an honest effort to solve these yourself. And even if you are stuck, try asking a community member for help first, as we would prefer to give you a small hint rather than spoil the exercise. If you found a more elegant solution (that only requires knowledge of the covered content), please send us a PR. 135 | 136 | The repo is continuously being revised, so be sure to check for updates before starting each new chapter. 137 | -------------------------------------------------------------------------------- /exercises/chapter4/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude hiding (gcd) 4 | import Test.MySolutions 5 | import Test.NoPeeking.Solutions -- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 6 | 7 | import ChapterExamples (Amp(..), current, fromString, gcd, gcdV2, isEmpty, livesInLA, lzs, partialFunction, showPerson, showPersonV2, sortPair, takeFive, toString, unknownPerson, Volt(..)) 8 | import Data.Int (round) 9 | import Data.Maybe (Maybe(Just, Nothing)) 10 | import Data.Person (Person) 11 | import Data.Picture (Shape(..), Picture, getCenter, origin) 12 | import Effect (Effect) 13 | import Test.Unit (TestSuite, suite, test) 14 | import Test.Unit.Assert as Assert 15 | import Test.Unit.Main (runTest) 16 | 17 | john :: Person 18 | john = { name: "John Smith", address: { street: "123 Test Lane", city: "Los Angeles" } } 19 | 20 | rose :: Person 21 | rose = { name: "Rose Jackson", address: { street: "464 Sample Terrace", city: "Los Angeles" } } 22 | 23 | amy :: Person 24 | amy = { name: "Amy Lopez", address: { street: "10 Purs Street", city: "Omaha" } } 25 | 26 | samplePicture :: Picture 27 | samplePicture = 28 | [ Circle origin 2.0 29 | , Circle { x: 2.0, y: 2.0 } 3.0 30 | , Rectangle { x: 5.0, y: 5.0 } 4.0 4.0 31 | ] 32 | 33 | main :: Effect Unit 34 | main = 35 | runTest do 36 | runChapterExamples 37 | {- Move this block comment starting point to enable more tests 38 | This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. -} 39 | suite "Exercise Group - Simple Pattern Matching" do 40 | test "Exercise - factorial" do 41 | Assert.equal 1 42 | $ factorial 0 43 | Assert.equal 1 44 | $ factorial 1 45 | Assert.equal 24 46 | $ factorial 4 47 | Assert.equal 3628800 48 | $ factorial 10 49 | test "Exercise - binomial" do 50 | Assert.equal 1 51 | $ binomial 10 0 52 | Assert.equal 0 53 | $ binomial 0 3 54 | Assert.equal 0 55 | $ binomial 2 5 56 | Assert.equal 252 57 | $ binomial 10 5 58 | Assert.equal 1 59 | $ binomial 5 5 60 | test "Exercise - pascal" do 61 | Assert.equal 1 62 | $ pascal 10 0 63 | Assert.equal 0 64 | $ pascal 0 3 65 | Assert.equal 0 66 | $ pascal 2 5 67 | Assert.equal 252 68 | $ pascal 10 5 69 | Assert.equal 1 70 | $ pascal 5 5 71 | suite "Exercise Group - Array and Record Patterns" do 72 | test "Exercise - sameCity" do 73 | Assert.equal true 74 | $ sameCity john rose 75 | Assert.equal false 76 | $ sameCity amy rose 77 | test "Exercise - fromSingleton" do 78 | Assert.equal "default" 79 | $ fromSingleton "default" [] 80 | Assert.equal "B" 81 | $ fromSingleton "default" ["B"] 82 | Assert.equal "default" 83 | $ fromSingleton "default" ["B", "C", "D"] 84 | suite "Exercise Group - Algebraic Data Types" do 85 | test "Exercise - circleAtOrigin" do 86 | Assert.equal origin 87 | $ getCenter circleAtOrigin 88 | test "Exercise - doubleScaleAndCenter" do 89 | Assert.equal (Circle origin 10.0) 90 | $ doubleScaleAndCenter $ Circle origin 5.0 91 | Assert.equal (Circle origin 10.0) 92 | $ doubleScaleAndCenter $ Circle { x: 2.0, y: 2.0 } 5.0 93 | Assert.equal (Rectangle origin 10.0 10.0) 94 | $ doubleScaleAndCenter $ Rectangle { x: 0.0, y: 0.0 } 5.0 5.0 95 | Assert.equal (Rectangle origin 40.0 40.0) 96 | $ doubleScaleAndCenter $ Rectangle { x: 30.0, y: 30.0 } 20.0 20.0 97 | Assert.equal (Line { x: -4.0, y: -4.0 } { x: 4.0, y: 4.0 }) 98 | $ doubleScaleAndCenter $ Line { x: -2.0, y: -2.0 } { x: 2.0, y: 2.0 } 99 | Assert.equal (Line { x: -4.0, y: -4.0 } { x: 4.0, y: 4.0 }) 100 | $ doubleScaleAndCenter $ Line { x: 0.0, y: 4.0 } { x: 4.0, y: 8.0 } 101 | Assert.equal (Text { x: 0.0, y: 0.0 } "Hello .purs!" ) 102 | $ doubleScaleAndCenter $ Text { x: 4.0, y: 6.0 } "Hello .purs!" 103 | test "Exercise - shapeText" do 104 | Assert.equal (Just "Hello .purs!") 105 | $ shapeText $ Text origin "Hello .purs!" 106 | Assert.equal Nothing 107 | $ shapeText $ Circle origin 1.0 108 | Assert.equal Nothing 109 | $ shapeText $ Rectangle origin 1.0 1.0 110 | Assert.equal Nothing 111 | $ shapeText $ Line origin { x: 1.0, y: 1.0 } 112 | suite "Exercise Group - Newtype" do 113 | test "Exercise - calculateWattage" do 114 | Assert.equal 60.0 115 | $ let (Watt w) = calculateWattage (Amp 0.5) (Volt 120.0) 116 | in w 117 | suite "Exercise Group - Vector Graphics" do 118 | test "Exercise - area" do 119 | Assert.equal 50 120 | $ round $ area $ Circle origin 4.0 121 | Assert.equal 40 122 | $ round $ area $ Rectangle origin 4.0 10.0 123 | Assert.equal 0 124 | $ round $ area $ Line origin { x: 2.0, y: 2.0 } 125 | Assert.equal 0 126 | $ round $ area $ Text origin "Text has no area!" 127 | test "Exercise - Clipped shapeBounds" do 128 | Assert.equal { top: -2.0, left: -2.0, right: 2.0, bottom: 2.0 } 129 | -- Note to users: You'll need to manually import shapeBounds 130 | -- from Data.Picture. Don't import from Test.NoPeeking.Solutions. 131 | $ shapeBounds (Clipped samplePicture { x: 0.0, y: 0.0 } 4.0 4.0) 132 | Assert.equal { top: 3.0, left: 3.0, right: 7.0, bottom: 7.0 } 133 | $ shapeBounds (Clipped samplePicture { x: 5.0, y: 5.0 } 4.0 4.0) 134 | Assert.equal { top: 2.0, left: 2.0, right: 7.0, bottom: 7.0 } 135 | $ shapeBounds (Clipped samplePicture { x: 5.0, y: 5.0 } 6.0 6.0) 136 | 137 | {- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 138 | -} 139 | runChapterExamples :: TestSuite 140 | runChapterExamples = 141 | suite "Chapter Examples" do 142 | test "gcd" do 143 | Assert.equal 20 144 | $ gcd 60 100 145 | test "fromString" do 146 | Assert.equal true 147 | $ fromString "true" 148 | test "toString" do 149 | Assert.equal "false" 150 | $ toString false 151 | test "gcdV2" do 152 | Assert.equal 20 153 | $ gcdV2 60 100 154 | test "isEmpty" do 155 | Assert.equal false 156 | $ isEmpty [2, 3] 157 | test "takeFive" do 158 | Assert.equal 6 159 | $ takeFive [0, 1, 2, 3, 4] 160 | test "showPerson" do 161 | Assert.equal "Lovelace, Ada" 162 | $ showPerson {first: "Ada", last: "Lovelace"} 163 | test "showPersonV2" do 164 | Assert.equal "Lovelace, Ada" 165 | $ showPersonV2 {first: "Ada", last: "Lovelace"} 166 | test "unknownPerson" do 167 | Assert.equal {first: "Jane", last: "Doe"} unknownPerson 168 | test "livesInLA" do 169 | Assert.equal true 170 | $ livesInLA {name: "Suraj", address: {street: "123 Main St", city: "Los Angeles"}} 171 | test "sortPair" do 172 | Assert.equal [1, 2] 173 | $ sortPair [2, 1] 174 | test "lzs" do 175 | Assert.equal [-1, -2, 3] 176 | $ lzs [1, -1, -2, 3] 177 | test "partialFunction" do 178 | Assert.equal true 179 | $ partialFunction true 180 | test "current" do 181 | Assert.equal (Amp 0.003) current 182 | -------------------------------------------------------------------------------- /exercises/chapter11/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude (Unit, discard, negate, ($), (*>), (<>), (==)) 4 | import Test.MySolutions 5 | import Game 6 | import Test.NoPeeking.Solutions -- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 7 | 8 | import Control.Monad.Except (runExceptT) 9 | import Control.Monad.RWS (RWSResult(..), runRWS) 10 | import Control.Monad.State (runStateT) 11 | import Control.Monad.Writer (runWriterT, execWriter) 12 | import Data.Either (Either(..)) 13 | import Data.GameEnvironment (GameEnvironment(..)) 14 | import Data.GameItem (GameItem(..)) 15 | import Data.GameState (GameState(..), initialGameState) 16 | import Data.List (List, (:)) 17 | import Data.List as L 18 | import Data.Map as M 19 | import Data.Monoid.Additive (Additive(..)) 20 | import Data.Newtype (unwrap) 21 | import Data.Set as S 22 | import Data.Tuple (Tuple(..)) 23 | import Effect (Effect) 24 | import Test.Unit (TestSuite, success, suite, test) 25 | import Test.Unit.Assert as Assert 26 | import Test.Unit.Main (runTest) 27 | 28 | main :: Effect Unit 29 | main = 30 | runTest do 31 | test "" success 32 | {- Move this block comment starting point to enable more tests 33 | This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. -} 34 | suite "Exercises Group - The State Monad" do 35 | suite "testParens" do 36 | let 37 | runTestParens :: Boolean -> String -> TestSuite 38 | runTestParens expected str = 39 | test testName do 40 | Assert.equal expected $ testParens str 41 | where testName = "str = \"" <> str <> "\"" 42 | runTestParens true "" 43 | runTestParens true "(()(())())" 44 | runTestParens true "(hello)" 45 | runTestParens false ")" 46 | runTestParens false "(()()" 47 | runTestParens false ")(" 48 | suite "Exercises Group - The Reader Monad" do 49 | suite "indents" do 50 | let 51 | expectedText = 52 | "Here is some indented text:\n\ 53 | \ I am indented\n\ 54 | \ So am I\n\ 55 | \ I am even more indented" 56 | test "should render with indentations" do 57 | Assert.equal expectedText 58 | $ render $ cat 59 | [ line "Here is some indented text:" 60 | , indent $ cat 61 | [ line "I am indented" 62 | , line "So am I" 63 | , indent $ line "I am even more indented" 64 | ] 65 | ] 66 | suite "Exercises Group - The Writer Monad" do 67 | suite "sumArrayWriter" do 68 | test "should sum arrays" do 69 | Assert.equal (Additive 21) 70 | $ execWriter $ do 71 | sumArrayWriter [1, 2, 3] 72 | sumArrayWriter [4, 5] 73 | sumArrayWriter [6] 74 | suite "collatz" do 75 | let 76 | expected_11 = 77 | Tuple 14 [11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1] 78 | expected_15 = 79 | Tuple 17 [15, 46, 23, 70, 35, 106, 53, 160, 80, 40, 20, 10, 5, 16, 8, 4, 2, 1] 80 | test "c = 11" do 81 | Assert.equal expected_11 82 | $ collatz 11 83 | test "c = 15" do 84 | Assert.equal expected_15 85 | $ collatz 15 86 | suite "Exercises Group - Monad Transformers" do 87 | suite "safeDivide" do 88 | test "should fail when dividing by zero" do 89 | Assert.equal (Left "Divide by zero!") 90 | $ unwrap $ runExceptT $ safeDivide 5 0 91 | test "should successfully divide for any other input" do 92 | Assert.equal (Right 2) $ unwrap $ runExceptT $ safeDivide 6 3 93 | suite "parser" do 94 | let 95 | runParser p s = unwrap $ runExceptT $ runWriterT $ runStateT p s 96 | test "should parse a string" do 97 | Assert.equal (Right (Tuple (Tuple "abc" "def") ["The state is abcdef"])) 98 | $ runParser (string "abc") "abcdef" 99 | test "should fail if string could not be parsed" do 100 | Assert.equal (Left ["Could not parse"]) 101 | $ runParser (string "abc") "foobar" 102 | suite "indents with ReaderT and WriterT" do 103 | let 104 | expectedText = 105 | "Here is some indented text:\n\ 106 | \ I am indented\n\ 107 | \ So am I\n\ 108 | \ I am even more indented" 109 | test "should render with indentations" do 110 | Assert.equal expectedText 111 | $ render' $ do 112 | line' "Here is some indented text:" 113 | indent' $ do 114 | line' "I am indented" 115 | line' "So am I" 116 | indent' $ do 117 | line' "I am even more indented" 118 | 119 | suite "Exercises Group - Monad Comprehensions/backtracking" do 120 | suite "parser" do 121 | let 122 | runParser p s = unwrap $ runExceptT $ runWriterT $ runStateT p s 123 | test "should parse as followed by bs" do 124 | Assert.equal (Right (Tuple (Tuple "aaabb" "cde") [ 125 | "The state is aaabbcde", 126 | "The state is aabbcde", 127 | "The state is abbcde", 128 | "The state is bbcde", 129 | "The state is bcde"])) 130 | $ runParser asFollowedByBs "aaabbcde" 131 | test "should fail if first is not a" do 132 | Assert.equal (Left ["Could not parse"]) 133 | $ runParser asFollowedByBs "bfoobar" 134 | test "should parse as and bs" do 135 | Assert.equal (Right (Tuple (Tuple "babbaa" "cde") [ 136 | "The state is babbaacde", 137 | "The state is abbaacde", 138 | "The state is bbaacde", 139 | "The state is baacde", 140 | "The state is aacde", 141 | "The state is acde"])) 142 | $ runParser asOrBs "babbaacde" 143 | test "should fail if first is not a or b" do 144 | Assert.equal (Left ["Could not parse","Could not parse"]) 145 | $ runParser asOrBs "foobar" 146 | 147 | suite "Exercises Group - The RWS Monad" do 148 | let 149 | runGame :: Game Unit -> RWSResult GameState Unit (List String) 150 | runGame testGame = runRWS testGame env initialGameState 151 | env = GameEnvironment { debugMode: false, playerName: "Phil" } 152 | 153 | playerHasAllItems (GameState {inventory}) = inventory == S.fromFoldable [Candle, Matches] 154 | mapIsEmpty (GameState {items}) = M.isEmpty items 155 | expectedLogs = ("You now have the Candle" : "You now have the Matches" : L.Nil) 156 | 157 | suite "adds all items to your inventory when cheating" do 158 | let 159 | runCheatTest label testGame = 160 | test label do 161 | let (RWSResult actualState _ log) = runGame testGame 162 | Assert.assert "Expected player to have both Candle and Matches" $ playerHasAllItems actualState 163 | Assert.assert "Expected map to no longer have any items" $ mapIsEmpty actualState 164 | Assert.equal expectedLogs $ L.sort log 165 | 166 | runCheatTest "only cheat" cheat 167 | runCheatTest "move and cheat" $ move 0 (-1) *> move 0 1 *> cheat 168 | runCheatTest "pickup matches and cheat" $ pickUp Matches *> cheat 169 | runCheatTest "pickup all, move, and cheat" $ pickUp Matches *> move 0 1 *> pickUp Candle *> cheat 170 | 171 | {- This line should have been automatically deleted by resetSolutions.sh. See Chapter 2 for instructions. 172 | -} -------------------------------------------------------------------------------- /exercises/chapter10/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Data.AddressBook (PhoneNumber, Person, examplePerson) 6 | import Data.AddressBook.Validation (Errors, validatePerson') 7 | import Data.Argonaut (Json, decodeJson, encodeJson, jsonParser, printJsonDecodeError, stringify) 8 | import Data.Array (length, mapWithIndex, updateAt) 9 | import Data.Bifunctor (lmap) 10 | import Data.Either (Either(..)) 11 | import Data.Maybe (Maybe(..), fromMaybe) 12 | import Data.Tuple (Tuple(..)) 13 | import Effect (Effect) 14 | import Effect.Alert (alert) 15 | import Effect.Console (log) 16 | import Effect.Exception (throw) 17 | import Effect.Storage (getItem, setItem) 18 | import React.Basic.DOM as D 19 | import React.Basic.DOM.Events (targetValue) 20 | import React.Basic.Events (handler, handler_) 21 | import React.Basic.Hooks (ReactComponent, element, reactComponent, useState) 22 | import React.Basic.Hooks as R 23 | import Web.DOM.NonElementParentNode (getElementById) 24 | import Web.HTML (window) 25 | import Web.HTML.HTMLDocument (toNonElementParentNode) 26 | import Web.HTML.Window (document) 27 | 28 | -- Note that there's a Purty formatting bug that 29 | -- adds an unwanted blank line 30 | -- https://gitlab.com/joneshf/purty/issues/77 31 | renderValidationErrors :: Errors -> Array R.JSX 32 | renderValidationErrors [] = [] 33 | 34 | renderValidationErrors xs = 35 | let 36 | renderError :: String -> R.JSX 37 | renderError err = D.li_ [ D.text err ] 38 | in 39 | [ D.div 40 | { className: "alert alert-danger row" 41 | , children: [ D.ul_ (map renderError xs) ] 42 | } 43 | ] 44 | 45 | -- Helper function to render a single form field with an 46 | -- event handler to update 47 | formField :: String -> String -> String -> (String -> Effect Unit) -> R.JSX 48 | formField name placeholder value setValue = 49 | D.div 50 | { className: "form-group row" 51 | , children: 52 | [ D.label 53 | { className: "col-sm col-form-label" 54 | , htmlFor: name 55 | , children: [ D.text name ] 56 | } 57 | , D.div 58 | { className: "col-sm" 59 | , children: 60 | [ D.input 61 | { className: "form-control" 62 | , id: name 63 | , placeholder 64 | , value 65 | , onChange: 66 | let 67 | handleValue :: Maybe String -> Effect Unit 68 | handleValue (Just v) = setValue v 69 | handleValue Nothing = pure unit 70 | in 71 | handler targetValue handleValue 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | 78 | mkAddressBookApp :: Effect (ReactComponent { initialPerson :: Person }) 79 | mkAddressBookApp = 80 | reactComponent "AddressBookApp" \props -> R.do 81 | -- `useState` takes a default initial value and returns the 82 | -- current value and a way to update the value. 83 | -- Consult react-hooks docs for a more detailed explanation of `useState`. 84 | Tuple person setPerson <- useState props.initialPerson 85 | let 86 | errors = case validatePerson' person of 87 | Left e -> e 88 | Right _ -> [] 89 | 90 | -- helper-function to return array unchanged instead of Nothing if index is out of bounds 91 | updateAt' :: forall a. Int -> a -> Array a -> Array a 92 | updateAt' i x xs = fromMaybe xs (updateAt i x xs) 93 | 94 | -- helper-function to render a single phone number at a given index 95 | renderPhoneNumber :: Int -> PhoneNumber -> R.JSX 96 | renderPhoneNumber index phone = 97 | formField 98 | (show phone."type") 99 | "XXX-XXX-XXXX" 100 | phone.number 101 | (\s -> setPerson _ { phones = updateAt' index phone { number = s } person.phones }) 102 | 103 | -- helper-function to render all phone numbers 104 | renderPhoneNumbers :: Array R.JSX 105 | renderPhoneNumbers = mapWithIndex renderPhoneNumber person.phones 106 | 107 | validateAndSave :: Effect Unit 108 | validateAndSave = do 109 | log "Running validators" 110 | case validatePerson' person of 111 | Left errs -> alert $ "There are " <> show (length errs) <> " validation errors." 112 | Right validPerson -> do 113 | setItem "person" $ stringify $ encodeJson validPerson 114 | log "Saved" 115 | 116 | -- helper-function to render saveButton 117 | saveButton :: R.JSX 118 | saveButton = 119 | D.label 120 | { className: "form-group row col-form-label" 121 | , children: 122 | [ D.button 123 | { className: "btn-primary btn" 124 | , onClick: handler_ validateAndSave 125 | , children: [ D.text "Save" ] 126 | } 127 | ] 128 | } 129 | pure 130 | $ D.div 131 | { className: "container" 132 | , children: 133 | renderValidationErrors errors 134 | <> [ D.div 135 | { className: "row" 136 | , children: 137 | [ D.form_ 138 | $ [ D.h3_ [ D.text "Basic Information" ] 139 | , formField "First Name" "First Name" person.firstName \s -> 140 | setPerson _ { firstName = s } 141 | , formField "Last Name" "Last Name" person.lastName \s -> 142 | setPerson _ { lastName = s } 143 | , D.h3_ [ D.text "Address" ] 144 | , formField "Street" "Street" person.homeAddress.street \s -> 145 | setPerson _ { homeAddress { street = s } } 146 | , formField "City" "City" person.homeAddress.city \s -> 147 | setPerson _ { homeAddress { city = s } } 148 | , formField "State" "State" person.homeAddress.state \s -> 149 | setPerson _ { homeAddress { state = s } } 150 | , D.h3_ [ D.text "Contact Information" ] 151 | ] 152 | <> renderPhoneNumbers 153 | ] 154 | } 155 | ] 156 | <> [ saveButton ] 157 | } 158 | 159 | processItem :: Json -> Either String Person 160 | processItem item = do 161 | jsonString <- lmap (\e -> "No string in local storage: " <> printJsonDecodeError e) $ decodeJson item 162 | j <- lmap ( "Cannot parse JSON string: " <> _) $ jsonParser jsonString 163 | lmap (\e -> "Cannot decode Person: " <> printJsonDecodeError e) $ decodeJson j 164 | 165 | main :: Effect Unit 166 | main = do 167 | log "Rendering address book component" 168 | -- Get window object 169 | w <- window 170 | -- Get window's HTML document 171 | doc <- document w 172 | -- Get "container" element in HTML 173 | ctr <- getElementById "container" $ toNonElementParentNode doc 174 | case ctr of 175 | Nothing -> throw "Container element not found." 176 | Just c -> do 177 | -- Create AddressBook react component 178 | addressBookApp <- mkAddressBookApp 179 | -- Retrieve person from local storage 180 | item <- getItem "person" 181 | initialPerson <- case processItem item of 182 | Left err -> do 183 | alert $ "Error: " <> err <> ". Loading examplePerson" 184 | pure examplePerson 185 | Right p -> pure p 186 | let 187 | -- Create JSX node from react component. 188 | app = element addressBookApp { initialPerson } 189 | -- Render AddressBook JSX node in DOM "container" element 190 | D.render app c 191 | --------------------------------------------------------------------------------