├── .gitignore ├── exercises ├── ch04 │ ├── solution_a.js │ ├── solution_b.js │ ├── solution_c.js │ ├── exercise_a.js │ ├── exercise_b.js │ ├── validation_a.js │ ├── exercise_c.js │ ├── validation_c.js │ └── validation_b.js ├── ch08 │ ├── solution_a.js │ ├── solution_b.js │ ├── solution_c.js │ ├── exercise_a.js │ ├── exercise_b.js │ ├── solution_d.js │ ├── validation_a.js │ ├── validation_b.js │ ├── exercise_c.js │ ├── validation_c.js │ ├── exercise_d.js │ └── validation_d.js ├── ch10 │ ├── solution_b.js │ ├── solution_a.js │ ├── solution_c.js │ ├── exercise_b.js │ ├── exercise_a.js │ ├── validation_c.js │ ├── exercise_c.js │ ├── validation_a.js │ └── validation_b.js ├── ch05 │ ├── solution_a.js │ ├── solution_b.js │ ├── solution_c.js │ ├── exercise_c.js │ ├── exercise_b.js │ ├── exercise_a.js │ ├── validation_c.js │ ├── validation_b.js │ └── validation_a.js ├── ch11 │ ├── solution_a.js │ ├── solution_c.js │ ├── solution_b.js │ ├── exercise_a.js │ ├── validation_c.js │ ├── exercise_b.js │ ├── exercise_c.js │ ├── validation_a.js │ └── validation_b.js ├── ch12 │ ├── solution_a.js │ ├── solution_b.js │ ├── solution_c.js │ ├── exercise_c.js │ ├── exercise_b.js │ ├── exercise_a.js │ ├── validation_c.js │ ├── validation_a.js │ └── validation_b.js ├── ch09 │ ├── solution_c.js │ ├── solution_b.js │ ├── solution_a.js │ ├── exercise_a.js │ ├── exercise_c.js │ ├── validation_b.js │ ├── exercise_b.js │ ├── validation_c.js │ └── validation_a.js ├── test │ ├── ch04.js │ ├── ch05.js │ ├── ch08.js │ ├── ch09.js │ ├── ch10.js │ ├── ch11.js │ ├── ch12.js │ ├── ch04.solutions.js │ ├── ch05.solutions.js │ ├── ch08.solutions.js │ ├── ch09.solutions.js │ ├── ch10.solutions.js │ ├── ch11.solutions.js │ └── ch12.solutions.js ├── .eslintrc.js ├── ch06 │ ├── index.html │ └── main.js ├── package.json ├── README.md └── test-utils.js ├── images ├── cat.png ├── jar.jpg ├── chain.jpg ├── cover.png ├── fists.jpg ├── onion.png ├── catmap.png ├── cats_ss.png ├── dominoes.jpg ├── fn_graph.png ├── canopener.jpg ├── cat_comp1.png ├── cat_comp2.png ├── cat_theory.png ├── console_ss.png ├── functormap.png ├── function-sets.gif ├── id_to_maybe.png ├── functormapmaybe.png ├── ship_in_a_bottle.jpg ├── triangle_identity.png ├── monad_associativity.png ├── natural_transformation.png └── relation-not-function.gif ├── support ├── .npmignore ├── CHANGELOG.md ├── .eslintrc.js ├── package.json ├── README.md └── build-appendixes ├── styles └── website.css ├── LICENSE ├── book.json ├── TRANSLATIONS.md ├── package.json ├── .github └── workflows │ └── build_gitbook.yml ├── CONTRIBUTING.md ├── appendix_a.md ├── README.md ├── ch02-kr.md ├── ch01-kr.md ├── appendix_c.md ├── SUMMARY.md ├── FAQ.md ├── ch02.md ├── ch04-kr.md ├── ch01.md ├── ch07-kr.md ├── ch04.md ├── appendix_b.md ├── ch03-kr.md ├── ch06.md ├── ch07.md ├── ch11.md └── ch03.md /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | *.lock -------------------------------------------------------------------------------- /exercises/ch04/solution_a.js: -------------------------------------------------------------------------------- 1 | const words = split(' '); 2 | -------------------------------------------------------------------------------- /exercises/ch08/solution_a.js: -------------------------------------------------------------------------------- 1 | const incrF = map(add(1)); 2 | -------------------------------------------------------------------------------- /exercises/ch10/solution_b.js: -------------------------------------------------------------------------------- 1 | const safeAdd = liftA2(add); 2 | -------------------------------------------------------------------------------- /exercises/ch04/solution_b.js: -------------------------------------------------------------------------------- 1 | const filterQs = filter(match(/q/i)); 2 | -------------------------------------------------------------------------------- /exercises/ch04/solution_c.js: -------------------------------------------------------------------------------- 1 | const max = reduce(keepHighest, -Infinity); 2 | -------------------------------------------------------------------------------- /exercises/ch05/solution_a.js: -------------------------------------------------------------------------------- 1 | const isLastInStock = compose(prop('in_stock'), last); 2 | -------------------------------------------------------------------------------- /exercises/ch08/solution_b.js: -------------------------------------------------------------------------------- 1 | const initial = compose(map(head), safeProp('name')); 2 | -------------------------------------------------------------------------------- /exercises/ch10/solution_a.js: -------------------------------------------------------------------------------- 1 | const safeAdd = curry((a, b) => Maybe.of(add).ap(a).ap(b)); 2 | -------------------------------------------------------------------------------- /exercises/ch11/solution_a.js: -------------------------------------------------------------------------------- 1 | const eitherToMaybe = either(always(nothing), Maybe.of); 2 | -------------------------------------------------------------------------------- /exercises/ch08/solution_c.js: -------------------------------------------------------------------------------- 1 | const eitherWelcome = compose(map(showWelcome), checkActive); 2 | -------------------------------------------------------------------------------- /images/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/cat.png -------------------------------------------------------------------------------- /images/jar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/jar.jpg -------------------------------------------------------------------------------- /exercises/ch05/solution_b.js: -------------------------------------------------------------------------------- 1 | const averageDollarValue = compose(average, map(prop('dollar_value'))); 2 | -------------------------------------------------------------------------------- /exercises/ch11/solution_c.js: -------------------------------------------------------------------------------- 1 | const strToList = split(''); 2 | 3 | const listToStr = intercalate(''); 4 | -------------------------------------------------------------------------------- /images/chain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/chain.jpg -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/cover.png -------------------------------------------------------------------------------- /images/fists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/fists.jpg -------------------------------------------------------------------------------- /images/onion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/onion.png -------------------------------------------------------------------------------- /images/catmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/catmap.png -------------------------------------------------------------------------------- /images/cats_ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/cats_ss.png -------------------------------------------------------------------------------- /images/dominoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/dominoes.jpg -------------------------------------------------------------------------------- /images/fn_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/fn_graph.png -------------------------------------------------------------------------------- /exercises/ch11/solution_b.js: -------------------------------------------------------------------------------- 1 | const findNameById = compose(map(prop('name')), chain(eitherToTask), findUserById); 2 | -------------------------------------------------------------------------------- /images/canopener.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/canopener.jpg -------------------------------------------------------------------------------- /images/cat_comp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/cat_comp1.png -------------------------------------------------------------------------------- /images/cat_comp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/cat_comp2.png -------------------------------------------------------------------------------- /images/cat_theory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/cat_theory.png -------------------------------------------------------------------------------- /images/console_ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/console_ss.png -------------------------------------------------------------------------------- /images/functormap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/functormap.png -------------------------------------------------------------------------------- /images/function-sets.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/function-sets.gif -------------------------------------------------------------------------------- /images/id_to_maybe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/id_to_maybe.png -------------------------------------------------------------------------------- /support/.npmignore: -------------------------------------------------------------------------------- 1 | ch* 2 | test* 3 | .eslintrc.js 4 | node-modules 5 | test-util.js 6 | support.js 7 | build-appendixes 8 | -------------------------------------------------------------------------------- /images/functormapmaybe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/functormapmaybe.png -------------------------------------------------------------------------------- /styles/website.css: -------------------------------------------------------------------------------- 1 | .book.color-theme-1 .book-body .page-wrapper .page-inner section.normal a { 2 | color: initial; 3 | } 4 | -------------------------------------------------------------------------------- /exercises/ch10/solution_c.js: -------------------------------------------------------------------------------- 1 | const startGame = IO.of(game) 2 | .ap(getFromCache('player1')) 3 | .ap(getFromCache('player2')); 4 | -------------------------------------------------------------------------------- /images/ship_in_a_bottle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/ship_in_a_bottle.jpg -------------------------------------------------------------------------------- /images/triangle_identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/triangle_identity.png -------------------------------------------------------------------------------- /images/monad_associativity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/monad_associativity.png -------------------------------------------------------------------------------- /exercises/ch12/solution_a.js: -------------------------------------------------------------------------------- 1 | // getJsons :: Map Route Route -> Task Error (Map Route JSON) 2 | const getJsons = traverse(Task.of, httpGet); 3 | -------------------------------------------------------------------------------- /images/natural_transformation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/natural_transformation.png -------------------------------------------------------------------------------- /images/relation-not-function.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MostlyAdequate/mostly-adequate-guide-kr/HEAD/images/relation-not-function.gif -------------------------------------------------------------------------------- /exercises/ch09/solution_c.js: -------------------------------------------------------------------------------- 1 | const joinMailingList = compose( 2 | map(compose(chain(emailBlast), addToMailingList)), 3 | validateEmail, 4 | ); 5 | -------------------------------------------------------------------------------- /exercises/ch09/solution_b.js: -------------------------------------------------------------------------------- 1 | const basename = compose(last, split('/')); 2 | 3 | const logFilename = compose(chain(pureLog), map(basename))(getFile); 4 | -------------------------------------------------------------------------------- /exercises/ch09/solution_a.js: -------------------------------------------------------------------------------- 1 | const getStreetName = compose( 2 | chain(safeProp('name')), 3 | chain(safeProp('street')), 4 | safeProp('address'), 5 | ); 6 | -------------------------------------------------------------------------------- /exercises/ch05/solution_c.js: -------------------------------------------------------------------------------- 1 | const fastestCar = compose( 2 | append(' is the fastest'), 3 | prop('name'), 4 | last, 5 | sortBy(prop('horsepower')), 6 | ); 7 | -------------------------------------------------------------------------------- /exercises/ch12/solution_b.js: -------------------------------------------------------------------------------- 1 | // startGame :: [Player] -> Either Error String 2 | const startGame = compose(map(always('game started!')), traverse(Either.of, validate)); 3 | -------------------------------------------------------------------------------- /exercises/ch04/exercise_a.js: -------------------------------------------------------------------------------- 1 | // Refactor to remove all arguments by partially applying the function. 2 | 3 | // words :: String -> [String] 4 | const words = str => split(' ', str); 5 | -------------------------------------------------------------------------------- /exercises/ch08/exercise_a.js: -------------------------------------------------------------------------------- 1 | // Use `add` and `map` to make a function that increments a value inside a functor. 2 | 3 | // incrF :: Functor f => f Int -> f Int 4 | const incrF = undefined; 5 | -------------------------------------------------------------------------------- /exercises/ch11/exercise_a.js: -------------------------------------------------------------------------------- 1 | // Write a natural transformation that converts `Either b a` to `Maybe a` 2 | 3 | // eitherToMaybe :: Either b a -> Maybe a 4 | const eitherToMaybe = undefined; 5 | -------------------------------------------------------------------------------- /exercises/ch10/exercise_b.js: -------------------------------------------------------------------------------- 1 | // Rewrite `safeAdd` from exercise_a to use `liftA2` instead of `ap`. 2 | 3 | // safeAdd :: Maybe Number -> Maybe Number -> Maybe Number 4 | const safeAdd = undefined; 5 | -------------------------------------------------------------------------------- /exercises/test/ch04.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runExercises } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 04', () => { 5 | runExercises('ch04'); 6 | }); 7 | -------------------------------------------------------------------------------- /exercises/test/ch05.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runExercises } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 05', () => { 5 | runExercises('ch05'); 6 | }); 7 | -------------------------------------------------------------------------------- /exercises/test/ch08.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runExercises } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 08', () => { 5 | runExercises('ch08'); 6 | }); 7 | -------------------------------------------------------------------------------- /exercises/test/ch09.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runExercises } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 09', () => { 5 | runExercises('ch09'); 6 | }); 7 | -------------------------------------------------------------------------------- /exercises/ch04/exercise_b.js: -------------------------------------------------------------------------------- 1 | // Refactor to remove all arguments by partially applying the functions. 2 | 3 | // filterQs :: [String] -> [String] 4 | const filterQs = xs => filter(x => x.match(/q/i), xs); 5 | -------------------------------------------------------------------------------- /exercises/ch12/solution_c.js: -------------------------------------------------------------------------------- 1 | // readFirst :: String -> Task Error (Maybe String) 2 | const readFirst = compose( 3 | chain(traverse(Task.of, readfile('utf-8'))), 4 | map(safeHead), 5 | readdir, 6 | ); 7 | -------------------------------------------------------------------------------- /exercises/test/ch10.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runExercises } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 10', () => { 5 | runExercises('ch10'); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /exercises/test/ch11.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runExercises } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 11', () => { 5 | runExercises('ch11'); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /exercises/test/ch12.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runExercises } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 12', () => { 5 | runExercises('ch12'); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /exercises/test/ch04.solutions.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runSolutions } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 04', () => { 5 | runSolutions('ch04'); 6 | }); 7 | -------------------------------------------------------------------------------- /exercises/test/ch05.solutions.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runSolutions } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 05', () => { 5 | runSolutions('ch05'); 6 | }); 7 | -------------------------------------------------------------------------------- /exercises/test/ch08.solutions.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runSolutions } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 08', () => { 5 | runSolutions('ch08'); 6 | }); 7 | -------------------------------------------------------------------------------- /exercises/test/ch09.solutions.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runSolutions } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 09', () => { 5 | runSolutions('ch09'); 6 | }); 7 | -------------------------------------------------------------------------------- /exercises/ch10/exercise_a.js: -------------------------------------------------------------------------------- 1 | // Write a function that adds two possibly null numbers together using `Maybe` and `ap`. 2 | 3 | // safeAdd :: Maybe Number -> Maybe Number -> Maybe Number 4 | const safeAdd = undefined; 5 | -------------------------------------------------------------------------------- /exercises/test/ch10.solutions.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runSolutions } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 10', () => { 5 | runSolutions('ch10'); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /exercises/test/ch11.solutions.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runSolutions } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 11', () => { 5 | runSolutions('ch11'); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /exercises/test/ch12.solutions.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('mocha'); 2 | const { runSolutions } = require('../test-utils'); 3 | 4 | describe('Exercises Chapter 12', () => { 5 | runSolutions('ch12'); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /exercises/ch11/validation_c.js: -------------------------------------------------------------------------------- 1 | /* globals strToList, listToStr */ 2 | 3 | const sortLetters = compose(listToStr, sortBy(identity), strToList); 4 | 5 | assert( 6 | sortLetters('sortme') === 'emorst', 7 | 'The function gives incorrect results', 8 | ); 9 | -------------------------------------------------------------------------------- /exercises/ch08/exercise_b.js: -------------------------------------------------------------------------------- 1 | // Given the following User object: 2 | // 3 | // const user = { id: 2, name: 'Albert', active: true }; 4 | // 5 | // Use `safeProp` and `head` to find the first initial of the user. 6 | 7 | // initial :: User -> Maybe String 8 | const initial = undefined; 9 | -------------------------------------------------------------------------------- /support/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.1 - 2019-06-26 2 | 3 | - Fix `Maybe.sequence` missing a `return` statement 4 | 5 | # 2.0.0 - 2019-05-24 6 | 7 | - Change `nothing :: () -> Maybe a` to `nothing :: Maybe a` 8 | 9 | # 1.2.0 - 2019-05-05 10 | 11 | - Fix `inspect` deprecation warning on node.js 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | * All text in this book is under: 2 | Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) 3 | http://creativecommons.org/licenses/by-sa/4.0/ 4 | 5 | * All artwork randomly stolen from google image search so the license doesn't apply. Please let me know if any artwork is yours and I'll give credit or remove. 6 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "3.2.2", 3 | "root": ".", 4 | "title": "Professor Frisby's Mostly Adequate Guide to Functional Programming", 5 | "cover": "images/cover.png", 6 | "plugins": [ 7 | "exercises@git+https://github.com/MostlyAdequate/plugin-exercises.git", 8 | "include-codeblock@3.1.2" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /exercises/ch04/validation_a.js: -------------------------------------------------------------------------------- 1 | /* globals words */ 2 | 3 | assert.arrayEqual( 4 | words('Jingle bells Batman smells'), 5 | ['Jingle', 'bells', 'Batman', 'smells'], 6 | 'The function gives incorrect results.', 7 | ); 8 | 9 | assert( 10 | split.partially, 11 | 'The answer is incorrect; hint: split is currified!', 12 | ); 13 | -------------------------------------------------------------------------------- /exercises/ch05/exercise_c.js: -------------------------------------------------------------------------------- 1 | // Refactor `fastestCar` using `compose()` and other functions in pointfree-style. 2 | 3 | // fastestCar :: [Car] -> String 4 | const fastestCar = (cars) => { 5 | const sorted = sortBy(car => car.horsepower, cars); 6 | const fastest = last(sorted); 7 | return concat(fastest.name, ' is the fastest'); 8 | }; 9 | -------------------------------------------------------------------------------- /exercises/ch08/solution_d.js: -------------------------------------------------------------------------------- 1 | const validateName = ({ name }) => (name.length > 3 2 | ? Either.of(null) 3 | : left('Your name need to be > 3') 4 | ); 5 | 6 | const saveAndWelcome = compose(map(showWelcome), save); 7 | 8 | const register = compose( 9 | either(IO.of, saveAndWelcome), 10 | validateUser(validateName), 11 | ); 12 | -------------------------------------------------------------------------------- /exercises/ch04/exercise_c.js: -------------------------------------------------------------------------------- 1 | // Considering the following function: 2 | // 3 | // const keepHighest = (x, y) => (x >= y ? x : y); 4 | // 5 | // Refactor `max` to not reference any arguments using the helper function `keepHighest`. 6 | 7 | // max :: [Number] -> Number 8 | const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs); 9 | -------------------------------------------------------------------------------- /exercises/ch11/exercise_b.js: -------------------------------------------------------------------------------- 1 | // Using `eitherToTask`, simplify `findNameById` to remove the nested `Either`. 2 | // 3 | // // eitherToTask :: Either a b -> Task a b 4 | // const eitherToTask = either(Task.rejected, Task.of); 5 | 6 | // findNameById :: Number -> Task Error (Either Error User) 7 | const findNameById = compose(map(map(prop('name'))), findUserById); 8 | -------------------------------------------------------------------------------- /exercises/ch08/validation_a.js: -------------------------------------------------------------------------------- 1 | /* globals incrF */ 2 | 3 | assert( 4 | incrF(Identity.of(2)).$value === 3, 5 | 'The function gives incorrect results.', 6 | ); 7 | 8 | assert( 9 | add.partially, 10 | 'The answer is incorrect; hint: add is currified!', 11 | ); 12 | 13 | assert( 14 | map.partially, 15 | 'The answer is incorrect; hint: map is currified!', 16 | ); 17 | -------------------------------------------------------------------------------- /exercises/ch10/validation_c.js: -------------------------------------------------------------------------------- 1 | /* globals startGame */ 2 | 3 | assert( 4 | startGame instanceof IO && typeof startGame.unsafePerformIO() === 'string', 5 | 'The answer has a wrong type; `startGame` should be an `IO String`', 6 | ); 7 | 8 | assert( 9 | startGame.unsafePerformIO() === `${albert.name} vs ${theresa.name}`, 10 | 'The answer gives incorrect results', 11 | ); 12 | -------------------------------------------------------------------------------- /exercises/ch12/exercise_c.js: -------------------------------------------------------------------------------- 1 | // Considering the following functions: 2 | // 3 | // readfile :: String -> String -> Task Error String 4 | // readdir :: String -> Task Error [String] 5 | // 6 | // Use traversable to rearrange and flatten the nested Tasks & Maybe 7 | 8 | // readFirst :: String -> Task Error (Maybe (Task Error String)) 9 | const readFirst = compose(map(map(readfile('utf-8'))), map(safeHead), readdir); 10 | -------------------------------------------------------------------------------- /exercises/ch05/exercise_b.js: -------------------------------------------------------------------------------- 1 | // Considering the following function: 2 | // 3 | // const average = xs => reduce(add, 0, xs) / xs.length; 4 | // 5 | // Use the helper function `average` to refactor `averageDollarValue` as a composition. 6 | 7 | // averageDollarValue :: [Car] -> Int 8 | const averageDollarValue = (cars) => { 9 | const dollarValues = map(c => c.dollar_value, cars); 10 | return average(dollarValues); 11 | }; 12 | -------------------------------------------------------------------------------- /exercises/ch11/exercise_c.js: -------------------------------------------------------------------------------- 1 | // Write the isomorphisms between String and [Char]. 2 | // 3 | // As a reminder, the following functions are available in the exercise's context: 4 | // 5 | // split :: String -> String -> [String] 6 | // intercalate :: String -> [String] -> String 7 | 8 | // strToList :: String -> [Char] 9 | const strToList = undefined; 10 | 11 | // listToStr :: [Char] -> String 12 | const listToStr = undefined; 13 | -------------------------------------------------------------------------------- /exercises/ch12/exercise_b.js: -------------------------------------------------------------------------------- 1 | // Using traversable, and the `validate` function, update `startGame` (and its signature) 2 | // to only start the game if all players are valid 3 | // 4 | // // validate :: Player -> Either String Player 5 | // validate = player => (player.name ? Either.of(player) : left('must have name')); 6 | 7 | // startGame :: [Player] -> [Either Error String] 8 | const startGame = compose(map(map(always('game started!'))), map(validate)); 9 | -------------------------------------------------------------------------------- /exercises/ch05/exercise_a.js: -------------------------------------------------------------------------------- 1 | // We consider Car objects of the following shape: 2 | // 3 | // { 4 | // name: 'Aston Martin One-77', 5 | // horsepower: 750, 6 | // dollar_value: 1850000, 7 | // in_stock: true, 8 | // } 9 | // 10 | // Use `compose()` to rewrite the function below. 11 | 12 | 13 | // isLastInStock :: [Car] -> Boolean 14 | const isLastInStock = (cars) => { 15 | const lastCar = last(cars); 16 | return prop('in_stock', lastCar); 17 | }; 18 | -------------------------------------------------------------------------------- /exercises/ch09/exercise_a.js: -------------------------------------------------------------------------------- 1 | // Considering a User object as follow: 2 | // 3 | // const user = { 4 | // id: 1, 5 | // name: 'Albert', 6 | // address: { 7 | // street: { 8 | // number: 22, 9 | // name: 'Walnut St', 10 | // }, 11 | // }, 12 | // }; 13 | // 14 | // Use `safeProp` and `map/join` or `chain` to safely get the street name when given a user 15 | 16 | // getStreetName :: User -> Maybe String 17 | const getStreetName = undefined; 18 | -------------------------------------------------------------------------------- /exercises/ch04/validation_c.js: -------------------------------------------------------------------------------- 1 | /* globals max */ 2 | 3 | assert( 4 | max([323, 523, 554, 123, 5234]) === 5234, 5 | 'The function gives incorrect results.', 6 | ); 7 | 8 | assert( 9 | reduce.partially, 10 | 'The answer is incorrect; hint: look at the arguments for `reduce`!', 11 | ); 12 | 13 | assert( 14 | keepHighest.calledBy && keepHighest.calledBy.name === '$reduceIterator', 15 | 'The answer is incorrect; hint: look closely to `reduce\'s` iterator and `keepHighest`!', 16 | ); 17 | -------------------------------------------------------------------------------- /exercises/ch12/exercise_a.js: -------------------------------------------------------------------------------- 1 | // Considering the following elements: 2 | // 3 | // // httpGet :: Route -> Task Error JSON 4 | // // routes :: Map Route Route 5 | // const routes = new Map({ '/': '/', '/about': '/about' }); 6 | // 7 | // Use the traversable interface to change the type signature of `getJsons`. 8 | // 9 | // getJsons :: Map Route Route -> Task Error (Map Route JSON) 10 | 11 | // getJsons :: Map Route Route -> Map Route (Task Error JSON) 12 | const getJsons = map(httpGet); 13 | -------------------------------------------------------------------------------- /exercises/ch09/exercise_c.js: -------------------------------------------------------------------------------- 1 | // For this exercise, we consider helpers with the following signatures: 2 | // 3 | // validateEmail :: Email -> Either String Email 4 | // addToMailingList :: Email -> IO([Email]) 5 | // emailBlast :: [Email] -> IO () 6 | // 7 | // Use `validateEmail`, `addToMailingList` and `emailBlast` to create a function 8 | // which adds a new email to the mailing list if valid, and then notify the whole 9 | // list. 10 | 11 | // joinMailingList :: Email -> Either String (IO ()) 12 | const joinMailingList = undefined; 13 | -------------------------------------------------------------------------------- /exercises/ch09/validation_b.js: -------------------------------------------------------------------------------- 1 | /* globals logFilename */ 2 | 3 | assert( 4 | logFilename instanceof IO, 5 | 'The function gives incorrect results; hint: `logFilename` should be an IO()', 6 | ); 7 | 8 | if (logFilename.unsafePerformIO() instanceof IO) { 9 | throw new Error('The function gives incorrect results; hint: make sure to `chain` effects as you go'); 10 | } 11 | 12 | assert( 13 | logFilename.unsafePerformIO() === 'ch09.md', 14 | 'The function gives incorrect results; hint: did you retrieve the file\'s basename ?', 15 | ); 16 | -------------------------------------------------------------------------------- /exercises/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const globals = Object 2 | .keys(require('./support')) 3 | .reduce((o, k) => ({ ...o, [k]: true }), { requirejs: true, assert: true }); 4 | 5 | module.exports = { 6 | extends: 'airbnb', 7 | env: { 8 | browser: true, 9 | amd: true, 10 | }, 11 | globals, 12 | rules: { 13 | 'import/no-amd': 0, 14 | 'import/no-dynamic-require': 0, 15 | 'no-unused-vars': 0, 16 | 'object-curly-newline': [2, { 17 | multiline: true, 18 | consistent: true, 19 | minProperties: 5, 20 | }], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /exercises/ch04/validation_b.js: -------------------------------------------------------------------------------- 1 | /* globals filterQs */ 2 | 3 | filter.calledPartial = false; 4 | match.calledPartial = false; 5 | 6 | assert.arrayEqual( 7 | filterQs(['quick', 'camels', 'quarry', 'over', 'quails']), 8 | ['quick', 'quarry', 'quails'], 9 | 'The function gives incorrect results.', 10 | ); 11 | 12 | assert( 13 | filter.partially, 14 | 'The answer is incorrect; hint: look at the arguments for `filter`.', 15 | ); 16 | 17 | assert( 18 | match.partially, 19 | 'The answer is incorrect; hint: look at the arguments for `match`.', 20 | ); 21 | -------------------------------------------------------------------------------- /support/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const globals = Object 2 | .keys(require('./support')) 3 | .reduce((o, k) => ({ ...o, [k]: true }), { requirejs: true, assert: true }); 4 | 5 | module.exports = { 6 | extends: 'airbnb', 7 | env: { 8 | browser: true, 9 | amd: true, 10 | }, 11 | globals, 12 | rules: { 13 | 'import/no-amd': 0, 14 | 'import/no-dynamic-require': 0, 15 | 'no-unused-vars': 0, 16 | 'object-curly-newline': [2, { 17 | multiline: true, 18 | consistent: true, 19 | minProperties: 5, 20 | }], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /exercises/ch09/exercise_b.js: -------------------------------------------------------------------------------- 1 | // We now consider the following items: 2 | // 3 | // // getFile :: IO String 4 | // const getFile = IO.of('/home/mostly-adequate/ch09.md'); 5 | // 6 | // // pureLog :: String -> IO () 7 | // const pureLog = str => new IO(() => console.log(str)); 8 | // 9 | // Use getFile to get the filepath, remove the directory and keep only the basename, 10 | // then purely log it. Hint: you may want to use `split` and `last` to obtain the 11 | // basename from a filepath. 12 | 13 | // logFilename :: IO () 14 | const logFilename = undefined; 15 | -------------------------------------------------------------------------------- /exercises/ch11/validation_a.js: -------------------------------------------------------------------------------- 1 | /* globals eitherToMaybe */ 2 | 3 | const just = eitherToMaybe(Either.of('one eyed willy')); 4 | const noth = eitherToMaybe(left('some error')); 5 | 6 | assert( 7 | just instanceof Maybe && just.isJust && just.$value === 'one eyed willy', 8 | 'The function maps the `Right()` side incorrectly; hint: `Right(14)` should be mapped to `Just(14)`', 9 | ); 10 | 11 | assert( 12 | noth instanceof Maybe && noth.isNothing, 13 | 'The function maps the `Left()` side incorrectly; hint: `Left(\'error\')` should be mapped to `Nothing`', 14 | ); 15 | -------------------------------------------------------------------------------- /exercises/ch08/validation_b.js: -------------------------------------------------------------------------------- 1 | /* globals initial */ 2 | 3 | if (!(initial(albert) instanceof Maybe) && initial.callees && initial.callees[0] === 'safeProp' && initial.callees[1] === 'head') { 4 | throw new Error('The function gives incorrect results; hint: look carefully at the signatures of `safeProp` and `head`!'); 5 | } 6 | 7 | assert( 8 | initial(albert).$value === 'A', 9 | 'The function gives incorrect results.', 10 | ); 11 | 12 | assert.arrayEqual( 13 | initial.callees || [], 14 | ['safeProp', 'map'], 15 | 'The answer is incorrect; hint: you can compose `safeProp` with `head` in a declarative way', 16 | ); 17 | -------------------------------------------------------------------------------- /exercises/ch10/exercise_c.js: -------------------------------------------------------------------------------- 1 | // For this exercise, we consider the following helpers: 2 | // 3 | // const localStorage = { 4 | // player1: { id:1, name: 'Albert' }, 5 | // player2: { id:2, name: 'Theresa' }, 6 | // }; 7 | // 8 | // // getFromCache :: String -> IO User 9 | // const getFromCache = x => new IO(() => localStorage[x]); 10 | // 11 | // // game :: User -> User -> String 12 | // const game = curry((p1, p2) => `${p1.name} vs ${p2.name}`); 13 | // 14 | // Write an IO that gets both player1 and player2 from the cache and starts the game. 15 | 16 | // startGame :: IO String 17 | const startGame = undefined; 18 | -------------------------------------------------------------------------------- /exercises/ch08/exercise_c.js: -------------------------------------------------------------------------------- 1 | // Given the following helper functions: 2 | // 3 | // // showWelcome :: User -> String 4 | // const showWelcome = compose(concat('Welcome '), prop('name')); 5 | // 6 | // // checkActive :: User -> Either String User 7 | // const checkActive = function checkActive(user) { 8 | // return user.active 9 | // ? Either.of(user) 10 | // : left('Your account is not active'); 11 | // }; 12 | // 13 | // Write a function that uses `checkActive` and `showWelcome` to grant access or return the error. 14 | 15 | // eitherWelcome :: User -> Either String String 16 | const eitherWelcome = undefined; 17 | -------------------------------------------------------------------------------- /exercises/ch06/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flickr App 6 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /exercises/ch05/validation_c.js: -------------------------------------------------------------------------------- 1 | /* globals fastestCar */ 2 | 3 | try { 4 | assert( 5 | fastestCar(cars) === 'Aston Martin One-77 is the fastest', 6 | 'The function gives incorrect results.', 7 | ); 8 | } catch (err) { 9 | const callees = fastestCar.callees || []; 10 | 11 | if (callees.length > 0 && callees[0] !== 'sortBy') { 12 | throw new Error('The answer is incorrect; hint: functions are composed from right to left!'); 13 | } 14 | 15 | throw err; 16 | } 17 | 18 | const callees = fastestCar.callees || []; 19 | 20 | assert.arrayEqual( 21 | callees.slice(0, 3), 22 | ['sortBy', 'last', 'prop'], 23 | 'The answer is incorrect; hint: Hindley-Milner signatures help a lot to reason about composition!', 24 | ); 25 | -------------------------------------------------------------------------------- /exercises/ch12/validation_c.js: -------------------------------------------------------------------------------- 1 | /* globals readFirst */ 2 | 3 | const res = readFirst('__dirname'); 4 | 5 | const throwUnexpected = () => { 6 | throw new Error('The function gives incorrect results; a Task has resolved unexpectedly!'); 7 | }; 8 | 9 | assert( 10 | res instanceof Task, 11 | 'The function has an invalid type; hint: `readFirst` must return a `Task`!', 12 | ); 13 | 14 | res.fork(throwUnexpected, ($res) => { 15 | assert( 16 | $res instanceof Maybe, 17 | 'The function has an invalid type; hint: `readFirst` must return a `Task Error (Maybe String)`!', 18 | ); 19 | 20 | assert( 21 | $res.isJust && $res.$value === 'content of file1 (utf-8)', 22 | 'The function gives incorrect results.', 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /exercises/ch05/validation_b.js: -------------------------------------------------------------------------------- 1 | /* globals averageDollarValue */ 2 | 3 | try { 4 | assert( 5 | averageDollarValue(cars) === 790700, 6 | 'The function gives incorrect results.', 7 | ); 8 | } catch (err) { 9 | const callees = averageDollarValue.callees || []; 10 | 11 | if (callees[0] === 'average' && callees[1] === 'map') { 12 | throw new Error('The answer is incorrect; hint: functions are composed from right to left!'); 13 | } 14 | 15 | throw err; 16 | } 17 | 18 | assert.arrayEqual( 19 | averageDollarValue.callees || [], 20 | ['map', 'average'], 21 | 'The answer is incorrect; hint: map is currified!', 22 | ); 23 | 24 | assert( 25 | prop.partially, 26 | 'The answer is almost correct; hint: you can use prop to access objects\' properties!', 27 | ); 28 | -------------------------------------------------------------------------------- /exercises/ch09/validation_c.js: -------------------------------------------------------------------------------- 1 | /* globals joinMailingList */ 2 | 3 | const res = joinMailingList('email@email.com'); 4 | 5 | assert( 6 | res instanceof Either, 7 | 'The function has an invalid type; hint: `joinMailingList` should return an Either String (IO ())', 8 | ); 9 | 10 | if (res.$value.unsafePerformIO() instanceof IO) { 11 | throw new Error('The function gives incorrect results; hint: make sure to `chain` effects as you go'); 12 | } 13 | 14 | const getResult = either(identity, unsafePerformIO); 15 | 16 | assert( 17 | getResult(joinMailingList('sleepy@grandpa.net')) === 'sleepy@grandpa.net', 18 | 'The function gives incorrect results.', 19 | ); 20 | 21 | assert( 22 | getResult(joinMailingList('notanemail')) === 'invalid email', 23 | 'The function gives incorrect results.', 24 | ); 25 | -------------------------------------------------------------------------------- /exercises/ch05/validation_a.js: -------------------------------------------------------------------------------- 1 | /* globals isLastInStock */ 2 | 3 | const fixture01 = cars.slice(0, 3); 4 | const fixture02 = cars.slice(3); 5 | 6 | try { 7 | assert( 8 | isLastInStock(fixture01), 9 | 'The function gives incorrect results.', 10 | ); 11 | 12 | assert( 13 | !isLastInStock(fixture02), 14 | 'The function gives incorrect results.', 15 | ); 16 | } catch (err) { 17 | const callees = isLastInStock.callees || []; 18 | 19 | if (callees[0] === 'prop' && callees[1] === 'last') { 20 | throw new Error('The answer is incorrect; hint: functions are composed from right to left!'); 21 | } 22 | 23 | throw err; 24 | } 25 | 26 | assert.arrayEqual( 27 | isLastInStock.callees || [], 28 | ['last', 'prop'], 29 | 'The answer is incorrect; hint: prop is currified!', 30 | ); 31 | -------------------------------------------------------------------------------- /exercises/ch08/validation_c.js: -------------------------------------------------------------------------------- 1 | /* globals eitherWelcome */ 2 | 3 | if (!(eitherWelcome(gary) instanceof Either) && eitherWelcome.callees && eitherWelcome.callees[0] === 'checkActive' && eitherWelcome.callees[1] === 'showWelcome') { 4 | throw new Error('The function gives incorrect results; hint: look carefully at the signatures of `checkActive` and `showWelcome`!'); 5 | } 6 | 7 | assert( 8 | eitherWelcome(gary).$value === 'Your account is not active', 9 | 'The function gives incorrect results.', 10 | ); 11 | 12 | assert( 13 | eitherWelcome(theresa).$value === 'Welcome Theresa', 14 | 'The function gives incorrect results.', 15 | ); 16 | 17 | assert.arrayEqual( 18 | eitherWelcome.callees || [], 19 | ['checkActive', 'map'], 20 | 'The answer is incorrect; hint: you can compose `checkActive` with `showWelcome` in a declarative way!', 21 | ); 22 | -------------------------------------------------------------------------------- /exercises/ch08/exercise_d.js: -------------------------------------------------------------------------------- 1 | // We now consider the following functions: 2 | // 3 | // // validateUser :: (User -> Either String ()) -> User -> Either String User 4 | // const validateUser = curry((validate, user) => validate(user).map(_ => user)); 5 | // 6 | // // save :: User -> IO User 7 | // const save = user => new IO(() => ({ ...user, saved: true })); 8 | // 9 | // Write a function `validateName` which checks whether a user has a name longer than 3 characters 10 | // or return an error message. Then use `either`, `showWelcome` and `save` to write a `register` 11 | // function to signup and welcome a user when the validation is ok. 12 | // 13 | // Remember either's two arguments must return the same type. 14 | 15 | // validateName :: User -> Either String () 16 | const validateName = undefined; 17 | 18 | // register :: User -> IO String 19 | const register = compose(undefined, validateUser(validateName)); 20 | -------------------------------------------------------------------------------- /exercises/ch12/validation_a.js: -------------------------------------------------------------------------------- 1 | /* globals getJsons */ 2 | 3 | const throwUnexpected = () => { 4 | throw new Error('The function gives incorrect results; a Task has resolved unexpectedly!'); 5 | }; 6 | 7 | const res = getJsons(routes); 8 | 9 | assert( 10 | res instanceof Task, 11 | 'The function has an invalid type; hint: `getJsons` must return a `Task`!', 12 | ); 13 | 14 | res.fork(throwUnexpected, ($res) => { 15 | assert( 16 | $res.$value['/'] === 'json for /' && $res.$value['/about'] === 'json for /about', 17 | 'The function gives incorrect results; hint: did you correctly map `httpGet` over the Map\'s values?', 18 | ); 19 | 20 | const callees = getJsons.callees; // eslint-disable-line prefer-destructuring 21 | 22 | if (callees && callees[0] === 'map' && callees[1] === 'sequence') { 23 | throw new Error('The function could be written in a simpler form; hint: compose(sequence(of), map(fn)) === traverse(of, fn)'); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /exercises/ch10/validation_a.js: -------------------------------------------------------------------------------- 1 | /* globals safeAdd */ 2 | 3 | const res = safeAdd(Maybe.of(2), Maybe.of(3)); 4 | assert( 5 | res instanceof Maybe && typeof res.$value === 'number', 6 | 'The function has a wrong type; make sure to wrap your numbers inside `Maybe.of`', 7 | ); 8 | 9 | assert( 10 | safeAdd(Maybe.of(2), Maybe.of(3)).$value === 5, 11 | 'The function gives incorrect results; did you use `add` ?', 12 | ); 13 | 14 | assert( 15 | safeAdd(Maybe.of(null), Maybe.of(3)).isNothing, 16 | 'The function gives incorrect results; `Nothing` should be returned when at least one value is `null`', 17 | ); 18 | 19 | assert( 20 | safeAdd(Maybe.of(2), Maybe.of(null)).isNothing, 21 | 'The function gives incorrect results; `Nothing` should be returned when at least one value is `null`', 22 | ); 23 | 24 | assert( 25 | withSpyOn('ap', Maybe.prototype, () => safeAdd(Maybe.of(2), Maybe.of(3))), 26 | 'The function seems incorrect; did you use `ap` ?', 27 | ); 28 | -------------------------------------------------------------------------------- /exercises/ch10/validation_b.js: -------------------------------------------------------------------------------- 1 | /* globals safeAdd */ 2 | 3 | const res = safeAdd(Maybe.of(2), Maybe.of(3)); 4 | assert( 5 | res instanceof Maybe && typeof res.$value === 'number', 6 | 'The function has a wrong type; make sure to wrap your numbers inside `Maybe.of`', 7 | ); 8 | 9 | assert( 10 | safeAdd(Maybe.of(2), Maybe.of(3)).$value === 5, 11 | 'The function gives incorrect results; did you use `add` ?', 12 | ); 13 | 14 | assert( 15 | safeAdd(Maybe.of(null), Maybe.of(3)).isNothing, 16 | 'The function gives incorrect results; `Nothing` should be returned when at least one value is `null`', 17 | ); 18 | 19 | assert( 20 | safeAdd(Maybe.of(2), Maybe.of(null)).isNothing, 21 | 'The function gives incorrect results; `Nothing` should be returned when at least one value is `null`', 22 | ); 23 | 24 | assert( 25 | liftA2.partially && safeAdd.name === 'liftA2', 26 | 'The function seems incorrect; did you use `liftA2`? Remember that `liftA2` is currified!', 27 | ); 28 | -------------------------------------------------------------------------------- /exercises/ch12/validation_b.js: -------------------------------------------------------------------------------- 1 | /* globals startGame */ 2 | 3 | const res = startGame(new List([albert, theresa])); 4 | 5 | assert( 6 | res instanceof Either, 7 | 'The function has an invalid type; hint: `startGame` must return a `Either`!', 8 | ); 9 | 10 | assert( 11 | res.isRight && res.$value === 'game started!', 12 | 'The function gives incorrect results; a game should have started for a list of valid players!', 13 | ); 14 | 15 | const rej = startGame(new List([gary, { what: 14 }])); 16 | assert( 17 | rej.isLeft && rej.$value === 'must have name', 18 | 'The function gives incorrect results; a game shouldn\'t be started if the list contains invalid players!', 19 | ); 20 | 21 | const callees = startGame.callees; // eslint-disable-line prefer-destructuring 22 | 23 | if (callees && callees[0] === 'map' && callees[1] === 'sequence') { 24 | throw new Error('The function could be written in a simpler form; hint: compose(sequence(of), map(fn)) === traverse(of, fn)'); 25 | } 26 | -------------------------------------------------------------------------------- /support/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mostly-adequate/support", 3 | "version": "2.0.1", 4 | "description": "Support functions and data-structures from the Mostly Adequate Guide to Functional Programming", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/MostlyAdequate/mostly-adequate-guide" 10 | }, 11 | "author": "@mostly-adequate", 12 | "bugs": { 13 | "url": "https://github.com/MostlyAdequate/mostly-adequate-guide/issues" 14 | }, 15 | "homepage": "https://github.com/MostlyAdequate/mostly-adequate-guide/support", 16 | "keywords": [ 17 | "functional programming", 18 | "mostly adequate", 19 | "guide", 20 | "fp" 21 | ], 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "eslint": "^5.9.0", 25 | "eslint-config-airbnb": "^16.1.0", 26 | "eslint-plugin-import": "^2.8.0", 27 | "eslint-plugin-jsx-a11y": "^6.0.2", 28 | "eslint-plugin-react": "^7.5.1" 29 | }, 30 | "scripts": { 31 | "lint": "eslint ." 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /exercises/ch09/validation_a.js: -------------------------------------------------------------------------------- 1 | /* globals getStreetName */ 2 | 3 | const res = getStreetName(albert); 4 | 5 | if ((!(res instanceof Maybe) || typeof res.$value !== 'string' || res.isNothing) && getStreetName.callees) { 6 | [1, 2, 3].forEach((i) => { 7 | if (getStreetName.callees[i] === 'map' && getStreetName.callees[i + 1] !== 'join') { 8 | throw new Error('The function gives incorrect results; hint: you can use `join` to flatten two monads!'); 9 | } 10 | }); 11 | 12 | [1, 2].forEach((i) => { 13 | if (!['map', 'chain'].includes(getStreetName.callees[i])) { 14 | throw new Error('The function gives incorrect results; hint: look carefully at the signature of `safeProp` and `chain`!'); 15 | } 16 | }); 17 | } 18 | 19 | assert( 20 | getStreetName(albert).$value === 'Walnut St', 21 | 'The function gives incorrect results.', 22 | ); 23 | 24 | assert( 25 | getStreetName(gary).isNothing, 26 | 'The function gives incorrect results.', 27 | ); 28 | 29 | assert( 30 | getStreetName(theresa).isNothing, 31 | 'The function gives incorrect results.', 32 | ); 33 | -------------------------------------------------------------------------------- /TRANSLATIONS.md: -------------------------------------------------------------------------------- 1 | # Available Translations 2 | 3 | * [中文版 (Chinese)](https://github.com/llh911001/mostly-adequate-guide-chinese) by Linghao Li @llh911001 4 | * [Русский (Russian)](https://github.com/MostlyAdequate/mostly-adequate-guide-ru) by Maksim Filippov @maksimf 5 | * [Українська (Ukrainian)](https://github.com/ivanzusko/mostly-adequate-guide-uk) by Ivan Zusko @ivanzusko 6 | * [Français (French)](https://github.com/MostlyAdequate/mostly-adequate-guide-fr) by Benkort Matthias @KtorZ 7 | * [Português (Portuguese)](https://github.com/MostlyAdequate/mostly-adequate-guide-pt-BR) by Palmer Oliveira @expalmer 8 | * [Español (Spanish)](https://github.com/MostlyAdequate/mostly-adequate-guide-es) by Gustavo Marin @guumaster 9 | * [Italiano (Italian)](https://github.com/MostlyAdequate/mostly-adequate-guide-it) by Elia Gentili @eliagentili 10 | * [한국어 (Korean)](https://github.com/enshahar/mostly-adequate-guide-kr) by Frank Hyunsok Oh @enshahar 11 | * [bahasa Indonesia (Indonesian)](https://github.com/MostlyAdequate/mostly-adequate-guide-id) by Ahmad Naufal Mukhtar @anaufalm 12 | 13 | 14 | ## Creating new Translations 15 | 16 | See [Creating new translations](CONTRIBUTING.md#Translations) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mostly-adequate-guide", 3 | "version": "1.0.0", 4 | "description": "The Mostly Adequate Guide to Functional Programming", 5 | "dependencies": { 6 | "gitbook-plugin-include-codeblock": "^3.2.2", 7 | "gitbook-plugin-exercises": "MostlyAdequate/plugin-exercises" 8 | }, 9 | "devDependencies": { 10 | "gitbook-cli": "^2.3.2" 11 | }, 12 | "scripts": { 13 | "setup": "$(npm bin)/gitbook install", 14 | "start": "$(npm bin)/gitbook serve", 15 | "generate-summary": "perl -n .generate-summary.pl ch*.md appendix_*.md > SUMMARY.md", 16 | "generate-epub": "gitbook epub", 17 | "generate-pdf": "gitbook pdf" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/christiantakle/mostly-adequate-guide.git" 22 | }, 23 | "keywords": [ 24 | "gitbook", 25 | "javascript", 26 | "guide", 27 | "functional programming" 28 | ], 29 | "author": "Mostly Adequate Core Team", 30 | "license": "CC BY-SA", 31 | "bugs": { 32 | "url": "https://github.com/christiantakle/mostly-adequate-guide/issues" 33 | }, 34 | "homepage": "https://github.com/christiantakle/mostly-adequate-guide#readme" 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/build_gitbook.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-18.04 11 | strategy: 12 | matrix: 13 | node-version: [12.14.1] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Cache 23 | uses: actions/cache@v2.1.1 24 | with: 25 | path: client/node_modules 26 | key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.OS }}-build- 29 | ${{ runner.OS }}- 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | registry-url: "https://npm.pkg.github.com" 35 | 36 | - name: Build 37 | run: | 38 | yarn 39 | yarn gitbook build 40 | - name: Deploy 41 | uses: peaceiris/actions-gh-pages@v3 42 | with: 43 | publish_branch: gh-pages 44 | publish_dir: ./_book 45 | github_token: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /exercises/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mostly-adequate/exercises", 3 | "version": "1.0.0", 4 | "description": "Exercises coming with the Mostly Adequate Guide to Functional Programming", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/MostlyAdequate/mostly-adequate-guide" 9 | }, 10 | "author": "@mostly-adequate", 11 | "bugs": { 12 | "url": "https://github.com/MostlyAdequate/mostly-adequate-guide/issues" 13 | }, 14 | "homepage": "https://github.com/MostlyAdequate/mostly-adequate-guide", 15 | "dependencies": {}, 16 | "devDependencies": { 17 | "cli": "^1.0.1", 18 | "eslint": "^5.9.0", 19 | "eslint-config-airbnb": "^16.1.0", 20 | "eslint-plugin-import": "^2.8.0", 21 | "eslint-plugin-jsx-a11y": "^6.0.2", 22 | "eslint-plugin-react": "^7.5.1", 23 | "live-server": "^1.2.1", 24 | "mocha": "^4.1.0" 25 | }, 26 | "scripts": { 27 | "test": "mocha test/*.solutions.js", 28 | "lint": "eslint .", 29 | "ch04": "mocha test/ch04.js", 30 | "ch05": "mocha test/ch05.js", 31 | "ch06": "live-server --ignore='node_modules/*/**' ch06", 32 | "ch08": "mocha test/ch08.js", 33 | "ch09": "mocha test/ch09.js", 34 | "ch10": "mocha test/ch10.js", 35 | "ch11": "mocha test/ch11.js", 36 | "ch12": "mocha test/ch12.js" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /support/README.md: -------------------------------------------------------------------------------- 1 | # Mostly Adequate Guide to Functional Programming - Support 2 | 3 | ## Overview 4 | 5 | This package contains all functions and data-structure referenced in the 6 | appendixes of the [Mostly Adequate Guide to Functional Programming](https://github.com/MostlyAdequate/mostly-adequate-guide). 7 | 8 | These functions have an educational purpose and aren't intended to be used in 9 | any production environment. They are however, a good learning material for anyone 10 | interested in functional programming. 11 | 12 | ## How to install 13 | 14 | The package is available on `npm` and can be installed via the following incantation: 15 | 16 | ``` 17 | npm install @mostly-adequate/support 18 | ``` 19 | 20 | ## How to use 21 | 22 | There's no particular structure to the module, everything is flat and exported 23 | from the root (the curious reader may have a quick glance at the `index.js` to 24 | get convinced about this). 25 | 26 | Also, all top-level functions are curried so you don't have to worry about calling 27 | `curry` on any of them. 28 | 29 | For example: 30 | 31 | ```js 32 | const { Maybe, liftA2, append, concat, reverse } = require('@mostly-adequate/support'); 33 | 34 | const a = Maybe.of("yltsoM").map(reverse); 35 | const b = Maybe.of("Adequate").map(concat(" ")); 36 | 37 | liftA2(append)(b)(a); 38 | // Just("Mostly Adequate") 39 | ``` 40 | -------------------------------------------------------------------------------- /exercises/ch06/main.js: -------------------------------------------------------------------------------- 1 | const CDN = s => `https://cdnjs.cloudflare.com/ajax/libs/${s}`; 2 | const ramda = CDN('ramda/0.21.0/ramda.min'); 3 | const jquery = CDN('jquery/3.0.0-rc1/jquery.min'); 4 | 5 | requirejs.config({ paths: { ramda, jquery } }); 6 | require(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => { 7 | // -- Utils ---------------------------------------------------------- 8 | const Impure = { 9 | trace: curry((tag, x) => { console.log(tag, x); return x; }), // eslint-disable-line no-console 10 | getJSON: curry((callback, url) => $.getJSON(url, callback)), 11 | setHtml: curry((sel, html) => $(sel).html(html)), 12 | }; 13 | 14 | // -- Pure ----------------------------------------------------------- 15 | const host = 'api.flickr.com'; 16 | const path = '/services/feeds/photos_public.gne'; 17 | const query = t => `?tags=${t}&format=json&jsoncallback=?`; 18 | const url = t => `https://${host}${path}${query(t)}`; 19 | 20 | const img = src => $('', { src }); 21 | const mediaUrl = compose(prop('m'), prop('media')); 22 | const mediaUrls = compose(map(mediaUrl), prop('items')); 23 | const images = compose(map(img), mediaUrls); 24 | 25 | // -- Impure --------------------------------------------------------- 26 | const render = compose(Impure.setHtml('#js-main'), images); 27 | const app = compose(Impure.getJSON(render), url); 28 | 29 | app('cats'); 30 | }); 31 | -------------------------------------------------------------------------------- /exercises/ch11/validation_b.js: -------------------------------------------------------------------------------- 1 | /* globals findNameById */ 2 | 3 | const throwUnexpected = () => { 4 | throw new Error('The function gives incorrect results; a Task has resolved unexpectedly!'); 5 | }; 6 | 7 | const res = findNameById(1); 8 | 9 | assert( 10 | res instanceof Task, 11 | 'The function has an incorrect type; hint: `findNameById` must return a `Task String User`!', 12 | ); 13 | 14 | res.fork(throwUnexpected, (val) => { 15 | assert( 16 | !(val instanceof Task), 17 | 'The function has an incorrect type; hint: `findNameById` must return a `Task String User`, make sure to flatten any nested Functor!', 18 | ); 19 | 20 | assert( 21 | !(val instanceof Either), 22 | 'The function has an incorrect type; hint: did you use `eitherToTask` ?', 23 | ); 24 | 25 | assert( 26 | val === 'Albert', 27 | 'The function gives incorrect results for the `Right` side of `Either`.', 28 | ); 29 | }); 30 | 31 | 32 | const rej = findNameById(999); 33 | 34 | assert( 35 | rej instanceof Task, 36 | 'The function has an incorrect type; hint: `findNameById` must return a `Task String User`!', 37 | ); 38 | 39 | rej.fork((val) => { 40 | assert( 41 | !(val instanceof Task), 42 | 'The function has an incorrect type; hint: `findNameById` must return a `Task String User`, make sure to flatten any nested Functor!', 43 | ); 44 | 45 | assert( 46 | val === 'not found', 47 | 'The function gives incorrect results for the `Left` side of `Either`.', 48 | ); 49 | }, throwUnexpected); 50 | -------------------------------------------------------------------------------- /exercises/README.md: -------------------------------------------------------------------------------- 1 | # Mostly Adequate Exercises 2 | 3 | ## Overview 4 | 5 | All exercises from the book can be completed either: 6 | 7 | - in browser (using the version of the book published on [gitbook.io](https://mostly-adequate.gitbooks.io/mostly-adequate-guide/)) 8 | - in your editor & terminal, using `npm` 9 | 10 | In every folder named `ch**` from this `exercises/` folder, you'll find three types of files: 11 | 12 | - exercises 13 | - solutions 14 | - validations 15 | 16 | Exercises are structured with a statement in comment, followed by an incomplete 17 | or incorrect function. For example, `exercise_a` from `ch04` looks like this: 18 | 19 | 20 | ```js 21 | // Refactor to remove all arguments by partially applying the function. 22 | 23 | // words :: String -> [String] 24 | const words = str => split(' ', str); 25 | ``` 26 | 27 | Following the statement, your goal is to refactor the given function `words`. Once done, 28 | your proposal can be verified by running: 29 | 30 | ``` 31 | npm run ch04 32 | ``` 33 | 34 | Alternatively, you can also have a peak at the corresponding solution file: in this case 35 | `solution_a.js`. 36 | 37 | > The files `validation_*.js` aren't really part of the exercises but are used 38 | > internally to verify your proposal and, offer hints when adequate. The curious 39 | > reader may have a look at them :). 40 | 41 | Now go and learn some functional programming λ! 42 | 43 | ## About the Appendixes 44 | 45 | Important notice: the exercise runner takes care of bringing all 46 | data-structures and functions from the appendixes into scope. Therefore, you 47 | may assume that any function present in the appendix is just available for you 48 | to use! Amazing, isn't it? 49 | -------------------------------------------------------------------------------- /support/build-appendixes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const { promisify } = require('util'); 6 | 7 | /* Input / Output Configuration */ 8 | const OUTPUT = path.join(__dirname, 'index.js'); 9 | const INPUTDIR = path.join(__dirname, '..'); 10 | 11 | 12 | /* Helpers */ 13 | Function.prototype.compose = function (f) { return x => f(this(x)); } 14 | 15 | function readFileP(filename, inDir = INPUTDIR) { 16 | return promisify(fs.readFile)(path.join(inDir, filename)); 17 | } 18 | 19 | function toString(x) { 20 | return x.toString(); 21 | } 22 | 23 | function parseDefinitions(data) { 24 | const re = new RegExp('```js(.*)```', 'sm'); 25 | const paragraphs = data.split(/###?#?/); 26 | return paragraphs 27 | .map(x => re.exec(x)) 28 | .filter(x => x != null) 29 | .map(x => x[1]); 30 | } 31 | 32 | function flatten(xss) { 33 | return xss.reduce((acc, xs) => acc.concat(xs), []); 34 | } 35 | 36 | const header = `/* LICENSE MIT-2.0 - @MostlyAdequate */ 37 | /* eslint-disable no-use-before-define, max-len, class-methods-use-this */ 38 | `; 39 | 40 | 41 | /* Actual function */ 42 | Promise 43 | .all([ 44 | readFileP('appendix_a.md').then(toString.compose(parseDefinitions)), 45 | readFileP('appendix_b.md').then(toString.compose(parseDefinitions)), 46 | readFileP('appendix_c.md').then(toString.compose(parseDefinitions)), 47 | ]) 48 | .then((definitions) => { 49 | definitions = flatten(definitions); 50 | const exports_ = definitions 51 | .map(x => x.match(/(const|class|function) ([a-zA-Z0-9]+)( |\()/)[2]) 52 | .map(x => `exports.${x} = ${x};`); 53 | const output = header + definitions.join('\n') + '\n\n' + exports_.join('\n'); 54 | return promisify(fs.writeFile)(OUTPUT, output); 55 | }) 56 | .catch(console.error); 57 | -------------------------------------------------------------------------------- /exercises/ch08/validation_d.js: -------------------------------------------------------------------------------- 1 | /* globals validateName, register */ 2 | 3 | const validateGary = validateName(gary); 4 | assert( 5 | validateGary instanceof Either && validateGary.isRight, 6 | 'The function `validateName` gives incorrect results.', 7 | ); 8 | 9 | const validateYi = validateName(yi); 10 | assert( 11 | validateYi instanceof Either && validateYi.isLeft && typeof validateYi.$value === 'string', 12 | 'The function `validateName` gives incorrect results!', 13 | ); 14 | 15 | const registerAlbert = register(albert); 16 | assert( 17 | registerAlbert instanceof IO, 18 | 'The right outcome to `register` is incorrect; hint: `save` returns an `IO` and you\'ll need `map` to manipulate the inner value!', 19 | ); 20 | 21 | const msgAlbert = registerAlbert.unsafePerformIO(); 22 | assert( 23 | typeof msgAlbert === 'string', 24 | 'The right outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO(String)` in every scenarios!', 25 | ); 26 | 27 | const callees = register.callees || []; 28 | 29 | assert( 30 | callees[callees.length - 1] === 'either', 31 | 'The function `register` seems incorrect; hint: you can use `either` to branch an `Either` to different outcomes!', 32 | ); 33 | 34 | assert( 35 | msgAlbert === showWelcome(albert), 36 | 'The function `register` returns a correct type, but the inner value is incorrect! Did you use `showWelcome`?', 37 | ); 38 | 39 | const registerYi = register(yi); 40 | assert( 41 | registerYi instanceof IO, 42 | 'The left outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO` in every scenarios!', 43 | ); 44 | 45 | const msgYi = registerYi.unsafePerformIO(); 46 | assert( 47 | typeof msgYi === 'string', 48 | 'The left outcome to `register` is incorrect; hint: look carefully at your signatures, `register` should return an `IO(String)` in every scenarios!', 49 | ); 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to Mostly Adequate Guide to Functional Programming 3 | 4 | ## Licensing 5 | 6 | By opening a pull request to this repository, you agree to provide your work under the [project license](LICENSE). Also, you agree to grant such license of your work as is required for the purposes of future print editions to @DrBoolean. Should your changes appear in a printed edition, you'll be included in the contributors list. 7 | 8 | ## Small Corrections 9 | 10 | Errata and basic clarifications will be accepted if we agree that they improve the content. You can also open an issue so we can figure out how or if it needs to be addressed. If you've never done this before, the [flow guide](https://guides.github.com/introduction/flow/) might be useful. 11 | 12 | ## Questions or Clarifications 13 | 14 | Please, have a look at the [FAQ](FAQ.md) before you open an issue. Your question may already 15 | have been answered. Should you still need to ask something? Feel free to open an issue and to 16 | explain yourself. 17 | 18 | ## Translations 19 | 20 | Translations to other languages are highly encouraged. Each official translation will be held as a separate repository in the [MostlyAdequate organization](https://github.com/MostlyAdequate) and linked from the English version book. 21 | Since each translation is a different repository, we can also have different maintainers for each project. 22 | 23 | ### Creating a New Translation Repo 24 | 25 | In order to create a new translation, you need to follow these steps: 26 | 27 | * Fork the [main repo](https://github.com/MostlyAdequate/mostly-adequate-guide). 28 | * Add yourself to the watch list of the main repo, to keep up with changes. 29 | * When translating chapters, **create NEW files with suffix of your language**. 30 | * For example, Spanish tranlation for `ch01.md` will be on `ch01-es.md`. 31 | * Open a [new issue](https://github.com/MostlyAdequate/mostly-adequate-guide/issues/new) and ask to be part of the organization. 32 | * Transfer the repo to the organization. 33 | * Merge new content from the main repo. 34 | * keep translating... 35 | * Rinse/repeat last two steps until the book is done. 36 | -------------------------------------------------------------------------------- /appendix_a.md: -------------------------------------------------------------------------------- 1 | # Appendix A: Essential Functions Support 2 | 3 | In this appendix, you'll find some basic JavaScript implementations of various functions 4 | described in the book. Keep in mind that these implementations may not be the fastest or the 5 | most efficient implementation out there; they *solely serve an educational purpose*. 6 | 7 | In order to find functions that are more production-ready, have a peek at 8 | [ramda](https://ramdajs.com/), [lodash](https://lodash.com/), or [folktale](http://folktale.origamitower.com/). 9 | 10 | Note that some functions also refer to algebraic structures defined in the [Appendix B](./appendix_b.md) 11 | 12 | ## always 13 | 14 | ```js 15 | // always :: a -> b -> a 16 | const always = curry((a, b) => a); 17 | ``` 18 | 19 | 20 | ## compose 21 | 22 | ```js 23 | // compose :: ((y -> z), (x -> y), ..., (a -> b)) -> a -> z 24 | const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0]; 25 | ``` 26 | 27 | 28 | ## curry 29 | 30 | ```js 31 | // curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c 32 | function curry(fn) { 33 | const arity = fn.length; 34 | 35 | return function $curry(...args) { 36 | if (args.length < arity) { 37 | return $curry.bind(null, ...args); 38 | } 39 | 40 | return fn.call(null, ...args); 41 | }; 42 | } 43 | ``` 44 | 45 | 46 | ## either 47 | 48 | ```js 49 | // either :: (a -> c) -> (b -> c) -> Either a b -> c 50 | const either = curry((f, g, e) => { 51 | if (e.isLeft) { 52 | return f(e.$value); 53 | } 54 | 55 | return g(e.$value); 56 | }); 57 | ``` 58 | 59 | 60 | ## identity 61 | 62 | ```js 63 | // identity :: x -> x 64 | const identity = x => x; 65 | ``` 66 | 67 | 68 | ## inspect 69 | 70 | ```js 71 | // inspect :: a -> String 72 | const inspect = (x) => { 73 | if (x && typeof x.inspect === 'function') { 74 | return x.inspect(); 75 | } 76 | 77 | function inspectFn(f) { 78 | return f.name ? f.name : f.toString(); 79 | } 80 | 81 | function inspectTerm(t) { 82 | switch (typeof t) { 83 | case 'string': 84 | return `'${t}'`; 85 | case 'object': { 86 | const ts = Object.keys(t).map(k => [k, inspect(t[k])]); 87 | return `{${ts.map(kv => kv.join(': ')).join(', ')}}`; 88 | } 89 | default: 90 | return String(t); 91 | } 92 | } 93 | 94 | function inspectArgs(args) { 95 | return Array.isArray(args) ? `[${args.map(inspect).join(', ')}]` : inspectTerm(args); 96 | } 97 | 98 | return (typeof x === 'function') ? inspectFn(x) : inspectArgs(x); 99 | }; 100 | ``` 101 | 102 | 103 | ## left 104 | 105 | ```js 106 | // left :: a -> Either a b 107 | const left = a => new Left(a); 108 | ``` 109 | 110 | 111 | ## liftA2 112 | 113 | ```js 114 | // liftA2 :: (Applicative f) => (a1 -> a2 -> b) -> f a1 -> f a2 -> f b 115 | const liftA2 = curry((fn, a1, a2) => a1.map(fn).ap(a2)); 116 | ``` 117 | 118 | 119 | ## liftA3 120 | 121 | ```js 122 | // liftA3 :: (Applicative f) => (a1 -> a2 -> a3 -> b) -> f a1 -> f a2 -> f a3 -> f b 123 | const liftA3 = curry((fn, a1, a2, a3) => a1.map(fn).ap(a2).ap(a3)); 124 | ``` 125 | 126 | 127 | ## maybe 128 | 129 | ```js 130 | // maybe :: b -> (a -> b) -> Maybe a -> b 131 | const maybe = curry((v, f, m) => { 132 | if (m.isNothing) { 133 | return v; 134 | } 135 | 136 | return f(m.$value); 137 | }); 138 | ``` 139 | 140 | 141 | ## nothing 142 | 143 | ```js 144 | // nothing :: Maybe a 145 | const nothing = Maybe.of(null); 146 | ``` 147 | 148 | 149 | ## reject 150 | 151 | ```js 152 | // reject :: a -> Task a b 153 | const reject = a => Task.rejected(a); 154 | ``` 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![cover](images/cover.png)](SUMMARY.md) 2 | 3 | ## 책 소개 4 | 5 | 이 책은 일반적인 함수형 프로그래밍 패러다임에 대한 책이다. 우리는 세계에서 가장 유명한 함수형 언어인 '자바스크립트'를 사용할 것이다. 현재의 자바스크립트 관련 문화가 주로 명령형imperative 패러다임이기 때문에, 이런 선택이 조금은 잘못된 것 아닌가 하고 느낄 지도 모르겠다. 하지만 다음과 같은 이유로, 나는 자바스크립트를 활용하는 것이 FP를 배우는 데 가장 좋은 선택이 될 수 있다고 믿는다. 6 | 7 | - **매일 업무에 자바스크립트를 사용하고 있는 프로그래머가 많다** 8 | 9 | 따라서, 소수만 좋아하는 함수형 프로그래밍 언어에서 자잘한 토이 프로젝트에나 적용해 보는 대신, 실전에서 매일매일 자신이 배운 함수형 프로그래밍 지식을 적용하고 연습할 수 있다. 10 | 11 | - **프로그램을 작성하기 위해 필요한 여러가지 다른 내용을 다시 배울 필요가 없다.** 12 | 13 | 순수 함수형 언어에서는 모나드monad의 도움 없이 변수의 로그를 찍어보거나, DOM 노드를 읽어볼 수 없다. 하지만 자바스크립트에서는 코드 기반을 더 깔끔하게 유지하기 위해서 그런 기능을 살짝살짝 사용할 수 있다. 또한, 자바스크립트가 다양한 패러다임을 지원하기 때문에, 여러분이 잘 모르는 부분이 있다면 최후의 수단으로 기존의 방식을 활용해 문제를 활용할 수도 있다. 14 | 15 | - **자바스크립트는 최상급의 함수형 코드를 작성할 수 있는 모든 기능을 완전히 지원한다.** 16 | 17 | 소수의 라이브러리만 추가하면 하스켈Haskell이나 스칼라Scala와 같은 언어가 제공하는 기능을 흉내낼 수 있는 모든 특징을 활용할 수 있다. 객체 지향 프로그래밍이 현재 업계를 지배하고 있지만, 자바스크립트의 OOP는 분명 약간 이상하다. 이는 마치 고속도로를 벗어나서 캠핑을 하거나, 구두 위에 고무덧신galoshes을 신고 탭댄스를 추는 것과 같다. `this`가 부지중에 바뀌지 않도록 `bind`를 여기저기서 사용해야 하며, (아직) 클래스class도 없고, `new` 키워드를 빼먹은 경우 생기는 이상한 작동을 막기 위한 여러가지 장치를 만들어야 하고, 클로저closure를 통해서만 전용private 멤버를 정의할 수 있다. 일반적인 프로그래머에게 이보다는 FP(함수형 프로그래밍)가 더 자연스러워 보인다. 18 | 19 | 정적 타입을 제공하는 함수형 언어가 의심할 여지 없이 이 책에서 설명하는 스타일로 코딩하는 가장 좋은 환경일 것이다. 자바스크립트는 단지 함수형 패러다임을 배우는 수단일 뿐이며, 그 패러다임을 적용하는 것은 여러분에게 달려있다. 운 좋게도, 함수형 패러다임에 접근하는 경로는 수학적이며, 따라서 어떤 어디에서든 통할 수 있다. 이 책을 다루고 나면, 수학쪽으로 경도된 환경이라면 스위프트제드swiftz, 스칼라제드scalaz, 하스켈, 퓨어스크립트purescript 등을 막론하고 편안함을 느낄 수 있을 것이다. 20 | 21 | ### (영문판) 온라인으로 보기 - 한글판은 나중에 제공 22 | 23 | [Gitbook](https://mostly-adequate.gitbooks.io/mostly-adequate-guide/)은 최적의 읽기 환경을 제공한다. 24 | 25 | - 사이드바 바로가기 26 | - 브라우저 안에서 연습문제 풀기 27 | - 깊이있는 연습문제 28 | 29 | ## 코드랑 놀기 30 | 31 | 다른 이야기를 하는 동안 효율적으로 훈련하고 지루해지지 않도록 책에 나온 여러가지 개념들과 놀아보도록 하라. 어떤 것들은 처음에는 이해하기 힘들겠지만 코드랑 놀면서 점점 더 이해하게 될 것이다. 32 | 이 책에 나온 모든 함수와 대수적 자료 구조들은 부록에 나와있고 npm 모듈로도 있다. 33 | 34 | ```bash 35 | $ npm i @mostly-adequate/support 36 | ``` 37 | 38 | 각 장의 연습문제 또한 실행 가능하고 당신의 에디터 안에서 풀 수 있다! 예를들어, `exercises/ch04`의 `exercise_*.js`를 푼 후 다음 명령어를 실행시켜라. 39 | 40 | ```bash 41 | $ npm run ch04 42 | ``` 43 | 44 | ## (영문판) 다운로드 45 | 46 | - [PDF 다운로드](https://www.gitbook.com/download/pdf/book/mostly-adequate/mostly-adequate-guide) 47 | - [EPUB 다운로드](https://www.gitbook.com/download/epub/book/mostly-adequate/mostly-adequate-guide) 48 | - [Mobi (킨들) 다운로드](https://www.gitbook.com/download/mobi/book/mostly-adequate/mostly-adequate-guide) 49 | 50 | ### (영문판) 직접 만드는 방법 51 | 52 | ``` 53 | git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git 54 | cd mostly-adequate-guide/ 55 | npm install 56 | npm run setup 57 | npm run generate-pdf 58 | npm run generate-epub 59 | ``` 60 | 61 | > 주의! ebook 버전을 만들기 위해서는 `ebook-convert`을 설치해야한다. [설치 가이드](https://toolchain.gitbook.com/ebook.html#installing-ebook-convert). 62 | 63 | 한글판을 직접 만드는 방법은 다음과 같다. 64 | 65 | ``` 66 | git clone https://github.com/MostlyAdequate/mostly-adequate-guide-kr.git 67 | cd mostly-adequate-guide-kr/ 68 | npm install 69 | npm run setup 70 | npm run generate-pdf 71 | npm run generate-epub 72 | ``` 73 | 74 | # 목차 75 | 76 | [SUMMARY.md](SUMMARY.md) 를 보라. 77 | 78 | ### 기여하기 79 | 80 | [CONTRIBUTING.md](CONTRIBUTING.md) 를 보라. 81 | 82 | ### 번역 83 | 84 | [TRANSLATIONS.md](TRANSLATIONS.md) 를 보라. 85 | 86 | ### FAQ 87 | 88 | [FAQ.md](FAQ.md) 를 보라. 89 | 90 | # 향후 계획 91 | 92 | - **1부** (1-7장)는 기본적인 내용에 대한 안내다. 초고에서 오류를 발견할 때마다 내용을 갱신할 것이다. 도움의 손길을 언제든 환영한다! 93 | - **2부** (8-13장)는 펑터functor나 모나드monad로부터 순회가능traversable에 이르는 여러 타입 클래스type class에 대해 다룬다. 변환기transformer나 순수한 적용pure application에 대해 다룰 여유가 있기를 바란다. 94 | - **3부** (14장 이후)는 실용적인 프로그래밍과 학술적인 불합리성 사이의 미세한 차이를 가늠해 볼 것이다. 코모나드comonad, f-대수f-algebra, 프리 모나드free monad, 요네다yoneda 및 다른 카테고리 이론적인 구성요소들을 살펴볼 것이다. 95 | 96 | --- 97 | 98 |

99 | 100 | Creative Commons License 101 | 102 |
103 | This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. 104 |

105 | -------------------------------------------------------------------------------- /ch02-kr.md: -------------------------------------------------------------------------------- 1 | # 02 장: 일급 함수 2 | 3 | ## 복습 시간 4 | 5 | 함수가 "일급" 이라는 말은 다른 보통의 것들과 똑같다는 것을 의미해요. 함수를 특별하게 취급하지 않고 다른 자료형처럼 다룰 수 있다는 것을요. 예를 들면 배열에 저장하거나 함수의 파라미터로 넘겨줄 수도 있어요. 그리고 변수에 할당하는 등 다른 모든 것들도 할 수 있어요. 6 | 7 | 지금부터 볼 것은 아주 기초적인 자바스크립트이지만 깃헙을 조금만 찾아봐도 이런 코드들을 볼 수 있지만 모두 일부러 이 문제를 회피하고 있기 때문에 한 번 다루고 싶군요. 한번 예시를 들어볼까요? 8 | 9 | ```js 10 | const hi = (name) => `Hi ${name}`; 11 | const greeting = (name) => hi(name); 12 | ``` 13 | 14 | 여기서 `hi`를 감싸는 `greeting` 함수는 아무것도 하는 일이 없어요. 왜일까요? 바로 자바스크립트에서 함수는 **호출할 수 있기**때문지요. `hi`의 끝에 `()`가 있을 때 `hi`는 실행되고 값을 반환합니다. 그렇지 않을 때는 그저 변수에 할당된 함수를 반환하지요. 15 | 16 | ```js 17 | hi; // name => `Hi ${name}` 18 | hi("jonas"); // "Hi jonas" 19 | ``` 20 | 21 | `greeting`은 그저 `hi`를 자신과 같은 인자로 호출하기 때문에 더 단순하게 쓸 수 있어요. 22 | 23 | ```js 24 | const greeting = hi; 25 | greeting("times"); // "Hi times" 26 | ``` 27 | 28 | 다시 말해서, `hi`가 벌써 하나의 인자를 받는 함수인데 왜 굳이 같은 인자로 `hi`를 호출하기만 하는 함수로 감싸나요? 전혀 쓸모가 없군요. 이건 마치 죽을 것 같이 더운 7월에 아주 무거운 파카를 입고 덥다고 불평하는 것 같아요. 29 | 30 | 이건 함수를 다른 함수로 감싸 단순히 계산을 느리게 하고 코드를 장황하게 하는 안좋은 방법입니다(잠시 후에 이유를 알아보겠지만 유지 보수와도 관련이 있어요). 31 | 32 | 더 나아가기 전에 이것을 잘 이해하는 것이 중요하기 때문에 npm 패키지에서 찾은 재미있는 예시를 보겠습니다. 33 | 34 | ```js 35 | // 무식한 방법 36 | const getServerStuff = (callback) => ajaxCall((json) => callback(json)); 37 | 38 | // 똑똑한 방법 39 | const getServerStuff = ajaxCall; 40 | ``` 41 | 42 | 세상은 이것과 똑같은 ajax 코드들로 더렵혀져 있어요. 위의 두 코드가 같은 이유를 설명해 볼게요. 43 | 44 | ```js 45 | // 이 줄은 46 | ajaxCall((json) => callback(json)); 47 | 48 | // 이 줄과 같아요. 49 | ajaxCall(callback); 50 | 51 | // 따라서 getServerStuff를 개선할 수 있어요. 52 | const getServerStuff = (callback) => ajaxCall(callback); 53 | 54 | // 그리고 다시 이 코드와 같고요. 55 | const getServerStuff = ajaxCall; // <-- 엄마 보세요, ()가 없어요. 56 | ``` 57 | 58 | 여러분, 이것이 제대로 된 코드입니다. 내가 왜 이렇게 끈질기게 구는지 이해할 수 있도록 한번 더 예시를 들어볼게요. 59 | 60 | ```js 61 | const BlogController = { 62 | index(posts) { 63 | return Views.index(posts); 64 | }, 65 | show(post) { 66 | return Views.show(post); 67 | }, 68 | create(attrs) { 69 | return Db.create(attrs); 70 | }, 71 | update(post, attrs) { 72 | return Db.update(post, attrs); 73 | }, 74 | destroy(post) { 75 | return Db.destroy(post); 76 | }, 77 | }; 78 | ``` 79 | 80 | 이 말도 안되는 컨트롤러는 99% 하는 것이 없어요. 그리고 다음처럼 고쳐쓸 수 있어요. 81 | 82 | ```js 83 | const BlogController = { 84 | index: Views.index, 85 | show: Views.show, 86 | create: Db.create, 87 | update: Db.update, 88 | destroy: Db.destroy, 89 | }; 90 | ``` 91 | 92 | 아니면 그저 Views와 Db를 한군데 모아둘 수도 있어요. View와 Db를 그저 묶어두기만 하기 때문이죠. 93 | 94 | ## 일급 함수의 장점 95 | 96 | 좋아요, 이제 일급 함수의 장점을 한번 찾아보죠. `getServerStuff`와 `BlogController` 예에서 본 것 처럼 유지보수를 해야할 쓸모없는 코드 양만 늘리면서 아무런 일도 하지 않는 레이어를 늘리기는 간단합니다. 97 | 98 | 더군다나 그런 필요없이 싸여진 함수를 변경해야 한다면 그 함수의 감싸고 있는 함수도 바꿔야해요. 99 | 100 | ```js 101 | httpGet("/post/2", (json) => renderPost(json)); 102 | ``` 103 | 104 | `httpGet`이 `err`도 줄 수도 있다고 한다면 우리는 돌아가서 "접착제"를 바꿔야해요. 105 | 106 | ```js 107 | // 어플리케이션의 모든 httpGet 호출로 가서 명시적으로 err 넘겨주기 108 | httpGet("/post/2", (json, err) => renderPost(json, err)); 109 | ``` 110 | 111 | 만약 일급함수를 이용했다면 바꿔야할 부분은 훨씬 적었을 거에요. 112 | 113 | ```js 114 | // renderPost는 httpGet 안에서 원하는 만큼의 인자를 받아서 호출됩니다 115 | httpGet("/post/2", renderPost); 116 | ``` 117 | 118 | 불필요한 함수를 만드는 것말고 다른 문제도 있어요. 우리는 인자의 변수명을 짓고 변수를 참조해야해요. 당신도 알다시피 이름붙이기는 꽤 문제가 있답니다. 특히 프로젝트가 오래되고 요구사항이 바뀔수록 잘못된 이름이 많아지. 119 | 120 | 프로젝트에서 같은 개념을 여러가지 이름으로 부르면 혼란 생기기 쉽습니다. 또 일반적인 코드상의 문제도 있어요. 예를 들면 다음 두 함수를 정확히 같은 일을 하지만 하나는 다른 것보다 훨씬 더 일반적이고 재사용할 수 있을 것 같이 느껴집니다. 121 | 122 | ```js 123 | // 우리 블로그에 특정된다. 124 | const validArticles = articles => 125 | articles.filter(article => article !== null && article !== undefined), 126 | 127 | // 다른 프로젝트에서도 사용 가능할 것 같다. 128 | const compact = xs => xs.filter(x => x !== null && x !== undefined); 129 | ``` 130 | 131 | 특정한 이름을 사용하면 우리는 우리 스스로를 특정한 데이터에 한정시킵니다(이 예제에서는 `articles`). 이런 일은 꽤나 자주 일어나고 똑같은 것을 다시 만들게 하는 일의 이유가 됩니다. 132 | 133 | 저는 객체 지향 코드에서처럼 당신이 `this`가 당신의 급소를 치러 온다는 것을 알고 있어야 한다고 말하고 싶어요. 만약 `this`를 사용하고 있는 함수를 일급이라고 부른다면 이 잘못된 추상화가 우리에게 화를 낼 것입니다. 134 | 135 | ```js 136 | const fs = require("fs"); 137 | 138 | // 무서워요 139 | fs.readFile("freaky_friday.txt", Db.save); 140 | 141 | // 덜 무서워요 142 | fs.readFile("freaky_friday.txt", Db.save.bind(Db)); 143 | ``` 144 | 145 | 함수를 자신에게 bind 해서 `Db`는 그의 쓰레기 같은 원래의 코드에 마음껏 접근할 수 있게 됐어요. 저는 `this`가 더러운 기저귀인 것처럼 피하려고 해요. 함수형 코드를 쓸 때 `this`를 쓸 필요가 하나도 없어요. 반면 다른 라이브러리와 대화할 때는 우리 주변의 잘못된 세상을 묵인해야 할지도 몰라요. 146 | 147 | 어떤 사람은 `this`가 성능을 최적화하는데 필요하다고 말해요. 그러나 당신이 세세한 것까지 최적화하는 사람이라면 이 책을 덮어주세요. 만약 환불이 안되더라고 좀 더 사소한 것과 교환할 수 있을 거에요. 148 | 149 | 이제 이것들과 함께 앞으로 나아갈 준비가 되었습니다. 150 | 151 | [03 장: 순수 함수와 함께하는 순수한 기쁨](ch03-kr.md) 152 | -------------------------------------------------------------------------------- /exercises/test-utils.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const cli = require('cli'); 6 | const process = require('process'); 7 | const { it } = require('mocha'); 8 | const { 9 | compose, 10 | concat, 11 | curry, 12 | forEach, 13 | map, 14 | reduce, 15 | } = require('./support'); 16 | 17 | 18 | /* -------- Internal -------- */ 19 | 20 | const filenameRe = base => new RegExp(`${base}_([a-z]).js`); 21 | 22 | const readDir = d => fs.readdirSync(d); 23 | 24 | const readFile = f => fs.readFileSync(f).toString('utf-8'); 25 | 26 | const writeFile = curry((f, data) => fs.writeFileSync(f, data)); 27 | 28 | const mkdir = d => fs.mkdirSync(d); 29 | 30 | const mconcat = zero => reduce(concat, zero); 31 | 32 | const evalFiles = compose(eval, mconcat(''), map(readFile)); // eslint-disable-line no-eval 33 | 34 | const listToMap = xs => xs.reduce((m, [k, v]) => m.set(k, v), new Map()); 35 | 36 | 37 | /* -------- Utils -------- */ 38 | 39 | const readExercises = (ch, sections = ['exercise', 'solution', 'validation']) => { 40 | const folder = path.join(__dirname, ch); 41 | 42 | const partition = reduce((pts, f) => { 43 | forEach((section) => { 44 | const re = filenameRe(section); 45 | 46 | if (re.test(f)) { 47 | const spec = re.exec(f)[1]; 48 | pts.get(section).set(spec, path.join(folder, f)); 49 | } 50 | }, sections); 51 | 52 | return pts; 53 | }, listToMap(map(s => [s, new Map()], sections))); 54 | 55 | return partition(readDir(folder)); 56 | }; 57 | 58 | const defineSpecs = curry((input, exercises) => { 59 | const support = path.join(__dirname, 'support.js'); 60 | 61 | forEach(([num, exercise]) => { 62 | it(`exercise_${num}`, () => { 63 | const validation = exercises.get('validation').get(num); 64 | 65 | evalFiles([support, exercise, validation]); 66 | }); 67 | }, [...exercises.get(input)]); 68 | }); 69 | 70 | const runSolutions = compose(defineSpecs('solution'), readExercises); 71 | 72 | const runExercises = compose(defineSpecs('exercise'), readExercises); 73 | 74 | 75 | /* -------- Cli -------- */ 76 | 77 | if (process.argv.length > 2 && process.argv[1] === __filename) { 78 | const usage = `Usage: test-utils new 79 | 80 | Available options: 81 | -h, --help Show this help text 82 | -c, --chapter New chapter to create`; 83 | 84 | const spec = curry((runner, chapter) => `const { describe } = require('mocha'); 85 | const { ${runner} } = require('../test-utils'); 86 | 87 | describe('Exercises Chapter ${chapter}', () => { 88 | ${runner}('ch${chapter}'); 89 | });`); 90 | 91 | const addTestScript = curry((pkg, chapter) => { 92 | const { scripts } = pkg; 93 | 94 | return { 95 | ...pkg, 96 | scripts: { 97 | ...scripts, 98 | [`ch${chapter}`]: `mocha test/ch${chapter}.js`, 99 | }, 100 | }; 101 | }); 102 | 103 | const failWith = curry((ctx, msg) => { 104 | ctx.fatal([msg, usage].join('\n\n')); 105 | }); 106 | 107 | // NOTE Override the `getUsage` method of the cli which is... not satisfactory. 108 | cli.getUsage = () => { console.error(usage); cli.exit(0); }; // eslint-disable-line no-console 109 | 110 | cli.parse({ 111 | chapter: ['c', 'New chapter to create', 'int'], 112 | }); 113 | 114 | cli.main(function main(args, options) { 115 | cli.failWith = failWith(this); 116 | 117 | if (args.length !== 1 || args[0] !== 'new') { 118 | cli.failWith(`Unknown command: ${args.join(' ')}`); 119 | } 120 | 121 | if (!options.chapter) { 122 | cli.failWith('Missing required Chapter'); 123 | } 124 | 125 | if (readDir(__dirname).includes(`ch${options.chapter}`)) { 126 | cli.failWith(`Chapter ${options.chapter} already exists`); 127 | } 128 | 129 | mkdir(path.join(__dirname, `ch${options.chapter}`)); 130 | 131 | writeFile( 132 | path.join(__dirname, 'test', `ch${options.chapter}.js`), 133 | spec('runExercises', options.chapter), 134 | ); 135 | 136 | writeFile( 137 | path.join(__dirname, 'test', `ch${options.chapter}.solutions.js`), 138 | spec('runSolutions', options.chapter), 139 | ); 140 | 141 | const pkg = addTestScript(require('./package.json'), options.chapter); // eslint-disable-line global-require 142 | 143 | writeFile( 144 | path.join(__dirname, 'package.json'), 145 | JSON.stringify(pkg, null, 2), 146 | ); 147 | 148 | console.error(`Bootstrapped Chapter ${options.chapter}.`); // eslint-disable-line no-console 149 | }); 150 | } 151 | 152 | 153 | /* -------- Exports -------- */ 154 | 155 | module.exports = { 156 | readExercises, 157 | defineSpecs, 158 | runSolutions, 159 | runExercises, 160 | }; 161 | -------------------------------------------------------------------------------- /ch01-kr.md: -------------------------------------------------------------------------------- 1 | # 01 장: 우리는 무얼 하고 있나? 2 | 3 | ## 소개 4 | 5 | 안녕하세요, 저는 Franklin Frisby 교수입니다. 만나서 반갑습니다. 당신과 어울리면서 함수형 프로그래밍에대해 조금 알려드리겠습니다. 그런데 당신은 어떤 사람입니까? 저는 당신이 최소한 자바스크립트에 대해 친숙하고 객체지향 프로그래밍 경혐이 조금 있기를 바랍니다. 그리고 당신 자신을 프로그래머라고 생각하고 있기를요. 곤충학 박사가 아니어도 되지만 곤충(bug)를 찾고 잡는 법을 알고 있어야 합니다. 6 | 7 | 저는 당신이 함수형 프로그래밍을 알고 있다고 생각하지는 않겠습니다. 그렇게 생각하면 어떻게 될지 우리 모두 알고 있잖아요. 하지만 변하는 상태(mutable state)와 제한되지 않은 부수효과(side effects), 규칙 없는 디자인이 만드는 불쾌한 상황을 겪어봤길 기대합니다. 이제 서로 충분히 알게 된 것 같군요. 이제 시작합니다. 8 | 9 | 저는 이 장에서 앞으로 우리가 무엇을 하게 될지 대략적으로 말해주려고 합니다. 무엇이 **함수형** 프로그램을 만드는지에 대해 어느정도 알고있어야지 이후의 내용을 이해할 수 있을 겁니다. 그렇지 않으면 우리는 목적을 잃고 헤메고 책을 읽기 위한 노력이 무용지물이 될 거에요. 우리는 코드를 볼 초롱초롱한 눈과 바다가 거칠어 졌을 때를 위한 천체 나침반이 필요합니다. 10 | 11 | 개발할 때 막막한 밤을 안내해주는 몇 가지 프로그래밍 법칙들이 있어요. DRY(반복하지 마라, don't repeat yourself), YAGNI(필요한 작업만 해라, ya ain't gonna need it), 낮은 결합도 높은 응집도, 최소한으로 놀라게 하라, 단일 책임 등등... 12 | 13 | 저는 제가 몇 년 동안 들어온 모든 조언을 말하면서 당신을 지루하게 하지는 않을거에요. 중요한 것은 이 격언들이 우리의 궁극적인 목표에는 조금밖에 맞닿아 있지 않지만 함수형 프로그래밍에서도 유효하다는 것이에요. 앞으로 더 나아가기 전에 우리가 키보드를 칠 때의 의도를 당신이 느꼈으면 느껴봤으면 좋겠어요. 우리의 함수형 프로그래밍의 무릉도원을요. 14 | 15 | 16 | 17 | ## 짧은 만남 18 | 19 | 여기 갈매기 프로그램이 있어요. 약간 이상하게 보일 수도 있어요. 무리들이 결합(cojoin)하면 더 큰 무리가 되고 새끼를 치면 다른 무리의 수만큼 곱해집니다. 좋은 객체지향 코드가 아니지만 할당에 기반한 현대적인 접근 방식이 위험하다는 것을 강조하기 위한 코드입니다. 한번 보세요. 20 | 21 | ```js 22 | class Flock { 23 | constructor(n) { 24 | this.seagulls = n; 25 | } 26 | 27 | conjoin(other) { 28 | this.seagulls += other.seagulls; 29 | return this; 30 | } 31 | 32 | breed(other) { 33 | this.seagulls = this.seagulls * other.seagulls; 34 | return this; 35 | } 36 | } 37 | 38 | const flockA = new Flock(4); 39 | const flockB = new Flock(2); 40 | const flockC = new Flock(0); 41 | const result = flockA 42 | .conjoin(flockC) 43 | .breed(flockB) 44 | .conjoin(flockA.breed(flockB)).seagulls; 45 | // 32 46 | ``` 47 | 48 | 세상의 어떤 사람이 이런 이상한 것을 만들겠어요? 내부 상태 변화를 추적하기가 너무 힘들군요. 그리고 세상에, 심지어 답도 틀렸어요! `16`이 되어야하지만 `flockA`가 중간에 바뀌어 값이 달라졌습니다. 불쌍한 `flockA`... 이것이 I.T.의 난세입니다! 이것이 야생의 수학입니다! 49 | 50 | 이 프로그램을 이해하지 못해도 괜찮아요. 저도 모르겠거든요. 기억해야할 점은 이렇게 작은 예제에서도 상태와 변하는 값(mutable values)은 추적하기 힘들다는 것입니다. 51 | 52 | 이번에는 함수형 프로그래밍의 접근방법을 사용해서 한번 더 시도해볼까요? 53 | 54 | ```js 55 | const conjoin = (flockX, flockY) => flockX + flockY; 56 | const breed = (flockX, flockY) => flockX * flockY; 57 | 58 | const flockA = 4; 59 | const flockB = 2; 60 | const flockC = 0; 61 | const result = conjoin( 62 | breed(flockB, conjoin(flockA, flockC)), 63 | breed(flockA, flockB) 64 | ); 65 | // 16 66 | ``` 67 | 68 | 와, 이번에는 올바른 답을 얻었네요. 그것도 좀 더 적은 코드로요. 함수 중첩은 약간 헷갈리긴 하네요... (5장에서 이 문제를 해결할 거예요). 좀 나아졌지만 조금 더 깊게 가볼까요. "ㄱ"을 "기역"이라고 부르는 장점이 있습니다. 한번 자세히 본다면 그저 덧셈(`cojoin`)과 곱셈(`breed`)를 하고 있다는 것을 알 수 있을 거에요. 69 | 70 | 이 두 함수에는 이름 말고 다른 특별한 점은 하나도 없어요. 우리의 함수들을 `multiply`와 `add`라고 다시 이름지어서 이들의 정체을 드러내봅시다. 71 | 72 | ```js 73 | const add = (x, y) => x + y; 74 | const multiply = (x, y) => x * y; 75 | 76 | const flockA = 4; 77 | const flockB = 2; 78 | const flockC = 0; 79 | const result = add( 80 | multiply(flockB, add(flockA, flockC)), 81 | multiply(flockA, flockB) 82 | ); 83 | // 16 84 | ``` 85 | 86 | 그리고 그것을 통해 우리는 오래전부터 알고 있던 지식을 얻을 수 있어요. 87 | 88 | ```js 89 | // 결합법칙(associative) 90 | add(add(x, y), z) === add(x, add(y, z)); 91 | 92 | // 교환법칙(commutative) 93 | add(x, y) === add(y, x); 94 | 95 | // 항등원(identity) 96 | add(x, 0) === x; 97 | 98 | // 분배법칙(distributive) 99 | multiply(x, add(y, z)) === add(multiply(x, y), multiply(x, z)); 100 | ``` 101 | 102 | 이 오래되고 믿음직한 수학 성질들은 곧 손에 익게 될 거에요. 하지만 지금 당장 이해하지 못해도 걱정하지 마세요. 우리 중 대부분은 이 산수 법직들을 배운지 오랜 시간이 지났으니깐요. 이 법칙들을 이용해 우리의 작은 갈매기 프로그램을 간단하게 만들 수 있는지 한 번 볼까요? 103 | 104 | ```js 105 | // 기존 코드 106 | add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB)); 107 | 108 | // 필요없는 덧셈을 줄이기 위해 항등원 규칙 적용하기 109 | // (add(flockA, flockC) == flockA) 110 | add(multiply(flockB, flockA), multiply(flockA, flockB)); 111 | 112 | // 분배법칙 적용하기 113 | multiply(flockB, add(flockA, flockA)); 114 | ``` 115 | 116 | 와!! 우리는 함수 호출하는 것 이외의 코드를 쓸 필요가 없었군요. 지금은 완전성을 위해 `add`와 `multiply`의 정의를 적었지만 굳이 사용할 필요는 없습니다. `add`와 `multiply`를 제공하는 라이브러리가 있을테니까요. 117 | 118 | 아마 당신은 "이런 수학적인 예제는 허수아비의 오류가 아닌가요" 내지는 "실제 프로그램은 이렇게 단순하지 않아서 이런 식으로 생각할 수 없어요"라고 말할지도 몰라요. 저는 우리 대부분이 덧셈과 곱셈에 대해 알고 있고 또 이 예제를 통해 수학이 얼마나 유용한지 보여주기 쉽기 때문에 이 예제를 선택했어요. 119 | 120 | 하지만 실망하지 마세요. 이 책을 읽는 동안 우리는 약간의 범주론(category theory)과 집합론, 람다 대수(lambda calculus)을 탐험할 것입니다. 그리고 실제 세상의 문제를 갈매기 프로그램 예제처럼 간단하지만 우아하게 풀어볼 것입니다. 당신이 수학자일 필요는 없어요. 당신은 함수형 프로그램이 "보통의" 프레임워크나 API를 사용하는 것처럼 쉽고 자연스럽고 느껴질 것이에요. 121 | 122 | 아마 모든 어플리케이션을 위와 같은 함수형 코드로 짤 수 있다는 것을 알면 당신은 놀라겠지요. 견실한 성질을 가지고 적은 코드를 쓰지만 이해하기 쉬운 프로그램을요. 또 매번 바퀴를 발명하지 않는 프로그램을요. 당신이 범죄자라면 무법지대를 좋아하겠지만 이 책에서는 견고한 수학의 법칙을 인정하고 따르고 싶어 질 것이에요. 123 | 124 | 우리는 모든 조각이 서로 딱 들어맞는 이론을 원하게 될 것이에요. 또 우리는 특정한 문제를 일반적이고 조합 가능한 조각으로 표현하고 우리 자신의 문제를 위해 이용하고 싶어질 거에요. 명령형 프로그램의 "모든지 가능한" 접근방식 보다는 좀 더 규율적인 접근방식을 택할 것이에요(나중에 "명령형"의 정확한 정의를 다루겠지만 지금은 함수형 프로그래밍 이외의 모든 것을 의미한다고 생각해주세요). 원리가 있는 수학적 프레임워크 안에서 일하는 것을 당신을 진실로 놀라게 할 것이에요. 125 | 126 | 우리는 함수형 세계의 북극성이 반짝이는 것을 잠깐 봤지만 본격적으로 우리의 여정을 시작하기 전에 알아야하는 구체적인 개념이 몇 가지 있습니다. 127 | 128 | [02 장: 일급 함수](ch02-kr.md) 129 | -------------------------------------------------------------------------------- /appendix_c.md: -------------------------------------------------------------------------------- 1 | # Appendix C: Pointfree Utilities 2 | 3 | In this appendix, you'll find pointfree versions of rather classic JavaScript functions 4 | described in the book. All of the following functions are seemingly available in exercises, as 5 | part of the global context. Keep in mind that these implementations may not be the fastest or 6 | the most efficient implementation out there; they *solely serve an educational purpose*. 7 | 8 | In order to find functions that are more production-ready, have a peek at 9 | [ramda](https://ramdajs.com/), [lodash](https://lodash.com/), or [folktale](http://folktale.origamitower.com/). 10 | 11 | Note that functions refer to the `curry` & `compose` functions defined in [Appendix A](./appendix_a.md) 12 | 13 | ## add 14 | 15 | ```js 16 | // add :: Number -> Number -> Number 17 | const add = curry((a, b) => a + b); 18 | ``` 19 | 20 | ## append 21 | 22 | ```js 23 | // append :: String -> String -> String 24 | const append = flip(concat); 25 | ``` 26 | 27 | ## chain 28 | 29 | ```js 30 | // chain :: Monad m => (a -> m b) -> m a -> m b 31 | const chain = curry((fn, m) => m.chain(fn)); 32 | ``` 33 | 34 | ## concat 35 | 36 | ```js 37 | // concat :: String -> String -> String 38 | const concat = curry((a, b) => a.concat(b)); 39 | ``` 40 | 41 | ## eq 42 | 43 | ```js 44 | // eq :: Eq a => a -> a -> Boolean 45 | const eq = curry((a, b) => a === b); 46 | ``` 47 | 48 | ## filter 49 | 50 | ```js 51 | // filter :: (a -> Boolean) -> [a] -> [a] 52 | const filter = curry((fn, xs) => xs.filter(fn)); 53 | ``` 54 | 55 | ## flip 56 | 57 | ```js 58 | // flip :: (a -> b -> c) -> b -> a -> c 59 | const flip = curry((fn, a, b) => fn(b, a)); 60 | ``` 61 | 62 | ## forEach 63 | 64 | ```js 65 | // forEach :: (a -> ()) -> [a] -> () 66 | const forEach = curry((fn, xs) => xs.forEach(fn)); 67 | ``` 68 | 69 | ## head 70 | 71 | ```js 72 | // head :: [a] -> a 73 | const head = xs => xs[0]; 74 | ``` 75 | 76 | ## intercalate 77 | 78 | ```js 79 | // intercalate :: String -> [String] -> String 80 | const intercalate = curry((str, xs) => xs.join(str)); 81 | ``` 82 | 83 | ## join 84 | 85 | ```js 86 | // join :: Monad m => m (m a) -> m a 87 | const join = m => m.join(); 88 | ``` 89 | 90 | ## last 91 | 92 | ```js 93 | // last :: [a] -> a 94 | const last = xs => xs[xs.length - 1]; 95 | ``` 96 | 97 | ## map 98 | 99 | ```js 100 | // map :: Functor f => (a -> b) -> f a -> f b 101 | const map = curry((fn, f) => f.map(fn)); 102 | ``` 103 | 104 | ## match 105 | 106 | ```js 107 | // match :: RegExp -> String -> Boolean 108 | const match = curry((re, str) => re.test(str)); 109 | ``` 110 | 111 | ## prop 112 | 113 | ```js 114 | // prop :: String -> Object -> a 115 | const prop = curry((p, obj) => obj[p]); 116 | ``` 117 | 118 | ## reduce 119 | 120 | ```js 121 | // reduce :: (b -> a -> b) -> b -> [a] -> b 122 | const reduce = curry((fn, zero, xs) => xs.reduce(fn, zero)); 123 | ``` 124 | 125 | ## replace 126 | 127 | ```js 128 | // replace :: RegExp -> String -> String -> String 129 | const replace = curry((re, rpl, str) => str.replace(re, rpl)); 130 | ``` 131 | 132 | ## reverse 133 | 134 | ```js 135 | // reverse :: [a] -> [a] 136 | const reverse = x => (Array.isArray(x) ? x.reverse() : x.split('').reverse().join('')); 137 | ``` 138 | 139 | ## safeHead 140 | 141 | ```js 142 | // safeHead :: [a] -> Maybe a 143 | const safeHead = compose(Maybe.of, head); 144 | ``` 145 | 146 | ## safeLast 147 | 148 | ```js 149 | // safeLast :: [a] -> Maybe a 150 | const safeLast = compose(Maybe.of, last); 151 | ``` 152 | 153 | ## safeProp 154 | 155 | ```js 156 | // safeProp :: String -> Object -> Maybe a 157 | const safeProp = curry((p, obj) => compose(Maybe.of, prop(p))(obj)); 158 | ``` 159 | 160 | ## sequence 161 | 162 | ```js 163 | // sequence :: (Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a) 164 | const sequence = curry((of, f) => f.sequence(of)); 165 | ``` 166 | 167 | ## sortBy 168 | 169 | ```js 170 | // sortBy :: Ord b => (a -> b) -> [a] -> [a] 171 | const sortBy = curry((fn, xs) => xs.sort((a, b) => { 172 | if (fn(a) === fn(b)) { 173 | return 0; 174 | } 175 | 176 | return fn(a) > fn(b) ? 1 : -1; 177 | })); 178 | ``` 179 | 180 | ## split 181 | 182 | ```js 183 | // split :: String -> String -> [String] 184 | const split = curry((sep, str) => str.split(sep)); 185 | ``` 186 | 187 | ## take 188 | 189 | ```js 190 | // take :: Number -> [a] -> [a] 191 | const take = curry((n, xs) => xs.slice(0, n)); 192 | ``` 193 | 194 | ## toLowerCase 195 | 196 | ```js 197 | // toLowerCase :: String -> String 198 | const toLowerCase = s => s.toLowerCase(); 199 | ``` 200 | 201 | ## toString 202 | 203 | ```js 204 | // toString :: a -> String 205 | const toString = String; 206 | ``` 207 | 208 | ## toUpperCase 209 | 210 | ```js 211 | // toUpperCase :: String -> String 212 | const toUpperCase = s => s.toUpperCase(); 213 | ``` 214 | 215 | ## traverse 216 | 217 | ```js 218 | // traverse :: (Applicative f, Traversable t) => (a -> f a) -> (a -> f b) -> t a -> f (t b) 219 | const traverse = curry((of, fn, f) => f.traverse(of, fn)); 220 | ``` 221 | 222 | ## unsafePerformIO 223 | 224 | ```js 225 | // unsafePerformIO :: IO a -> a 226 | const unsafePerformIO = io => io.unsafePerformIO(); 227 | ``` 228 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [01 장: 우리는 무엇을 하고 있나?](ch01-kr.md) 4 | - [소개](ch01-kr.md#소개) 5 | - [짧은 만남](ch01-kr.md#짧은-만남) 6 | - [02 장: 일급 함수](ch02-kr.md) 7 | - [복습 시간](ch02-kr.md#복습-시간) 8 | - [일급 함수의 장점](ch02-kr.md#일급-함수의-장점) 9 | - [03 장: 순수 함수와 순수한 기쁨을](ch03-kr.md) 10 | - [한번 더 순수함에 대하여](ch03-kr.md#한번-더-순수함에-대하여) 11 | - [부수효과는 ...를 포함할 수 있어요](ch03-kr.md#부수효과는-를-포함할-수-있어요) 12 | - [중학교 2학년 수학](ch03-kr.md#중학교-2학년-수학) 13 | - [순수성의 예](ch03-kr.md#순수성의-예) 14 | - [요약](ch03-kr.md#요약) 15 | - [04 장: 커링](ch04-kr.md) 16 | - [Can't Live If Livin' Is without You](ch04-kr.md#cant-live-if-livin-is-without-you) 17 | - [말장난 조금 더 / 특별한 소스](ch04-kr.md#말장난-조금-더--특별한-소스) 18 | - [요약](ch04.md-kr#요약) 19 | - [연습문제](ch04.md-kr#연습문제) 20 | - [Chapter 05: Coding by Composing](ch05.md) 21 | - [Functional Husbandry](ch05.md#functional-husbandry) 22 | - [Pointfree](ch05.md#pointfree) 23 | - [Debugging](ch05.md#debugging) 24 | - [Category Theory](ch05.md#category-theory) 25 | - [In Summary](ch05.md#in-summary) 26 | - [Exercises](ch05.md#exercises) 27 | - [Chapter 06: Example Application](ch06.md) 28 | - [Declarative Coding](ch06.md#declarative-coding) 29 | - [A Flickr of Functional Programming](ch06.md#a-flickr-of-functional-programming) 30 | - [A Principled Refactor](ch06.md#a-principled-refactor) 31 | - [In Summary](ch06.md#in-summary) 32 | - [07 장: 힌들리-밀너와 나](ch07-kr.md) 33 | - [당신은 무슨 타입인가요?](ch07-kr.md#당신은-무슨-타입인가요) 34 | - [비밀 이야기](ch07-kr.md#비밀-이야기) 35 | - [가능성을 높이기](ch07-kr.md#가능성을-높이기) 36 | - [공짜로 정리 얻기](ch07-kr.md#공짜로-정리-얻기) 37 | - [제약](ch07-kr.md#제약) 38 | - [요약](ch07-kr.md#요약) 39 | - [Chapter 08: Tupperware](ch08.md) 40 | - [The Mighty Container](ch08.md#the-mighty-container) 41 | - [My First Functor](ch08.md#my-first-functor) 42 | - [Schrödinger's Maybe](ch08.md#schrödingers-maybe) 43 | - [Use Cases](ch08.md#use-cases) 44 | - [Releasing the Value](ch08.md#releasing-the-value) 45 | - [Pure Error Handling](ch08.md#pure-error-handling) 46 | - [Old McDonald Had Effects...](ch08.md#old-mcdonald-had-effects) 47 | - [Asynchronous Tasks](ch08.md#asynchronous-tasks) 48 | - [A Spot of Theory](ch08.md#a-spot-of-theory) 49 | - [In Summary](ch08.md#in-summary) 50 | - [Exercises](ch08.md#exercises) 51 | - [Chapter 09: Monadic Onions](ch09.md) 52 | - [Pointy Functor Factory](ch09.md#pointy-functor-factory) 53 | - [Mixing Metaphors](ch09.md#mixing-metaphors) 54 | - [My Chain Hits My Chest](ch09.md#my-chain-hits-my-chest) 55 | - [Power Trip](ch09.md#power-trip) 56 | - [Theory](ch09.md#theory) 57 | - [In Summary](ch09.md#in-summary) 58 | - [Exercises](ch09.md#exercises) 59 | - [Chapter 10: Applicative Functors](ch10.md) 60 | - [Applying Applicatives](ch10.md#applying-applicatives) 61 | - [Ships in Bottles](ch10.md#ships-in-bottles) 62 | - [Coordination Motivation](ch10.md#coordination-motivation) 63 | - [Bro, Do You Even Lift?](ch10.md#bro-do-you-even-lift) 64 | - [Operators](ch10.md#operators) 65 | - [Free Can Openers](ch10.md#free-can-openers) 66 | - [Laws](ch10.md#laws) 67 | - [In Summary](ch10.md#in-summary) 68 | - [Exercises](ch10.md#exercises) 69 | - [Chapter 11: Transform Again, Naturally](ch11.md) 70 | - [Curse This Nest](ch11.md#curse-this-nest) 71 | - [A Situational Comedy](ch11.md#a-situational-comedy) 72 | - [All Natural](ch11.md#all-natural) 73 | - [Principled Type Conversions](ch11.md#principled-type-conversions) 74 | - [Feature Envy](ch11.md#feature-envy) 75 | - [Isomorphic JavaScript](ch11.md#isomorphic-javascript) 76 | - [A Broader Definition](ch11.md#a-broader-definition) 77 | - [One Nesting Solution](ch11.md#one-nesting-solution) 78 | - [In Summary](ch11.md#in-summary) 79 | - [Exercises](ch11.md#exercises) 80 | - [Chapter 12: Traversing the Stone](ch12.md) 81 | - [Types n' Types](ch12.md#types-n-types) 82 | - [Type Feng Shui](ch12.md#type-feng-shui) 83 | - [Effect Assortment](ch12.md#effect-assortment) 84 | - [Waltz of the Types](ch12.md#waltz-of-the-types) 85 | - [No Law and Order](ch12.md#no-law-and-order) 86 | - [In Summary](ch12.md#in-summary) 87 | - [Exercises](ch12.md#exercises) 88 | - [Appendix A: Essential Functions Support](appendix_a.md) 89 | - [always](appendix_a.md#always) 90 | - [compose](appendix_a.md#compose) 91 | - [curry](appendix_a.md#curry) 92 | - [either](appendix_a.md#either) 93 | - [identity](appendix_a.md#identity) 94 | - [inspect](appendix_a.md#inspect) 95 | - [left](appendix_a.md#left) 96 | - [liftA\*](appendix_a.md#lifta) 97 | - [maybe](appendix_a.md#maybe) 98 | - [nothing](appendix_a.md#nothing) 99 | - [reject](appendix_a.md#reject) 100 | - [Appendix B: Algebraic Structures Support](appendix_b.md) 101 | - [Compose](appendix_b.md#compose) 102 | - [Either](appendix_b.md#either) 103 | - [Identity](appendix_b.md#identity) 104 | - [IO](appendix_b.md#io) 105 | - [List](appendix_b.md#list) 106 | - [Map](appendix_b.md#map) 107 | - [Maybe](appendix_b.md#maybe) 108 | - [Task](appendix_b.md#task) 109 | - [Appendix C: Pointfree Utilities](appendix_c.md) 110 | - [add](appendix_c.md#add) 111 | - [append](appendix_c.md#append) 112 | - [chain](appendix_c.md#chain) 113 | - [concat](appendix_c.md#concat) 114 | - [eq](appendix_c.md#eq) 115 | - [filter](appendix_c.md#filter) 116 | - [flip](appendix_c.md#flip) 117 | - [forEach](appendix_c.md#foreach) 118 | - [head](appendix_c.md#head) 119 | - [intercalate](appendix_c.md#intercalate) 120 | - [join](appendix_c.md#join) 121 | - [last](appendix_c.md#last) 122 | - [map](appendix_c.md#map) 123 | - [match](appendix_c.md#match) 124 | - [prop](appendix_c.md#prop) 125 | - [reduce](appendix_c.md#reduce) 126 | - [replace](appendix_c.md#replace) 127 | - [safeHead](appendix_c.md#safehead) 128 | - [safeLast](appendix_c.md#safelast) 129 | - [safeProp](appendix_c.md#safeprop) 130 | - [sequence](appendix_c.md#sequence) 131 | - [sortBy](appendix_c.md#sortby) 132 | - [split](appendix_c.md#split) 133 | - [take](appendix_c.md#take) 134 | - [toLowerCase](appendix_c.md#tolowercase) 135 | - [toString](appendix_c.md#tostring) 136 | - [toUpperCase](appendix_c.md#touppercase) 137 | - [traverse](appendix_c.md#traverse) 138 | - [unsafePerformIO](appendix_c.md#unsafeperformio) 139 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | ## FAQ 2 | 3 | - [Why are snippets written sometimes with semicolons and sometimes 4 | without?](#why-are-snippets-written-sometimes-with-semicolons-and-sometimes-without) 5 | - [Aren't external libraries like _ (ramda) or $ (jquery) making calls impure?](#arent-external-libraries-like-_-ramda-or--jquery-making-calls-impure) 6 | - [What is the meaning of `f a` in signature?](#what-is-the-meaning-of-f-a-in-signature) 7 | - [Is there any "real world" examples available?](#is-there-any-real-world-examples-available) 8 | - [Why does the book uses ES5? Is any ES6 version available?](#why-does-the-book-uses-es5-is-any-es6-version-available) 9 | - [What the heck is that reduce function about?](#what-the-heck-is-that-reduce-function-about) 10 | - [Wouldn't you use a simplified English rather than the current style?](#wouldnt-you-use-a-simplified-english-rather-than-the-current-style) 11 | - [What is Either? What is Future? What is Task?](#what-is-either-what-is-future-what-is-task) 12 | - [Where do map, filter, compose ... methods come from?](#where-do-map-filter-compose--methods-come-from) 13 | 14 | ### Why are snippets written sometimes with semicolons and sometimes without? 15 | 16 | > see [#6] 17 | 18 | There are two schools in JavaScript, people who use them, and people who don't. We've made the 19 | choice here to use them, and now, we strive to be consistent with that choice. If some are 20 | missing, please let us know and we will take care of the oversight. 21 | 22 | ### Aren't external libraries like _ (ramda) or $ (jquery) making calls impure? 23 | 24 | > see [#50] 25 | 26 | Those dependencies are available as if they were in the global context, part of the language. 27 | So, no, calls can still be considered as pure. 28 | For further reading, have a look at [this article about 29 | CoEffects](http://tomasp.net/blog/2014/why-coeffects-matter/) 30 | 31 | ### What is the meaning of `f a` in signature? 32 | 33 | > see [#62] 34 | 35 | In a signature, like: 36 | 37 | `map :: Functor f => (a -> b) -> f a -> f b` 38 | 39 | `f` refers to a `functor` that could be, for instance, Maybe or IO. Thus, the signature abstracts 40 | the choice of that functor by using a type variable which basically means that any functor 41 | might be used where `f` stands as long as all `f` are of the same type (if the first `f a` in 42 | the signature represents a `Maybe a`, then the second one **cannot refer to** an `IO b` but 43 | should be a `Maybe b`. For instance: 44 | 45 | ```javascript 46 | let maybeString = Maybe.of("Patate") 47 | let f = function (x) { return x.length } 48 | let maybeNumber = map(f, maybeString) // Maybe(6) 49 | 50 | // With the following 'refined' signature: 51 | // map :: (string -> number) -> Maybe string -> Maybe number 52 | ``` 53 | 54 | ### Is there any "real world" examples available? 55 | 56 | > see [#77], [#192] 57 | 58 | Should you haven't reached it yet, you may have a look at the [Chapter 59 | 6](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch06.md) which presents a 60 | simple flickr application. 61 | Other examples are likely to come later on. By the way, feel free to share with us your 62 | experience! 63 | 64 | ### Why does the book uses ES5? Is any ES6 version available? 65 | 66 | > see [#83], [#235] 67 | 68 | The book aims at being widely accessible. It started before ES6 went out, and now, as the new 69 | standard is being more and more accepted, we are considering making two separated editions with 70 | ES5 and ES6. Members of the community are already working on the ES6 version (have a look to 71 | [#235] for more information). 72 | 73 | ### What the heck is that reduce function about? 74 | 75 | > see [#109] 76 | 77 | Reduce, accumulate, fold, inject are all usual functions in functional programming used to 78 | combine the elements of a data structure successively. You might have a look at [this 79 | talk](https://www.youtube.com/watch?v=JZSoPZUoR58&ab_channel=NewCircleTraining) to get some 80 | more insights about the reduce function. 81 | 82 | ### Wouldn't you use a simplified English rather than the current style? 83 | 84 | > see [#176] 85 | 86 | The book is written in its own style which contributes to make it consistent as a whole. If 87 | you're not familiar with English, see it as good training. Nevertheless, should you need help 88 | to understand the meaning sometimes, there are now [several 89 | translations](https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/TRANSLATIONS.md) 90 | available that could probably help you. 91 | 92 | ### What is Either? What is Future? What is Task? 93 | 94 | > see [#194] 95 | 96 | We introduce all of those structures throughout the book. Therefore, you won't find any use of a 97 | structure that hasn't previously be defined. Do not hesitate to read again some old parts if 98 | you ever feel uncomfortable with those types. 99 | A glossary/vade mecum will come at the end to synthesize all these notions. 100 | 101 | ### Where do map, filter, compose ... methods come from? 102 | 103 | > see [#198] 104 | 105 | Most of the time, those methods are defined in specific vendor libraries such as `ramda` or 106 | `underscore`. You should also have a look at [Appendix A](./appendix_a.md), 107 | [Appendix B](./appendix_b.md) and [Appendix C](./appendix_c.md) in which we define 108 | several implementations used for the exercises. Those functions are really 109 | common in functional programming and even though their implementations may vary a bit, their 110 | meanings remain fairly consistent between libraries. 111 | 112 | 113 | [#6]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/6 114 | [#50]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/50 115 | [#62]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/62 116 | [#77]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/77 117 | [#83]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/83 118 | [#109]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/109 119 | [#176]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/176 120 | [#192]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/192 121 | [#194]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/194 122 | [#198]: https://github.com/MostlyAdequate/mostly-adequate-guide/issues/198 123 | [#235]: https://github.com/MostlyAdequate/mostly-adequate-guide/pull/235 124 | -------------------------------------------------------------------------------- /ch02.md: -------------------------------------------------------------------------------- 1 | # Chapter 02: First Class Functions 2 | 3 | ## A Quick Review 4 | When we say functions are "first class", we mean they are just like everyone else... so in other words a normal class. We can treat functions like any other data type and there is nothing particularly special about them - they may be stored in arrays, passed around as function parameters, assigned to variables, and what have you. 5 | 6 | That is JavaScript 101, but worth mentioning since a quick code search on github will reveal the collective evasion, or perhaps widespread ignorance of this concept. Shall we go for a feigned example? We shall. 7 | 8 | ```js 9 | const hi = name => `Hi ${name}`; 10 | const greeting = name => hi(name); 11 | ``` 12 | 13 | Here, the function wrapper around `hi` in `greeting` is completely redundant. Why? Because functions are *callable* in JavaScript. When `hi` has the `()` at the end it will run and return a value. When it does not, it simply returns the function stored in the variable. Just to be sure, have a look yourself: 14 | 15 | 16 | ```js 17 | hi; // name => `Hi ${name}` 18 | hi("jonas"); // "Hi jonas" 19 | ``` 20 | 21 | Since `greeting` is merely in turn calling `hi` with the very same argument, we could simply write: 22 | 23 | ```js 24 | const greeting = hi; 25 | greeting("times"); // "Hi times" 26 | ``` 27 | 28 | In other words, `hi` is already a function that expects one argument, why place another function around it that simply calls `hi` with the same bloody argument? It doesn't make any damn sense. It's like donning your heaviest parka in the dead of July just to blast the air and demand an ice lolly. 29 | 30 | It is obnoxiously verbose and, as it happens, bad practice to surround a function with another function merely to delay evaluation (we'll see why in a moment, but it has to do with maintenance) 31 | 32 | A solid understanding of this is critical before moving on, so let's examine a few more fun examples excavated from the library of npm packages. 33 | 34 | ```js 35 | // ignorant 36 | const getServerStuff = callback => ajaxCall(json => callback(json)); 37 | 38 | // enlightened 39 | const getServerStuff = ajaxCall; 40 | ``` 41 | 42 | The world is littered with ajax code exactly like this. Here is the reason both are equivalent: 43 | 44 | ```js 45 | // this line 46 | ajaxCall(json => callback(json)); 47 | 48 | // is the same as this line 49 | ajaxCall(callback); 50 | 51 | // so refactor getServerStuff 52 | const getServerStuff = callback => ajaxCall(callback); 53 | 54 | // ...which is equivalent to this 55 | const getServerStuff = ajaxCall; // <-- look mum, no ()'s 56 | ``` 57 | 58 | And that, folks, is how it is done. Once more so that we understand why I'm being so persistent. 59 | 60 | ```js 61 | const BlogController = { 62 | index(posts) { return Views.index(posts); }, 63 | show(post) { return Views.show(post); }, 64 | create(attrs) { return Db.create(attrs); }, 65 | update(post, attrs) { return Db.update(post, attrs); }, 66 | destroy(post) { return Db.destroy(post); }, 67 | }; 68 | ``` 69 | 70 | This ridiculous controller is 99% fluff. We could either rewrite it as: 71 | 72 | ```js 73 | const BlogController = { 74 | index: Views.index, 75 | show: Views.show, 76 | create: Db.create, 77 | update: Db.update, 78 | destroy: Db.destroy, 79 | }; 80 | ``` 81 | 82 | ... or scrap it altogether since it does nothing more than just bundle our Views and Db together. 83 | 84 | ## Why Favor First Class? 85 | 86 | Okay, let's get down to the reasons to favor first class functions. As we saw in the `getServerStuff` and `BlogController` examples, it's easy to add layers of indirection that provide no added value and only increase the amount of redundant code to maintain and search through. 87 | 88 | In addition, if such a needlessly wrapped function must be changed, we must also need to change our wrapper function as well. 89 | 90 | ```js 91 | httpGet('/post/2', json => renderPost(json)); 92 | ``` 93 | 94 | If `httpGet` were to change to send a possible `err`, we would need to go back and change the "glue". 95 | 96 | ```js 97 | // go back to every httpGet call in the application and explicitly pass err along. 98 | httpGet('/post/2', (json, err) => renderPost(json, err)); 99 | ``` 100 | 101 | Had we written it as a first class function, much less would need to change: 102 | 103 | ```js 104 | // renderPost is called from within httpGet with however many arguments it wants 105 | httpGet('/post/2', renderPost); 106 | ``` 107 | 108 | Besides the removal of unnecessary functions, we must name and reference arguments. Names are a bit of an issue, you see. We have potential misnomers - especially as the codebase ages and requirements change. 109 | 110 | Having multiple names for the same concept is a common source of confusion in projects. There is also the issue of generic code. For instance, these two functions do exactly the same thing, but one feels infinitely more general and reusable: 111 | 112 | ```js 113 | // specific to our current blog 114 | const validArticles = articles => 115 | articles.filter(article => article !== null && article !== undefined), 116 | 117 | // vastly more relevant for future projects 118 | const compact = xs => xs.filter(x => x !== null && x !== undefined); 119 | ``` 120 | 121 | By using specific naming, we've seemingly tied ourselves to specific data (in this case `articles`). This happens quite a bit and is a source of much reinvention. 122 | 123 | I must mention that, just like with Object-Oriented code, you must be aware of `this` coming to bite you in the jugular. If an underlying function uses `this` and we call it first class, we are subject to this leaky abstraction's wrath. 124 | 125 | ```js 126 | const fs = require('fs'); 127 | 128 | // scary 129 | fs.readFile('freaky_friday.txt', Db.save); 130 | 131 | // less so 132 | fs.readFile('freaky_friday.txt', Db.save.bind(Db)); 133 | ``` 134 | 135 | Having been bound to itself, the `Db` is free to access its prototypical garbage code. I avoid using `this` like a dirty nappy. There's really no need when writing functional code. However, when interfacing with other libraries, you might have to acquiesce to the mad world around us. 136 | 137 | Some will argue that `this` is necessary for optimizing speed. If you are the micro-optimization sort, please close this book. If you cannot get your money back, perhaps you can exchange it for something more fiddly. 138 | 139 | And with that, we're ready to move on. 140 | 141 | [Chapter 03: Pure Happiness with Pure Functions](ch03.md) 142 | -------------------------------------------------------------------------------- /ch04-kr.md: -------------------------------------------------------------------------------- 1 | # 04 장: 커링 2 | 3 | ## Can't Live If Livin' Is without You 4 | 5 | 제 아버지는 사람이 그것들을 얻기 전까지 없이 살 수 있는 것들이 있다고 말씀하신 적이 있어요. 전자레인지나 스마트폰 등등이 그렇지요. 우리 중의 오래된 사람들은 인터넷 없는 삶을 사는 것을 기억할 것이에요. 저에게는 커링이 이 목록에 있습니다. 6 | 7 | 커링의 개념은 간단해요. 함수가 예상하는 인자보다 적은 인자로 함수를 부르는 것이지요. 그리고 이 때 함수는 나머지 인자를 함수를 받는 함수를 반환해요. 8 | 9 | 당신은 모든 인자를 한번에 넘겨주거나 단순히 인자를 하나씩 넘겨주는 것 중에 선택할 수 있어요. 10 | 11 | ```js 12 | const add = (x) => (y) => x + y; 13 | const increment = add(1); 14 | const addTen = add(10); 15 | 16 | increment(2); // 3 17 | addTen(2); // 12 18 | ``` 19 | 20 | 우리는 인자를 하나 받고 함수를 반환하는 `add`를 만들었어요. 이것을 호출해서 반환되는 함수는 클로져를 이용해 첫번째 인자를 기억하지요. 하지만 두 인자를 한번에 부르는 것은 좀 복잡한 일입니다. 그래서 우리는 함수를 조금 더 쉽게 만들고 호출할 수 있게 해주는 특별한 도우미 함수인 `curry`를 사용해 볼 것입니다. 21 | 22 | 재미를 위해 몇가지 커리된 함수를 만들어볼까요. 이제부터 우리는 [Appendix A - Essential Function Support](./appendix_a.md)에 정의된 `curry`를 소환할것입니다. 23 | 24 | ```js 25 | const match = curry((what, s) => s.match(what)); 26 | const replace = curry((what, replacement, s) => s.replace(what, replacement)); 27 | const filter = curry((f, xs) => xs.filter(f)); 28 | const map = curry((f, xs) => xs.map(f)); 29 | ``` 30 | 31 | 여기서 사용한 패턴은 간단하지만 주목할 점이 있습니다. 저는 의도적으로 데이터(String, Array)를 마지막 인자에 두었어요. 왜 이렇게 했는지는 차차 명확해질겁니다. 32 | 33 | (`/r/g` 문법은 **모든 'r'**을 가르키는 정규 표현식입니다. 궁금하다면 [정규 표현식](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)에 대한 글을 읽어보세요.) 34 | 35 | ```js 36 | match(/r/g, "hello world"); // [ 'r' ] 37 | 38 | const hasLetterR = match(/r/g); // x => x.match(/r/g) 39 | hasLetterR("hello world"); // [ 'r' ] 40 | hasLetterR("just j and s and t etc"); // null 41 | 42 | filter(hasLetterR, ["rock and roll", "smooth jazz"]); // ['rock and roll'] 43 | 44 | const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(x => x.match(/r/g)) 45 | removeStringsWithoutRs(["rock and roll", "smooth jazz", "drum circle"]); // ['rock and roll', 'drum circle'] 46 | 47 | const noVowels = replace(/[aeiou]/gi); // (r,x) => x.replace(/[aeiou]/ig, r) 48 | const censored = noVowels("*"); // x => x.replace(/[aeiou]/ig, '*') 49 | censored("Chocolate Rain"); // 'Ch*c*l*t* R**n' 50 | ``` 51 | 52 | 여기서 하나나 두개의 인자를 함수에 "미리" 설정하고 함수는 이 인자들을 기억하는 것을 볼 수 있어요. 53 | 54 | 저는 당신이 Mostly Adequate repository (`git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git`)를 클론하고 위의 코드를 복사한 후에 REPL에서 돌려보는 것을 추천해요. 커리를 해주는 함수는 부록에 정의된 다른 것들과 마찬가지로 `support/index.js` 모듈에 있습니다. 55 | 56 | 아니면 `npm`에 배포된 버전을 한번 보세요. 57 | 58 | ``` 59 | npm install @mostly-adequate/support 60 | ``` 61 | 62 | ## 말장난 조금 더 / 특별한 소스 63 | 64 | 커링은 여러 방면으로 유용합니다. 우리는 `hasLetterR`과 `removeStringsWithoutRs`, `censored`의 예에서 기본 함수에 몇가지 인자를 줌으로써 새로운 함수를 만드는 것을 봤어요. 65 | 66 | 또한 하나의 인자에만 작용하는 함수가 배열에 작용할 수 있도록 `map`으로 감싸는 것도 보았지요. 67 | 68 | ```js 69 | const getChildren = (x) => x.childNodes; 70 | const allTheChildren = map(getChildren); 71 | ``` 72 | 73 | 보통 함수가 예상하는 것보다 덜 인자를 넘겨주는 것을 **부분 적용(partial application)**이라고 부릅니다. 함수에 부분 적용을 하면 보일러 플레이트 코드를 많이 줄여줍니다. losash의 커리되지 않은 `map`을 위의 `allTheChildren`에 사용하는 것을 생각해봅시다. 인자의 순서가 다르다는 것을 눈여겨보세요. 74 | 75 | ```js 76 | const allTheChildren = (elements) => map(elements, getChildren); 77 | ``` 78 | 79 | 우리는 그냥 인라인에서 `map(getChildren)`으로 호출할 수 있기 때문에 보통 배열에 적용되는 함수를 따로 정의하지는 않습니다. `sort`, `filter`와 다른 고차 함수들도 마찬가지입니다(**고차 함수**는 함수를 받거나 반환하는 함수입니다.). 80 | 81 | **순수 함수**에 대해서 말할 때 그들이 1개의 입력과 1개의 출력을 가지고 있다고 말했지요. 바로 커링이 이것을 합니다. 하나의 인자를 받을 때마다 나머지 인자를 기대하는 함수를 반환합니다. 이것이 오래된 스포츠는 1대 1의 입력입니다. 82 | 83 | 출력이 다른 함수이지만 이것은 순수합니다. 우리는 한번에 한개 이상의 인자를 줄 수도 있지만 이것은 단순히 편의를 위해 여분의 `()`를 제거하는 것으로 볼 수 있어요. 84 | 85 | ## 요약 86 | 87 | 커링은 편리하고 저는 매일 커리된 함수와 함께하는 것을 즐기고 있어요. 커링은 함수형 프로그래밍을 좀 덜 장황하고 지루하게 만들어주는 도구입니다. 88 | 89 | 우리는 인자를 조금만 넘겨주면서 즉석에서 유용하고 새로운 함수를 만들 수 있어요. 또 함수가 여러 인자를 요구함에도 수학적인 함수 정의를 얻을 수 있어요. 90 | 91 | 이제 또 다른 도구인 함성(`compose`)를 만나볼까요? 92 | 93 | [Chapter 05: Coding by Composing](ch05.md) 94 | 95 | ## 연습문제 96 | 97 | #### 연습문제에 대하여 98 | 99 | Throughout the book, you might encounter an 'Exercises' section like this one. Exercises can be 100 | done directly in-browser provided you're reading from [gitbook](https://mostly-adequate.gitbooks.io/mostly-adequate-guide) (recommended). 101 | 102 | Note that, for all exercises of the book, you always have a handful of helper functions 103 | available in the global scope. Hence, anything that is defined in [Appendix A](./appendix_a.md), 104 | [Appendix B](./appendix_b.md) and [Appendix C](./appendix_c.md) is available for you! And, as 105 | if it wasn't enough, some exercises will also define functions specific to the problem 106 | they present; as a matter of fact, consider them available as well. 107 | 108 | > Hint: you can submit your solution by doing `Ctrl + Enter` in the embedded editor! 109 | 110 | #### Running Exercises on Your Machine (optional) 111 | 112 | Should you prefer to do exercises directly in files using your own editor: 113 | 114 | - clone the repository (`git clone git@github.com:MostlyAdequate/mostly-adequate-guide.git`) 115 | - go in the _exercises_ section (`cd mostly-adequate-guide/exercises`) 116 | - install the necessary plumbing using [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (`npm install`) 117 | - complete answers by modifying the files named \*exercises\_\*\* in the corresponding chapter's folder 118 | - run the correction with npm (e.g. `npm run ch04`) 119 | 120 | Unit tests will run against your answers and provide hints in case of mistake. By the by, the 121 | answers to the exercises are available in files named \*answers\_\*\*. 122 | 123 | #### Let's Practice! 124 | 125 | {% exercise %} 126 | Refactor to remove all arguments by partially applying the function. 127 | 128 | {% initial src="./exercises/ch04/exercise_a.js#L3;" %} 129 | 130 | ```js 131 | const words = (str) => split(" ", str); 132 | ``` 133 | 134 | {% solution src="./exercises/ch04/solution_a.js" %} 135 | {% validation src="./exercises/ch04/validation_a.js" %} 136 | {% context src="./exercises/support.js" %} 137 | {% endexercise %} 138 | 139 | --- 140 | 141 | {% exercise %} 142 | Refactor to remove all arguments by partially applying the functions. 143 | 144 | {% initial src="./exercises/ch04/exercise_b.js#L3;" %} 145 | 146 | ```js 147 | const filterQs = (xs) => filter((x) => match(/q/i, x), xs); 148 | ``` 149 | 150 | {% solution src="./exercises/ch04/solution_b.js" %} 151 | {% validation src="./exercises/ch04/validation_b.js" %} 152 | {% context src="./exercises/support.js" %} 153 | {% endexercise %} 154 | 155 | --- 156 | 157 | Considering the following function: 158 | 159 | ```js 160 | const keepHighest = (x, y) => (x >= y ? x : y); 161 | ``` 162 | 163 | {% exercise %} 164 | Refactor `max` to not reference any arguments using the helper function `keepHighest`. 165 | 166 | {% initial src="./exercises/ch04/exercise_c.js#L7;" %} 167 | 168 | ```js 169 | const max = (xs) => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs); 170 | ``` 171 | 172 | {% solution src="./exercises/ch04/solution_c.js" %} 173 | {% validation src="./exercises/ch04/validation_c.js" %} 174 | {% context src="./exercises/support.js" %} 175 | {% endexercise %} 176 | -------------------------------------------------------------------------------- /ch01.md: -------------------------------------------------------------------------------- 1 | # Chapter 01: What Ever Are We Doing? 2 | 3 | ## Introductions 4 | 5 | Hi there! I'm Professor Franklin Frisby. Pleased to make your acquaintance. We'll be spending some time together, as I'm supposed to teach you a bit about functional programming. But enough about me, what about you? I'm hoping that you're at least a bit familiar with the JavaScript language, have a teensy bit of Object-Oriented experience, and fancy yourself a working class programmer. You don't need to have a PhD in Entomology, you just need to know how to find and kill some bugs. 6 | 7 | I won't assume that you have any previous functional programming knowledge, because we both know what happens when you assume. I will, however, expect you to have run into some of the unfavorable situations that arise when working with mutable state, unrestricted side effects, and unprincipled design. Now that we've been properly introduced, let's get on with it. 8 | 9 | The purpose of this chapter is to give you a feel for what we're after when we write functional programs. In order to be able to understand the following chapters, we must have some idea about what makes a program *functional*. Otherwise we'll find ourselves scribbling aimlessly, avoiding objects at all costs - a clumsy endeavor indeed. We need a clear bullseye to hurl our code at, some celestial compass for when the waters get rough. 10 | 11 | Now, there are some general programming principles - various acronymic credos that guide us through the dark tunnels of any application: DRY (don't repeat yourself), YAGNI (ya ain't gonna need it), loose coupling high cohesion, the principle of least surprise, single responsibility, and so on. 12 | 13 | I won't belabor you by listing each and every guideline I've heard throughout the years... The point of the matter is that they hold up in a functional setting, although they're merely tangential to our ultimate goal. What I'd like you to get a feel for now, before we get any further, is our intention when we poke and prod at the keyboard; our functional Xanadu. 14 | 15 | 16 | 17 | ## A Brief Encounter 18 | 19 | Let's start with a touch of insanity. Here is a seagull application. When flocks conjoin they become a larger flock, and when they breed, they increase by the number of seagulls with whom they're breeding. Now, this is not intended to be good Object-Oriented code, mind you, it is here to highlight the perils of our modern, assignment based approach. Behold: 20 | 21 | ```js 22 | class Flock { 23 | constructor(n) { 24 | this.seagulls = n; 25 | } 26 | 27 | conjoin(other) { 28 | this.seagulls += other.seagulls; 29 | return this; 30 | } 31 | 32 | breed(other) { 33 | this.seagulls = this.seagulls * other.seagulls; 34 | return this; 35 | } 36 | } 37 | 38 | const flockA = new Flock(4); 39 | const flockB = new Flock(2); 40 | const flockC = new Flock(0); 41 | const result = flockA 42 | .conjoin(flockC) 43 | .breed(flockB) 44 | .conjoin(flockA.breed(flockB)) 45 | .seagulls; 46 | // 32 47 | ``` 48 | 49 | Who on earth would craft such a ghastly abomination? It is unreasonably difficult to keep track of the mutating internal state. And, good heavens, the answer is even incorrect! It should have been `16`, but `flockA` wound up permanently altered in the process. Poor `flockA`. This is anarchy in the I.T.! This is wild animal arithmetic! 50 | 51 | If you don't understand this program, it's okay, neither do I. The point to remember here is that state and mutable values are hard to follow, even in such a small example. 52 | 53 | Let's try again, this time using a more functional approach: 54 | 55 | ```js 56 | const conjoin = (flockX, flockY) => flockX + flockY; 57 | const breed = (flockX, flockY) => flockX * flockY; 58 | 59 | const flockA = 4; 60 | const flockB = 2; 61 | const flockC = 0; 62 | const result = 63 | conjoin(breed(flockB, conjoin(flockA, flockC)), breed(flockA, flockB)); 64 | // 16 65 | ``` 66 | 67 | Well, this time we got the right answer. With much less code. The function nesting is a tad confusing... (we'll remedy this situation in ch5). It's better, but let's dig a little bit deeper. There are benefits to calling a spade a spade. Had we scrutinized our custom functions more closely, we would have discovered that we're just working with simple addition (`conjoin`) and multiplication (`breed`). 68 | 69 | There's really nothing special at all about these two functions other than their names. Let's rename our custom functions to `multiply` and `add` in order to reveal their true identities. 70 | 71 | ```js 72 | const add = (x, y) => x + y; 73 | const multiply = (x, y) => x * y; 74 | 75 | const flockA = 4; 76 | const flockB = 2; 77 | const flockC = 0; 78 | const result = 79 | add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB)); 80 | // 16 81 | ``` 82 | And with that, we gain the knowledge of the ancients: 83 | 84 | ```js 85 | // associative 86 | add(add(x, y), z) === add(x, add(y, z)); 87 | 88 | // commutative 89 | add(x, y) === add(y, x); 90 | 91 | // identity 92 | add(x, 0) === x; 93 | 94 | // distributive 95 | multiply(x, add(y,z)) === add(multiply(x, y), multiply(x, z)); 96 | ``` 97 | 98 | Ah yes, those old faithful mathematical properties should come in handy. Don't worry if you didn't know them right off the top of your head. For a lot of us, it's been a while since we learned about these laws of arithmetic. Let's see if we can use these properties to simplify our little seagull program. 99 | 100 | ```js 101 | // Original line 102 | add(multiply(flockB, add(flockA, flockC)), multiply(flockA, flockB)); 103 | 104 | // Apply the identity property to remove the extra add 105 | // (add(flockA, flockC) == flockA) 106 | add(multiply(flockB, flockA), multiply(flockA, flockB)); 107 | 108 | // Apply distributive property to achieve our result 109 | multiply(flockB, add(flockA, flockA)); 110 | ``` 111 | 112 | Brilliant! We didn't have to write a lick of custom code other than our calling function. We include `add` and `multiply` definitions here for completeness, but there is really no need to write them - we surely have an `add` and `multiply` provided by some existing library. 113 | 114 | You may be thinking "how very strawman of you to put such a mathy example up front". Or "real programs are not this simple and cannot be reasoned about in such a way." I've chosen this example because most of us already know about addition and multiplication, so it's easy to see how math is very useful for us here. 115 | 116 | Don't despair - throughout this book, we'll sprinkle in some category theory, set theory, and lambda calculus and write real world examples that achieve the same elegant simplicity and results as our flock of seagulls example. You needn't be a mathematician either. It will feel natural and easy, just like you were using a "normal" framework or API. 117 | 118 | It may come as a surprise to hear that we can write full, everyday applications along the lines of the functional analog above. Programs that have sound properties. Programs that are terse, yet easy to reason about. Programs that don't reinvent the wheel at every turn. Lawlessness is good if you're a criminal, but in this book, we'll want to acknowledge and obey the laws of math. 119 | 120 | We'll want to use a theory where every piece tends to fit together so politely. We'll want to represent our specific problem in terms of generic, composable bits and then exploit their properties for our own selfish benefit. It will take a bit more discipline than the "anything goes" approach of imperative programming (we'll go over the precise definition of "imperative" later in the book, but for now consider it anything other than functional programming). The payoff of working within a principled, mathematical framework will truly astound you. 121 | 122 | We've seen a flicker of our functional northern star, but there are a few concrete concepts to grasp before we can really begin our journey. 123 | 124 | [Chapter 02: First Class Functions](ch02.md) 125 | -------------------------------------------------------------------------------- /ch07-kr.md: -------------------------------------------------------------------------------- 1 | # 07 장: 힌들리-밀너와 나 2 | 3 | ## 당신은 무슨 타입인가요? 4 | 5 | 당신이 함수형 세상에 처음 왔다면 머지않아 타입 시그니처에 깊이 빠지게 될 거에요. 타입은 서로 다른 배경을 가진 사람들이 간단명료하고 효과적으로 소통할 수 있게 해주는 메타 언어입니다. 대부분의 경우 이 장에서 다루는 "힌들리-밀너(Hindley-Milner, HM)" 체계를 이용해 타입을 표현할 것입니다. 6 | 7 | 순수 함수를 사용할 때 한국어로는 전혀 다루지 못할 것을 타입 시그니처를 통해 표현할 수 있어요. 타입 시그니처는 함수의 내밀한 비밀을 당신 귀에 속삭여 줄 것입니다. 간결한 한 줄만으로 행동과 의도를 알려줄 거에요. "공짜 정리"를 도출 할 수도 있습니다. 또 타입은 추론될 수도 있어서 명시적으로 적지 않아도 작동합니다. 아주 세밀하게 조절될 수도 있고 일반적이고 추상적으로 남을 수도 있어요. 컴파일 타임 체크에 유용할 뿐만 아니라 문서화에도 사용할 수 있고요. 타입 시그니처는 함수형 프로그래밍에서 당신이 처음에 생각하는 것보다 더 중요합니다. 8 | 9 | 자바스크립트는 동적 언어지만 이것이 타입을 전혀 고려하지 않는다는 말은 아닙니다. 우리는 문자열과 숫자, 불리언을 사용합니다. 그저 언어 차원에서 지원해주지 않기 때문에 이 정보들을 우리가 기억하고 있어야 된다는 것을 의미합니다. 그러나 걱정하지 마세요. 이미 문서화를 위해 사용하는 것처럼 주석을 통해 설명할 수 있습니다. 10 | 11 | [Flow](https://flow.org/)나 [TypeScript](https://www.typescriptlang.org/) 같이 자바스크립트에서 이용할 수 있는 타입 검사 도구들도 있어요. 이 책의 목적은 여러 FP 언어에 걸쳐 사용할 수 있는 일반적인 타입 체계를 갖추는 것입니다. 12 | 13 | ## 비밀 이야기 14 | 15 | 낡은 수학 책에서, 햐얀 논문의 광활한 바다에서, 토요일 아침의 일상적인 블로그 글에서, 그리고 소스 코드의 밑바닥에서 우리는 힌들리-밀너 타입 시그니처를 볼 수 있어요. 이 체계은 매우 단순하지만 빠르게 설명할 수 있고 이 작은 언어를 배우기 위한 연습들도 많습니다. 16 | 17 | ```js 18 | // capitalize :: String -> String 19 | const capitalize = (s) => toUpperCase(head(s)) + toLowerCase(tail(s)); 20 | 21 | capitalize("smurf"); // 'Smurf' 22 | ``` 23 | 24 | 위의 `capitalize`는 `String`을 받아서 `String`을 반환합니다. 구현은 신경쓰지 마세요. 우리가 관심있는 것은 타입입니다. 25 | 26 | HM에서 함수는 `a -> b`로 쓰여집니다. 여기서 `a`와 `b`는 어떤 타입을 나타내는 변수입니다. 따라서 `capitalize`의 명세는 "`String`에서 `String`으로 가는 함수"라고 읽을 수 있어요. 또는 `String`을 입력으로 받고 `String`을 출력으로 반환한다고 볼 수도 있습니다. 27 | 28 | 또 다른 함수의 명세도 봅시다. 29 | 30 | ```js 31 | // strLength :: String -> Number 32 | const strLength = (s) => s.length; 33 | 34 | // join :: String -> [String] -> String 35 | const join = curry((what, xs) => xs.join(what)); 36 | 37 | // match :: Regex -> String -> [String] 38 | const match = curry((reg, s) => s.match(reg)); 39 | 40 | // replace :: Regex -> String -> String -> String 41 | const replace = curry((reg, sub, s) => s.replace(reg, sub)); 42 | ``` 43 | 44 | `strLength`도 앞의 예제와 비슷합니다. `String`을 받아서 `Number`를 반환해요. 45 | 46 | 다른 것들은 처음 볼 때 헷갈릴 수 있어요. 완전히 이해하지 않아도 마지막 타입을 언제나 반환 타입으로 볼 수 있어요. `match`는 `Regex`과 `String`을 받아서 `[String]`을 반환한다고 해석할 수 있습니다. 하지만 제가 따로 설명하고 싶은 재미있는 현상도 있어요. 47 | 48 | `match`의 명세를 다음과 같이 짝지을 수도 있어요. 49 | 50 | ```js 51 | // match :: Regex -> (String -> [String]) 52 | const match = curry((reg, s) => s.match(reg)); 53 | ``` 54 | 55 | 뒷부분을 괄호로 묶는것은 더 많은 정보를 드러내 줍니다. 이제 이 함수는 `Regex`를 받아서 `String`에서 `[String]`으로 가는 함수를 반환하는 것으로 보입니다. 커링 덕분에 사실 이 함수에게 `Regex`를 주면 우리는 `String`을 받기를 기다리는 함수를 얻습니다. 당연히 이런식으로 생각할 필요는 없어요. 하지만 왜 마지막 타입을 반환 타입으로 볼 수 있는지 이해하고 있으면 좋습니다. 56 | 57 | ```js 58 | // match :: Regex -> (String -> [String]) 59 | // onHoliday :: String -> [String] 60 | const onHoliday = match(/holiday/gi); 61 | ``` 62 | 63 | 각각의 인자들이 타입 가장 앞의 하나를 차지합니다. `onHoliday`는 `Regex`를 받은 `match`입니다. 64 | 65 | ```js 66 | // replace :: Regex -> (String -> (String -> String)) 67 | const replace = curry((reg, sub, s) => s.replace(reg, sub)); 68 | ``` 69 | 70 | `replace`의 모든 괄호처럼 추가적으로 명시하면 약간 지저분하고 불필요하게 보이기 때문에 그냥 생략하겠습니다. 모든 인자들을 한번에 주기로 생각한다면 `replace`를 단순히 `Regex`와 두개의 `String`을 받아서 `String`을 반환하는 함수로 볼 수 있답니다. 71 | 72 | 예제를 몇가지 더 볼까요. 73 | 74 | ```js 75 | // id :: a -> a 76 | const id = (x) => x; 77 | 78 | // map :: (a -> b) -> [a] -> [b] 79 | const map = curry((f, xs) => xs.map(f)); 80 | ``` 81 | 82 | `id` 함수는 `a` 타입을 받아서 같은 타입 `a`의 어떤 것을 반환합니다. 우리는 코드 안에서 처럼 타입을 나타내는 변수를 사용할 수 있어요. `a`나 `b` 같은 변수명은 관습이지만 당신이 원하는 다른 임의의 이름으로 바꿀 수 있어요. 만약 같은 변수라면 같은 타입입니다. 이것은 중요한 규칙이기 때문에 다시 한번 살펴보겠습니다. `a -> b`는 어떤 타입 `a`를 어떤 타입 `b`로 보내지만 `a -> a`는 서로 같은 타입임을 뜻합니다. 예를들어 `id`는 `String -> String`이나 `Number -> Number`가 될 수 있지만 `String -> Bool`은 될 수 없어요. 83 | 84 | `map`도 마찬가지로 타입 변수를 사용하지만 이번에는 `a`와 같은 타입일수도, 다른 타입일 수도 있는 `b`를 도입합니다. `map`을 어떤 타입 `a`를 받아서 같거나 다를 수 있는 타입 `b`를 반환하는 함수를 받은 다음에 `a`의 배열을 받아서 `b`의 배열을 반환하는 함수로 볼 수 있어요. 85 | 86 | 이런 타입 시그니처의 풍부한 표현력의 아름다움에 압도당했기를 바랍니다. 함수가 무엇을 하는지 정확히 나태내 있어요. 이것은 `a`에서 `b`로 가는 함수와 `a`의 배열을 받아서 `b`의 배열을 주지요. 유일하게 합리적인 작동 방식은 이 함수를 `a`에 각각 적용하는 거지요. 나머지는 어불성설일 거에요. 87 | 88 | 타입과 그 구현에 대해 생각할 수 있게 되면 함수형 세상 깊이 갈 수 있게 될거에요. 논문, 블로그, 문서 등을 좀 더 쉽게 이해하게 될 뿐만 아니라 타입 그 자체만으로 기능에 대해 알 수 있을 것입니다. 수월하게 읽으려면 연습을 해야겠지만 포기하지 않는다면 복잡한 매뉴얼을 읽지 않고도 많은 정보를 알 수 있을 거에요. 89 | 90 | 다음 예제를 스스로 해석할 수 있는지 한 번 연습해 보세요. 91 | 92 | ```js 93 | // head :: [a] -> a 94 | const head = (xs) => xs[0]; 95 | 96 | // filter :: (a -> Bool) -> [a] -> [a] 97 | const filter = curry((f, xs) => xs.filter(f)); 98 | 99 | // reduce :: ((b, a) -> b) -> b -> [a] -> b 100 | const reduce = curry((f, x, xs) => xs.reduce(f, x)); 101 | ``` 102 | 103 | 아마 이 중에서 `reduce`가 가장 표현력이 있을 거에요. 약간 어려우니까 이해하기 어렵더라도 당신이 멍청하다고 생각하지 마세요. 궁금하신 분들을 위해 제가 설명해 보겠지만 당신 스스로 해보는 것이 더 도움이 될겁니다. 104 | 105 | 흠.. 해 볼 수 있는 곳까지 해볼까요. 타입 시그니처를 보면 첫번째 인자는 `b`와 `a`를 받는 함수입니다. 이 `a`와 `b`는 어디서 가져올까요? 명세의 다음 인자는 `b`와 `a`의 배열이네요. `b`와 각각의 `a`가 입력된다고 생각해 볼 수 있을 거에요. 그리고 함수의 반환값이 `b`니까 함수로 입력된 마지막 값이 결과값이라고 생각할 수 있을 거에요. `reduce`가 무엇을 하는지 알게되면 위의 설명이 정확하다는 것을 알 수 있을 거에요. 106 | 107 | ## 가능성을 높이기 108 | 109 | 타입 변수가 도입되면 **[parametricity](https://en.wikipedia.org/wiki/Parametricity)**라고 불리는 특이한 속성이 등장합니다. 이 속성은 함수가 **모든 타입에 걸쳐 일관된 방식으로 작동한다고** 말합니다. 한번 알아볼까요. 110 | 111 | ```js 112 | // head :: [a] -> a 113 | ``` 114 | 115 | `head`를 보면 `[a]`를 받아서 `a`를 반환합니다. 구체적 타입인 `array`외의 다른 정보가 없어서 함수가 배열에 적용된다는 것 말고 다른 말은 못합니다. `a`에 대해 아무것도 모르는데 `a`를 가지고 무엇을 할 수 있겠어요? 다른 말로는 `a`가 **특정한** 타입이 될 수 없고 따라서 **어떤** 타입도 될 수 있습니다. 그리고 이것은 함수가 생각할 수 있는 **모든** 타입에 대해 일관적으로 작동한다는 것을 말합니다. 이것이 **parametricity**가 말하는 전부입니다. 구현을 추측해볼 때 합리적인 가정은 배열의 첫번째 또는 마지막 또는 임의의 원소를 반환한다는 것 밖에 없습니다. 그리고 `head`라는 이름이 귀뜸해주네요. 116 | 117 | 다른 것을 한번 봅시다. 118 | 119 | ```js 120 | // reverse :: [a] -> [a] 121 | ``` 122 | 123 | 타입 시그니처만으로 `reverse`가 무엇을 할 수 있을까요? 다시 한번 특정한 `a`에 대해서만 적용되지는 않습니다. 또 `a`를 다른 타입으로 바꿀 꾸거나 새로운 타입 `b`를 도입할 수도 없습니다. 정렬할 수 있을까요? 흠.. 가능한 모든 타입에 대해 어떻게 정렬하는지 충분한 알 수 없기 때문에 하지 못합니다. 재배열을 할 수 있을까요? 할 수 있을 것 같군요. 하지만 마찬가지로 예상 가능하게 작동해야합니다. 중요한 점은 함수의 행동이 polymorphic type에 의해 많이 제약된다는 것입니다. 124 | 125 | 이렇게 가능성을 줄이는 것은 [Hoogle](https://hoogle.haskell.org/) 같이 타입 시그니처로 함수를 검색하는 검색 엔진이 가능하게 합니다. 실제로 타입이 내포하는 정보는 매우 강력합니다. 126 | 127 | ## 공짜로 정리 얻기 128 | 129 | 이런 종류의 사고 방식은 가능한 기능을 추측하는 것 이외에도 **공짜 정리**를 줄 수 있습니다. [Wadler의 관련 논문](http://ttic.uchicago.edu/~dreyer/course/papers/wadler.pdf)에 나오는 몇가지 예제를 볼까요. 130 | 131 | ```js 132 | // head :: [a] -> a 133 | compose(f, head) === compose(head, map(f)); 134 | 135 | // filter :: (a -> Bool) -> [a] -> [a] 136 | compose(map(f), filter(compose(p, f))) === compose(filter(p), map(f)); 137 | ``` 138 | 139 | 이 정리는 어떤 코드도 없이 타입에서 바로 도출됩니다. 첫번째 것은 배열에서 `head`를 얻고 함수 `f`를 적용한 것이 먼저 `map(f)`를 모든 원소에 적용한 후에 `head`를 얻는 것 과 같고 우연하지만 훨신 더 빠르다는 것을 의미합니다. 140 | 141 | 아마 단지 상식이 아니냐고 생각할 수도 있겠지요. 그러나 제가 마지막으로 확인했을 때 컴퓨터는 상식이 없더군요. 사실 컴퓨터는 이런 종류의 코드 최적화를 자동으로 할 수 있는 형식적인 방법을 가지고 있어요. 수학은 직관을 형식을 갖추어 표현할 수 있게 해주고 이것은 견고한 컴퓨터 논리 영역 사이에서 도움이 됩니다. 142 | 143 | `filter`도 마찬가지입니다. `f`와 `p`를 합성해서 필터한 후 `map`을 통해서 `f`를 적용한 것(`filter`는 원소를 바꾸지 않는다는 것을 명심하세요. 타입 시그니처가 `a`가 변하지 않는다고 말합니다)과 `f`를 매핑한 후 `p`를 이용해 필터를 한 것은 언제나 같다고 말하고 있습니다. 144 | 145 | 두개의 예시일 뿐이지만 이런 사고 방식을 모든 polymorphic type에도 적용할 수 있고 언제나 올바른 답을 줄 것입니다. 자바스크립트에는 다시 쓰기 규칙을 선언하게 해 주는 몇가지 도구들이 있어요. `compose` 함수를 통해 직접 할 수도 있어요. 결과는 얻기 쉽지만 가능성은 끝이 없어요. 146 | 147 | ## 제약 148 | 149 | 마지막으로 말할 것은 타입을 어떤 인터페이스에 제한할 수 있다는 것입니다. 150 | 151 | ```js 152 | // sort :: Ord a => [a] -> [a] 153 | ``` 154 | 155 | 뚱뚱한 화살표 왼쪽에 있는 것은 `a`가 반드시 `Ord`라고 말하고 있습니다. 또 다른 말로 `a`는 반드시 `Ord` 인터페이스를 구현해야합니다. `Ord`는 무엇이고 어디서 왔을까요? 타입이 있는 언어에서 `Ord`는 순서를 매길수 있는 인터페이스를 지칭합니다. 이것은 우리에게 `a`와 `sort` 함수가 무엇을 하는지 뿐 만 아니라 도메인을 제약하기도 합니다. 우리는 이런 인터페이스 선언을 **타입 제약(type constraints)**라고 부릅니다. 156 | 157 | ```js 158 | // assertEqual :: (Eq a, Show a) => a -> a -> Assertion 159 | ``` 160 | 161 | 여기 `Eq`와 `Show` 제약이 있습니다. 이는 `a`의 동일성을 검사할 수 있고 만약 둘이 다르면 그 차이를 출력할수 있음을 보장합니다. 162 | 163 | 우리는 이후의 장에서 다른 제약들도 보게 될 것이고 좀 더 형태를 갖추게 될 거에요. 164 | 165 | ## 요약 166 | 167 | 힌들리-밀너 타입 시그니처는 함수형 세상에서 일반적으로 쓰입니다. 읽고 쓰기 쉽지만 타입 시그니처 만으로 프로그램을 이해하는 기술을 통달하기에는 시간이 걸립니다. 우리는 이제 모든 줄마다 타입 시그니처를 적을거에요. 168 | 169 | [08 장: Tupperware](ch08-kr.md) 170 | -------------------------------------------------------------------------------- /ch04.md: -------------------------------------------------------------------------------- 1 | # Chapter 04: Currying 2 | 3 | ## Can't Live If Livin' Is without You 4 | My Dad once explained how there are certain things one can live without until one acquires them. A microwave is one such thing. Smart phones, another. The older folks among us will remember a fulfilling life sans internet. For me, currying is on this list. 5 | 6 | The concept is simple: You can call a function with fewer arguments than it expects. It returns a function that takes the remaining arguments. 7 | 8 | You can choose to call it all at once or simply feed in each argument piecemeal. 9 | 10 | ```js 11 | const add = x => y => x + y; 12 | const increment = add(1); 13 | const addTen = add(10); 14 | 15 | increment(2); // 3 16 | addTen(2); // 12 17 | ``` 18 | 19 | Here we've made a function `add` that takes one argument and returns a function. By calling it, the returned function remembers the first argument from then on via the closure. Calling it with both arguments all at once is a bit of a pain, however, so we can use a special helper function called `curry` to make defining and calling functions like this easier. 20 | 21 | Let's set up a few curried functions for our enjoyment. From now on, we'll summon our `curry` 22 | function defined in the [Appendix A - Essential Function Support](./appendix_a.md). 23 | 24 | ```js 25 | const match = curry((what, s) => s.match(what)); 26 | const replace = curry((what, replacement, s) => s.replace(what, replacement)); 27 | const filter = curry((f, xs) => xs.filter(f)); 28 | const map = curry((f, xs) => xs.map(f)); 29 | ``` 30 | 31 | The pattern I've followed is a simple, but important one. I've strategically positioned the data we're operating on (String, Array) as the last argument. It will become clear as to why upon use. 32 | 33 | (The syntax `/r/g` is a regular expression that means _match every letter 'r'_. Read [more about regular expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) if you like.) 34 | 35 | ```js 36 | match(/r/g, 'hello world'); // [ 'r' ] 37 | 38 | const hasLetterR = match(/r/g); // x => x.match(/r/g) 39 | hasLetterR('hello world'); // [ 'r' ] 40 | hasLetterR('just j and s and t etc'); // null 41 | 42 | filter(hasLetterR, ['rock and roll', 'smooth jazz']); // ['rock and roll'] 43 | 44 | const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(x => x.match(/r/g)) 45 | removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']); // ['rock and roll', 'drum circle'] 46 | 47 | const noVowels = replace(/[aeiou]/ig); // (r,x) => x.replace(/[aeiou]/ig, r) 48 | const censored = noVowels('*'); // x => x.replace(/[aeiou]/ig, '*') 49 | censored('Chocolate Rain'); // 'Ch*c*l*t* R**n' 50 | ``` 51 | 52 | What's demonstrated here is the ability to "pre-load" a function with an argument or two in order to receive a new function that remembers those arguments. 53 | 54 | I encourage you to clone the Mostly Adequate repository (`git clone 55 | https://github.com/MostlyAdequate/mostly-adequate-guide.git`), copy the code above and have a 56 | go at it in the REPL. The curry function, as well as actually anything defined in the appendixes, 57 | are available in the `support/index.js` module. 58 | 59 | Alternatively, have a look at a published version on `npm`: 60 | 61 | ``` 62 | npm install @mostly-adequate/support 63 | ``` 64 | 65 | ## More Than a Pun / Special Sauce 66 | 67 | Currying is useful for many things. We can make new functions just by giving our base functions some arguments as seen in `hasLetterR`, `removeStringsWithoutRs`, and `censored`. 68 | 69 | We also have the ability to transform any function that works on single elements into a function that works on arrays simply by wrapping it with `map`: 70 | 71 | ```js 72 | const getChildren = x => x.childNodes; 73 | const allTheChildren = map(getChildren); 74 | ``` 75 | 76 | Giving a function fewer arguments than it expects is typically called *partial application*. Partially applying a function can remove a lot of boiler plate code. Consider what the above `allTheChildren` function would be with the uncurried `map` from lodash (note the arguments are in a different order): 77 | 78 | ```js 79 | const allTheChildren = elements => map(elements, getChildren); 80 | ``` 81 | 82 | We typically don't define functions that work on arrays, because we can just call `map(getChildren)` inline. Same with `sort`, `filter`, and other higher order functions (a *higher order function* is a function that takes or returns a function). 83 | 84 | When we spoke about *pure functions*, we said they take 1 input to 1 output. Currying does exactly this: each single argument returns a new function expecting the remaining arguments. That, old sport, is 1 input to 1 output. 85 | 86 | No matter if the output is another function - it qualifies as pure. We do allow more than one argument at a time, but this is seen as merely removing the extra `()`'s for convenience. 87 | 88 | 89 | ## In Summary 90 | 91 | Currying is handy and I very much enjoy working with curried functions on a daily basis. It is a tool for the belt that makes functional programming less verbose and tedious. 92 | 93 | We can make new, useful functions on the fly simply by passing in a few arguments and as a bonus, we've retained the mathematical function definition despite multiple arguments. 94 | 95 | Let's acquire another essential tool called `compose`. 96 | 97 | [Chapter 05: Coding by Composing](ch05.md) 98 | 99 | ## Exercises 100 | 101 | #### Note about Exercises 102 | 103 | Throughout the book, you might encounter an 'Exercises' section like this one. Exercises can be 104 | done directly in-browser provided you're reading from [gitbook](https://mostly-adequate.gitbooks.io/mostly-adequate-guide) (recommended). 105 | 106 | Note that, for all exercises of the book, you always have a handful of helper functions 107 | available in the global scope. Hence, anything that is defined in [Appendix A](./appendix_a.md), 108 | [Appendix B](./appendix_b.md) and [Appendix C](./appendix_c.md) is available for you! And, as 109 | if it wasn't enough, some exercises will also define functions specific to the problem 110 | they present; as a matter of fact, consider them available as well. 111 | 112 | > Hint: you can submit your solution by doing `Ctrl + Enter` in the embedded editor! 113 | 114 | #### Running Exercises on Your Machine (optional) 115 | 116 | Should you prefer to do exercises directly in files using your own editor: 117 | 118 | - clone the repository (`git clone git@github.com:MostlyAdequate/mostly-adequate-guide.git`) 119 | - go in the *exercises* section (`cd mostly-adequate-guide/exercises`) 120 | - install the necessary plumbing using [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (`npm install`) 121 | - complete answers by modifying the files named *exercises\_\** in the corresponding chapter's folder 122 | - run the correction with npm (e.g. `npm run ch04`) 123 | 124 | Unit tests will run against your answers and provide hints in case of mistake. By the by, the 125 | answers to the exercises are available in files named *answers\_\**. 126 | 127 | #### Let's Practice! 128 | 129 | {% exercise %} 130 | Refactor to remove all arguments by partially applying the function. 131 | 132 | {% initial src="./exercises/ch04/exercise_a.js#L3;" %} 133 | ```js 134 | const words = str => split(' ', str); 135 | ``` 136 | 137 | {% solution src="./exercises/ch04/solution_a.js" %} 138 | {% validation src="./exercises/ch04/validation_a.js" %} 139 | {% context src="./exercises/support.js" %} 140 | {% endexercise %} 141 | 142 | 143 | --- 144 | 145 | 146 | {% exercise %} 147 | Refactor to remove all arguments by partially applying the functions. 148 | 149 | {% initial src="./exercises/ch04/exercise_b.js#L3;" %} 150 | ```js 151 | const filterQs = xs => filter(x => match(/q/i, x), xs); 152 | ``` 153 | 154 | {% solution src="./exercises/ch04/solution_b.js" %} 155 | {% validation src="./exercises/ch04/validation_b.js" %} 156 | {% context src="./exercises/support.js" %} 157 | {% endexercise %} 158 | 159 | 160 | --- 161 | 162 | 163 | Considering the following function: 164 | 165 | ```js 166 | const keepHighest = (x, y) => (x >= y ? x : y); 167 | ``` 168 | 169 | {% exercise %} 170 | Refactor `max` to not reference any arguments using the helper function `keepHighest`. 171 | 172 | {% initial src="./exercises/ch04/exercise_c.js#L7;" %} 173 | ```js 174 | const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs); 175 | ``` 176 | 177 | {% solution src="./exercises/ch04/solution_c.js" %} 178 | {% validation src="./exercises/ch04/validation_c.js" %} 179 | {% context src="./exercises/support.js" %} 180 | {% endexercise %} 181 | -------------------------------------------------------------------------------- /appendix_b.md: -------------------------------------------------------------------------------- 1 | # Appendix B: Algebraic Structures Support 2 | 3 | In this appendix, you'll find some basic JavaScript implementations of various algebraic 4 | structures described in the book. Keep in mind that these implementations may not be the fastest or the 5 | most efficient implementation out there; they *solely serve an educational purpose*. 6 | 7 | In order to find structures that are more production-ready, have a peek at [folktale](http://folktale.origamitower.com/) 8 | or [fantasy-land](https://github.com/fantasyland). 9 | 10 | Note that some methods also refer to functions defined in the [Appendix A](./appendix_a.md) 11 | 12 | ## Compose 13 | 14 | ```js 15 | const createCompose = curry((F, G) => class Compose { 16 | constructor(x) { 17 | this.$value = x; 18 | } 19 | 20 | [util.inspect.custom]() { 21 | return `Compose(${inspect(this.$value)})`; 22 | } 23 | 24 | // ----- Pointed (Compose F G) 25 | static of(x) { 26 | return new Compose(F(G(x))); 27 | } 28 | 29 | // ----- Functor (Compose F G) 30 | map(fn) { 31 | return new Compose(this.$value.map(x => x.map(fn))); 32 | } 33 | 34 | // ----- Applicative (Compose F G) 35 | ap(f) { 36 | return f.map(this.$value); 37 | } 38 | }); 39 | ``` 40 | 41 | 42 | ## Either 43 | 44 | ```js 45 | class Either { 46 | constructor(x) { 47 | this.$value = x; 48 | } 49 | 50 | // ----- Pointed (Either a) 51 | static of(x) { 52 | return new Right(x); 53 | } 54 | } 55 | ``` 56 | 57 | #### Left 58 | 59 | ```js 60 | class Left extends Either { 61 | get isLeft() { 62 | return true; 63 | } 64 | 65 | get isRight() { 66 | return false; 67 | } 68 | 69 | static of(x) { 70 | throw new Error('`of` called on class Left (value) instead of Either (type)'); 71 | } 72 | 73 | [util.inspect.custom]() { 74 | return `Left(${inspect(this.$value)})`; 75 | } 76 | 77 | // ----- Functor (Either a) 78 | map() { 79 | return this; 80 | } 81 | 82 | // ----- Applicative (Either a) 83 | ap() { 84 | return this; 85 | } 86 | 87 | // ----- Monad (Either a) 88 | chain() { 89 | return this; 90 | } 91 | 92 | join() { 93 | return this; 94 | } 95 | 96 | // ----- Traversable (Either a) 97 | sequence(of) { 98 | return of(this); 99 | } 100 | 101 | traverse(of, fn) { 102 | return of(this); 103 | } 104 | } 105 | ``` 106 | 107 | #### Right 108 | 109 | ```js 110 | class Right extends Either { 111 | get isLeft() { 112 | return false; 113 | } 114 | 115 | get isRight() { 116 | return true; 117 | } 118 | 119 | static of(x) { 120 | throw new Error('`of` called on class Right (value) instead of Either (type)'); 121 | } 122 | 123 | [util.inspect.custom]() { 124 | return `Right(${inspect(this.$value)})`; 125 | } 126 | 127 | // ----- Functor (Either a) 128 | map(fn) { 129 | return Either.of(fn(this.$value)); 130 | } 131 | 132 | // ----- Applicative (Either a) 133 | ap(f) { 134 | return f.map(this.$value); 135 | } 136 | 137 | // ----- Monad (Either a) 138 | chain(fn) { 139 | return fn(this.$value); 140 | } 141 | 142 | join() { 143 | return this.$value; 144 | } 145 | 146 | // ----- Traversable (Either a) 147 | sequence(of) { 148 | return this.traverse(of, identity); 149 | } 150 | 151 | traverse(of, fn) { 152 | return fn(this.$value).map(Either.of); 153 | } 154 | } 155 | ``` 156 | 157 | ## Identity 158 | 159 | ```js 160 | class Identity { 161 | constructor(x) { 162 | this.$value = x; 163 | } 164 | 165 | [util.inspect.custom]() { 166 | return `Identity(${inspect(this.$value)})`; 167 | } 168 | 169 | // ----- Pointed Identity 170 | static of(x) { 171 | return new Identity(x); 172 | } 173 | 174 | // ----- Functor Identity 175 | map(fn) { 176 | return Identity.of(fn(this.$value)); 177 | } 178 | 179 | // ----- Applicative Identity 180 | ap(f) { 181 | return f.map(this.$value); 182 | } 183 | 184 | // ----- Monad Identity 185 | chain(fn) { 186 | return this.map(fn).join(); 187 | } 188 | 189 | join() { 190 | return this.$value; 191 | } 192 | 193 | // ----- Traversable Identity 194 | sequence(of) { 195 | return this.traverse(of, identity); 196 | } 197 | 198 | traverse(of, fn) { 199 | return fn(this.$value).map(Identity.of); 200 | } 201 | } 202 | ``` 203 | 204 | ## IO 205 | 206 | ```js 207 | class IO { 208 | constructor(fn) { 209 | this.unsafePerformIO = fn; 210 | } 211 | 212 | [util.inspect.custom]() { 213 | return 'IO(?)'; 214 | } 215 | 216 | // ----- Pointed IO 217 | static of(x) { 218 | return new IO(() => x); 219 | } 220 | 221 | // ----- Functor IO 222 | map(fn) { 223 | return new IO(compose(fn, this.unsafePerformIO)); 224 | } 225 | 226 | // ----- Applicative IO 227 | ap(f) { 228 | return this.chain(fn => f.map(fn)); 229 | } 230 | 231 | // ----- Monad IO 232 | chain(fn) { 233 | return this.map(fn).join(); 234 | } 235 | 236 | join() { 237 | return new IO(() => this.unsafePerformIO().unsafePerformIO()); 238 | } 239 | } 240 | ``` 241 | 242 | ## List 243 | 244 | ```js 245 | class List { 246 | constructor(xs) { 247 | this.$value = xs; 248 | } 249 | 250 | [util.inspect.custom]() { 251 | return `List(${inspect(this.$value)})`; 252 | } 253 | 254 | concat(x) { 255 | return new List(this.$value.concat(x)); 256 | } 257 | 258 | // ----- Pointed List 259 | static of(x) { 260 | return new List([x]); 261 | } 262 | 263 | // ----- Functor List 264 | map(fn) { 265 | return new List(this.$value.map(fn)); 266 | } 267 | 268 | // ----- Traversable List 269 | sequence(of) { 270 | return this.traverse(of, identity); 271 | } 272 | 273 | traverse(of, fn) { 274 | return this.$value.reduce( 275 | (f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f), 276 | of(new List([])), 277 | ); 278 | } 279 | } 280 | ``` 281 | 282 | 283 | ## Map 284 | 285 | ```js 286 | class Map { 287 | constructor(x) { 288 | this.$value = x; 289 | } 290 | 291 | [util.inspect.custom]() { 292 | return `Map(${inspect(this.$value)})`; 293 | } 294 | 295 | insert(k, v) { 296 | const singleton = {}; 297 | singleton[k] = v; 298 | return Map.of(Object.assign({}, this.$value, singleton)); 299 | } 300 | 301 | reduceWithKeys(fn, zero) { 302 | return Object.keys(this.$value) 303 | .reduce((acc, k) => fn(acc, this.$value[k], k), zero); 304 | } 305 | 306 | // ----- Functor (Map a) 307 | map(fn) { 308 | return this.reduceWithKeys( 309 | (m, v, k) => m.insert(k, fn(v)), 310 | new Map({}), 311 | ); 312 | } 313 | 314 | // ----- Traversable (Map a) 315 | sequence(of) { 316 | return this.traverse(of, identity); 317 | } 318 | 319 | traverse(of, fn) { 320 | return this.reduceWithKeys( 321 | (f, a, k) => fn(a).map(b => m => m.insert(k, b)).ap(f), 322 | of(new Map({})), 323 | ); 324 | } 325 | } 326 | ``` 327 | 328 | 329 | ## Maybe 330 | 331 | > Note that `Maybe` could also be defined in a similar fashion as we did for `Either` with two 332 | > child classes `Just` and `Nothing`. This is simply a different flavor. 333 | 334 | ```js 335 | class Maybe { 336 | get isNothing() { 337 | return this.$value === null || this.$value === undefined; 338 | } 339 | 340 | get isJust() { 341 | return !this.isNothing; 342 | } 343 | 344 | constructor(x) { 345 | this.$value = x; 346 | } 347 | 348 | [util.inspect.custom]() { 349 | return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`; 350 | } 351 | 352 | // ----- Pointed Maybe 353 | static of(x) { 354 | return new Maybe(x); 355 | } 356 | 357 | // ----- Functor Maybe 358 | map(fn) { 359 | return this.isNothing ? this : Maybe.of(fn(this.$value)); 360 | } 361 | 362 | // ----- Applicative Maybe 363 | ap(f) { 364 | return this.isNothing ? this : f.map(this.$value); 365 | } 366 | 367 | // ----- Monad Maybe 368 | chain(fn) { 369 | return this.map(fn).join(); 370 | } 371 | 372 | join() { 373 | return this.isNothing ? this : this.$value; 374 | } 375 | 376 | // ----- Traversable Maybe 377 | sequence(of) { 378 | return this.traverse(of, identity); 379 | } 380 | 381 | traverse(of, fn) { 382 | return this.isNothing ? of(this) : fn(this.$value).map(Maybe.of); 383 | } 384 | } 385 | ``` 386 | 387 | ## Task 388 | 389 | ```js 390 | class Task { 391 | constructor(fork) { 392 | this.fork = fork; 393 | } 394 | 395 | [util.inspect.custom]() { 396 | return 'Task(?)'; 397 | } 398 | 399 | static rejected(x) { 400 | return new Task((reject, _) => reject(x)); 401 | } 402 | 403 | // ----- Pointed (Task a) 404 | static of(x) { 405 | return new Task((_, resolve) => resolve(x)); 406 | } 407 | 408 | // ----- Functor (Task a) 409 | map(fn) { 410 | return new Task((reject, resolve) => this.fork(reject, compose(resolve, fn))); 411 | } 412 | 413 | // ----- Applicative (Task a) 414 | ap(f) { 415 | return this.chain(fn => f.map(fn)); 416 | } 417 | 418 | // ----- Monad (Task a) 419 | chain(fn) { 420 | return new Task((reject, resolve) => this.fork(reject, x => fn(x).fork(reject, resolve))); 421 | } 422 | 423 | join() { 424 | return this.chain(identity); 425 | } 426 | } 427 | ``` 428 | -------------------------------------------------------------------------------- /ch03-kr.md: -------------------------------------------------------------------------------- 1 | # 03 장: 순수 함수와 순수한 기쁨을 2 | 3 | ## 한번 더 순수함에 대하여 4 | 5 | 순수 함수라는 개념을 한번 확실하게 할 필요가 있습니다. 6 | 7 | > 순수함수란 부작용(side effed)가 없고 같은 입력에 대해 항상 같은 출력을 반환하는 함수이다. 8 | 9 | `slice`와 `splice`를 비교해보죠. 이 둘은 정확하게 같은 기능을 합니다. 하지만 많이 다른 방식으로 작동하지요. 우리는 `slice`가 같은 입력에 대해 항상 같은 출력을 주기 때문에 이 함수를 **순수**하다고 합니다. 하지만 `splice`는 배열을 집어 삼키고 내뱉을 때 그 값을 영원히 바꾸어 관측가능한 효과를 만듭니다. 10 | 11 | ```js 12 | const xs = [1, 2, 3, 4, 5]; 13 | 14 | // 순수 15 | xs.slice(0, 3); // [1,2,3] 16 | 17 | xs.slice(0, 3); // [1,2,3] 18 | 19 | xs.slice(0, 3); // [1,2,3] 20 | 21 | // 비순수 22 | xs.splice(0, 3); // [1,2,3] 23 | 24 | xs.splice(0, 3); // [4,5] 25 | 26 | xs.splice(0, 3); // [] 27 | ``` 28 | 29 | 함수형 프로그래밍에서는 `splice` 같이 데이터를 **바꾸어** 통제하기 어려운 함수를 싫어합니다. `splice`처럼 뒤죽박죽인 함수가 아니라 매번 똑같은 결과를 신뢰성 있게 반환하는 함수를 만들기 위해 노력하고 있기 때문이죠. 30 | 31 | 다른 예를 볼까요? 32 | 33 | ```js 34 | // 비순수 35 | let minimum = 21; 36 | const checkAge = (age) => age >= minimum; 37 | 38 | // 순수 39 | const checkAge = (age) => { 40 | const minimum = 21; 41 | return age >= minimum; 42 | }; 43 | ``` 44 | 45 | 순수하지 않은 부분에서 `checkAge`는 결과값을 결정할 때 변하는 값(mutable variable)인 `minimum`에 의존합니다. 다시 말해서, 함수가 시스템의의 상태에 의존하고 이는 외부 환경에 대해 생각해야하는 [인지 부담](https://en.wikipedia.org/wiki/Cognitive_load)을 증가시키는 좋지 않은 결과를 낳습니다.. 46 | 47 | 이 예에서 그렇게 보이지는 않을지도 모르지만 상태에 의존하는 것은 시스템의 복잡성을 높이는 가장 큰 원인 중의 하나입니다(http://curtclifton.net/papers/MoseleyMarks06a.pdf). `checkAge`는 입력이 아니라 외부요소에 의존해서 다른 결과를 반환할 수도 있어요. 이것은 순수성에 위배될 뿐만 아니라 소프트웨어에 대해 생각할 때마다 우리의 머리를 쥐어짜게 만듭니다. 48 | 49 | 반면, 순수한 형태에서 함수는 완전히 자기 충족적(self sufficient)입니다. 변수 `minimum`가 불변하게 만들기 때문에 순수성을 해치지 않죠. 이렇게 하기 위해서 우리는 객체를 얼려야(freeze)합니다. 50 | 51 | ```js 52 | const immutableState = Object.freeze({ minimum: 21 }); 53 | ``` 54 | 55 | ## 부수효과는 ...를 포함할 수 있어요 56 | 57 | 직관을 향상시키기 위해 "부작용"에 대해 좀 더 들여다봅시다. 그래서 **순수함수**의 정의에 있는, 의심할 여지 없이 사악한 **부수효과**란 무엇인가요? 우리는 계산을 하는 동안 결과를 만들기 위한 것이 아닌 모든 것을 **효과**라고 부를 것입니다. 58 | 59 | 효과에 내재된 나쁜 것은 없기 때문에 우리는 이후의 장에서 효과를 계속 사용할 것입니다. 부적적인 것이 내포된 것은 **부수** 쪽입니다. 물만있다고 벌레가 자라지는 않습니다. **흐르지 않는** 물이 벌레들을 만들지요. 장담컨대 **부수**효과는 당신의 프로그램에서의 번식지입니다. 60 | 61 | > **부수효과**는 계산을 하는 동안 시스템의 상태를 바꾸거나 외부 환경과 **관측 가능한 상호작용**을 하는 것입니다. 62 | 63 | 부수 효과는 다음과 같은 것이 있습니다. 64 | 65 | - 파일 시스템을 바꾸기 66 | - 데이터베이스에 레코드를 삽입하기 67 | - http 호출을 하기 68 | - 변경(mutations) 69 | - 화면에 출력하기 / 로깅 70 | - 유저에게 인풋을 받기 71 | - DOM을 쿼리하기 72 | - 시스템 상태에 접근하기 73 | 74 | 그리도 이것 말고도 많지요. 함수 밖 세상과 상호작용하는 어떠한 것도 부수효과입니다. 아마 이런 것들 없이 쓸모있는 프로그래을 만들 수 있을 지 의심이 갈거에요. 함수형 프로그래밍의 철학은 부수효과가 잘못된 행동을 야기하는 첫번째 원인이라고 말합니다. 75 | 76 | 오히려 부수효과를 사용하는 것을 금지하지는 않고 그들을 통제받는 환경에 담아서 실행합니다. 이후에 펑터나 모나드를 다루게 되면서 어떻게 할 수 있는 지 배우게 되겠지만 일단 지금은 이런 음흉한 함수들을 순수한 함수에서 분리하는 것을 연습해봅시다. 77 | 78 | 부수효과는 함수가 **순수**하지 않게 만듭니다. 정의상 순수 함수는 같은 입력에 대해 같은 출력을 반환하는데 만약 지역적인 함수의 밖과 관련되면 이것을 보장할 수 없다는 것은 일리가 있습니다. 79 | 80 | 이제 왜 우리가 같은 입력에 같은 출력을 주장하는지 좀 더 자세히 볼까요? 교복을 단정하게 입고 중학교 2학년 수학을 보러 갑시다. 81 | 82 | ## 중학교 2학년 수학 83 | 84 | mathisfun.com 에서: 85 | 86 | > 함수는 값 사이의 특별한 관계입니다: 87 | > 각각의 입력은 딱 하나의 출력을 주지요. 88 | 89 | 다시 말해서, 함수는 단순히 두 값 사이의 관계입니다. 입력과 출력이요. 각각의 입력이 딱 하나의 출력을 가지지만 출력은 각 입력에 대해 유일할 필요는 없어요. 아래 그림은 `x`에서 `y`로 가는 유효한 함수를 보여줍니다. 90 | 91 | function sets(https://www.mathsisfun.com/sets/function.html) 92 | 93 | 반대로, 아래 그림은 입력 `5`를 여러 출력과 연결하기 때문에 함수가 **아닙니다.** 94 | 95 | relation not function(https://www.mathsisfun.com/sets/function.html) 96 | 97 | 함수는 (입력, 출력) 쌍으로 이루어진 집합으로 생각할 수도 있어요. `[(1,2), (3,6), (5,10)]` (이 함수는 입력을 두배하네요) 98 | 99 | 아니면 표로 나타낼 수도 있죠. 100 | 101 |
입력 출력
1 2
2 4
3 6
102 | 103 | 또는 `x`를 입력, `y`를 출력으로 하는 그래프로도 나타낼 수 있어요. 104 | 105 | function graph 106 | 107 | 상세한 구현이 없어도 입력이 출력을 가르키기만 한다면 됩니다. 함수는 그저 입력과 출력 사이의 단순한 매핑이기 때문에 객체 리터럴로 적고 `()`대신 `[]`로 실행할 수도 있지요 108 | 109 | ```js 110 | const toLowerCase = { 111 | A: "a", 112 | B: "b", 113 | C: "c", 114 | D: "d", 115 | E: "e", 116 | F: "f", 117 | }; 118 | toLowerCase["C"]; // 'c' 119 | 120 | const isPrime = { 121 | 1: false, 122 | 2: true, 123 | 3: true, 124 | 4: false, 125 | 5: true, 126 | 6: false, 127 | }; 128 | isPrime[3]; // true 129 | ``` 130 | 131 | 당연하겠지만 당신은 손으로 적는 대신 계산을 하고 싶을 수도 있어요. 하지만 이건 함수를 보는 또 다른 관점입니다. ("인자가 여러개인 함수는 어떻하나요"라는 질문이 생길 수도 있어요. 사실 이 경우를 수학적인 언어로 생각하는 것은 조금 불편해요. 지금은 입력들을 하나의 배열이나 그냥 `arguments` 객체로 생각하세요. **커링**에 대해 배울 때 함수의 수학적인 정의를 바로 모델링할 수 있을 거에요.) 132 | 133 | 한가지 극적인 폭로를 하자면, 순수 함수는 수학적인 함수이고 이것이 함수형 프로그래밍의 모든 것이에요. 이 작은 천사들로 프로그래밍을 하는 것은 많은 장점이 있습니다. 이제 왜 우리가 순수성을 지키기 위한 기나긴 여정을 떠나려고 하는지에 대한 이유를 살펴볼까요? 134 | 135 | ## 순수성의 예 136 | 137 | ### 캐시 가능함 138 | 139 | 처음 온 분들을 위해, 순수 함수는 언제나 입력으로 캐시를 할 수 있어요. 이것은 보통 memoization라는 기술을 사용해서 구현하지요. 140 | 141 | ```js 142 | const squareNumber = memoize((x) => x * x); 143 | 144 | squareNumber(4); // 16 145 | 146 | squareNumber(4); // 16, returns cache for input 4 147 | 148 | squareNumber(5); // 25 149 | 150 | squareNumber(5); // 25, returns cache for input 5 151 | ``` 152 | 153 | 여기 단순화된 구현이 있어요. 이것 말고도 좀 더 견고한 버전의 구현들도 많이 있습니다. 154 | 155 | ```js 156 | const memoize = (f) => { 157 | const cache = {}; 158 | 159 | return (...args) => { 160 | const argStr = JSON.stringify(args); 161 | cache[argStr] = cache[argStr] || f(...args); 162 | return cache[argStr]; 163 | }; 164 | }; 165 | ``` 166 | 167 | 계산을 미룸으로써 비순수 함수를 순수함수로 바꿀 수 있다는 것을 주목할만 합니다. 168 | 169 | ```js 170 | const pureHttpCall = memoize((url, params) => () => $.getJSON(url, params)); 171 | ``` 172 | 173 | 재밌는 점은 여기서 실제로 http 호출을 하는 것은 아니라는 거에요. 대신 호출되었을 때 호출을 하는 함수를 반환하지요. `pureHttpCall` 함수는 같은 입력에 대해 항상 같은 출력을 반환하기 때문에 순수합니다. 주어진 `url`과 `params`에 대해 http 호출을 하는 함수를 반환하니까요. 174 | 175 | `memoize` 함수는 http 호출의 결과를 캐시하는 대신 생성된 함수를 캐시하지만 잘 작동하긴 합니다. 176 | 177 | 지금은 그다지 유용하지 않지만 곧 이것을 유용하게 사용하게 해주는 몇가지 트릭을 배울거에요. 중요한 점은 아무리 파괴적으로 보여도 모든 함수를 캐싱할 수 있다는 것입니다. 178 | 179 | ### 휴대성 / 자기 문서화 180 | 181 | 순수 함수는 완전히 자급자족합니다. 함수가 필요로하는 모든 것은 은쟁반 위에 담겨서 함수에게 전해지지요. 잠깐 고민해볼까요... 어떻게 이게 장점이 될까요? 함수의 의존성이 명시하면 코드를 처음 보는 사람들이 함수를 쉽게 이해할 수 있습니다. - 그 아래에서 어떠한 이상한 일도 일어나지 않아요. 182 | 183 | ```js 184 | // impure 185 | const signUp = (attrs) => { 186 | const user = saveUser(attrs); 187 | welcomeUser(user); 188 | }; 189 | 190 | // pure 191 | const signUp = (Db, Email, attrs) => () => { 192 | const user = saveUser(Db, attrs); 193 | welcomeUser(Email, user); 194 | }; 195 | ``` 196 | 197 | 이 예에서는 순수함수는 의존성에 대해 정직하게 말고 있고 자신이 무엇을 하는지 정확히 말해줍니다. 함수의 타입만으로도 이 함수가 `Db`와 `Email`, `attrs`를 사용한다는 것을 알 수 있어요. 198 | 199 | 우리는 단순히 계산을 미루지 않고 함수를 순수하게 만드는 법을 배울것이지만 요점은 순수한 형태가 음흉한 비순수한 형태보다 정보를 더 많이 준다는 것을 명확히 해야합니다. 200 | 201 | 또 우리는 의존성을 인자를 통해 "주입"하거나 넘겨주는것을 강요받고 있다는 점도 주목할만 합니다. 이것은 데이터베이스나 메일 클라이언트, 아니면 나머지 당신이 가진 것을 파라미터화함으로써 프로그램을 더 유연하게 만들어주지요(걱정하지는 마세요, 들리는 것보다 좀 더 쉽게 하는 방법을 곧 배울거에요). 만약 다른 Db를 사용해야 한다면 우리는 그저 그 Db로 함수를 호출하면 됩니다. 또 새 어플리케이션에서 이 신뢰할 수 있는 함수를 재사용하고 싶다면 그저 그 때 사용하고 있는 `Db`나 `Email`을 주면 됩니다. 202 | 203 | 자바스크립트 환경에서 휴대성이란 함수를 직렬화하고 소켓을 통해 보내는 것을 의미할 수도 있어요. 또는 우리의 모든 앱 코드를 웹워커에서 돌리는 것을 의미할 수도 있지요. 휴대성은 매우 강력한 특징입니다. 204 | 205 | 명령형 프로그램의 "일반적인" 메소드와 프로시져는 상태, 의존성, 사용가능한 효과를 통해 바깥 환경과 밀접하게 연관되어 있지만 순수함수는 우리가 원하는 곳 어디서든 실행할 수 있어요. 206 | 207 | 언제 마지막으로 메소드를 새 프로그램으로 복사했었나요? 얼랭(Erlang)의 제작자인 Jow Armstrong이 한 말을 저는 좋아합니다. "객체지향 언어의 문제점은 그들이 그들 주위에 있는 모든 암시적인 환경을 가지고 있다는 점입니다. 당신은 바나나를 원했지만 바나나를 들고있는 고릴라를 얻게 됩니다. 그리고 정글 전체도요.." 208 | 209 | ### 테스트 가능함 210 | 211 | 다음으로 순수함수가 테스트를 훨씬 쉽게 만들어준다는 것을 알 수 있을 거에요. "실제" 결제 게이트웨이를 모킹하거나 매번 외부 환경의 상태를 설정한 후 테스트를 실행하면서 세계의 상태를 검사 할 필요가 없어요. 그저 함수에게 입력을 주고 결과물을 확인하면 됩니다. 212 | 213 | 사실, 함수형 커퓨니티에서는 입력을 자동으로 만들고 출력이 갖출 속성에 대해 설정하는 새로운 테스트 도구를 개척하고 있습니다. 이 책의 범위를 벗어나지만 **Quickcheck**라는 툴을 한번 찾아보고 시도해 보는 것을 권장합니다. 이 툴은 순수한 함수 환경을 위해 제작되었어요. 214 | 215 | ### 합리성 216 | 217 | 많은 사람들이 순수 함수와 함께 할 때 얻는 가장 큰 이득이 **참조 투명성**이라고 믿습니다. 어떤 코드 조각이 프로그램 행동에 변경 없이 그것의 평가결과와 대체할 수 있을 때 그것을 참조 투명하다고 말합니다. 218 | 219 | 순수함수는 부작용을 가지지 않기 때문에 결과물을 통해서만 프로그램의 행동에 영향을 줄 수 있어요. 더욱이, 결과값은 입력된 값에만 의존하기 때문에 순수 함수는 언제나 참조 투명성을 지닙니다. 한번 예를 볼까요? 220 | 221 | ```js 222 | const { Map } = require("immutable"); 223 | 224 | // Aliases: p = player, a = attacker, t = target 225 | const jobe = Map({ name: "Jobe", hp: 20, team: "red" }); 226 | const michael = Map({ name: "Michael", hp: 20, team: "green" }); 227 | const decrementHP = (p) => p.set("hp", p.get("hp") - 1); 228 | const isSameTeam = (p1, p2) => p1.get("team") === p2.get("team"); 229 | const punch = (a, t) => (isSameTeam(a, t) ? t : decrementHP(t)); 230 | 231 | punch(jobe, michael); // Map({name:'Michael', hp:19, team: 'green'}) 232 | ``` 233 | 234 | `decrementHP`와 `isSameTeam`, `punch`는 모두 순수하고 따라서 참조 투명합니다. 이제 **equational reasoning**이란 기술을 사용할 수 있어요. 이것은 코드를 보면서 "같은 것을 같은 것으로" 치환하는 방법을 말해요. 프로그램의 이상한 계산을 염두하지 않고 손수 코드를 계산하는 것과 약간 비슷합니다. 이 코드에 참조 투명성을 적용해볼까요? 235 | `decrementHP`, `isSameTeam` and `punch` are all pure and therefore referentially transparent. We can use a technique called _equational reasoning_ wherein one substitutes "equals for equals" to reason about code. It's a bit like manually evaluating the code without taking into account the quirks of programmatic evaluation. Using referential transparency, let's play with this code a bit. 236 | 237 | 먼저, `isSameTeam` 함수를 인라인시켜 봅시다. 238 | 239 | ```js 240 | const punch = (a, t) => (a.get("team") === t.get("team") ? t : decrementHP(t)); 241 | ``` 242 | 243 | 우리 데이터는 불변하기 때문에 그냥 팀들 그들의 실제 값으로 바꿀 수 있어요. 244 | 245 | ```js 246 | const punch = (a, t) => ("red" === "green" ? t : decrementHP(t)); 247 | ``` 248 | 249 | 그리고 비교분의 결과가 거짓이라는 것도 알 수 있기 때문에 전체 if 분기를 제거할 수 있어요. 250 | 251 | ```js 252 | const punch = (a, t) => decrementHP(t); 253 | ``` 254 | 255 | `decrementHP`를 한번 더 인라인화 하면 이 상황에서 punch는 `hp`를 1 깎는 호출이 됩니다. 256 | 257 | ```js 258 | const punch = (a, t) => t.set("hp", t.get("hp") - 1); 259 | ``` 260 | 261 | 이런식으로 코드를 다룰 수 있는 것은 일반적인 리팩토링을 하거나 코드를 이해할 때 큰 도움이 됩니다. 사실 우리는 갈매기 무리 프로그램을 리팩토링 할 때 이 방법을 사용했었어요. 덧셈과 곱셈에 마구(harness)를 채울 때 equational reasoning을 사용했지요. 또 우리는 이 기술을 이 책에 걸쳐서 사용하게 될 거에요. 262 | 263 | ### 병렬 코드 264 | 265 | 마침내, 마지막 일격입니다. 우리는 순수함수라면 무엇이든 병렬적으로 실행할 수 있어요. 순수함수는 공유 메모리에 접근할 필요가 없고 부수효과가 없기 때문에 부수효과때문에 생기는 race condition이 없기 때문이죠. 266 | 267 | 이것은 쓰레드를 사용하는 서버 사이드 js 환경과 웹 워커를 사용하는 브라우저 모두 가능합니다. 비록 현재의 개발 문화는 비순수 함수를 다루는 복잡성을 피하기 위해 이것을 피하지만요. 268 | 269 | ## 요약 270 | 271 | 우리는 순수 함수가 무엇인지, 그리고 왜 함수형 프로그래머로써 우리가 그들이 아주 멋지다고(cat's evening wear) 믿는지 봤습니다. 지금부터 우리는 모든 함수들을 순수한 방법으로 적으려고 애를 쓸 것입니다. 우리는 우리를 도와줄 몇가지 도구들을 더 필요하지만 순수하지 않은 함수을 나머지 순수한 코드와 격리시키려고 노력할 것입니다. 272 | 273 | 추가적인 도구 없이 순수 함수로 프로그램을 만드는 것은 좀 귀찮아요. 모든 인자들을 한꺼번에 넘겨주면서 데이터를 저글링해야하고요, 효과는 말할 것도 없이 상태 조차도 사용할 수 없어요. 어떻게 이런 마조히스틱 프로그램을 만들 수 있을까요? 이제 새로운 도구인 커링을 볼 준비가 되셨나요? 274 | 275 | [04 장: 커링](ch04-kr.md) 276 | -------------------------------------------------------------------------------- /ch06.md: -------------------------------------------------------------------------------- 1 | # Chapter 06: Example Application 2 | 3 | ## Declarative Coding 4 | 5 | We are going to switch our mindset. From here on out, we'll stop telling the computer how to do its job and instead write a specification of what we'd like as a result. I'm sure you'll find it much less stressful than trying to micromanage everything all the time. 6 | 7 | Declarative, as opposed to imperative, means that we will write expressions, as opposed to step by step instructions. 8 | 9 | Think of SQL. There is no "first do this, then do that". There is one expression that specifies what we'd like from the database. We don't decide how to do the work, it does. When the database is upgraded and the SQL engine optimized, we don't have to change our query. This is because there are many ways to interpret our specification and achieve the same result. 10 | 11 | For some folks, myself included, it's hard to grasp the concept of declarative coding at first so let's point out a few examples to get a feel for it. 12 | 13 | ```js 14 | // imperative 15 | const makes = []; 16 | for (let i = 0; i < cars.length; i += 1) { 17 | makes.push(cars[i].make); 18 | } 19 | 20 | // declarative 21 | const makes = cars.map(car => car.make); 22 | ``` 23 | 24 | The imperative loop must first instantiate the array. The interpreter must evaluate this statement before moving on. Then it directly iterates through the list of cars, manually increasing a counter and showing its bits and pieces to us in a vulgar display of explicit iteration. 25 | 26 | The `map` version is one expression. It does not require any order of evaluation. There is much freedom here for how the map function iterates and how the returned array may be assembled. It specifies *what*, not *how*. Thus, it wears the shiny declarative sash. 27 | 28 | In addition to being clearer and more concise, the map function may be optimized at will and our precious application code needn't change. 29 | 30 | For those of you who are thinking "Yes, but it's much faster to do the imperative loop", I suggest you educate yourself on how the JIT optimizes your code. Here's a [terrific video that may shed some light](https://www.youtube.com/watch?v=g0ek4vV7nEA) 31 | 32 | Here is another example. 33 | 34 | ```js 35 | // imperative 36 | const authenticate = (form) => { 37 | const user = toUser(form); 38 | return logIn(user); 39 | }; 40 | 41 | // declarative 42 | const authenticate = compose(logIn, toUser); 43 | ``` 44 | 45 | Though there's nothing necessarily wrong with the imperative version, there is still an encoded step-by-step evaluation baked in. The `compose` expression simply states a fact: Authentication is the composition of `toUser` and `logIn`. Again, this leaves wiggle room for support code changes and results in our application code being a high level specification. 46 | 47 | In the example above, the order of evaluation is specified (`toUser` must be called before `logIn`), but there are many scenarios where the order is not important, and this is easily specified with declarative coding (more on this later). 48 | 49 | Because we don't have to encode the order of evaluation, declarative coding lends itself to parallel computing. This coupled with pure functions is why FP is a good option for the parallel future - we don't really need to do anything special to achieve parallel/concurrent systems. 50 | 51 | ## A Flickr of Functional Programming 52 | 53 | We will now build an example application in a declarative, composable way. We'll still cheat and use side effects for now, but we'll keep them minimal and separate from our pure codebase. We are going to build a browser widget that sucks in flickr images and displays them. Let's start by scaffolding the app. Here's the html: 54 | 55 | 56 | ```html 57 | 58 | 59 | 60 | 61 | Flickr App 62 | 63 | 64 |
65 | 66 | 67 | 68 | 69 | ``` 70 | 71 | And here's the main.js skeleton: 72 | 73 | ```js 74 | const CDN = s => `https://cdnjs.cloudflare.com/ajax/libs/${s}`; 75 | const ramda = CDN('ramda/0.21.0/ramda.min'); 76 | const jquery = CDN('jquery/3.0.0-rc1/jquery.min'); 77 | 78 | requirejs.config({ paths: { ramda, jquery } }); 79 | requirejs(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => { 80 | // app goes here 81 | }); 82 | ``` 83 | 84 | We're pulling in [ramda](https://ramdajs.com) instead of lodash or some other utility library. It includes `compose`, `curry`, and more. I've used requirejs, which may seem like overkill, but we'll be using it throughout the book and consistency is key. 85 | 86 | Now that that's out of the way, on to the spec. Our app will do 4 things. 87 | 88 | 1. Construct a url for our particular search term 89 | 2. Make the flickr api call 90 | 3. Transform the resulting json into html images 91 | 4. Place them on the screen 92 | 93 | There are 2 impure actions mentioned above. Do you see them? Those bits about getting data from the flickr api and placing it on the screen. Let's define those first so we can quarantine them. Also, I'll add our nice `trace` function for easy debugging. 94 | 95 | ```js 96 | const Impure = { 97 | getJSON: curry((callback, url) => $.getJSON(url, callback)), 98 | setHtml: curry((sel, html) => $(sel).html(html)), 99 | trace: curry((tag, x) => { console.log(tag, x); return x; }), 100 | }; 101 | ``` 102 | 103 | Here we've simply wrapped jQuery's methods to be curried and we've swapped the arguments to a more favorable position. I've namespaced them with `Impure` so we know these are dangerous functions. In a future example, we will make these two functions pure. 104 | 105 | Next we must construct a url to pass to our `Impure.getJSON` function. 106 | 107 | ```js 108 | const host = 'api.flickr.com'; 109 | const path = '/services/feeds/photos_public.gne'; 110 | const query = t => `?tags=${t}&format=json&jsoncallback=?`; 111 | const url = t => `https://${host}${path}${query(t)}`; 112 | ``` 113 | 114 | There are fancy and overly complex ways of writing `url` pointfree using monoids(we'll learn about these later) or combinators. We've chosen to stick with a readable version and assemble this string in the normal pointful fashion. 115 | 116 | Let's write an app function that makes the call and places the contents on the screen. 117 | 118 | ```js 119 | const app = compose(Impure.getJSON(Impure.trace('response')), url); 120 | app('cats'); 121 | ``` 122 | 123 | This calls our `url` function, then passes the string to our `getJSON` function, which has been partially applied with `trace`. Loading the app will show the response from the api call in the console. 124 | 125 | console response 126 | 127 | We'd like to construct images out of this json. It looks like the `mediaUrls` are buried in `items` then each `media`'s `m` property. 128 | 129 | Anyhow, to get at these nested properties we can use a nice universal getter function from ramda called `prop`. Here's a homegrown version so you can see what's happening: 130 | 131 | ```js 132 | const prop = curry((property, object) => object[property]); 133 | ``` 134 | 135 | It's quite dull actually. We just use `[]` syntax to access a property on whatever object. Let's use this to get at our `mediaUrls`. 136 | 137 | ```js 138 | const mediaUrl = compose(prop('m'), prop('media')); 139 | const mediaUrls = compose(map(mediaUrl), prop('items')); 140 | ``` 141 | 142 | Once we gather the `items`, we must `map` over them to extract each media url. This results in a nice array of `mediaUrls`. Let's hook this up to our app and print them on the screen. 143 | 144 | ```js 145 | const render = compose(Impure.setHtml('#js-main'), mediaUrls); 146 | const app = compose(Impure.getJSON(render), url); 147 | ``` 148 | 149 | All we've done is make a new composition that will call our `mediaUrls` and set the `
` html with them. We've replaced the `trace` call with `render` now that we have something to render besides raw json. This will crudely display our `mediaUrls` within the body. 150 | 151 | Our final step is to turn these `mediaUrls` into bonafide `images`. In a bigger application, we'd use a template/dom library like Handlebars or React. For this application though, we only need an img tag so let's stick with jQuery. 152 | 153 | ```js 154 | const img = src => $('', { src }); 155 | ``` 156 | 157 | jQuery's `html` method will accept an array of tags. We only have to transform our mediaUrls into images and send them along to `setHtml`. 158 | 159 | ```js 160 | const images = compose(map(img), mediaUrls); 161 | const render = compose(Impure.setHtml('#js-main'), images); 162 | const app = compose(Impure.getJSON(render), url); 163 | ``` 164 | 165 | And we're done! 166 | 167 | cats grid 168 | 169 | Here is the finished script: 170 | [include](./exercises/ch06/main.js) 171 | 172 | Now look at that. A beautifully declarative specification of what things are, not how they come to be. We now view each line as an equation with properties that hold. We can use these properties to reason about our application and refactor. 173 | 174 | ## A Principled Refactor 175 | 176 | There is an optimization available - we map over each item to turn it into a media url, then we map again over those mediaUrls to turn them into img tags. There is a law regarding map and composition: 177 | 178 | 179 | ```js 180 | // map's composition law 181 | compose(map(f), map(g)) === map(compose(f, g)); 182 | ``` 183 | 184 | We can use this property to optimize our code. Let's have a principled refactor. 185 | 186 | ```js 187 | // original code 188 | const mediaUrl = compose(prop('m'), prop('media')); 189 | const mediaUrls = compose(map(mediaUrl), prop('items')); 190 | const images = compose(map(img), mediaUrls); 191 | ``` 192 | 193 | Let's line up our maps. We can inline the call to `mediaUrls` in `images` thanks to equational reasoning and purity. 194 | 195 | ```js 196 | const mediaUrl = compose(prop('m'), prop('media')); 197 | const images = compose(map(img), map(mediaUrl), prop('items')); 198 | ``` 199 | 200 | Now that we've lined up our `map`s we can apply the composition law. 201 | 202 | ```js 203 | /* 204 | compose(map(f), map(g)) === map(compose(f, g)); 205 | compose(map(img), map(mediaUrl)) === map(compose(img, mediaUrl)); 206 | */ 207 | 208 | const mediaUrl = compose(prop('m'), prop('media')); 209 | const images = compose(map(compose(img, mediaUrl)), prop('items')); 210 | ``` 211 | 212 | Now the bugger will only loop once while turning each item into an img. Let's just make it a little more readable by extracting the function out. 213 | 214 | ```js 215 | const mediaUrl = compose(prop('m'), prop('media')); 216 | const mediaToImg = compose(img, mediaUrl); 217 | const images = compose(map(mediaToImg), prop('items')); 218 | ``` 219 | 220 | ## In Summary 221 | 222 | We have seen how to put our new skills into use with a small, but real world app. We've used our mathematical framework to reason about and refactor our code. But what about error handling and code branching? How can we make the whole application pure instead of merely namespacing destructive functions? How can we make our app safer and more expressive? These are the questions we will tackle in part 2. 223 | 224 | [Chapter 07: Hindley-Milner and Me](ch07.md) 225 | -------------------------------------------------------------------------------- /ch07.md: -------------------------------------------------------------------------------- 1 | # Chapter 07: Hindley-Milner and Me 2 | 3 | ## What's Your Type? 4 | If you're new to the functional world, it won't be long before you find yourself knee deep in type signatures. Types are the meta language that enables people from all different backgrounds to communicate succinctly and effectively. For the most part, they're written with a system called "Hindley-Milner", which we'll be examining together in this chapter. 5 | 6 | When working with pure functions, type signatures have an expressive power to which the English language cannot hold a candle. These signatures whisper in your ear the intimate secrets of a function. In a single, compact line, they expose behaviour and intention. We can derive "free theorems" from them. Types can be inferred so there's no need for explicit type annotations. They can be tuned to fine point precision or left general and abstract. They are not only useful for compile time checks, but also turn out to be the best possible documentation available. Type signatures thus play an important part in functional programming - much more than you might first expect. 7 | 8 | JavaScript is a dynamic language, but that does not mean we avoid types all together. We're still working with strings, numbers, booleans, and so on. It's just that there isn't any language level integration so we hold this information in our heads. Not to worry, since we're using signatures for documentation, we can use comments to serve our purpose. 9 | 10 | There are type checking tools available for JavaScript such as [Flow](https://flow.org/) or the typed dialect, [TypeScript](https://www.typescriptlang.org/). The aim of this book is to equip one with the tools to write functional code so we'll stick with the standard type system used across FP languages. 11 | 12 | 13 | ## Tales from the Cryptic 14 | 15 | From the dusty pages of math books, across the vast sea of white papers, amongst casual Saturday morning blog posts, down into the source code itself, we find Hindley-Milner type signatures. The system is quite simple, but warrants a quick explanation and some practice to fully absorb the little language. 16 | 17 | ```js 18 | // capitalize :: String -> String 19 | const capitalize = s => toUpperCase(head(s)) + toLowerCase(tail(s)); 20 | 21 | capitalize('smurf'); // 'Smurf' 22 | ``` 23 | 24 | Here, `capitalize` takes a `String` and returns a `String`. Never mind the implementation, it's the type signature we're interested in. 25 | 26 | In HM, functions are written as `a -> b` where `a` and `b` are variables for any type. So the signatures for `capitalize` can be read as "a function from `String` to `String`". In other words, it takes a `String` as its input and returns a `String` as its output. 27 | 28 | Let's look at some more function signatures: 29 | 30 | ```js 31 | // strLength :: String -> Number 32 | const strLength = s => s.length; 33 | 34 | // join :: String -> [String] -> String 35 | const join = curry((what, xs) => xs.join(what)); 36 | 37 | // match :: Regex -> String -> [String] 38 | const match = curry((reg, s) => s.match(reg)); 39 | 40 | // replace :: Regex -> String -> String -> String 41 | const replace = curry((reg, sub, s) => s.replace(reg, sub)); 42 | ``` 43 | 44 | `strLength` is the same idea as before: we take a `String` and return you a `Number`. 45 | 46 | The others might perplex you at first glance. Without fully understanding the details, you could always just view the last type as the return value. So for `match` you can interpret as: It takes a `Regex` and a `String` and returns you `[String]`. But an interesting thing is going on here that I'd like to take a moment to explain if I may. 47 | 48 | For `match` we are free to group the signature like so: 49 | 50 | ```js 51 | // match :: Regex -> (String -> [String]) 52 | const match = curry((reg, s) => s.match(reg)); 53 | ``` 54 | 55 | Ah yes, grouping the last part in parenthesis reveals more information. Now it is seen as a function that takes a `Regex` and returns us a function from `String` to `[String]`. Because of currying, this is indeed the case: give it a `Regex` and we get a function back waiting for its `String` argument. Of course, we don't have to think of it this way, but it is good to understand why the last type is the one returned. 56 | 57 | ```js 58 | // match :: Regex -> (String -> [String]) 59 | // onHoliday :: String -> [String] 60 | const onHoliday = match(/holiday/ig); 61 | ``` 62 | 63 | Each argument pops one type off the front of the signature. `onHoliday` is `match` that already has a `Regex`. 64 | 65 | ```js 66 | // replace :: Regex -> (String -> (String -> String)) 67 | const replace = curry((reg, sub, s) => s.replace(reg, sub)); 68 | ``` 69 | 70 | As you can see with the full parenthesis on `replace`, the extra notation can get a little noisy and redundant so we simply omit them. We can give all the arguments at once if we choose so it's easier to just think of it as: `replace` takes a `Regex`, a `String`, another `String` and returns you a `String`. 71 | 72 | A few last things here: 73 | 74 | 75 | ```js 76 | // id :: a -> a 77 | const id = x => x; 78 | 79 | // map :: (a -> b) -> [a] -> [b] 80 | const map = curry((f, xs) => xs.map(f)); 81 | ``` 82 | 83 | The `id` function takes any old type `a` and returns something of the same type `a`. We're able to use variables in types just like in code. Variable names like `a` and `b` are convention, but they are arbitrary and can be replaced with whatever name you'd like. If they are the same variable, they have to be the same type. That's an important rule so let's reiterate: `a -> b` can be any type `a` to any type `b`, but `a -> a` means it has to be the same type. For example, `id` may be `String -> String` or `Number -> Number`, but not `String -> Bool`. 84 | 85 | `map` similarly uses type variables, but this time we introduce `b` which may or may not be the same type as `a`. We can read it as: `map` takes a function from any type `a` to the same or different type `b`, then takes an array of `a`'s and results in an array of `b`'s. 86 | 87 | Hopefully, you've been overcome by the expressive beauty in this type signature. It literally tells us what the function does almost word for word. It's given a function from `a` to `b`, an array of `a`, and it delivers us an array of `b`. The only sensible thing for it to do is call the bloody function on each `a`. Anything else would be a bold face lie. 88 | 89 | Being able to reason about types and their implications is a skill that will take you far in the functional world. Not only will papers, blogs, docs, etc, become more digestible, but the signature itself will practically lecture you on its functionality. It takes practice to become a fluent reader, but if you stick with it, heaps of information will become available to you sans RTFMing. 90 | 91 | Here's a few more just to see if you can decipher them on your own. 92 | 93 | ```js 94 | // head :: [a] -> a 95 | const head = xs => xs[0]; 96 | 97 | // filter :: (a -> Bool) -> [a] -> [a] 98 | const filter = curry((f, xs) => xs.filter(f)); 99 | 100 | // reduce :: ((b, a) -> b) -> b -> [a] -> b 101 | const reduce = curry((f, x, xs) => xs.reduce(f, x)); 102 | ``` 103 | 104 | `reduce` is perhaps, the most expressive of all. It's a tricky one, however, so don't feel inadequate should you struggle with it. For the curious, I'll try to explain in English though working through the signature on your own is much more instructive. 105 | 106 | Ahem, here goes nothing....looking at the signature, we see the first argument is a function that expects `b` and `a`, and produces a `b`. Where might it get these `a`s and `b`s? Well, the following arguments in the signature are a `b` and an array of `a`s so we can only assume that the `b` and each of those `a`s will be fed in. We also see that the result of the function is a `b` so the thinking here is our final incantation of the passed in function will be our output value. Knowing what reduce does, we can state that the above investigation is accurate. 107 | 108 | 109 | ## Narrowing the Possibility 110 | 111 | Once a type variable is introduced, there emerges a curious property called *[parametricity](https://en.wikipedia.org/wiki/Parametricity)*. This property states that a function will *act on all types in a uniform manner*. Let's investigate: 112 | 113 | ```js 114 | // head :: [a] -> a 115 | ``` 116 | 117 | Looking at `head`, we see that it takes `[a]` to `a`. Besides the concrete type `array`, it has no other information available and, therefore, its functionality is limited to working on the array alone. What could it possibly do with the variable `a` if it knows nothing about it? In other words, `a` says it cannot be a *specific* type, which means it can be *any* type, which leaves us with a function that must work uniformly for *every* conceivable type. This is what *parametricity* is all about. Guessing at the implementation, the only reasonable assumptions are that it takes the first, last, or a random element from that array. The name `head` should tip us off. 118 | 119 | Here's another one: 120 | 121 | ```js 122 | // reverse :: [a] -> [a] 123 | ``` 124 | 125 | From the type signature alone, what could `reverse` possibly be up to? Again, it cannot do anything specific to `a`. It cannot change `a` to a different type or we'd introduce a `b`. Can it sort? Well, no, it wouldn't have enough information to sort every possible type. Can it re-arrange? Yes, I suppose it can do that, but it has to do so in exactly the same predictable way. Another possibility is that it may decide to remove or duplicate an element. In any case, the point is, the possible behaviour is massively narrowed by its polymorphic type. 126 | 127 | This narrowing of possibility allows us to use type signature search engines like [Hoogle](https://hoogle.haskell.org/) to find a function we're after. The information packed tightly into a signature is quite powerful indeed. 128 | 129 | ## Free as in Theorem 130 | 131 | Besides deducing implementation possibilities, this sort of reasoning gains us *free theorems*. What follows are a few random example theorems lifted directly from [Wadler's paper on the subject](http://ttic.uchicago.edu/~dreyer/course/papers/wadler.pdf). 132 | 133 | ```js 134 | // head :: [a] -> a 135 | compose(f, head) === compose(head, map(f)); 136 | 137 | // filter :: (a -> Bool) -> [a] -> [a] 138 | compose(map(f), filter(compose(p, f))) === compose(filter(p), map(f)); 139 | ``` 140 | 141 | 142 | You don't need any code to get these theorems, they follow directly from the types. The first one says that if we get the `head` of our array, then run some function `f` on it, that is equivalent to, and incidentally, much faster than, if we first `map(f)` over every element then take the `head` of the result. 143 | 144 | You might think, well that's just common sense. But last I checked, computers don't have common sense. Indeed, they must have a formal way to automate these kind of code optimizations. Maths has a way of formalizing the intuitive, which is helpful amidst the rigid terrain of computer logic. 145 | 146 | The `filter` theorem is similar. It says that if we compose `f` and `p` to check which should be filtered, then actually apply the `f` via `map` (remember `filter` will not transform the elements - its signature enforces that `a` will not be touched), it will always be equivalent to mapping our `f` then filtering the result with the `p` predicate. 147 | 148 | These are just two examples, but you can apply this reasoning to any polymorphic type signature and it will always hold. In JavaScript, there are some tools available to declare rewrite rules. One might also do this via the `compose` function itself. The fruit is low hanging and the possibilities are endless. 149 | 150 | ## Constraints 151 | 152 | One last thing to note is that we can constrain types to an interface. 153 | 154 | ```js 155 | // sort :: Ord a => [a] -> [a] 156 | ``` 157 | 158 | What we see on the left side of our fat arrow here is the statement of a fact: `a` must be an `Ord`. Or in other words, `a` must implement the `Ord` interface. What is `Ord` and where did it come from? In a typed language it would be a defined interface that says we can order the values. This not only tells us more about the `a` and what our `sort` function is up to, but also restricts the domain. We call these interface declarations *type constraints*. 159 | 160 | ```js 161 | // assertEqual :: (Eq a, Show a) => a -> a -> Assertion 162 | ``` 163 | 164 | Here, we have two constraints: `Eq` and `Show`. Those will ensure that we can check equality of our `a`s and print the difference if they are not equal. 165 | 166 | We'll see more examples of constraints and the idea should take more shape in later chapters. 167 | 168 | ## In Summary 169 | 170 | Hindley-Milner type signatures are ubiquitous in the functional world. Though they are simple to read and write, it takes time to master the technique of understanding programs through signatures alone. We will add type signatures to each line of code from here on out. 171 | 172 | [Chapter 08: Tupperware](ch08.md) 173 | -------------------------------------------------------------------------------- /ch11.md: -------------------------------------------------------------------------------- 1 | # Chapter 11: Transform Again, Naturally 2 | 3 | We are about to discuss *natural transformations* in the context of practical utility in every day code. It just so happens they are a pillar of category theory and absolutely indispensable when applying mathematics to reason about and refactor our code. As such, I believe it is my duty to inform you about the lamentable injustice you are about to witness undoubtedly due to my limited scope. Let's begin. 4 | 5 | ## Curse This Nest 6 | 7 | I'd like to address the issue of nesting. Not the instinctive urge felt by soon to be parents wherein they tidy and rearrange with obsessive compulsion, but the...well actually, come to think of it, that isn't far from the mark as we'll see in the coming chapters... In any case, what I mean by *nesting* is to have two or more different types all huddled together around a value, cradling it like a newborn, as it were. 8 | 9 | ```js 10 | Right(Maybe('b')); 11 | 12 | IO(Task(IO(1000))); 13 | 14 | [Identity('bee thousand')]; 15 | ``` 16 | 17 | Until now, we've managed to evade this common scenario with carefully crafted examples, but in practice, as one codes, types tend to tangle themselves up like earbuds in an exorcism. If we don't meticulously keep our types organized as we go along, our code will read hairier than a beatnik in a cat café. 18 | 19 | ## A Situational Comedy 20 | 21 | ```js 22 | // getValue :: Selector -> Task Error (Maybe String) 23 | // postComment :: String -> Task Error Comment 24 | // validate :: String -> Either ValidationError String 25 | 26 | // saveComment :: () -> Task Error (Maybe (Either ValidationError (Task Error Comment))) 27 | const saveComment = compose( 28 | map(map(map(postComment))), 29 | map(map(validate)), 30 | getValue('#comment'), 31 | ); 32 | ``` 33 | 34 | The gang is all here, much to our type signature's dismay. Allow me to briefly explain the code. We start by getting the user input with `getValue('#comment')` which is an action which retrieves text on an element. Now, it might error finding the element or the value string may not exist so it returns `Task Error (Maybe String)`. After that, we must `map` over both the `Task` and the `Maybe` to pass our text to `validate`, which in turn, gives us back `Either` a `ValidationError` or our `String`. Then onto mapping for days to send the `String` in our current `Task Error (Maybe (Either ValidationError String))` into `postComment` which returns our resulting `Task`. 35 | 36 | What a frightful mess. A collage of abstract types, amateur type expressionism, polymorphic Pollock, monolithic Mondrian. There are many solutions to this common issue. We can compose the types into one monstrous container, sort and `join` a few, homogenize them, deconstruct them, and so on. In this chapter, we'll focus on homogenizing them via *natural transformations*. 37 | 38 | ## All Natural 39 | 40 | A *Natural Transformation* is a "morphism between functors", that is, a function which operates on the containers themselves. Typewise, it is a function `(Functor f, Functor g) => f a -> g a`. What makes it special is that we cannot, for any reason, peek at the contents of our functor. Think of it as an exchange of highly classified information - the two parties oblivious to what's in the sealed manila envelope stamped "top secret". This is a structural operation. A functorial costume change. Formally, a *natural transformation* is any function for which the following holds: 41 | 42 | natural transformation diagram 43 | 44 | or in code: 45 | 46 | ```js 47 | // nt :: (Functor f, Functor g) => f a -> g a 48 | compose(map(f), nt) === compose(nt, map(f)); 49 | ``` 50 | 51 | Both the diagram and the code say the same thing: We can run our natural transformation then `map` or `map` then run our natural transformation and get the same result. Incidentally, that follows from a [free theorem](ch07.md#free-as-in-theorem) though natural transformations (and functors) are not limited to functions on types. 52 | 53 | ## Principled Type Conversions 54 | 55 | As programmers we are familiar with type conversions. We transform types like `Strings` into `Booleans` and `Integers` into `Floats` (though JavaScript only has `Numbers`). The difference here is simply that we're working with algebraic containers and we have some theory at our disposal. 56 | 57 | Let's look at some of these as examples: 58 | 59 | ```js 60 | // idToMaybe :: Identity a -> Maybe a 61 | const idToMaybe = x => Maybe.of(x.$value); 62 | 63 | // idToIO :: Identity a -> IO a 64 | const idToIO = x => IO.of(x.$value); 65 | 66 | // eitherToTask :: Either a b -> Task a b 67 | const eitherToTask = either(Task.rejected, Task.of); 68 | 69 | // ioToTask :: IO a -> Task () a 70 | const ioToTask = x => new Task((reject, resolve) => resolve(x.unsafePerform())); 71 | 72 | // maybeToTask :: Maybe a -> Task () a 73 | const maybeToTask = x => (x.isNothing ? Task.rejected() : Task.of(x.$value)); 74 | 75 | // arrayToMaybe :: [a] -> Maybe a 76 | const arrayToMaybe = x => Maybe.of(x[0]); 77 | ``` 78 | 79 | See the idea? We're just changing one functor to another. We are permitted to lose information along the way so long as the value we'll `map` doesn't get lost in the shape shift shuffle. That is the whole point: `map` must carry on, according to our definition, even after the transformation. 80 | 81 | One way to look at it is that we are transforming our effects. In that light, we can view `ioToTask` as converting synchronous to asynchronous or `arrayToMaybe` from nondeterminism to possible failure. Note that we cannot convert asynchronous to synchronous in JavaScript so we cannot write `taskToIO` - that would be a supernatural transformation. 82 | 83 | ## Feature Envy 84 | 85 | Suppose we'd like to use some features from another type like `sortBy` on a `List`. *Natural transformations* provide a nice way to convert to the target type knowing our `map` will be sound. 86 | 87 | ```js 88 | // arrayToList :: [a] -> List a 89 | const arrayToList = List.of; 90 | 91 | const doListyThings = compose(sortBy(h), filter(g), arrayToList, map(f)); 92 | const doListyThings_ = compose(sortBy(h), filter(g), map(f), arrayToList); // law applied 93 | ``` 94 | 95 | A wiggle of our nose, three taps of our wand, drop in `arrayToList`, and voilà! Our `[a]` is a `List a` and we can `sortBy` if we please. 96 | 97 | Also, it becomes easier to optimize / fuse operations by moving `map(f)` to the left of *natural transformation* as shown in `doListyThings_`. 98 | 99 | ## Isomorphic JavaScript 100 | 101 | When we can completely go back and forth without losing any information, that is considered an *isomorphism*. That's just a fancy word for "holds the same data". We say that two types are *isomorphic* if we can provide the "to" and "from" *natural transformations* as proof: 102 | 103 | ```js 104 | // promiseToTask :: Promise a b -> Task a b 105 | const promiseToTask = x => new Task((reject, resolve) => x.then(resolve).catch(reject)); 106 | 107 | // taskToPromise :: Task a b -> Promise a b 108 | const taskToPromise = x => new Promise((resolve, reject) => x.fork(reject, resolve)); 109 | 110 | const x = Promise.resolve('ring'); 111 | taskToPromise(promiseToTask(x)) === x; 112 | 113 | const y = Task.of('rabbit'); 114 | promiseToTask(taskToPromise(y)) === y; 115 | ``` 116 | 117 | Q.E.D. `Promise` and `Task` are *isomorphic*. We can also write a `listToArray` to complement our `arrayToList` and show that they are too. As a counter example, `arrayToMaybe` is not an *isomorphism* since it loses information: 118 | 119 | ```js 120 | // maybeToArray :: Maybe a -> [a] 121 | const maybeToArray = x => (x.isNothing ? [] : [x.$value]); 122 | 123 | // arrayToMaybe :: [a] -> Maybe a 124 | const arrayToMaybe = x => Maybe.of(x[0]); 125 | 126 | const x = ['elvis costello', 'the attractions']; 127 | 128 | // not isomorphic 129 | maybeToArray(arrayToMaybe(x)); // ['elvis costello'] 130 | 131 | // but is a natural transformation 132 | compose(arrayToMaybe, map(replace('elvis', 'lou')))(x); // Just('lou costello') 133 | // == 134 | compose(map(replace('elvis', 'lou'), arrayToMaybe))(x); // Just('lou costello') 135 | ``` 136 | 137 | They are indeed *natural transformations*, however, since `map` on either side yields the same result. I mention *isomorphisms* here, mid-chapter while we're on the subject, but don't let that fool you, they are an enormously powerful and pervasive concept. Anyways, let's move on. 138 | 139 | ## A Broader Definition 140 | 141 | These structural functions aren't limited to type conversions by any means. 142 | 143 | Here are a few different ones: 144 | 145 | ```hs 146 | reverse :: [a] -> [a] 147 | 148 | join :: (Monad m) => m (m a) -> m a 149 | 150 | head :: [a] -> a 151 | 152 | of :: a -> f a 153 | ``` 154 | 155 | The natural transformation laws hold for these functions too. One thing that might trip you up is that `head :: [a] -> a` can be viewed as `head :: [a] -> Identity a`. We are free to insert `Identity` wherever we please whilst proving laws since we can, in turn, prove that `a` is isomorphic to `Identity a` (see, I told you *isomorphisms* were pervasive). 156 | 157 | ## One Nesting Solution 158 | 159 | Back to our comedic type signature. We can sprinkle in some *natural transformations* throughout the calling code to coerce each varying type so they are uniform and, therefore, `join`able. 160 | 161 | ```js 162 | // getValue :: Selector -> Task Error (Maybe String) 163 | // postComment :: String -> Task Error Comment 164 | // validate :: String -> Either ValidationError String 165 | 166 | // saveComment :: () -> Task Error Comment 167 | const saveComment = compose( 168 | chain(postComment), 169 | chain(eitherToTask), 170 | map(validate), 171 | chain(maybeToTask), 172 | getValue('#comment'), 173 | ); 174 | ``` 175 | 176 | So what do we have here? We've simply added `chain(maybeToTask)` and `chain(eitherToTask)`. Both have the same effect; they naturally transform the functor our `Task` is holding into another `Task` then `join` the two. Like pigeon spikes on a window ledge, we avoid nesting right at the source. As they say in the city of light, "Mieux vaut prévenir que guérir" - an ounce of prevention is worth a pound of cure. 177 | 178 | ## In Summary 179 | 180 | *Natural transformations* are functions on our functors themselves. They are an extremely important concept in category theory and will start to appear everywhere once more abstractions are adopted, but for now, we've scoped them to a few concrete applications. As we saw, we can achieve different effects by converting types with the guarantee that our composition will hold. They can also help us with nested types, although they have the general effect of homogenizing our functors to the lowest common denominator, which in practice, is the functor with the most volatile effects (`Task` in most cases). 181 | 182 | This continual and tedious sorting of types is the price we pay for having materialized them - summoned them from the ether. Of course, implicit effects are much more insidious and so here we are fighting the good fight. We'll need a few more tools in our tackle before we can reel in the larger type amalgamations. Next up, we'll look at reordering our types with *Traversable*. 183 | 184 | [Chapter 12: Traversing the Stone](ch12.md) 185 | 186 | 187 | ## Exercises 188 | 189 | {% exercise %} 190 | Write a natural transformation that converts `Either b a` to `Maybe a` 191 | 192 | {% initial src="./exercises/ch11/exercise_a.js#L3;" %} 193 | ```js 194 | // eitherToMaybe :: Either b a -> Maybe a 195 | const eitherToMaybe = undefined; 196 | ``` 197 | 198 | 199 | {% solution src="./exercises/ch11/solution_a.js" %} 200 | {% validation src="./exercises/ch11/validation_a.js" %} 201 | {% context src="./exercises/support.js" %} 202 | {% endexercise %} 203 | 204 | 205 | --- 206 | 207 | 208 | ```js 209 | // eitherToTask :: Either a b -> Task a b 210 | const eitherToTask = either(Task.rejected, Task.of); 211 | ``` 212 | 213 | {% exercise %} 214 | Using `eitherToTask`, simplify `findNameById` to remove the nested `Either`. 215 | 216 | {% initial src="./exercises/ch11/exercise_b.js#L6;" %} 217 | ```js 218 | // findNameById :: Number -> Task Error (Either Error User) 219 | const findNameById = compose(map(map(prop('name'))), findUserById); 220 | ``` 221 | 222 | 223 | {% solution src="./exercises/ch11/solution_b.js" %} 224 | {% validation src="./exercises/ch11/validation_b.js" %} 225 | {% context src="./exercises/support.js" %} 226 | {% endexercise %} 227 | 228 | 229 | --- 230 | 231 | 232 | As a reminder, the following functions are available in the exercise's context: 233 | 234 | ```hs 235 | split :: String -> String -> [String] 236 | intercalate :: String -> [String] -> String 237 | ``` 238 | 239 | {% exercise %} 240 | Write the isomorphisms between String and [Char]. 241 | 242 | {% initial src="./exercises/ch11/exercise_c.js#L8;" %} 243 | ```js 244 | // strToList :: String -> [Char] 245 | const strToList = undefined; 246 | 247 | // listToStr :: [Char] -> String 248 | const listToStr = undefined; 249 | ``` 250 | 251 | 252 | {% solution src="./exercises/ch11/solution_c.js" %} 253 | {% validation src="./exercises/ch11/validation_c.js" %} 254 | {% context src="./exercises/support.js" %} 255 | {% endexercise %} 256 | -------------------------------------------------------------------------------- /ch03.md: -------------------------------------------------------------------------------- 1 | # Chapter 03: Pure Happiness with Pure Functions 2 | 3 | ## Oh to Be Pure Again 4 | 5 | One thing we need to get straight is the idea of a pure function. 6 | 7 | >A pure function is a function that, given the same input, will always return the same output and does not have any observable side effect. 8 | 9 | Take `slice` and `splice`. They are two functions that do the exact same thing - in a vastly different way, mind you, but the same thing nonetheless. We say `slice` is *pure* because it returns the same output per input every time, guaranteed. `splice`, however, will chew up its array and spit it back out forever changed which is an observable effect. 10 | 11 | ```js 12 | const xs = [1,2,3,4,5]; 13 | 14 | // pure 15 | xs.slice(0,3); // [1,2,3] 16 | 17 | xs.slice(0,3); // [1,2,3] 18 | 19 | xs.slice(0,3); // [1,2,3] 20 | 21 | 22 | // impure 23 | xs.splice(0,3); // [1,2,3] 24 | 25 | xs.splice(0,3); // [4,5] 26 | 27 | xs.splice(0,3); // [] 28 | ``` 29 | 30 | In functional programming, we dislike unwieldy functions like `splice` that *mutate* data. This will never do as we're striving for reliable functions that return the same result every time, not functions that leave a mess in their wake like `splice`. 31 | 32 | Let's look at another example. 33 | 34 | ```js 35 | // impure 36 | let minimum = 21; 37 | const checkAge = age => age >= minimum; 38 | 39 | // pure 40 | const checkAge = (age) => { 41 | const minimum = 21; 42 | return age >= minimum; 43 | }; 44 | ``` 45 | 46 | In the impure portion, `checkAge` depends on the mutable variable `minimum` to determine the result. In other words, it depends on system state which is disappointing because it increases the [cognitive load](https://en.wikipedia.org/wiki/Cognitive_load) by introducing an external environment. 47 | 48 | It might not seem like a lot in this example, but this reliance upon state is one of the largest contributors to system complexity (http://curtclifton.net/papers/MoseleyMarks06a.pdf). This `checkAge` may return different results depending on factors external to input, which not only disqualifies it from being pure, but also puts our minds through the wringer each time we're reasoning about the software. 49 | 50 | Its pure form, on the other hand, is completely self sufficient. We can also make `minimum` immutable, which preserves the purity as the state will never change. To do this, we must create an object to freeze. 51 | 52 | ```js 53 | const immutableState = Object.freeze({ minimum: 21 }); 54 | ``` 55 | 56 | ## Side Effects May Include... 57 | 58 | Let's look more at these "side effects" to improve our intuition. So what is this undoubtedly nefarious *side effect* mentioned in the definition of *pure function*? We'll be referring to *effect* as anything that occurs in our computation other than the calculation of a result. 59 | 60 | There's nothing intrinsically bad about effects and we'll be using them all over the place in the chapters to come. It's that *side* part that bears the negative connotation. Water alone is not an inherent larvae incubator, it's the *stagnant* part that yields the swarms, and I assure you, *side* effects are a similar breeding ground in your own programs. 61 | 62 | >A *side effect* is a change of system state or *observable interaction* with the outside world that occurs during the calculation of a result. 63 | 64 | Side effects may include, but are not limited to 65 | 66 | * changing the file system 67 | * inserting a record into a database 68 | * making an http call 69 | * mutations 70 | * printing to the screen / logging 71 | * obtaining user input 72 | * querying the DOM 73 | * accessing system state 74 | 75 | And the list goes on and on. Any interaction with the world outside of a function is a side effect, which is a fact that may prompt you to suspect the practicality of programming without them. The philosophy of functional programming postulates that side effects are a primary cause of incorrect behavior. 76 | 77 | It is not that we're forbidden to use them, rather we want to contain them and run them in a controlled way. We'll learn how to do this when we get to functors and monads in later chapters, but for now, let's try to keep these insidious functions separate from our pure ones. 78 | 79 | Side effects disqualify a function from being *pure*. And it makes sense: pure functions, by definition, must always return the same output given the same input, which is not possible to guarantee when dealing with matters outside our local function. 80 | 81 | Let's take a closer look at why we insist on the same output per input. Pop your collars, we're going to look at some 8th grade math. 82 | 83 | ## 8th Grade Math 84 | 85 | From mathisfun.com: 86 | 87 | > A function is a special relationship between values: 88 | > Each of its input values gives back exactly one output value. 89 | 90 | In other words, it's just a relation between two values: the input and the output. Though each input has exactly one output, that output doesn't necessarily have to be unique per input. Below shows a diagram of a perfectly valid function from `x` to `y`; 91 | 92 | function sets(https://www.mathsisfun.com/sets/function.html) 93 | 94 | To contrast, the following diagram shows a relation that is *not* a function since the input value `5` points to several outputs: 95 | 96 | relation not function(https://www.mathsisfun.com/sets/function.html) 97 | 98 | Functions can be described as a set of pairs with the position (input, output): `[(1,2), (3,6), (5,10)]` (It appears this function doubles its input). 99 | 100 | Or perhaps a table: 101 |
Input Output
1 2
2 4
3 6
102 | 103 | Or even as a graph with `x` as the input and `y` as the output: 104 | 105 | function graph 106 | 107 | 108 | There's no need for implementation details if the input dictates the output. Since functions are simply mappings of input to output, one could simply jot down object literals and run them with `[]` instead of `()`. 109 | 110 | ```js 111 | const toLowerCase = { 112 | A: 'a', 113 | B: 'b', 114 | C: 'c', 115 | D: 'd', 116 | E: 'e', 117 | F: 'f', 118 | }; 119 | toLowerCase['C']; // 'c' 120 | 121 | const isPrime = { 122 | 1: false, 123 | 2: true, 124 | 3: true, 125 | 4: false, 126 | 5: true, 127 | 6: false, 128 | }; 129 | isPrime[3]; // true 130 | ``` 131 | 132 | Of course, you might want to calculate instead of hand writing things out, but this illustrates a different way to think about functions. (You may be thinking "what about functions with multiple arguments?". Indeed, that presents a bit of an inconvenience when thinking in terms of mathematics. For now, we can bundle them up in an array or just think of the `arguments` object as the input. When we learn about *currying*, we'll see how we can directly model the mathematical definition of a function.) 133 | 134 | Here comes the dramatic reveal: Pure functions *are* mathematical functions and they're what functional programming is all about. Programming with these little angels can provide huge benefits. Let's look at some reasons why we're willing to go to great lengths to preserve purity. 135 | 136 | ## The Case for Purity 137 | 138 | ### Cacheable 139 | 140 | For starters, pure functions can always be cached by input. This is typically done using a technique called memoization: 141 | 142 | ```js 143 | const squareNumber = memoize(x => x * x); 144 | 145 | squareNumber(4); // 16 146 | 147 | squareNumber(4); // 16, returns cache for input 4 148 | 149 | squareNumber(5); // 25 150 | 151 | squareNumber(5); // 25, returns cache for input 5 152 | ``` 153 | 154 | Here is a simplified implementation, though there are plenty of more robust versions available. 155 | 156 | ```js 157 | const memoize = (f) => { 158 | const cache = {}; 159 | 160 | return (...args) => { 161 | const argStr = JSON.stringify(args); 162 | cache[argStr] = cache[argStr] || f(...args); 163 | return cache[argStr]; 164 | }; 165 | }; 166 | ``` 167 | 168 | Something to note is that you can transform some impure functions into pure ones by delaying evaluation: 169 | 170 | ```js 171 | const pureHttpCall = memoize((url, params) => () => $.getJSON(url, params)); 172 | ``` 173 | 174 | The interesting thing here is that we don't actually make the http call - we instead return a function that will do so when called. This function is pure because it will always return the same output given the same input: the function that will make the particular http call given the `url` and `params`. 175 | 176 | Our `memoize` function works just fine, though it doesn't cache the results of the http call, rather it caches the generated function. 177 | 178 | This is not very useful yet, but we'll soon learn some tricks that will make it so. The takeaway is that we can cache every function no matter how destructive they seem. 179 | 180 | ### Portable / Self-documenting 181 | 182 | Pure functions are completely self contained. Everything the function needs is handed to it on a silver platter. Ponder this for a moment... How might this be beneficial? For starters, a function's dependencies are explicit and therefore easier to see and understand - no funny business going on under the hood. 183 | 184 | ```js 185 | // impure 186 | const signUp = (attrs) => { 187 | const user = saveUser(attrs); 188 | welcomeUser(user); 189 | }; 190 | 191 | // pure 192 | const signUp = (Db, Email, attrs) => () => { 193 | const user = saveUser(Db, attrs); 194 | welcomeUser(Email, user); 195 | }; 196 | ``` 197 | 198 | The example here demonstrates that the pure function must be honest about its dependencies and, as such, tell us exactly what it's up to. Just from its signature, we know that it will use a `Db`, `Email`, and `attrs` which should be telling to say the least. 199 | 200 | We'll learn how to make functions like this pure without merely deferring evaluation, but the point should be clear that the pure form is much more informative than its sneaky impure counterpart which is up to who knows what. 201 | 202 | Something else to notice is that we're forced to "inject" dependencies, or pass them in as arguments, which makes our app much more flexible because we've parameterized our database or mail client or what have you (don't worry, we'll see a way to make this less tedious than it sounds). Should we choose to use a different Db we need only to call our function with it. Should we find ourselves writing a new application in which we'd like to reuse this reliable function, we simply give this function whatever `Db` and `Email` we have at the time. 203 | 204 | In a JavaScript setting, portability could mean serializing and sending functions over a socket. It could mean running all our app code in web workers. Portability is a powerful trait. 205 | 206 | Contrary to "typical" methods and procedures in imperative programming rooted deep in their environment via state, dependencies, and available effects, pure functions can be run anywhere our hearts desire. 207 | 208 | When was the last time you copied a method into a new app? One of my favorite quotes comes from Erlang creator, Joe Armstrong: "The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana... and the entire jungle". 209 | 210 | ### Testable 211 | 212 | Next, we come to realize pure functions make testing much easier. We don't have to mock a "real" payment gateway or setup and assert the state of the world after each test. We simply give the function input and assert output. 213 | 214 | In fact, we find the functional community pioneering new test tools that can blast our functions with generated input and assert that properties hold on the output. It's beyond the scope of this book, but I strongly encourage you to search for and try *Quickcheck* - a testing tool that is tailored for a purely functional environment. 215 | 216 | ### Reasonable 217 | 218 | Many believe the biggest win when working with pure functions is *referential transparency*. A spot of code is referentially transparent when it can be substituted for its evaluated value without changing the behavior of the program. 219 | 220 | Since pure functions don't have side effects, they can only influence the behavior of a program through their output values. Furthermore, since their output values can reliably be calculated using only their input values, pure functions will always preserve referential transparency. Let's see an example. 221 | 222 | 223 | ```js 224 | const { Map } = require('immutable'); 225 | 226 | // Aliases: p = player, a = attacker, t = target 227 | const jobe = Map({ name: 'Jobe', hp: 20, team: 'red' }); 228 | const michael = Map({ name: 'Michael', hp: 20, team: 'green' }); 229 | const decrementHP = p => p.set('hp', p.get('hp') - 1); 230 | const isSameTeam = (p1, p2) => p1.get('team') === p2.get('team'); 231 | const punch = (a, t) => (isSameTeam(a, t) ? t : decrementHP(t)); 232 | 233 | punch(jobe, michael); // Map({name:'Michael', hp:19, team: 'green'}) 234 | ``` 235 | 236 | `decrementHP`, `isSameTeam` and `punch` are all pure and therefore referentially transparent. We can use a technique called *equational reasoning* wherein one substitutes "equals for equals" to reason about code. It's a bit like manually evaluating the code without taking into account the quirks of programmatic evaluation. Using referential transparency, let's play with this code a bit. 237 | 238 | First we'll inline the function `isSameTeam`. 239 | 240 | ```js 241 | const punch = (a, t) => (a.get('team') === t.get('team') ? t : decrementHP(t)); 242 | ``` 243 | 244 | Since our data is immutable, we can simply replace the teams with their actual value 245 | 246 | ```js 247 | const punch = (a, t) => ('red' === 'green' ? t : decrementHP(t)); 248 | ``` 249 | 250 | We see that it is false in this case so we can remove the entire if branch 251 | 252 | ```js 253 | const punch = (a, t) => decrementHP(t); 254 | ``` 255 | 256 | And if we inline `decrementHP`, we see that, in this case, punch becomes a call to decrement the `hp` by 1. 257 | 258 | ```js 259 | const punch = (a, t) => t.set('hp', t.get('hp') - 1); 260 | ``` 261 | 262 | This ability to reason about code is terrific for refactoring and understanding code in general. In fact, we used this technique to refactor our flock of seagulls program. We used equational reasoning to harness the properties of addition and multiplication. Indeed, we'll be using these techniques throughout the book. 263 | 264 | ### Parallel Code 265 | 266 | Finally, and here's the coup de grâce, we can run any pure function in parallel since it does not need access to shared memory and it cannot, by definition, have a race condition due to some side effect. 267 | 268 | This is very much possible in a server side js environment with threads as well as in the browser with web workers though current culture seems to avoid it due to complexity when dealing with impure functions. 269 | 270 | 271 | ## In Summary 272 | 273 | We've seen what pure functions are and why we, as functional programmers, believe they are the cat's evening wear. From this point on, we'll strive to write all our functions in a pure way. We'll require some extra tools to help us do so, but in the meantime, we'll try to separate the impure functions from the rest of the pure code. 274 | 275 | Writing programs with pure functions is a tad laborious without some extra tools in our belt. We have to juggle data by passing arguments all over the place, we're forbidden to use state, not to mention effects. How does one go about writing these masochistic programs? Let's acquire a new tool called curry. 276 | 277 | [Chapter 04: Currying](ch04.md) 278 | --------------------------------------------------------------------------------