├── .gitignore ├── Cask ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── README.org ├── book ├── chapter-1.org ├── images │ ├── REPL-when-not.png │ ├── REPL.png │ ├── The-Prestige.png │ ├── Type_N_Search.png │ ├── a-monad-is-just-a-monoid-in-the-category-of-endofunctors.png │ ├── applicative-functor.png │ ├── applicative.png │ ├── big-bang-lego.png │ ├── call-stack.png │ ├── compose.png │ ├── cover.png │ ├── csp.png │ ├── event-loop-model.png │ ├── everyscript.png │ ├── flatmap-stream.png │ ├── func-value.png │ ├── functor.png │ ├── interstellar.png │ ├── lazy-oreo.png │ ├── lift.png │ ├── message-queue.png │ ├── monad.png │ ├── monoid.png │ ├── multithread.png │ ├── mutable-push.png │ ├── paradigm.png │ ├── patten-matching.jpg │ ├── persistent-conj.png │ ├── pipeline.png │ ├── react-flow.png │ ├── react-flow.png.cache │ ├── simpsons-lego.png │ ├── tail-off.png │ ├── tailoff.png │ ├── thunk.png │ ├── update-dom.png │ ├── update-dom.png.cache │ ├── vec-conj-11.png │ ├── vec-conj-8.png │ ├── vec-conj-9.png │ ├── vec-conj11.png │ ├── vec-conj8.png │ ├── vec-conj9.png │ ├── vecconj11.png │ ├── vecconj8.png │ ├── vecconj9.png │ └── you_hang_up_first.png ├── index.org ├── style │ ├── fonts │ │ ├── droid-sans-mono.woff │ │ ├── droid-sans-mono.woff2 │ │ ├── droid-sans.woff │ │ ├── droid-sans.woff2 │ │ ├── droid-serif.woff │ │ ├── droid-serif.woff2 │ │ ├── merriweater-heavy-itaic.woff2 │ │ ├── merriweater-heavy.woff2 │ │ ├── merriweater-italic.woff2 │ │ └── merriweater-light.woff2 │ ├── gh-fork-ribbon.css │ ├── gh-fork-ribbon.ie.css │ ├── pixyll.css │ └── worg.css ├── tufte-book.cls ├── tufte-common.def └── zh │ ├── images │ ├── index.org │ ├── index.pdf │ ├── mitpress.cls │ ├── style │ ├── 前言.org │ ├── 序.org │ ├── 第一章.org │ ├── 第七章.org │ ├── 第二章.org │ └── 第八章.org ├── emacs.el └── html ├── header.html └── postamble.html /.gitignore: -------------------------------------------------------------------------------- 1 | public 2 | .cask 3 | # Created by https://www.gitignore.io/api/node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 33 | node_modules 34 | 35 | .eslintrc 36 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | (source org) 4 | 5 | (depends-on "org-plus-contrib") 6 | (depends-on "htmlize") 7 | (depends-on "dash") 8 | (depends-on "color-theme") 9 | (depends-on "clojure-mode") 10 | 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://ruby.taobao.org' 2 | 3 | gem 'guard' 4 | gem 'guard-shell' 5 | gem 'guard-livereload' 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://ruby.taobao.org/ 3 | specs: 4 | coderay (1.1.0) 5 | em-websocket (0.5.1) 6 | eventmachine (>= 0.12.9) 7 | http_parser.rb (~> 0.6.0) 8 | eventmachine (1.0.7) 9 | ffi (1.9.10) 10 | formatador (0.2.5) 11 | guard (2.13.0) 12 | formatador (>= 0.2.4) 13 | listen (>= 2.7, <= 4.0) 14 | lumberjack (~> 1.0) 15 | nenv (~> 0.1) 16 | notiffany (~> 0.0) 17 | pry (>= 0.9.12) 18 | shellany (~> 0.0) 19 | thor (>= 0.18.1) 20 | guard-compat (1.2.1) 21 | guard-livereload (2.4.0) 22 | em-websocket (~> 0.5) 23 | guard (~> 2.8) 24 | multi_json (~> 1.8) 25 | guard-shell (0.7.1) 26 | guard (>= 2.0.0) 27 | guard-compat (~> 1.0) 28 | http_parser.rb (0.6.0) 29 | listen (3.0.3) 30 | rb-fsevent (>= 0.9.3) 31 | rb-inotify (>= 0.9) 32 | lumberjack (1.0.9) 33 | method_source (0.8.2) 34 | multi_json (1.11.2) 35 | nenv (0.2.0) 36 | notiffany (0.0.7) 37 | nenv (~> 0.1) 38 | shellany (~> 0.0) 39 | pry (0.10.1) 40 | coderay (~> 1.1.0) 41 | method_source (~> 0.8.1) 42 | slop (~> 3.4) 43 | rb-fsevent (0.9.5) 44 | rb-inotify (0.9.5) 45 | ffi (>= 0.5.0) 46 | shellany (0.0.1) 47 | slop (3.6.0) 48 | thor (0.19.1) 49 | 50 | PLATFORMS 51 | ruby 52 | 53 | DEPENDENCIES 54 | guard 55 | guard-livereload 56 | guard-shell 57 | 58 | BUNDLED WITH 59 | 1.10.3 60 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | Bundler.require(:default) 2 | 3 | guard 'shell' do 4 | watch(/book\/[^.].+\.org$/) {|files| 5 | `cask exec emacs --batch -l emacs.el -f org-publish-all` 6 | } 7 | end 8 | 9 | guard 'livereload' do 10 | watch(/public\/[^.].+\.html$/) 11 | end 12 | 13 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Clojure Flavored JavaScript 2 | 3 | This is Draft Version of Book /Clojure Flavored JavaScript/, 4 | it's free to read and contribution welcome. 5 | 6 | source code in the book is in branch [[https://github.com/jcouyang/clojure-flavored-javascript/tree/source][source]] 7 | * Install 8 | the manuscript is written in org-mode, which means you have to install emacs & friends to compile 9 | #+BEGIN_SRC shell 10 | brew install emacs --with-cocoa 11 | brew install cask 12 | cask install 13 | cask exec emacs --batch -l emacs.el -f org-publish-all 14 | #+END_SRC 15 | 16 | then it will generate pdf and html version of this book under =public= folder. 17 | 18 | * BUY EARLY ACCESS VERSION PDF 19 | Comming soon ... 20 | 21 | * License 22 | #+HTML: Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License. 23 | 24 | Pull Request Welcome, please see the contribution guide for how this works for accepting pull requests. 25 | 26 | 27 | ----------------------------- 28 | 29 | * Clojure风格JavaScript编程 30 | 31 | [[https://img3.doubanio.com/lpic/s29070174.jpg]] 32 | 33 | 这里是[[https://book.douban.com/subject/26883736/][《前端函数式攻城指南》]] 的草稿和[[https://github.com/jcouyang/clojure-flavored-javascript/tree/source][源码]], 中文纸质版由电子工业出版社出版。英文版只有电子版。 34 | 35 | * 版权 36 | 本草稿的版权为: 37 | #+HTML: 創用 CC 授權條款
本著作係採用創用 CC 姓名標示-非商業性-禁止改作 3.0 未本地化 授權條款授權. 38 | -------------------------------------------------------------------------------- /book/chapter-1.org: -------------------------------------------------------------------------------- 1 | * Functional JavaScript 2 | 3 | This is an introducing chapter will explain the following: 4 | 5 | 1. Why JavaScript is a functional language? 6 | 2. Why do we care? 7 | 3. Why underscore is not functional? 8 | 4. What's missing in JavaScript as a functional language? 9 | 10 | ** Before Reading This Book, You Better... 11 | 12 | *** Be able to read JavaScript code, that's it 13 | It's neither a textbook for Clojure nor for JavaScript, of course I'm gonna borrow the functional part of clojure and introduce into JavaScript, but it's all about functional programming in JavaScript, Clojure code in this book is relatively simple and for demonstrating functional thinking. 14 | 15 | **** You bought the wrong book if you're 16 | ***** trying to learn JavaScript 17 | Still, it's not textbook for JavaScript, for whoever want to learn JavaScript I highly recommend /JavaScript The Good Parts/ and [[https://leanpub.com/javascriptallongesix/read][JavaScript Allong]]. 18 | ***** trying to learn Clojure 19 | Again, it's not a textbook for Clojure/Script either, all Clojure snippet in this book is for demonstrating purpose only, for whoever want to learn Clojure, /The Joy of Clojure/ is a good choice, and alternately, [[http://braveclojure.com/][Brave Clojure]] for online reading. 20 | **** For expert of functional language 21 | If you already programming with languages such as Scala, Clojure or Haskell, you're probably not going to get much benafit from the functional part of this book. Howerver, if you want to migrate to JavaScript, this book will be pretty helpful for you, for you will find similar approach from those languages in javascript. 22 | *** Env Setup 23 | Before start reading, if you like to run all the example code in this book, you better setup a cozy environment first. it won't take long :) 24 | **** JavaScript 25 | 26 | There are two options you can run native javascript code, Modern Browser(Firefox,Chrome) and Node.js. Eventually, there will be some code including macro that which require sweet.js to compile. Alternately, pasting codes in http://ru-lang.org/try is a good choice though. 27 | ***** Install Node/iojs 28 | - Download from https://nodejs.org/ 29 | - or if you're using mac, just 30 | #+BEGIN_SRC sh 31 | brew install node 32 | #+END_SRC 33 | 3. using n to maintain multiple version of node 34 | 35 | #+BEGIN_SRC sh 36 | npm install n -g 37 | # then 38 | n install stable 39 | #+END_SRC 40 | ***** Install sweet.js 41 | #+BEGIN_SRC sh 42 | npm install -g sweet.js 43 | #+END_SRC 44 | **** Clojure 45 | If you like to run all clojure expamle as well, please make sure you have [[http://www.oracle.com/technetwork/java/javase/downloads/index.html][JVM/JDK]] installed first, then... 46 | ***** install leiningen 47 | leiningen package manager of clojure, equivalent of npm to node, bundle to ruby, pip to ruby. Plus, you can also using leinigen as scaffolding tool. you can get install instruction from [[http://leiningen.org/][official website]]。 48 | 49 | then, all clojure code can be safely running in repl 50 | #+BEGIN_SRC sh 51 | lein repl 52 | user=> (+ 1 1) 53 | 2 54 | 55 | #+END_SRC 56 | ***** Editor 57 | [[http://lighttable.com/][LightTable]] is good clojure editor for non-emacs user.Off cause emacs still the best editor I ever used. 58 | 59 | ** So, Functional JavaScript, you sure? 60 | When people taking about functional language, probably Haskell, Scala, Clojure will come to your mind first. But, JavaScript can be functional as well as those languages. JavaScript is actually a multi-paradigm programming language. 61 | 62 | There are about 3 kinds of programming paradigm we normally using: 63 | /Imperative/, /Object Oriented/ and /Functional/ of cause. (and there is a 4th kind of paradigm: Logic Programming we will take about it in the last chapter). 64 | 65 | *** Imperative 66 | It's the probably most used and less need of design kind of programming paradigm. You basically write program like just some kind of ordered list of commands. 67 | #+BEGIN_SRC javascript 68 | for(var i=0;i<10;i++){ 69 | console.log('command #',i) 70 | } 71 | #+END_SRC 72 | 73 | *** Object Oriented 74 | Also, it's very common, and with a little bit design needed while you programming. So when you programming, you modeling or abstracting you App like Objects in the real world. 75 | Objects can be compose by other objects, or inherent from other objects. 76 | 77 | *** Functional 78 | It's like solving Math problems, you need to abstract some sort of expressions, and eval them with known input, the you get a value based on that input. But it's never such simple, when you abstract a good expression, it better be /pure/, /immutable/, /composable/... 79 | 80 | #+caption: Programming Paradigms 81 | [[./images/paradigm.png]] 82 | 83 | *** Logic 84 | You may not hear this term very often, but it's basically just like SQL query which we may sometimes writing. So, it's just questions, for example if I want to count how many foot 3 chickens have, simply just multiply chicken number and foot number each chickens have. But what if I ask "how many foot chicken boys have?", how can you get a function to give the answer? with SQL query it's easy: 85 | #+BEGIN_SRC sql 86 | select count(*) from chicken where sex='boy' 87 | #+END_SRC 88 | 89 | so for Logic Programming, Clojure provide a handy library called =core.logic=, but I'm going to talk about [[https://github.com/tonsky/datascript/][datascript]] in the last chapter for explaining more about logic programming in JavaScript. 90 | 91 | 92 | *** JavaScript Native Functional Support 93 | Let's focus on how functional JavaScript can be? 94 | 95 | **** First Class Function 96 | Fist class function means function that can be treat as value, so it can get anywhere value can. So just like a value, a function can be other function's parameter, a function can be return by other function, and that *other function* is aka /Higher-order Function/. 97 | 98 | ***** Function as Parameter 99 | So one typical usage of function as parameter is =map=, so you can 100 | #+BEGIN_SRC javascript 101 | [1,2,3,4].map(function(x){return ++x}) 102 | // => [2,3,4,5] 103 | #+END_SRC 104 | 105 | Think about how we gonna implement the same thing if not using map: 106 | 107 | #+BEGIN_SRC js 108 | var array = [1,2,3,4]; 109 | var result = []; 110 | for(var i in array){ 111 | result.push(++i); 112 | } 113 | console.log(result); 114 | #+END_SRC 115 | 116 | #+RESULTS: 117 | | 1 | 2 | 3 | 4 | 118 | 119 | The old imperative code just look more lower lever, no any abstraction at all, programmer need to think about nearly every step the machine can understand. But with the abstraction of =map= for /Array/, we kind of just don't care about how the Array should be iterated -- the process, we need to specify what we want to do with each element, and Array will take care of everything else. 120 | 121 | ***** Return a Function 122 | You may seeing this eventually and maybe you didn't know. They have very fancy name depends how we return the function. 123 | 124 | ****** Currying 125 | A function that consume one argument at a time is called /Curried Function/. how to make it so is called /Curry/. 126 | 127 | #+BEGIN_SRC js 128 | var curriedSum = curry(sum) 129 | var sum5 = curriedSum(5) 130 | var sum5and4 = sum5(4) //=> 9 131 | sum5and4(3) // => 12 132 | #+END_SRC 133 | 134 | The rationale of doing so is simple, we can configure part of the function and reuse it later, so every function can be composable when giving it each parameter. 135 | I will talk more about Curry in Chapter 4 anyway. 136 | 137 | ****** Thunk 138 | A thunk is something not executed yet, but waiting someone to click the *Button*. I'll explain thunk in Section 2.4 - /Lazy Sequence/. 139 | 140 | #+CAPTION: Thunk is like a container with a open button 141 | [[./images/thunk.png]] 142 | 143 | 144 | ** The Missing Things from JavaScript for a Functional Language 145 | JavaScript is always rekon as a prototype-based language, but first-class function support made everthing functional also possiable in JavaScript. But we need tools and libs to make JavaScript more functional. 146 | 147 | *** Immutable Data Structure 148 | 149 | First thing JS really missing is /Immutable Datastrcuture/, which mean thing that once created can never be changed. What I mean missing is, for example, a very common code in JS like this: 150 | #+BEGIN_SRC js 151 | var a = [1, 2, 3]; 152 | a.push(4); 153 | #+END_SRC 154 | 155 | Now, a is something else, not =[1, 2, 3]= anymore, it's modified to =[1, 2, 3, 4]=. 156 | 157 | Comparing to Clojure, who's datastructure is almost all immutable, we cannot do anything to modify a thing, instead, it should return a new one instead. 158 | 159 | #+BEGIN_QUOTE 160 | There're 6 primitive types in JavaScript, Boolean, Null, Undefined, Number and String(and Symbol in ES6). All primitive type is immutable, so Object is only mutable type. 161 | #+END_QUOTE 162 | 163 | #+BEGIN_SRC clojure 164 | (def a [1 2 3]) 165 | (conj a 4) ;; => [1 2 3 4] 166 | a ;; => [1 2 3] 167 | #+END_SRC 168 | 169 | #+RESULTS: 170 | : #'user/a[1 2 3 4][1 2 3] 171 | 172 | As you can see, nothing is change here, a is still =[1 2 3]=, but =conj= return a pretty new Vector, instead of modifing a. 173 | 174 | *** Lazy evaluation 175 | /Lazy evaluation/ means something has not happen yet, unless you need it. The opposite one is called /eager evaluation/. 176 | 177 | #+BEGIN_SRC js 178 | wholeNameOf(getFirstName(), getLastName()) 179 | #+END_SRC 180 | 181 | the evaluation order of =wholeNameOf= in JavaScript will be: 182 | 1. =getFirstName= 183 | 2. =getLastName= 184 | 3. expressions in =wholeNameOf='s body 185 | 186 | so something like this in JavaScript will eval the result to you immediately: 187 | #+Begin_src js 188 | map(function(x) {return ++x}, [1, 2, 3, 4]); 189 | #+END_SRC 190 | 191 | And this will lead to very slow performance if the list is really large or infinite. 192 | 193 | so in clojure, you never get result from =map=, unless you use it. 194 | #+BEGIN_SRC clojure 195 | (map inc [1 2 3 4]) 196 | ;; A lazy sequence 197 | (take 3 (map inc [1 2 3 4])) 198 | ;; (1 2 3) 199 | #+END_SRC 200 | 201 | *** Composable Function 202 | Instead of abstracting object and inherent one from another, Functional Programming tend to abstract actions to functions, and compose what you have to a new function. 203 | 204 | But composing function in neither JavaScript nor popular lib such as Underscore is not as convenient as Functional Programming Language. 205 | I'll talk about more about functional composition in Chapter 4. 206 | 207 | *** Tail Recursion Optimization 208 | Recursion is very important concept in FP, but since we're in 209 | Tail Recursion is actually very special form of loop in FP, unlike other FP languages, JavaScript doesn't have a proper way to optimize tail recursion into a loop. 210 | 211 | #+BEGIN_SRC js 212 | var a = [1, 2, 3, 4] 213 | var b = [4, 3, 2, 1] 214 | for (var i = 0; i < 4; i++){ 215 | a[i]+=b[i] 216 | } 217 | console.log(a); 218 | // => [5,5,5,5] 219 | #+END_SRC 220 | 221 | Such code in JavaScript will be the same as in Clojure: 222 | 223 | #+BEGIN_SRC clojure 224 | (loop [a [1 2 3 4] 225 | b [4 3 2 1] 226 | i (dec (count a))] 227 | (if (< i 0) a 228 | (recur (update a i #(+ % (get b i))) b (dec i)))) 229 | #+End_src 230 | 231 | Looks very similar, but actually the Clojure way of loop is actually functional, since no variable or mutation happen in this =loop recur=. 232 | 233 | =recur= is like tail recursively calling =loop= again and again, the recursion will not end until the =if= express return =true=. 234 | 235 | Also, you can turn the =loop= expression into a function: 236 | #+BEGIN_SRC clojure 237 | (defn zipping-add [a b i] 238 | (if (< i 0) a 239 | (recur (update a i #(+ % (get b i))) b (dec i)))) 240 | (zipping-add [1 2 3 4] [4 3 2 1] 3) 241 | #+END_SRC 242 | 243 | It return the same result as =loop recur= expression. 244 | 245 | ** Well, Underscore You are Doing it Wrong! 246 | If you familiar with Undercore, or lodash, I think the first impression would be that they are functional programming library. 247 | 248 | But I doubt that eversince I saw the =map= method from underscore: 249 | #+BEGIN_SRC js 250 | _.map([1,2,3], function(x){return x+1}) 251 | #+END_SRC 252 | 253 | Well, seems nothing is wrong about it, take a array, map over each of them with the function following. 254 | 255 | But if you have a glance at other languages or libs: 256 | 257 | [[http://ramdajs.com/][*ramdajs*]]: 258 | #+BEGIN_SRC js 259 | R.map(function(x){return x+1}, [1,2,3]) 260 | #+END_SRC 261 | 262 | [[http://functionaljs.com/][*functionaljs:*]] 263 | #+BEGIN_SRC js 264 | fjs.map(function(x){return x+1}, [1,2,3]) 265 | #+END_SRC 266 | 267 | *Clojure:* 268 | #+BEGIN_SRC clojure 269 | (map inc [1 2 3]) 270 | #+END_SRC 271 | 272 | *Haskell:* 273 | #+BEGIN_SRC haskell 274 | map (1+) [1,2,3] 275 | #+END_SRC 276 | 277 | Did you notice that the array actually always come as the last parameter. 278 | Got to be something wrong with underscore. 279 | 280 | ** ClojureScript 281 | 282 | To understand why underscore doing it wrong, I can take any of those languages or libs, and descript how it doing thing this way and why it's more reasonable and better. Howerver, I tend to choose one of the most simple and eligant language -- Clojure. Since I'm talking about JavaScript, ClojureScript become the first choice to compare and discuss about. Even further, with library such as [[https://github.com/swannodette/mori][mori]][fn:1], we can event get almost all the benefits from ClojureScript for JavaScript. 283 | 284 | ** Mori 285 | 286 | Since it's ported from ClojureScript, which means you can do anything same as ClojureScript through JavaScript API. It's perfect amend to the fact that missing persistent datastructure in JavaScript. 287 | 288 | Of course it's no the only option to get persistent datastructure in JavaScript. Facebook has a very nice opensource lib =Immutable.js= as well. Howerver, mori is more suitable for undestanding FP. 289 | 290 | 291 | * Footnotes 292 | 293 | [fn:1] ClojureScript Persistent Datastrcuture ported to JavaScript by David Nolen. 294 | -------------------------------------------------------------------------------- /book/images/REPL-when-not.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/REPL-when-not.png -------------------------------------------------------------------------------- /book/images/REPL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/REPL.png -------------------------------------------------------------------------------- /book/images/The-Prestige.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/The-Prestige.png -------------------------------------------------------------------------------- /book/images/Type_N_Search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/Type_N_Search.png -------------------------------------------------------------------------------- /book/images/a-monad-is-just-a-monoid-in-the-category-of-endofunctors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/a-monad-is-just-a-monoid-in-the-category-of-endofunctors.png -------------------------------------------------------------------------------- /book/images/applicative-functor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/applicative-functor.png -------------------------------------------------------------------------------- /book/images/applicative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/applicative.png -------------------------------------------------------------------------------- /book/images/big-bang-lego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/big-bang-lego.png -------------------------------------------------------------------------------- /book/images/call-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/call-stack.png -------------------------------------------------------------------------------- /book/images/compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/compose.png -------------------------------------------------------------------------------- /book/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/cover.png -------------------------------------------------------------------------------- /book/images/csp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/csp.png -------------------------------------------------------------------------------- /book/images/event-loop-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/event-loop-model.png -------------------------------------------------------------------------------- /book/images/everyscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/everyscript.png -------------------------------------------------------------------------------- /book/images/flatmap-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/flatmap-stream.png -------------------------------------------------------------------------------- /book/images/func-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/func-value.png -------------------------------------------------------------------------------- /book/images/functor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/functor.png -------------------------------------------------------------------------------- /book/images/interstellar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/interstellar.png -------------------------------------------------------------------------------- /book/images/lazy-oreo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/lazy-oreo.png -------------------------------------------------------------------------------- /book/images/lift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/lift.png -------------------------------------------------------------------------------- /book/images/message-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/message-queue.png -------------------------------------------------------------------------------- /book/images/monad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/monad.png -------------------------------------------------------------------------------- /book/images/monoid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/monoid.png -------------------------------------------------------------------------------- /book/images/multithread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/multithread.png -------------------------------------------------------------------------------- /book/images/mutable-push.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/mutable-push.png -------------------------------------------------------------------------------- /book/images/paradigm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/paradigm.png -------------------------------------------------------------------------------- /book/images/patten-matching.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/patten-matching.jpg -------------------------------------------------------------------------------- /book/images/persistent-conj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/persistent-conj.png -------------------------------------------------------------------------------- /book/images/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/pipeline.png -------------------------------------------------------------------------------- /book/images/react-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/react-flow.png -------------------------------------------------------------------------------- /book/images/react-flow.png.cache: -------------------------------------------------------------------------------- 1 | {"checksum":"05e5d77fe8d2fe5013656e346aa5a7f7","width":706,"height":555} -------------------------------------------------------------------------------- /book/images/simpsons-lego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/simpsons-lego.png -------------------------------------------------------------------------------- /book/images/tail-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/tail-off.png -------------------------------------------------------------------------------- /book/images/tailoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/tailoff.png -------------------------------------------------------------------------------- /book/images/thunk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/thunk.png -------------------------------------------------------------------------------- /book/images/update-dom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/update-dom.png -------------------------------------------------------------------------------- /book/images/update-dom.png.cache: -------------------------------------------------------------------------------- 1 | {"checksum":"49fffe72a6d192975dbdb343f319b454","width":710,"height":263} -------------------------------------------------------------------------------- /book/images/vec-conj-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vec-conj-11.png -------------------------------------------------------------------------------- /book/images/vec-conj-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vec-conj-8.png -------------------------------------------------------------------------------- /book/images/vec-conj-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vec-conj-9.png -------------------------------------------------------------------------------- /book/images/vec-conj11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vec-conj11.png -------------------------------------------------------------------------------- /book/images/vec-conj8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vec-conj8.png -------------------------------------------------------------------------------- /book/images/vec-conj9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vec-conj9.png -------------------------------------------------------------------------------- /book/images/vecconj11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vecconj11.png -------------------------------------------------------------------------------- /book/images/vecconj8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vecconj8.png -------------------------------------------------------------------------------- /book/images/vecconj9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/vecconj9.png -------------------------------------------------------------------------------- /book/images/you_hang_up_first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/images/you_hang_up_first.png -------------------------------------------------------------------------------- /book/index.org: -------------------------------------------------------------------------------- 1 | #+title: Clojure Flavored JavaScript 2 | #+author: Jichao Ouyang 3 | #+keywords: clojure, javascript, functional, mori, clojurescript 4 | #+OPTIONS: num:3 5 | 6 | #+include: "./chapter-1.org" 7 | -------------------------------------------------------------------------------- /book/style/fonts/droid-sans-mono.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/droid-sans-mono.woff -------------------------------------------------------------------------------- /book/style/fonts/droid-sans-mono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/droid-sans-mono.woff2 -------------------------------------------------------------------------------- /book/style/fonts/droid-sans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/droid-sans.woff -------------------------------------------------------------------------------- /book/style/fonts/droid-sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/droid-sans.woff2 -------------------------------------------------------------------------------- /book/style/fonts/droid-serif.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/droid-serif.woff -------------------------------------------------------------------------------- /book/style/fonts/droid-serif.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/droid-serif.woff2 -------------------------------------------------------------------------------- /book/style/fonts/merriweater-heavy-itaic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/merriweater-heavy-itaic.woff2 -------------------------------------------------------------------------------- /book/style/fonts/merriweater-heavy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/merriweater-heavy.woff2 -------------------------------------------------------------------------------- /book/style/fonts/merriweater-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/merriweater-italic.woff2 -------------------------------------------------------------------------------- /book/style/fonts/merriweater-light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/style/fonts/merriweater-light.woff2 -------------------------------------------------------------------------------- /book/style/gh-fork-ribbon.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * "Fork me on GitHub" CSS ribbon v0.1.1 | MIT License 3 | * https://github.com/simonwhitaker/github-fork-ribbon-css 4 | */ 5 | 6 | /* Left will inherit from right (so we don't need to duplicate code) */ 7 | .github-fork-ribbon { 8 | /* The right and left classes determine the side we attach our banner to */ 9 | position: absolute; 10 | 11 | /* Add a bit of padding to give some substance outside the "stitching" */ 12 | padding: 2px 0; 13 | 14 | /* Set the base colour */ 15 | background-color: #a00; 16 | 17 | /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */ 18 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0)), to(rgba(0, 0, 0, 0.15))); 19 | background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 20 | background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 21 | background-image: -ms-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 22 | background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 23 | background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)); 24 | 25 | /* Add a drop shadow */ 26 | -webkit-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); 27 | -moz-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); 28 | box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5); 29 | 30 | /* Set the font */ 31 | font: 700 13px "Helvetica Neue", Helvetica, Arial, sans-serif; 32 | 33 | z-index: 9999; 34 | pointer-events: auto; 35 | } 36 | 37 | .github-fork-ribbon a, 38 | .github-fork-ribbon a:hover { 39 | /* Set the text properties */ 40 | color: #fff; 41 | text-decoration: none; 42 | text-shadow: 0 -1px rgba(0, 0, 0, 0.5); 43 | text-align: center; 44 | 45 | /* Set the geometry. If you fiddle with these you'll also need 46 | to tweak the top and right values in .github-fork-ribbon. */ 47 | width: 200px; 48 | line-height: 20px; 49 | 50 | /* Set the layout properties */ 51 | display: inline-block; 52 | padding: 2px 0; 53 | 54 | /* Add "stitching" effect */ 55 | border-width: 1px 0; 56 | border-style: dotted; 57 | border-color: #fff; 58 | border-color: rgba(255, 255, 255, 0.7); 59 | } 60 | 61 | .github-fork-ribbon-wrapper { 62 | width: 150px; 63 | height: 150px; 64 | position: absolute; 65 | overflow: hidden; 66 | top: 0; 67 | z-index: 9999; 68 | pointer-events: none; 69 | } 70 | 71 | .github-fork-ribbon-wrapper.fixed { 72 | position: fixed; 73 | } 74 | 75 | .github-fork-ribbon-wrapper.left { 76 | left: 0; 77 | } 78 | 79 | .github-fork-ribbon-wrapper.right { 80 | right: 0; 81 | } 82 | 83 | .github-fork-ribbon-wrapper.left-bottom { 84 | position: fixed; 85 | top: inherit; 86 | bottom: 0; 87 | left: 0; 88 | } 89 | 90 | .github-fork-ribbon-wrapper.right-bottom { 91 | position: fixed; 92 | top: inherit; 93 | bottom: 0; 94 | right: 0; 95 | } 96 | 97 | .github-fork-ribbon-wrapper.right .github-fork-ribbon { 98 | top: 42px; 99 | right: -43px; 100 | 101 | -webkit-transform: rotate(45deg); 102 | -moz-transform: rotate(45deg); 103 | -ms-transform: rotate(45deg); 104 | -o-transform: rotate(45deg); 105 | transform: rotate(45deg); 106 | } 107 | 108 | .github-fork-ribbon-wrapper.left .github-fork-ribbon { 109 | top: 42px; 110 | left: -43px; 111 | 112 | -webkit-transform: rotate(-45deg); 113 | -moz-transform: rotate(-45deg); 114 | -ms-transform: rotate(-45deg); 115 | -o-transform: rotate(-45deg); 116 | transform: rotate(-45deg); 117 | } 118 | 119 | 120 | .github-fork-ribbon-wrapper.left-bottom .github-fork-ribbon { 121 | top: 80px; 122 | left: -43px; 123 | 124 | -webkit-transform: rotate(45deg); 125 | -moz-transform: rotate(45deg); 126 | -ms-transform: rotate(45deg); 127 | -o-transform: rotate(45deg); 128 | transform: rotate(45deg); 129 | } 130 | 131 | .github-fork-ribbon-wrapper.right-bottom .github-fork-ribbon { 132 | top: 80px; 133 | right: -43px; 134 | 135 | -webkit-transform: rotate(-45deg); 136 | -moz-transform: rotate(-45deg); 137 | -ms-transform: rotate(-45deg); 138 | -o-transform: rotate(-45deg); 139 | transform: rotate(-45deg); 140 | } 141 | -------------------------------------------------------------------------------- /book/style/gh-fork-ribbon.ie.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * "Fork me on GitHub" CSS ribbon v0.1.1 | MIT License 3 | * https://github.com/simonwhitaker/github-fork-ribbon-css 4 | */ 5 | 6 | /* IE voodoo courtesy of http://stackoverflow.com/a/4617511/263871 and 7 | * http://www.useragentman.com/IETransformsTranslator */ 8 | 9 | .github-fork-ribbon { 10 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#000000', EndColorStr='#000000'); 11 | } 12 | 13 | .github-fork-ribbon-wrapper.right .github-fork-ribbon { 14 | /* IE positioning hack (couldn't find a transform-origin alternative for IE) */ 15 | top: -22px; 16 | right: -62px; 17 | 18 | /* IE8+ */ 19 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.7071067811865474, M12=-0.7071067811865477, M21=0.7071067811865477, M22=0.7071067811865474, SizingMethod='auto expand')"; 20 | /* IE6 and 7 */ 21 | filter: progid:DXImageTransform.Microsoft.Matrix( 22 | M11=0.7071067811865474, 23 | M12=-0.7071067811865477, 24 | M21=0.7071067811865477, 25 | M22=0.7071067811865474, 26 | SizingMethod='auto expand' 27 | ); 28 | } 29 | 30 | .github-fork-ribbon-wrapper.left .github-fork-ribbon { 31 | top: -22px; 32 | left: -22px; 33 | 34 | /* IE8+ */ 35 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.7071067811865483, M12=0.7071067811865467, M21=-0.7071067811865467, M22=0.7071067811865483, SizingMethod='auto expand')"; 36 | /* IE6 and 7 */ 37 | filter: progid:DXImageTransform.Microsoft.Matrix( 38 | M11=0.7071067811865483, 39 | M12=0.7071067811865467, 40 | M21=-0.7071067811865467, 41 | M22=0.7071067811865483, 42 | SizingMethod='auto expand' 43 | ); 44 | } 45 | 46 | .github-fork-ribbon-wrapper.left-bottom .github-fork-ribbon { 47 | /* IE positioning hack (couldn't find a transform-origin alternative for IE) */ 48 | top: 12px; 49 | left: -22px; 50 | 51 | 52 | /* IE8+ */ 53 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.7071067811865474, M12=-0.7071067811865477, M21=0.7071067811865477, M22=0.7071067811865474, SizingMethod='auto expand')"; 54 | /* IE6 and 7 */ 55 | /* filter: progid:DXImageTransform.Microsoft.Matrix( 56 | M11=0.7071067811865474, 57 | M12=-0.7071067811865477, 58 | M21=0.7071067811865477, 59 | M22=0.7071067811865474, 60 | SizingMethod='auto expand' 61 | ); 62 | */} 63 | 64 | .github-fork-ribbon-wrapper.right-bottom .github-fork-ribbon { 65 | top: 12px; 66 | right: -62px; 67 | 68 | /* IE8+ */ 69 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.7071067811865483, M12=0.7071067811865467, M21=-0.7071067811865467, M22=0.7071067811865483, SizingMethod='auto expand')"; 70 | /* IE6 and 7 */ 71 | filter: progid:DXImageTransform.Microsoft.Matrix( 72 | M11=0.7071067811865483, 73 | M12=0.7071067811865467, 74 | M21=-0.7071067811865467, 75 | M22=0.7071067811865483, 76 | SizingMethod='auto expand' 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /book/style/pixyll.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | @font-face { 3 | font-family: 'Merriweather'; 4 | font-style: normal; 5 | font-weight: 300; 6 | src: local('Merriweather Light'), local('Merriweather-Light'), url(fonts/merriweater-light.woff2) format('woff2'); 7 | } 8 | @font-face { 9 | font-family: 'Merriweather'; 10 | font-style: normal; 11 | font-weight: 900; 12 | src: local('Merriweather Heavy'), local('Merriweather-Heavy'), url(fonts/merriweater-heavy.woff2) format('woff2'); 13 | } 14 | @font-face { 15 | font-family: 'Merriweather'; 16 | font-style: italic; 17 | font-weight: 300; 18 | src: local('Merriweather Light Italic'), local('Merriweather-LightItalic'), url(fonts/merriweater-italic.woff2) format('woff2'); 19 | } 20 | @font-face { 21 | font-family: 'Merriweather'; 22 | font-style: italic; 23 | font-weight: 900; 24 | src: local('Merriweather Heavy Italic'), local('Merriweather-HeavyItalic'), url(fonts/merriweater-heavy-italic.woff2) format('woff2'); 25 | } 26 | @charset "UTF-8"; 27 | /* 28 | BASSCSS 29 | Next-level CSS toolkit - http://jxnblk.github.io/basscss 30 | Made with love by Jxnblk - ©2014 Brent Jackson MIT License http://opensource.org/licenses/MIT 31 | 32 | */ 33 | body, button, input, select, textarea, pre { margin: 0; } 34 | 35 | hr { border: .5px solid rgb(243, 243, 243);} 36 | 37 | h1, h2, h3, h4, h5, h6, p, dl, ol, ul { margin-top: 0; margin-bottom: 1rem; } 38 | 39 | button, input, select, textarea { font-family: inherit; font-size: 100%; } 40 | 41 | article, aside, details, figcaption, figure, footer, header, main, nav, section, summary { display: block; } 42 | 43 | body { font-family: 'Helvetica Neue', Helvetica, sans-serif; line-height: 1.5rem; font-weight: 400; } 44 | 45 | h1, .h1, .f1 { font-size: 2rem; line-height: 2.5rem; } 46 | 47 | h2, .h2, .f2 { font-size: 1.5rem; line-height: 2rem; } 48 | 49 | h3, .h3, .f3 { font-size: 1.25rem; line-height: 1.5rem; } 50 | 51 | p, .p, .f4, h4, h5, h6, dl, ol, ul { font-size: 1.125rem; line-height: 1.5rem; } 52 | 53 | small, .small, .f5 { font-size: .875rem; line-height: 1.5rem; } 54 | 55 | @media screen and (min-width: 48em) { .h1 { font-size: 4rem; line-height: 5rem; } 56 | .h2 { font-size: 2rem; line-height: 2.5rem; } 57 | .h3 { font-size: 1.5rem; line-height: 2rem; } 58 | .p { font-size: 1.25rem; line-height: 2rem; } 59 | .small { font-size: 1rem; } } 60 | @media screen and (min-width: 64em) { .h1 { font-size: 6rem; line-height: 8rem; } 61 | .h2 { font-size: 3rem; line-height: 3.5rem; } } 62 | strong, .bold { font-weight: bold; } 63 | 64 | #content,#postamble { max-width: 48rem; } 65 | 66 | #content, #postamble { margin-right: auto; margin-left: auto; } 67 | body { padding: 1.5rem; } 68 | 69 | @media screen and (min-width: 48em) and (max-width: 64em) { .p-responsive { padding: 3rem; } 70 | .px-responsive { padding-right: 3rem; padding-left: 3rem; } 71 | .py-responsive { padding-top: 3rem; padding-bottom: 3rem; } } 72 | @media screen and (min-width: 64em) { .p-responsive { padding: 4rem; } 73 | .px-responsive { padding-right: 4rem; padding-left: 4rem; } 74 | .py-responsive { padding-top: 4rem; padding-bottom: 4rem; } } 75 | body { color: #333333; background-color: #fff; } 76 | 77 | a { color: #0096cc; text-decoration: none; } 78 | 79 | a:hover { color: #007199; } 80 | 81 | 82 | /*! 83 | * Pixyll - A simple, beautiful Jekyll theme that's mobile first. 84 | * @author John Otander (http://johnotander.com/) 85 | * @link https://github.com/johnotander/pixyll 86 | * @license MIT 87 | */ 88 | /*! 89 | 90 | BASSCSS 91 | 92 | Next-level CSS toolkit - basscss.com 93 | 94 | Made with love by Jxnblk - ©2014 Brent Jackson 95 | MIT License http://opensource.org/licenses/MIT 96 | 97 | */ 98 | /* Basscss Base Buttons */ 99 | button, .button { font-family: "Lato", "Helvetica Neue", Helvetica, sans-serif; font-size: inherit; font-weight: normal; text-decoration: none; cursor: pointer; display: inline-block; box-sizing: border-box; line-height: 1.125rem; padding: 0.5rem 1rem; margin: 0; height: auto; border: 1px solid transparent; vertical-align: middle; -webkit-appearance: none; } 100 | 101 | ::-moz-focus-inner { border: 0; padding: 0; } 102 | 103 | .button:hover { text-decoration: none; } 104 | 105 | /* Basscss Base Forms */ 106 | input, select, textarea, fieldset { font-size: 1rem; margin-top: 0; margin-bottom: 0.5rem; } 107 | 108 | input[type=text], input[type=datetime], input[type=datetime-local], input[type=email], input[type=month], input[type=number], input[type=password], input[type=search], input[type=tel], input[type=time], input[type=url], input[type=week] { box-sizing: border-box; height: 2.25rem; padding: 0.5rem 0.5rem; vertical-align: middle; -webkit-appearance: none; } 109 | 110 | select { box-sizing: border-box; line-height: 1.75; padding: 0.5rem 0.5rem; } 111 | 112 | select:not([multiple]) { height: 2.25rem; vertical-align: middle; } 113 | 114 | textarea { box-sizing: border-box; line-height: 1.75; padding: 0.5rem 0.5rem; } 115 | 116 | .fieldset-reset { padding: 0; margin-left: 0; margin-right: 0; border: 0; } 117 | 118 | .fieldset-reset legend { padding: 0; } 119 | 120 | body, button { margin: 0; } 121 | 122 | button, input, select, textarea { font-family: inherit; font-size: 100%; } 123 | 124 | img { max-width: 100%; } 125 | 126 | svg { max-height: 100%; } 127 | 128 | /* Basscss Base Typography */ 129 | body { font-family: "Merriweather", "PT Serif", Georgia, "Times New Roman", serif; line-height: 1.5; font-size: 100%; } 130 | 131 | h1, h2, h3, h4, h5, h6 { font-family: "Lato", "Helvetica Neue", Helvetica, sans-serif; font-weight: 900; line-height: 1.25; margin-top: 1em; margin-bottom: .5em; } 132 | 133 | p, dl, ol, ul { font-size: 1rem; margin-top: 0; margin-bottom: 1rem; } 134 | 135 | ol, ul { padding-left: 2rem; } 136 | 137 | pre, code, samp { font-family: "Source Code Pro", Consolas, monospace; font-size: inherit; } 138 | 139 | pre { margin-top: 0; margin-bottom: 1rem; overflow-x: scroll; } 140 | 141 | hr { margin-top: 2rem; margin-bottom: 2rem; } 142 | 143 | blockquote { margin-top: 2rem; margin-bottom: 2rem; margin-left: 0; padding-left: 1rem; padding-right: 1rem; } 144 | 145 | blockquote, blockquote p { font-size: 1.25rem; font-style: italic; } 146 | 147 | h1, .h1 { font-size: 2.998rem; } 148 | 149 | h2, .h2 { font-size: 1.5rem; } 150 | 151 | h3, .h3 { font-size: 1.25rem; } 152 | 153 | h4, .h4 { font-size: 1rem; } 154 | 155 | h5, .h5 { font-size: 0.875rem; } 156 | 157 | h6, .h6 { font-size: 0.75rem; } 158 | 159 | .list-reset { list-style: none; padding-left: 0; } 160 | 161 | .button-blue { color: white; background-color: #0076df; border-radius: 3px; transition-duration: .1s; transition-timing-function: ease-out; transition-property: box-shadow, background-color; } 162 | 163 | .button-blue:hover { opacity: .875; } 164 | 165 | .button-blue:active, .button-blue.is-active { box-shadow: inset 0 0 0 32px rgba(0, 0, 0, 0.125), inset 0 2px 3px 0 rgba(0, 0, 0, 0.25); } 166 | 167 | .button-blue:focus { outline: none; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5), 0 0 1px 4px rgba(0, 118, 223, 0.5); } 168 | 169 | .button-blue:disabled, .button-blue.is-disabled { opacity: .5; } 170 | 171 | .highlight { -webkit-text-size-adjust: none; } 172 | 173 | .highlight .c, .highlight .cs, .highlight .cm, .highlight .cp, .highlight .c1 { color: #777; font-style: italic; } 174 | 175 | .highlight .k, .highlight .kc, .highlight .kd, .highlight .kn, .highlight .kr, .highlight .kt, .highlight .kp { color: #00369f; } 176 | 177 | .highlight .na, .highlight .nb, .highlight .nc, .highlight .no, .highlight .nd, .highlight .ni, .highlight .ne, .highlight .nf, .highlight .nl, .highlight .nn, .highlight .nx { color: #333; } 178 | 179 | .highlight .mi, .highlight .il { color: #00ab37; } 180 | 181 | .highlight .s, .highlight .sb, .highlight .sc, .highlight .sd, .highlight .s2, .highlight .s3, .highlight .sh, .highlight .si, .highlight .sx, .highlight .sr, .highlight .ss, .highlight .s1 { color: #f95020; } 182 | 183 | .hljs-title, .hljs-id, .scss .hljs-preprocessor { color: #f95020; font-weight: bold; } 184 | 185 | .highlight .k { font-weight: normal; } 186 | 187 | .highlight .nc, .highlight .no { color: #00369f; } 188 | 189 | .highlight .o { color: #00369f; font-weight: normal; } 190 | 191 | .highlight .nb { color: #00ab37; } 192 | 193 | .highlight .sr { color: #00ab37; } 194 | 195 | .highlight .ss { color: #f92080; } 196 | 197 | .hljs-built_in { color: #00369f; } 198 | 199 | .hljs-preprocessor, .hljs-pragma, .hljs-pi, .hljs-doctype, .hljs-shebang, .hljs-cdata { color: #777; font-weight: bold; } 200 | 201 | .hljs-deletion { background: #ffdddd; } 202 | 203 | .hljs-addition { background: #ddffdd; } 204 | 205 | .diff .hljs-change { background: #00369f; } 206 | 207 | .hljs-chunk { color: #ccc; } 208 | 209 | /* Basscss Color Base */ 210 | body { color: #333; background-color: white; } 211 | 212 | a { color: #0076df; text-decoration: none; } 213 | 214 | a:hover { text-decoration: underline; } 215 | 216 | pre, code { background-color: #eee; border-radius: 3px; } 217 | 218 | hr { border: 0; border-bottom-style: solid; border-bottom-width: 1px; border-bottom-color: #ccc; } 219 | 220 | /* Basscss Colors */ 221 | .dark-gray { color: #333; } 222 | 223 | .white { color: white; } 224 | 225 | .blue { color: #0076df; } 226 | 227 | .mid-gray { color: #777; } 228 | 229 | .light-gray { color: #ccc; } 230 | 231 | .lighter-gray { color: #eee; } 232 | 233 | .red { color: #f95020; } 234 | 235 | .green { color: #00cf26; } 236 | 237 | .yellow { color: #efcc00; } 238 | 239 | .orange { color: #ffcc22; } 240 | 241 | .bg-dark-gray { background-color: #333; } 242 | 243 | .bg-white { background-color: white; } 244 | 245 | .bg-blue { background-color: #0076df; } 246 | 247 | .bg-mid-gray { background-color: #777; } 248 | 249 | .bg-light-gray { background-color: #ccc; } 250 | 251 | .bg-lighter-gray { background-color: #eee; } 252 | 253 | .bg-red { background-color: #f95020; } 254 | 255 | .bg-green { background-color: #00cf26; } 256 | 257 | .bg-yellow { background-color: #efcc00; } 258 | 259 | .bg-orange { background-color: #ffcc22; } 260 | 261 | .bg-darken-1 { background-color: rgba(0, 0, 0, 0.0625); } 262 | 263 | .bg-darken-2 { background-color: rgba(0, 0, 0, 0.125); } 264 | 265 | .bg-darken-3 { background-color: rgba(0, 0, 0, 0.25); } 266 | 267 | .bg-darken-4 { background-color: rgba(0, 0, 0, 0.5); } 268 | 269 | /* Basscss Utility Headings */ 270 | .h00 { font-size: 4rem; } 271 | 272 | .h0 { font-size: 3rem; } 273 | 274 | @media (min-width: 52em) { .h00-responsive { font-size: 8vw; } 275 | .h0-responsive { font-size: 6vw; } 276 | .h1-responsive { font-size: 4vw; } } 277 | @media (min-width: 96em) { .h00-responsive { font-size: 7.68rem; } 278 | .h0-responsive { font-size: 5.76rem; } 279 | .h1-responsive { font-size: 3.84rem; } } 280 | /* Basscss Utility Typography */ 281 | .bold { font-weight: bold; } 282 | 283 | .regular { font-weight: normal; } 284 | 285 | .italic, .post-footer { font-style: italic; } 286 | 287 | .caps { text-transform: uppercase; letter-spacing: .2em; } 288 | 289 | .left-align { text-align: left; } 290 | 291 | .center { text-align: center; } 292 | 293 | .right-align { text-align: right; } 294 | 295 | .justify { text-align: justify; } 296 | 297 | .nowrap { white-space: nowrap; } 298 | 299 | /* Basscss Utility White Space */ 300 | .m0 { margin: 0; } 301 | 302 | .mt0 { margin-top: 0; } 303 | 304 | .mr0 { margin-right: 0; } 305 | 306 | .mb0 { margin-bottom: 0; } 307 | 308 | .ml0 { margin-left: 0; } 309 | 310 | .m1 { margin: 0.5rem; } 311 | 312 | .mt1 { margin-top: 0.5rem; } 313 | 314 | .mr1 { margin-right: 0.5rem; } 315 | 316 | .mb1 { margin-bottom: 0.5rem; } 317 | 318 | .ml1 { margin-left: 0.5rem; } 319 | 320 | .m2 { margin: 1rem; } 321 | 322 | .mt2 { margin-top: 1rem; } 323 | 324 | .mr2 { margin-right: 1rem; } 325 | 326 | .mb2 { margin-bottom: 1rem; } 327 | 328 | .ml2 { margin-left: 1rem; } 329 | 330 | .m3 { margin: 2rem; } 331 | 332 | .mt3 { margin-top: 2rem; } 333 | 334 | .mr3 { margin-right: 2rem; } 335 | 336 | .mb3 { margin-bottom: 2rem; } 337 | 338 | .ml3 { margin-left: 2rem; } 339 | 340 | .m4 { margin: 4rem; } 341 | 342 | .mt4 { margin-top: 4rem; } 343 | 344 | .mr4 { margin-right: 4rem; } 345 | 346 | .mb4 { margin-bottom: 4rem; } 347 | 348 | .ml4 { margin-left: 4rem; } 349 | 350 | .mxn1 { margin-left: -0.5rem; margin-right: -0.5rem; } 351 | 352 | .mxn2 { margin-left: -1rem; margin-right: -1rem; } 353 | 354 | .mxn3 { margin-left: -2rem; margin-right: -2rem; } 355 | 356 | .mxn4 { margin-left: -4rem; margin-right: -4rem; } 357 | 358 | .mx-auto { margin-left: auto; margin-right: auto; } 359 | 360 | .p1 { padding: 0.5rem; } 361 | 362 | .py1 { padding-top: 0.5rem; padding-bottom: 0.5rem; } 363 | 364 | .px1 { padding-left: 0.5rem; padding-right: 0.5rem; } 365 | 366 | .p2 { padding: 1rem; } 367 | 368 | .py2 { padding-top: 1rem; padding-bottom: 1rem; } 369 | 370 | .px2 { padding-left: 1rem; padding-right: 1rem; } 371 | 372 | .p3 { padding: 2rem; } 373 | 374 | .py3 { padding-top: 2rem; padding-bottom: 2rem; } 375 | 376 | .px3 { padding-left: 2rem; padding-right: 2rem; } 377 | 378 | .p4 { padding: 4rem; } 379 | 380 | .py4 { padding-top: 4rem; padding-bottom: 4rem; } 381 | 382 | .px4 { padding-left: 4rem; padding-right: 4rem; } 383 | 384 | /*! 385 | 386 | Pixyll 387 | 388 | A simple, beautiful theme for Jekyll that emphasizes content rather than 389 | aesthetic fluff. 390 | 391 | Built upon BASSCSS (http://jxnblk.github.io/basscss). 392 | 393 | Crafted with <3 by John Otander (@4lpine) - ©2015 John Otander 394 | MIT License http://opensource.org/licenses/MIT 395 | 396 | */ 397 | html, body { height: auto; min-height: 100%; } 398 | 399 | img { max-width: 100%; } 400 | 401 | em img { max-width: 100%; margin-left: 0; } 402 | 403 | body { box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; } 404 | 405 | a { color: #0076df; } 406 | 407 | a:hover, a:focus, a:active { border: 0; color: #000a13; text-decoration: none; } 408 | 409 | .left { float: left; } 410 | 411 | .right { float: right; } 412 | 413 | .clearfix:before, .clearfix:after { content: ' '; display: table; } 414 | 415 | .clearfix:after { clear: both; } 416 | 417 | pre, pre code { background-color: transparent; border-radius: 0; } 418 | 419 | pre, code { font-family: "Source Code Pro", Consolas, monospace; } 420 | 421 | code { color: #7a7a7a; } 422 | 423 | pre { padding: 1.125em; line-height: 1.11; overflow-x: scroll; margin-bottom: 0.88em; background-color: #fafafa; } 424 | 425 | .highlight .p { font-size: 1.125rem; line-height: 1; } 426 | 427 | pre { counter-reset: line-numbering; white-space: pre; overflow-x: auto; word-break: inherit; word-wrap: inherit; } 428 | 429 | pre a::before { content: counter(line-numbering); counter-increment: line-numbering; padding-right: 1em; /* space after numbers */ width: 25px; text-align: right; opacity: 0.7; display: inline-block; color: #ccc; margin-right: 16px; font-size: 13px; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } 430 | 431 | pre a:first-of-type::before { padding-top: 10px; } 432 | 433 | pre a:last-of-type::before { padding-bottom: 10px; } 434 | 435 | pre a:only-of-type::before { padding: 10px; } 436 | 437 | input, select, textarea, fieldset { font-size: 1rem; margin-top: 0; margin-bottom: 0.5rem, 0.5rem; } 438 | 439 | input[type=text], input[type=datetime], input[type=datetime-local], input[type=email], input[type=month], input[type=number], input[type=password], input[type=search], input[type=tel], input[type=time], input[type=url], input[type=week] { box-sizing: border-box; height: 2.25rem; padding: 0.5rem 0.5rem; vertical-align: middle; -webkit-appearance: none; } 440 | 441 | select { box-sizing: border-box; line-height: 1.75; padding: 0.5rem 0.5rem; } 442 | 443 | select:not([multiple]) { height: 2.25rem; vertical-align: middle; } 444 | 445 | textarea { box-sizing: border-box; line-height: 1.75; padding: 0.5rem 0.5rem; } 446 | 447 | .form-stacked input, .form-stacked textarea, .form-stacked select { width: 100%; } 448 | 449 | .field-light { background-color: white; transition: box-shadow .2s ease; border-style: solid; border-width: 1px; border-color: #ccc; border-radius: 3px; } 450 | 451 | .field-light:focus { outline: none; border-color: #0076df; box-shadow: 0 0 2px rgba(0, 118, 223, 0.5); } 452 | 453 | .field-light:disabled { color: #777; background-color: rgba(0, 0, 0, 0.125); } 454 | 455 | .field-light:read-only:not(select) { background-color: rgba(0, 0, 0, 0.125); } 456 | 457 | .field-light:invalid { border-color: #f95020; } 458 | 459 | .field-light.is-success { border-color: #00cf26; } 460 | 461 | .field-light.is-warning { border-color: #efcc00; } 462 | 463 | .field-light.is-error { border-color: #f95020; } 464 | 465 | .radio-light, .checkbox-light { transition: box-shadow .2s ease; } 466 | 467 | .radio-light { border-radius: 50%; } 468 | 469 | .radio-light:focus, .checkbox-light:focus { outline: none; box-shadow: 0 0 2px rgba(0, 118, 223, 0.5); } 470 | 471 | html { font-size: 14px; } 472 | 473 | abbr { border-bottom: 1px black dotted; cursor: help; } 474 | 475 | p { color: #333; line-height: 1.5; } 476 | 477 | small, .small { font-size: 0.707rem; } 478 | 479 | .site-header { padding-top: 0.5rem; padding-bottom: 1rem; } 480 | 481 | .site-header a { color: #333; font-size: 1.25rem; font-weight: 300; } 482 | 483 | .site-header .site-title { font-size: 1.5rem; } 484 | 485 | .site-nav { margin-top: 1rem; } 486 | 487 | .site-header nav a { color: #666; } 488 | 489 | .site-header nav a:hover, .site-header nav a:focus, .site-header nav a:active { color: #444; opacity: 1; border-bottom: 2px solid #444; } 490 | 491 | .site-nav a + a { margin-left: 1em; } 492 | 493 | .site-header a:hover, .posts .post a:hover .post-meta, .posts .post a:hover .post-title, .posts .post a:hover .post-summary { opacity: 0.88; } 494 | 495 | .site-header { text-align: center; } 496 | 497 | .site-header .site-nav { text-align: center; } 498 | 499 | .social-icons .left, .social-icons .right { text-align: center; float: none; } 500 | 501 | /* Table styles copied from Bootstrap Copyright (c) 2013 Twitter, Inc 502 | */ 503 | table { width: 100%; max-width: 100%; margin-bottom: 1.5; font-size: 1.125rem; } 504 | table > thead > tr > th, table > thead > tr > td, table > tbody > tr > th, table > tbody > tr > td, table > tfoot > tr > th, table > tfoot > tr > td { padding: 12px; line-height: 1.2; vertical-align: top; border-top: 1px solid #333; } 505 | table > thead > tr > th { vertical-align: bottom; border-bottom: 2px solid #333; } 506 | table > caption + thead > tr:first-child > th, table > caption + thead > tr:first-child > td, table > colgroup + thead > tr:first-child > th, table > colgroup + thead > tr:first-child > td, table > thead:first-child > tr:first-child > th, table > thead:first-child > tr:first-child > td { border-top: 0; } 507 | table > tbody + tbody { border-top: 2px solid #333; } 508 | 509 | /*! 510 | Animate.css - http://daneden.me/animate 511 | Licensed under the MIT license - http://opensource.org/licenses/MIT 512 | 513 | Copyright (c) 2014 Daniel Eden 514 | */ 515 | .animated { -webkit-animation-duration: 1s; animation-duration: 1s; -webkit-animation-fill-mode: both; animation-fill-mode: both; } 516 | 517 | .animated.infinite { -webkit-animation-iteration-count: infinite; animation-iteration-count: infinite; } 518 | 519 | .animated.hinge { -webkit-animation-duration: 2s; animation-duration: 2s; } 520 | 521 | @-webkit-keyframes fadeInDown { 0% { opacity: 0; -webkit-transform: translateY(-20px); transform: translateY(-20px); } 522 | 100% { opacity: 1; -webkit-transform: translateY(0); transform: translateY(0); } } 523 | @keyframes fadeInDown { 0% { opacity: 0; -webkit-transform: translateY(-20px) translate3d(0, 0, 0); -ms-transform: translateY(-20px) translate3d(0, 0, 0); transform: translateY(-20px) translate3d(0, 0, 0); } 524 | 100% { opacity: 1; -webkit-transform: translateY(0) translate3d(0, 0, 0); -ms-transform: translateY(0) translate3d(0, 0, 0); transform: translateY(0) translate3d(0, 0, 0); } } 525 | .fade-in-down { -webkit-animation-name: fadeInDown; animation-name: fadeInDown; } 526 | 527 | /* Table styles copied from Bootstrap Copyright (c) 2013 Twitter, Inc 528 | */ 529 | table { width: 100%; max-width: 100%; margin-bottom: 1.5; font-size: 1.125rem; } 530 | table > thead > tr > th, table > thead > tr > td, table > tbody > tr > th, table > tbody > tr > td, table > tfoot > tr > th, table > tfoot > tr > td { padding: 12px; line-height: 1.2; vertical-align: top; border-top: 1px solid #333; } 531 | table > thead > tr > th { vertical-align: bottom; border-bottom: 2px solid #333; } 532 | table > caption + thead > tr:first-child > th, table > caption + thead > tr:first-child > td, table > colgroup + thead > tr:first-child > th, table > colgroup + thead > tr:first-child > td, table > thead:first-child > tr:first-child > th, table > thead:first-child > tr:first-child > td { border-top: 0; } 533 | table > tbody + tbody { border-top: 2px solid #333; } 534 | 535 | .site { display: flex; flex-direction: column; min-height: 100vh; } 536 | 537 | .site-wrap { flex: 1; } 538 | 539 | footer { background-color: #fafafa; border-top: thin solid #f3f3f3; color: #7a7a7a; font-size: 0.75rem; font-weight: 300; padding: 2rem; text-align: center; } 540 | 541 | .social-icons { font-size: 1.25rem; padding: 0.5em 0 0 0; width: 100%; } 542 | 543 | .social-icons a.fa { cursor: pointer; opacity: 0.8; padding: 0.2em; } 544 | 545 | .social-icons a.fa:hover { opacity: 1; } 546 | 547 | .social-icons iframe[title=Flattr] { position: relative; top: 0.1em; } 548 | 549 | blockquote { border-left: 5px solid #7a7a7a; font-style: italic; margin-left: 0.5rem; padding: 0.5rem; } 550 | 551 | blockquote footer { background-color: #fff; border-color: transparent; color: #7a7a7a; font-size: .85rem; font-style: normal; text-align: left; padding: 0; } 552 | 553 | .posts { margin: 0; } 554 | 555 | .posts .post { margin-bottom: 0.75em; border-bottom: thin solid #f3f3f3; } 556 | 557 | .posts .post:last-child { border-bottom: none; margin-bottom: .375em; padding-bottom: 0; } 558 | 559 | .post-link .post-title { margin-top: 0; font-weight: 600; color: #333; } 560 | 561 | .post-footer { margin-top: .75rem; text-align: center; } 562 | 563 | .post-footer .avatar { margin: 2rem 0; width: 100px; border-radius: 50%; } 564 | 565 | .meta, .post-meta { width: auto; font-weight: 300; margin: 0; padding: .25em 0; color: #7a7a7a; font-style: italic; } 566 | 567 | .related-post-title { border-bottom: thin solid #f3f3f3; } 568 | 569 | @media screen and (min-width: 32em) { html { font-size: 16px; } 570 | h1, .h1 { font-size: 2.998rem; } 571 | .site-header { text-align: left; } 572 | .site-nav { margin-top: 0; } 573 | .site-header a { font-size: 1rem; } 574 | .site-header .site-title { font-size: 1.25rem; float: left; } 575 | .site-header .site-nav { float: right; margin-top: .25rem; } 576 | blockquote { margin-left: 2rem; padding: 2rem; } } 577 | @media screen and (min-width: 48em) { html { font-size: 18px; } } 578 | @media screen and (min-width: 64em) { html { font-size: 20px; } } 579 | @media screen and (min-width: 78em) { em img { max-width: 56rem; margin-left: -7em; } } 580 | .gist, .gist .highlight .p { font-size: .75rem; } 581 | 582 | .gist .lines { width: 100%; } 583 | 584 | .measure { margin: 0 auto; max-width: 42rem; } 585 | 586 | .pagination { font-size: 1rem; font-family: 'Lato', 'Helvetica Neue', Helvetica, sans-serif; font-weight: 300; text-align: center; } 587 | 588 | .pagination a, .pagination .disabled { -webkit-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out; background: #fafafa; border-radius: 0.1875em; border: 1px solid #f3f3f3; color: #333333; padding: 1em 1.5em; } 589 | 590 | .pagination .disabled { opacity: 0.5; } 591 | 592 | .pagination a:hover, .pagination a:focus { background: white; color: #477dca; } 593 | 594 | .pagination a:active { background: #f7f7f7; } 595 | 596 | .pagination .button { font-size: 1rem; font-weight: 300; letter-spacing: 1px; } 597 | 598 | .button-disabled { opacity: 0.55; background-color: #999; } 599 | 600 | .button-disabled:hover, .button-disabled:active, .button-disabled:focus { cursor: not-allowed; background-color: #999; } 601 | 602 | /* TOC inspired by http://jashkenas.github.com/coffee-script */ 603 | #table-of-contents { 604 | font-size: 10pt; 605 | position: fixed; 606 | right: 0em; 607 | top: 0em; 608 | background: #F9F9F9; 609 | line-height: 12pt; 610 | text-align: right; 611 | box-shadow: 0 0 1em #777777; 612 | -webkit-box-shadow: 0 0 1em #777777; 613 | -moz-box-shadow: 0 0 1em #777777; 614 | -webkit-border-bottom-left-radius: 5px; 615 | -moz-border-radius-bottomleft: 5px; 616 | /* ensure doesn't flow off the screen when expanded */ 617 | max-height: 80%; 618 | overflow: auto; } 619 | #table-of-contents h2 { 620 | font-size: 13pt; 621 | max-width: 9em; 622 | border: 0; 623 | font-weight: normal; 624 | padding-left: 0.5em; 625 | padding-right: 0.5em; 626 | padding-top: 0.05em; 627 | padding-bottom: 0.05em; } 628 | #table-of-contents #text-table-of-contents { 629 | display: none; 630 | text-align: left; } 631 | #table-of-contents:hover #text-table-of-contents { 632 | display: block; 633 | padding: 0.5em; 634 | margin-top: -1.5em; } 635 | .code-highlighted { background-color: #ffff00; } 636 | .todo { color: red; } 637 | .todo.IN_PROGRESS { color: orange;} 638 | .todo.HOLD { color: orange;} 639 | .done { display: none; } 640 | .org-center { 641 | margin-left: auto; 642 | margin-right: auto; 643 | text-align: center; 644 | } -------------------------------------------------------------------------------- /book/style/worg.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Droid Sans'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Droid Sans'), local('DroidSans'), url(/style/fonts/droid-sans.woff2) format('woff2'), url(/style/fonts/droid-sans.woff) format('woff'); 6 | } 7 | @font-face { 8 | font-family: 'Droid Sans Mono'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Droid Sans Mono'), local('DroidSansMono'), url(/style/fonts/droid-sans-mono.woff2) format('woff2'), url(/style/fonts/droid-sans-mono.woff) format('woff'); 12 | } 13 | @font-face { 14 | font-family: 'Droid Serif'; 15 | font-style: normal; 16 | font-weight: 400; 17 | src: local('Droid Serif'), local('DroidSerif'), url(/style/fonts/droid-serif.woff2) format('woff2'), url(/style/fonts/droid-serif.woff) format('woff'); 18 | } 19 | 20 | @media all 21 | { 22 | html { 23 | margin: 0; 24 | font-family: sans-serif; 25 | background-attachment: fixed; 26 | background-position: right bottom; 27 | background-repeat: no-repeat; 28 | background-color: #F9F9F9; 29 | } 30 | 31 | body { 32 | font-size: 12pt; 33 | line-height: 18pt; 34 | color: black; 35 | margin-top: 0; 36 | 37 | } 38 | body #content { 39 | font-family: "Droid Serif","Lucida Grande","Lucida Sans Unicode","DejaVu Sans",Verdana,sans-serif; 40 | padding-top: 0px; 41 | max-width: 80%; 42 | margin-left: 20px; 43 | background-color: #F9F9F9; 44 | letter-spacing: 0.01rem; 45 | font-weight: 400; 46 | font-style: normal; 47 | font-size: 22px; 48 | line-height: 1.5; 49 | } 50 | body .title { 51 | margin-left: 0px; 52 | } 53 | 54 | #org-div-home-and-up{ 55 | position: fixed; 56 | right: 10px; 57 | top: 4em; 58 | } 59 | 60 | /* TOC inspired by http://jashkenas.github.com/coffee-script */ 61 | #table-of-contents { 62 | font-size: 10pt; 63 | position: fixed; 64 | right: 0em; 65 | top: 0em; 66 | background: #F9F9F9; 67 | line-height: 12pt; 68 | text-align: right; 69 | box-shadow: 0 0 1em #777777; 70 | -webkit-box-shadow: 0 0 1em #777777; 71 | -moz-box-shadow: 0 0 1em #777777; 72 | -webkit-border-bottom-left-radius: 5px; 73 | -moz-border-radius-bottomleft: 5px; 74 | /* ensure doesn't flow off the screen when expanded */ 75 | max-height: 80%; 76 | overflow: auto; } 77 | #table-of-contents h2 { 78 | font-size: 13pt; 79 | max-width: 9em; 80 | border: 0; 81 | font-weight: normal; 82 | padding-left: 0.5em; 83 | padding-right: 0.5em; 84 | padding-top: 0.05em; 85 | padding-bottom: 0.05em; } 86 | #table-of-contents #text-table-of-contents { 87 | display: none; 88 | text-align: left; } 89 | #table-of-contents:hover #text-table-of-contents { 90 | display: block; 91 | padding: 0.5em; 92 | margin-top: -1.5em; } 93 | 94 | #license { 95 | /* padding: .3em; */ 96 | /* border: 1px solid gray; */ 97 | background-color: #eeeeee; 98 | } 99 | 100 | h1 { 101 | /* 102 | font-family:Sans; 103 | font-weight:bold; */ 104 | font-weight: 700; 105 | font-style: normal; 106 | font-size: 60px; 107 | line-height: 1; 108 | letter-spacing: -0.04em; 109 | padding:0 0 30px 0; 110 | margin-top: 10px; 111 | margin-bottom: 10px; 112 | margin-right: 7%; 113 | /* color: #6C5D4F; */ 114 | color: grey; 115 | } 116 | 117 | /* 118 | h2:before { 119 | content: "* " 120 | } 121 | 122 | h3:before { 123 | content: "** " 124 | } 125 | 126 | h4:before { 127 | content: "*** " 128 | } 129 | */ 130 | 131 | h2 { 132 | font-family:sans-serif; 133 | font-size:1.45em; 134 | line-height:1em; 135 | padding:10px 0 10px 0; 136 | color: black; 137 | border-bottom: 1px solid #ddd; 138 | } 139 | 140 | .outline-text-2 { 141 | margin-left: 0.1em 142 | } 143 | 144 | h3 { 145 | font-family:sans-serif; 146 | font-size:1.3em; 147 | color: grey; 148 | margin-left: 0.6em; 149 | } 150 | 151 | /* #A34D32;*/ 152 | 153 | 154 | .outline-text-3 { 155 | margin-left: 0.9em; 156 | } 157 | 158 | h4 { 159 | font-family:sans-serif; 160 | font-size:1.2em; 161 | margin-left: 1.2em; 162 | color: #A5573E; 163 | } 164 | 165 | .outline-text-4 { 166 | margin-left: 1.45em; 167 | } 168 | 169 | a {text-decoration: none; font-weight: 400;} 170 | a:visited {text-decoration: none; font-weight: 400;} 171 | a:hover {text-decoration: underline;} 172 | 173 | .todo { 174 | color: #CA0000; 175 | } 176 | 177 | .done { 178 | color: #006666; 179 | } 180 | 181 | .timestamp-kwd { 182 | color: #444; 183 | } 184 | 185 | .tag { 186 | 187 | } 188 | 189 | li { 190 | margin: .4em; 191 | } 192 | 193 | table { 194 | border: none; 195 | } 196 | 197 | td { 198 | border: none; 199 | } 200 | 201 | th { 202 | border: none; 203 | } 204 | 205 | code { 206 | font-size: 100%; 207 | color: black; 208 | border: 1px solid #DEDEDE; 209 | padding: 0px 0.2em; 210 | } 211 | 212 | img { 213 | border: none; 214 | max-width: 700px; 215 | max-height: 700px; 216 | } 217 | 218 | .share img { 219 | opacity: .4; 220 | -moz-opacity: .4; 221 | filter: alpha(opacity=40); 222 | } 223 | 224 | .share img:hover { 225 | opacity: 1; 226 | -moz-opacity: 1; 227 | filter: alpha(opacity=100); 228 | } 229 | 230 | /* pre {border: 1px solid #555; */ 231 | /* background: #EEE; */ 232 | /* font-size: 9pt; */ 233 | /* padding: 1em; */ 234 | /* } */ 235 | 236 | /* pre { */ 237 | /* color: #e5e5e5; */ 238 | /* background-color: #000000; */ 239 | /* padding: 1.4em; */ 240 | /* border: 2px solid gray; */ 241 | /* } */ 242 | 243 | /* pre { */ 244 | /* background-color: #2b2b2b; */ 245 | /* border: 4px solid gray; */ 246 | /* color: #EEE; */ 247 | /* overflow: auto; */ 248 | /* padding: 1em; */ 249 | /* } */ 250 | 251 | pre { 252 | font-family: Droid Sans Mono, Monaco, Consolas, "Lucida Console", monospace; 253 | color: black; 254 | font-size: 90%; 255 | background-color: #ffffff; 256 | padding: 1.2em; 257 | border: 2px solid #dddddd; 258 | overflow: auto; 259 | } 260 | 261 | .org-info-box { 262 | clear:both; 263 | margin-left:auto; 264 | margin-right:auto; 265 | padding:0.7em; 266 | /* border:1px solid #CCC; */ 267 | /* border-radius:10px; */ 268 | /* -moz-border-radius:10px; */ 269 | } 270 | .org-info-box img { 271 | float:left; 272 | margin:0em 0.5em 0em 0em; 273 | } 274 | .org-info-box p { 275 | margin:0em; 276 | padding:0em; 277 | } 278 | 279 | 280 | .builtin { 281 | /* font-lock-builtin-face */ 282 | color: #f4a460; 283 | } 284 | .comment { 285 | /* font-lock-comment-face */ 286 | color: #737373; 287 | } 288 | .comment-delimiter { 289 | /* font-lock-comment-delimiter-face */ 290 | color: #666666; 291 | } 292 | .constant { 293 | /* font-lock-constant-face */ 294 | color: #db7093; 295 | } 296 | .doc { 297 | /* font-lock-doc-face */ 298 | color: #b3b3b3; 299 | } 300 | .function-name { 301 | /* font-lock-function-name-face */ 302 | color: #5f9ea0; 303 | } 304 | .headline { 305 | /* headline-face */ 306 | color: #ffffff; 307 | background-color: #000000; 308 | font-weight: bold; 309 | } 310 | .keyword { 311 | /* font-lock-keyword-face */ 312 | color: #4682b4; 313 | } 314 | .negation-char { 315 | } 316 | .regexp-grouping-backslash { 317 | } 318 | .regexp-grouping-construct { 319 | } 320 | .string { 321 | /* font-lock-string-face */ 322 | color: #ccc79a; 323 | } 324 | .todo-comment { 325 | /* todo-comment-face */ 326 | color: #ffffff; 327 | background-color: #000000; 328 | font-weight: bold; 329 | } 330 | .variable-name { 331 | /* font-lock-variable-name-face */ 332 | color: #ff6a6a; 333 | } 334 | .warning { 335 | /* font-lock-warning-face */ 336 | color: #ffffff; 337 | background-color: #cd5c5c; 338 | font-weight: bold; 339 | } 340 | pre.a { 341 | color: inherit; 342 | background-color: inherit; 343 | font: inherit; 344 | text-decoration: inherit; 345 | } 346 | pre.a:hover { 347 | text-decoration: underline; 348 | } 349 | 350 | /* Styles for org-info.js */ 351 | 352 | .org-info-js_info-navigation 353 | { 354 | border-style:none; 355 | } 356 | 357 | #org-info-js_console-label 358 | { 359 | font-size:10px; 360 | font-weight:bold; 361 | white-space:nowrap; 362 | } 363 | 364 | .org-info-js_search-highlight 365 | { 366 | background-color:#ffff00; 367 | color:#000000; 368 | font-weight:bold; 369 | } 370 | 371 | #org-info-js-window 372 | { 373 | border-bottom:1px solid black; 374 | padding-bottom:10px; 375 | margin-bottom:10px; 376 | } 377 | 378 | 379 | 380 | .org-info-search-highlight 381 | { 382 | background-color:#adefef; /* same color as emacs default */ 383 | color:#000000; 384 | font-weight:bold; 385 | } 386 | 387 | .org-bbdb-company { 388 | /* bbdb-company */ 389 | font-style: italic; 390 | } 391 | .org-bbdb-field-name { 392 | } 393 | .org-bbdb-field-value { 394 | } 395 | .org-bbdb-name { 396 | /* bbdb-name */ 397 | text-decoration: underline; 398 | } 399 | .org-bold { 400 | /* bold */ 401 | font-weight: bold; 402 | } 403 | .org-bold-italic { 404 | /* bold-italic */ 405 | font-weight: bold; 406 | font-style: italic; 407 | } 408 | .org-border { 409 | /* border */ 410 | background-color: #000000; 411 | } 412 | .org-buffer-menu-buffer { 413 | /* buffer-menu-buffer */ 414 | font-weight: bold; 415 | } 416 | .org-builtin { 417 | /* font-lock-builtin-face */ 418 | color: #da70d6; 419 | } 420 | .org-button { 421 | /* button */ 422 | text-decoration: underline; 423 | } 424 | .org-c-nonbreakable-space { 425 | /* c-nonbreakable-space-face */ 426 | background-color: #ff0000; 427 | font-weight: bold; 428 | } 429 | .org-calendar-today { 430 | /* calendar-today */ 431 | text-decoration: underline; 432 | } 433 | .org-comment { 434 | /* font-lock-comment-face */ 435 | color: #b22222; 436 | } 437 | .org-comment-delimiter { 438 | /* font-lock-comment-delimiter-face */ 439 | color: #b22222; 440 | } 441 | .org-constant { 442 | /* font-lock-constant-face */ 443 | color: #5f9ea0; 444 | } 445 | .org-cursor { 446 | /* cursor */ 447 | background-color: #000000; 448 | } 449 | .org-default { 450 | /* default */ 451 | color: #000000; 452 | background-color: #ffffff; 453 | } 454 | .org-diary { 455 | /* diary */ 456 | color: #ff0000; 457 | } 458 | .org-doc { 459 | /* font-lock-doc-face */ 460 | color: #bc8f8f; 461 | } 462 | .org-escape-glyph { 463 | /* escape-glyph */ 464 | color: #a52a2a; 465 | } 466 | .org-file-name-shadow { 467 | /* file-name-shadow */ 468 | color: #7f7f7f; 469 | } 470 | .org-fixed-pitch { 471 | } 472 | .org-fringe { 473 | /* fringe */ 474 | background-color: #f2f2f2; 475 | } 476 | .org-function-name { 477 | /* font-lock-function-name-face */ 478 | color: #0000ff; 479 | } 480 | .org-header-line { 481 | /* header-line */ 482 | color: #333333; 483 | background-color: #e5e5e5; 484 | } 485 | .org-help-argument-name { 486 | /* help-argument-name */ 487 | font-style: italic; 488 | } 489 | .org-highlight { 490 | /* highlight */ 491 | background-color: #b4eeb4; 492 | } 493 | .org-holiday { 494 | /* holiday */ 495 | background-color: #ffc0cb; 496 | } 497 | .org-info-header-node { 498 | /* info-header-node */ 499 | color: #a52a2a; 500 | font-weight: bold; 501 | font-style: italic; 502 | } 503 | .org-info-header-xref { 504 | /* info-header-xref */ 505 | color: #0000ff; 506 | text-decoration: underline; 507 | } 508 | .org-info-menu-header { 509 | /* info-menu-header */ 510 | font-weight: bold; 511 | } 512 | .org-info-menu-star { 513 | /* info-menu-star */ 514 | color: #ff0000; 515 | } 516 | .org-info-node { 517 | /* info-node */ 518 | color: #a52a2a; 519 | font-weight: bold; 520 | font-style: italic; 521 | } 522 | .org-info-title-1 { 523 | /* info-title-1 */ 524 | font-size: 172%; 525 | font-weight: bold; 526 | } 527 | .org-info-title-2 { 528 | /* info-title-2 */ 529 | font-size: 144%; 530 | font-weight: bold; 531 | } 532 | .org-info-title-3 { 533 | /* info-title-3 */ 534 | font-size: 120%; 535 | font-weight: bold; 536 | } 537 | .org-info-title-4 { 538 | /* info-title-4 */ 539 | font-weight: bold; 540 | } 541 | .org-info-xref { 542 | /* info-xref */ 543 | color: #0000ff; 544 | text-decoration: underline; 545 | } 546 | .org-isearch { 547 | /* isearch */ 548 | color: #b0e2ff; 549 | background-color: #cd00cd; 550 | } 551 | .org-italic { 552 | /* italic */ 553 | font-style: italic; 554 | } 555 | .org-keyword { 556 | /* font-lock-keyword-face */ 557 | color: #a020f0; 558 | } 559 | .org-lazy-highlight { 560 | /* lazy-highlight */ 561 | background-color: #afeeee; 562 | } 563 | .org-link { 564 | /* link */ 565 | color: #0000ff; 566 | text-decoration: underline; 567 | } 568 | .org-link-visited { 569 | /* link-visited */ 570 | color: #8b008b; 571 | text-decoration: underline; 572 | } 573 | .org-match { 574 | /* match */ 575 | background-color: #ffff00; 576 | } 577 | .org-menu { 578 | } 579 | .org-message-cited-text { 580 | /* message-cited-text */ 581 | color: #ff0000; 582 | } 583 | .org-message-header-cc { 584 | /* message-header-cc */ 585 | color: #191970; 586 | } 587 | .org-message-header-name { 588 | /* message-header-name */ 589 | color: #6495ed; 590 | } 591 | .org-message-header-newsgroups { 592 | /* message-header-newsgroups */ 593 | color: #00008b; 594 | font-weight: bold; 595 | font-style: italic; 596 | } 597 | .org-message-header-other { 598 | /* message-header-other */ 599 | color: #4682b4; 600 | } 601 | .org-message-header-subject { 602 | /* message-header-subject */ 603 | color: #000080; 604 | font-weight: bold; 605 | } 606 | .org-message-header-to { 607 | /* message-header-to */ 608 | color: #191970; 609 | font-weight: bold; 610 | } 611 | .org-message-header-xheader { 612 | /* message-header-xheader */ 613 | color: #0000ff; 614 | } 615 | .org-message-mml { 616 | /* message-mml */ 617 | color: #228b22; 618 | } 619 | .org-message-separator { 620 | /* message-separator */ 621 | color: #a52a2a; 622 | } 623 | .org-minibuffer-prompt { 624 | /* minibuffer-prompt */ 625 | color: #0000cd; 626 | } 627 | .org-mm-uu-extract { 628 | /* mm-uu-extract */ 629 | color: #006400; 630 | background-color: #ffffe0; 631 | } 632 | .org-mode-line { 633 | /* mode-line */ 634 | color: #000000; 635 | background-color: #bfbfbf; 636 | } 637 | .org-mode-line-buffer-id { 638 | /* mode-line-buffer-id */ 639 | font-weight: bold; 640 | } 641 | .org-mode-line-highlight { 642 | } 643 | .org-mode-line-inactive { 644 | /* mode-line-inactive */ 645 | color: #333333; 646 | background-color: #e5e5e5; 647 | } 648 | .org-mouse { 649 | /* mouse */ 650 | background-color: #000000; 651 | } 652 | .org-negation-char { 653 | } 654 | .org-next-error { 655 | /* next-error */ 656 | background-color: #eedc82; 657 | } 658 | .org-nobreak-space { 659 | /* nobreak-space */ 660 | color: #a52a2a; 661 | text-decoration: underline; 662 | } 663 | .org-org-agenda-date { 664 | /* org-agenda-date */ 665 | color: #0000ff; 666 | } 667 | .org-org-agenda-date-weekend { 668 | /* org-agenda-date-weekend */ 669 | color: #0000ff; 670 | font-weight: bold; 671 | } 672 | .org-org-agenda-restriction-lock { 673 | /* org-agenda-restriction-lock */ 674 | background-color: #ffff00; 675 | } 676 | .org-org-agenda-structure { 677 | /* org-agenda-structure */ 678 | color: #0000ff; 679 | } 680 | .org-org-archived { 681 | /* org-archived */ 682 | color: #7f7f7f; 683 | } 684 | .org-org-code { 685 | /* org-code */ 686 | color: #7f7f7f; 687 | } 688 | .org-org-column { 689 | /* org-column */ 690 | background-color: #e5e5e5; 691 | } 692 | .org-org-column-title { 693 | /* org-column-title */ 694 | background-color: #e5e5e5; 695 | font-weight: bold; 696 | text-decoration: underline; 697 | } 698 | .org-org-date { 699 | /* org-date */ 700 | color: #a020f0; 701 | text-decoration: underline; 702 | } 703 | .org-org-done { 704 | /* org-done */ 705 | color: #228b22; 706 | font-weight: bold; 707 | } 708 | .org-org-drawer { 709 | /* org-drawer */ 710 | color: #0000ff; 711 | } 712 | .org-org-ellipsis { 713 | /* org-ellipsis */ 714 | color: #b8860b; 715 | text-decoration: underline; 716 | } 717 | .org-org-formula { 718 | /* org-formula */ 719 | color: #b22222; 720 | } 721 | .org-org-headline-done { 722 | /* org-headline-done */ 723 | color: #bc8f8f; 724 | } 725 | .org-org-hide { 726 | /* org-hide */ 727 | color: #e5e5e5; 728 | } 729 | .org-org-latex-and-export-specials { 730 | /* org-latex-and-export-specials */ 731 | color: #8b4513; 732 | } 733 | .org-org-level-1 { 734 | /* org-level-1 */ 735 | color: #0000ff; 736 | } 737 | .org-org-level-2 { 738 | /* org-level-2 */ 739 | color: #b8860b; 740 | } 741 | .org-org-level-3 { 742 | /* org-level-3 */ 743 | color: #a020f0; 744 | } 745 | .org-org-level-4 { 746 | /* org-level-4 */ 747 | color: #b22222; 748 | } 749 | .org-org-level-5 { 750 | /* org-level-5 */ 751 | color: #228b22; 752 | } 753 | .org-org-level-6 { 754 | /* org-level-6 */ 755 | color: #5f9ea0; 756 | } 757 | .org-org-level-7 { 758 | /* org-level-7 */ 759 | color: #da70d6; 760 | } 761 | .org-org-level-8 { 762 | /* org-level-8 */ 763 | color: #bc8f8f; 764 | } 765 | .org-org-link { 766 | /* org-link */ 767 | color: #a020f0; 768 | text-decoration: underline; 769 | } 770 | .org-org-property-value { 771 | } 772 | .org-org-scheduled-previously { 773 | /* org-scheduled-previously */ 774 | color: #b22222; 775 | } 776 | .org-org-scheduled-today { 777 | /* org-scheduled-today */ 778 | color: #006400; 779 | } 780 | .org-org-sexp-date { 781 | /* org-sexp-date */ 782 | color: #a020f0; 783 | } 784 | .org-org-special-keyword { 785 | /* org-special-keyword */ 786 | color: #bc8f8f; 787 | } 788 | .org-org-table { 789 | /* org-table */ 790 | color: #0000ff; 791 | } 792 | .org-org-tag { 793 | /* org-tag */ 794 | font-weight: bold; 795 | } 796 | .org-org-target { 797 | /* org-target */ 798 | text-decoration: underline; 799 | } 800 | .org-org-time-grid { 801 | /* org-time-grid */ 802 | color: #b8860b; 803 | } 804 | .org-org-todo { 805 | /* org-todo */ 806 | color: #ff0000; 807 | } 808 | .org-org-upcoming-deadline { 809 | /* org-upcoming-deadline */ 810 | color: #b22222; 811 | } 812 | .org-org-verbatim { 813 | /* org-verbatim */ 814 | color: #7f7f7f; 815 | text-decoration: underline; 816 | } 817 | .org-org-warning { 818 | /* org-warning */ 819 | color: #ff0000; 820 | font-weight: bold; 821 | } 822 | .org-outline-1 { 823 | /* outline-1 */ 824 | color: #0000ff; 825 | } 826 | .org-outline-2 { 827 | /* outline-2 */ 828 | color: #b8860b; 829 | } 830 | .org-outline-3 { 831 | /* outline-3 */ 832 | color: #a020f0; 833 | } 834 | .org-outline-4 { 835 | /* outline-4 */ 836 | color: #b22222; 837 | } 838 | .org-outline-5 { 839 | /* outline-5 */ 840 | color: #228b22; 841 | } 842 | .org-outline-6 { 843 | /* outline-6 */ 844 | color: #5f9ea0; 845 | } 846 | .org-outline-7 { 847 | /* outline-7 */ 848 | color: #da70d6; 849 | } 850 | .org-outline-8 { 851 | /* outline-8 */ 852 | color: #bc8f8f; 853 | } 854 | .outline-text-1, .outline-text-2, .outline-text-3, .outline-text-4, .outline-text-5, .outline-text-6 { 855 | /* Add more spacing between section. Padding, so that folding with org-info.js works as expected. */ 856 | 857 | } 858 | 859 | .org-preprocessor { 860 | /* font-lock-preprocessor-face */ 861 | color: #da70d6; 862 | } 863 | .org-query-replace { 864 | /* query-replace */ 865 | color: #b0e2ff; 866 | background-color: #cd00cd; 867 | } 868 | .org-regexp-grouping-backslash { 869 | /* font-lock-regexp-grouping-backslash */ 870 | font-weight: bold; 871 | } 872 | .org-regexp-grouping-construct { 873 | /* font-lock-regexp-grouping-construct */ 874 | font-weight: bold; 875 | } 876 | .org-region { 877 | /* region */ 878 | background-color: #eedc82; 879 | } 880 | .org-rmail-highlight { 881 | } 882 | .org-scroll-bar { 883 | /* scroll-bar */ 884 | background-color: #bfbfbf; 885 | } 886 | .org-secondary-selection { 887 | /* secondary-selection */ 888 | background-color: #ffff00; 889 | } 890 | .org-shadow { 891 | /* shadow */ 892 | color: #7f7f7f; 893 | } 894 | .org-show-paren-match { 895 | /* show-paren-match */ 896 | background-color: #40e0d0; 897 | } 898 | .org-show-paren-mismatch { 899 | /* show-paren-mismatch */ 900 | color: #ffffff; 901 | background-color: #a020f0; 902 | } 903 | .org-string { 904 | /* font-lock-string-face */ 905 | color: #bc8f8f; 906 | } 907 | .org-texinfo-heading { 908 | /* texinfo-heading */ 909 | color: #0000ff; 910 | } 911 | .org-tool-bar { 912 | /* tool-bar */ 913 | color: #000000; 914 | background-color: #bfbfbf; 915 | } 916 | .org-tooltip { 917 | /* tooltip */ 918 | color: #000000; 919 | background-color: #ffffe0; 920 | } 921 | .org-trailing-whitespace { 922 | /* trailing-whitespace */ 923 | background-color: #ff0000; 924 | } 925 | .org-type { 926 | /* font-lock-type-face */ 927 | color: #228b22; 928 | } 929 | .org-underline { 930 | /* underline */ 931 | text-decoration: underline; 932 | } 933 | .org-variable-name { 934 | /* font-lock-variable-name-face */ 935 | color: #b8860b; 936 | } 937 | .org-variable-pitch { 938 | } 939 | .org-vertical-border { 940 | } 941 | .org-warning { 942 | /* font-lock-warning-face */ 943 | color: #ff0000; 944 | font-weight: bold; 945 | } 946 | .rss_box {} 947 | .rss_title, rss_title a {} 948 | .rss_items {} 949 | .rss_item a:link, .rss_item a:visited, .rss_item a:active {} 950 | .rss_item a:hover {} 951 | .rss_date {} 952 | 953 | label.org-src-name { 954 | font-size: 80%; 955 | font-style: italic; 956 | } 957 | 958 | #show_source {margin: 0; padding: 0;} 959 | 960 | #postamble { 961 | font-size: 75%; 962 | max-width: 80%; 963 | margin-left: 20px; 964 | margin-top: 10px; 965 | padding: .2em; 966 | background-color: #F9F9F9; 967 | z-index: -1000; 968 | } 969 | 970 | 971 | } /* END OF @media all */ 972 | 973 | @media all and (max-width:640px){ 974 | html {font-size: 15px;} 975 | body #content { 976 | margin-left: 0px; 977 | max-width: 100%; 978 | } 979 | } 980 | 981 | @media screen 982 | { 983 | #table-of-contents { 984 | float: right; 985 | border: 1px solid #CCC; 986 | max-width: 50%; 987 | overflow: auto; 988 | } 989 | } /* END OF @media screen */ 990 | -------------------------------------------------------------------------------- /book/tufte-book.cls: -------------------------------------------------------------------------------- 1 | \NeedsTeXFormat{LaTeX2e}[1994/06/01] 2 | 3 | \ProvidesClass{tufte-book}[2009/12/11 v3.5.0 Tufte-book class] 4 | 5 | %% 6 | % Declare we're tufte-book 7 | \newcommand{\@tufte@class}{book}% the base LaTeX class (defaults to the article/handout style) 8 | \newcommand{\@tufte@pkgname}{tufte-book}% the name of the package (defaults to tufte-handout) 9 | 10 | %% 11 | % Load the common style elements 12 | \input{tufte-common.def} 13 | 14 | 15 | %% 16 | % Set up any book-specific stuff now 17 | 18 | %% 19 | % The front matter in Tufte's /Beautiful Evidence/ contains everything up 20 | % to the opening page of Chapter 1. The running heads, when they appear, 21 | % contain only the (arabic) page number in the outside corner. 22 | %\newif\if@mainmatter \@mainmattertrue 23 | \renewcommand\frontmatter{% 24 | \cleardoublepage% 25 | \@mainmatterfalse% 26 | \pagenumbering{arabic}% 27 | %\pagestyle{plain}% 28 | \fancyhf{}% 29 | \ifthenelse{\boolean{@tufte@twoside}}% 30 | {\fancyhead[LE,RO]{\thepage}}% 31 | {\fancyhead[RE,RO]{\thepage}}% 32 | } 33 | 34 | 35 | %% 36 | % The main matter in Tufte's /Beautiful Evidence/ doesn't restart the page 37 | % numbering---it continues where it left off in the front matter. 38 | \renewcommand\mainmatter{% 39 | \cleardoublepage% 40 | \@mainmattertrue% 41 | \fancyhf{}% 42 | \ifthenelse{\boolean{@tufte@twoside}}% 43 | {% two-side 44 | \renewcommand{\chaptermark}[1]{\markboth{##1}{}}% 45 | \fancyhead[LE]{\thepage\quad\smallcaps{\newlinetospace{\plaintitle}}}% book title 46 | \fancyhead[RO]{\smallcaps{\newlinetospace{\leftmark}}\quad\thepage}% chapter title 47 | }% 48 | {% one-side 49 | \fancyhead[RE,RO]{\smallcaps{\newlinetospace{\plaintitle}}\quad\thepage}% book title 50 | }% 51 | } 52 | 53 | 54 | %% 55 | % The back matter contains appendices, indices, glossaries, endnotes, 56 | % biliographies, list of contributors, illustration credits, etc. 57 | \renewcommand\backmatter{% 58 | \if@openright% 59 | \cleardoublepage% 60 | \else% 61 | \clearpage% 62 | \fi% 63 | \@mainmatterfalse% 64 | } 65 | 66 | %% 67 | % Only show the chapter titles in the table of contents 68 | \setcounter{tocdepth}{0} 69 | 70 | %% 71 | % If there is a `tufte-book-local.sty' file, load it. 72 | 73 | \IfFileExists{tufte-book-local.tex} 74 | {\input{tufte-book-local} 75 | \TufteInfoNL{Loading tufte-book-local.tex}} 76 | {} 77 | 78 | %% 79 | % End of file 80 | \endinput 81 | -------------------------------------------------------------------------------- /book/zh/images: -------------------------------------------------------------------------------- 1 | ../images -------------------------------------------------------------------------------- /book/zh/index.org: -------------------------------------------------------------------------------- 1 | #+title: Clojure Flavored JavaScript 2 | #+author: Jichao Ouyang 3 | #+keywords: clojure, javascript, functional, mori, clojurescript 4 | #+OPTIONS: num:3 5 | #+LATEX_CLASS: ./mitpress.cls 6 | #+LATEX_HEADER: \usepackage{fontspec} 7 | #+LATEX_HEADER: \usepackage{xeCJK} 8 | #+LATEX_HEADER: \setCJKmainfont{STXihei} 9 | #+LANGUAGE: zh-CN 10 | #+include: "./前言.org" 11 | #+include: "./第一章.org" 12 | #+include: "./第二章.org" 13 | #+include: "./第八章.org" 14 | -------------------------------------------------------------------------------- /book/zh/index.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcouyang/clojure-flavored-javascript/378a04b13e4addb918d80fb3622896beabb3ecff/book/zh/index.pdf -------------------------------------------------------------------------------- /book/zh/style: -------------------------------------------------------------------------------- 1 | ../style -------------------------------------------------------------------------------- /book/zh/前言.org: -------------------------------------------------------------------------------- 1 | * 前言 2 | ** 最好在看本书之前 3 | 4 | *** 能看懂JavaScript代码 5 | 这既不是一本介绍 Clojure 也不是介绍 JavaScript 的书,这是一本介绍如何用 JavaScript 函数式编程的书。其中一些函数式的思想和表现形式都借用了 Clojure,因此叫做 Clojure 风格的函数式 JavaScript,但是并不要求在读本书前会 Clojure[fn:1],而只需要能阅读 JavaScript 代码,那就足够了。 如果你会 Clojure,可以完全忽略我解释 Clojure 代码的段落,当然 JavaScript 的部分才是重点。 6 | 7 | *** 你可能买错书了,如果你是 8 | 9 | **** 想学 JavaScript 10 | 11 | 这不是一本 JavaScript 的教科书,这里只会介绍如何用 JavaScript 进行函数式编程,所以如果想要系统学习 JavaScript 的话,我猜看一看《JavaScript 语言精粹》已经足够了。另外如果读者的英文好的话,还有一本可以在线免费阅读的[[https://leanpub.com/javascriptallongesix/read][《JavaScript Allonge》]]。 12 | 13 | **** 想学 Clojure 14 | 15 | 同样的,这也不是 Clojure 的教科书,这里只含有一些用于阐述函数式编程思想的 Clojure 代码。作为副作用,你确实可以学到一些 Clojure 编程的知识,但很可能是非常零碎不完整的知识。如果是想要系统的了解和学习 Clojure 的话,非常推荐《The Joy of Clojure》[fn:2],另外,如果读者英文比较好,还有一本可以免费在线阅读的[[http://braveclojure.com][《CLOJURE for the BRAVE and TRUE》]] 也非常的不错。 16 | 17 | **** 函数式编程的专家 18 | 如果你已经在日常工作或学习中使用 Scala,Clojure 或者 Haskell 等函数式语言编程的话,那么本书对你在函数式编程上的帮助不会太大。 *不过* :这本书对解你从函数式语言迁移到 JavaScript 编程的不适应该是非常有效的,当然,这也正是本书的目的之一。 19 | 20 | *** 准备环境 21 | 在开始阅读本书之前,如果你希望能运行书中的代码的话,可能需要一些环境的配置。而且书中的所有源码和运行方式都可以在本书的 Github 仓库[fn:15]中找到。当然如果你使用 emacs(同时还配置了 org babel 的话) 阅读本书的源码的话,对于大部分代码只需要光标放在在代码处按 =c-c c-c= 即可。 22 | 23 | **** JavaScript 24 | 25 | 原生的 JavaScript 没有什么好准备的,可以通过 Node 或者 Firefox(推荐)的 Console 运行代码。当然第五章会有一些使用 sweet.js 写的 macro,则需要安装 sweet.js。 26 | 27 | ***** 安装 Node/iojs 28 | 29 | 1. 下载[[https://nodejs.org/][ nodejs]] 30 | 31 | 2. 如果使用 mac,可以直接用 brew 安装 32 | #+BEGIN_SRC sh 33 | brew install node 34 | # 或者 35 | brew install iojs 36 | #+END_SRC 37 | 38 | ***** 安装 sweet.js 39 | 在安装完 node 之后命令行输入: 40 | #+BEGIN_SRC sh 41 | npm install -g sweet.js 42 | #+END_SRC 43 | 44 | **** Clojure 45 | 46 | 书中的 Clojure 代码大多都用来描述函数式编程的概念,当然如果想要运行书中的 Clojure 代码,首先需要安装 [[http://www.oracle.com/technetwork/java/javase/downloads/index.html][JVM 或者 JDK]],至少需要1.6,推荐安装1.8。 47 | 48 | ***** 安装 leiningen 49 | leiningen 是 clojure 的包管理工具,类似于 node 的 npm,ruby 的 bundle,python 的 pip。 另外 leinigen 还提供脚手架的功能。可以通过[[http://leiningen.org/][官网]]的脚本安装。 mac 用户可以简单的使用 =brew install leiningen= 安装。 50 | 51 | 安装完之后,就可以运行 =lein repl= 打开 repl,试试输入下列 clojure 代码,你将会立马看见结果。 52 | #+BEGIN_SRC clojure 53 | (+ 1 1) 54 | ;=> 2 55 | #+END_SRC 56 | 57 | ***** 编辑器 58 | 如果更喜欢使用编辑器用来编辑更长一段的代码,我推荐非 emacs 用户使用 [[http://lighttable.com/][Light Table]], intellij 用户对使用 [[https://cursive-ide.com/][cursive]]。当然如果读者已经在使用 Emacs,那就更完美了,emacs [[https://github.com/clojure-emacs/cider][cider mode]] 是 Clojure 编程不错的选择。 59 | 60 | ** 本书中的代码 61 | 书中的所有源码和运行方式都可以在本书的 Github 仓库[fn:15]中找到,书中几乎所有的例子都以测试的形式完成。并且,本书的文本源代码也在该仓库中,并且可以通过 https://oyanglul.us/clojure-flavored-javascript 访问到在线版。 62 | 63 | ** 代码风格约定 64 | 本书的 JavaScript 代码都遵循 [[https://github.com/airbnb/javascript][Airbnb JavaScript Style Guide]][fn:3] 中的 ES5 和 React 的风格约定。 65 | ** TODO 本书的组织结构 66 | *** 第一章 67 | 将介绍 JavaScript 的基本函数式背景,简要的介绍为什么要关心函数式编程,为什么说 Underscore 不够函数式,JavaScript 要作为完整的函数式语言还缺些什么? 68 | *** 69 | 70 | ** 本书使用的约定 71 | 本书使用以下字体排版约定。 72 | *** /斜体/ 73 | 74 | 表示新的术语。 75 | *** =等宽字体= 76 | 77 | 代码清单,出现在段落之内则表示变量,函数名,关键字等。 78 | *** *粗体* 79 | 80 | 重点概念。 81 | *** _下划线_ 82 | 83 | 需要填入的词,我可能已经帮大家填上了。 84 | *** +横线+ 85 | 86 | 可以忽略掉的词。 87 | 88 | * Footnotes 89 | 90 | [fn:3] https://github.com/airbnb/javascript 91 | 92 | [fn:15] https://github.com/jcouyang/clojure-flavored-javascript/tree/source。 93 | 94 | [fn:1] 就像计算机程序构造与解释中说的,lisp 语言基本没有语法,就像学习象棋的规则只用花很少的时间,而如何下好棋,才是学习的关键,也是乐趣所在。 95 | 96 | [fn:2] 中文叫 Clojure 编程乐趣,但是只有第一版的,原书已经第二版了。 我刚好有幸翻译了作者 Michael Fogus 另一本《JavaScript 函数式编程》。 97 | -------------------------------------------------------------------------------- /book/zh/序.org: -------------------------------------------------------------------------------- 1 | * 序 2 | #+BEGIN_QUOTE 3 | 这是一本看起来 Clojure 的 JavaScript 编程书。 4 | #+END_QUOTE 5 | 6 | 函数式编程可以说是非常古老的编程范式, 7 | 但是近年来函数式编程越来越受到关注。不管是 Google 力推的 Go, 学术派的 Scala 与 Haskell,还是 lisp 的新方言 Clojure,这些新的函数式编程语言也越来越受到关注. 8 | 9 | 当然不仅是后端函数式语言层出不穷,前端也不甘示弱。 10 | 虽然前端浏览器只支持一门语言——JavaScript,但是 11 | 能支持函数式编程的 JavaScript 库越来越多,比如 Functional 12 | JavaScript[fn:6],Underscore, lodash...不仅如此, 13 | 还有一些能编译成 JavaScript 的语言, 能让前端的函数式编程发挥到极致, 14 | Haskell 的 PureScript, Scala 的 scalajs, Clojure 的 ClojureScript。 15 | 16 | 我两次都以clojure结尾,是因为我喜欢把重点的留到最后。 17 | Clojure 独特于其他的语言,它即是一门新的语言, 18 | 一门函数式编程范式的语言, 19 | 同时又流淌着着古老的血液——lisp[fn:1]。 20 | 这是我选择用 Clojure 来诠释函数式编程的原因之一。 21 | 22 | 那么为什么我要选 JavaScript 作为函数式编程的目标呢。Michael Fogus 23 | 用二百多页向大家展示了不一样的《Functional JavaScript》编程方式, 24 | 可惜 Fogus 作为 ClojureScript 编译器的贡献者, 25 | 竟然选择了 Underscore 作为函数式库, 26 | 直接导致并不能完全展示 JavaScript 所能达到的函数式编程能力。有趣的是, 27 | ClojureScript 的作者把 ClojureScript 的不可变(Immutable)数据结构移植到了 JavaScript, 28 | 这下彻底将 JavaScript 的函数式编程提升到了用其他库都完成不了的新高度[fn:2]。 29 | 不仅如此, Mozilla 的 Sweet.js(https://github.com/mozilla/sweet.js) 30 | 更是完成了另一个突破 —— JavaScript 的 macro, 31 | 虽然不能算是函数式的概念,但也算是 lisp 语言的一项独门绝技了[fn:3]。 32 | 33 | 这一切的一切,都让我忍不住要帮 Fogus 出一本续集,用 JavaScript 实现其他函数式编程语言如 Clojure 甚至是 Haskell[fn:4] 的奇淫巧技, 34 | 让大家进一步感受用 JavaScript 这门不完美的语言, 35 | 同样可以编写出优雅的函数式代码,以不一样的方式思考和解决问题。 36 | 于是不管你是想转行 JavaScript 的 Clojure 开发者, 37 | 亦或是像了解 Clojure 或函数式编程的 JavaScript 开发者, 38 | 都可以在此找到一些启发。 39 | 但这并不是一本 JavaScript 入门的好书[fn:5]。 40 | 41 | * Footnotes 42 | 43 | [fn:6] 没错,名字就叫这个。 44 | 45 | [fn:5] Douglas 的《JavaScript 语言精粹》 是个不错的选择。另外如果读者的英文好的话,还有一本可以在线免费阅读的[[https://leanpub.com/javascriptallongesix/read][《JavaScript Allonge》]](https://leanpub.com/javascriptallongesix/read) 46 | 47 | [fn:4] 就当是 bonus 吧。 48 | 49 | [fn:3] 虽然很多新的语言都在 50 | 尝试实现 macro,比如 scala, rust 和 julia,但是由于语法过于复杂。 51 | 52 | [fn:2] 虽然 facebook 也尝试实现自己的一个不可变数据结构 Immutable.js (https://facebook.github.io/immutable-js)。 53 | 54 | [fn:1] 还记得《计算机程序构造与解释》里面用的怪怪的括号语言 scheme 吗? 55 | -------------------------------------------------------------------------------- /book/zh/第一章.org: -------------------------------------------------------------------------------- 1 | ** 函数式 JavaScript 2 | 3 | 本章将介绍 JavaScript 的函数式背景: 4 | 1. 为什么说 JavaScript 是函数式语言? 5 | 2. 我们为什么要关心函数式编程? 6 | 3. 为什么说 Underscore 不够函数式? 7 | 4. 要作为完整的函数式语言,JavaScript 还缺些什么? 8 | 9 | ** JavaScript 也是函数式语言? 10 | 11 | 说到 JavaScript 可能第一反应会是一门面向对象的语言。事实上,JavaScript 是基于原型(prototype-based)的 *多范式* 编程语言。也就是说面向对象只是 JavaScript 支持的其中一种范式而已,由于 JavaScript 的函数是一等公民,它也支持函数式编程范式。 12 | 13 | *** 编程范式 14 | 15 | 常见的编程范式有三种,命令式,面向对象以及函数式,事实上还有第四种,逻辑式编程。 如我们在大学时学过的C语言,就是标准的命令式语言。而如果你在大学自学过Java打过黑工的话,那么你对面向对象也再熟悉不过了吧。而可能大部分人(以为)接触函数式的机会比较少,因为它是更接近于数学和代数的一种编程范式。让我们分别看看这几种主要的编程范式。 16 | 17 | **** 命令式 18 | 19 | 这恐怕是我们最熟悉的编程范式了(大部分计算机课程都会是C),命令式顾名思义就是以一条条命令的方式编程,告诉计算机我需要先做这个任务,然后另一个任务。还有一些控制命令执行过程的流控制,比如我们熟悉的循环语句: 20 | 21 | #+BEGIN_SRC js 22 | for (let i = 0; i < 10; i++) { 23 | console.log('命令', i); 24 | } 25 | #+END_SRC 26 | 27 | 当然还有分支语句,switch等等,都是用来控制命令的执行 /过程/ 。 28 | 29 | **** 面向对象 30 | 31 | 这恐怕是目前最常见的编程范式了,绝大部分的工程项目的语言都会是面向对象语言。而面向对象的思想则更接近于现实世界,封装好的对象之间通过消息互相传递信息,以这种熟悉的方式来建模显然要更容易一线。面向对象有一些我们熟悉的概念组成,比如封装,继承,多态等等。而面向对象的思维主要是通过抽象成包含状态和一些方法的对象来解决问题,可以通过继承关系复用一些方法和行为。 32 | 33 | **** 函数式 34 | 35 | 函数式则更接近于数学,简单来说就是对表达式求值。跟面向对象有所不同的是函数式对问题的抽象方式是抽象成 带有动作的函数。其思维更像是我们小时候解应用题时需要套用各种公式来求解的感觉。当然函数式跟面向对象一样还包含了很多的概念,比如高阶函数,不可变性,惰性求值等等。 36 | 37 | #+CAPTION: 主要的编程范式 38 | [[./images/paradigm.png]] 39 | 40 | **** 逻辑式编程 41 | 42 | 可能这个名词听的比较少,但是我们经常在用而却可呢过没有意识到的 SQL 的 query 语句就是逻辑式编程。所谓逻辑式,就是通过提问找到答案的编程方式。比如: 43 | 44 | #+BEGIN_SRC sql 45 | select lastname from someTable where sex='女' and firstname in ('连顺','女神') 46 | #+END_SRC 47 | 48 | 这里问了两个问题: 49 | 50 | 1. 性别是女? 51 | 2. 名字必须是“连顺”或者“女神”? 52 | 53 | 那么得到的答案就是符合问题描述的结果集了。 54 | 55 | 除了最常见的 SQL,Clojure 也提供了 =core.logic= 的库方便进行逻辑式编程。[fn:3] 56 | 57 | *** JavaScript 的函数式支持 58 | 59 | 说了这么多种编程范式,JavaScript 对函数式的支持到底如何呢? 60 | 61 | 首先如果语言中的函数不是一等的,那么也就跟函数式编程也就基本划清界限了。比如 Java 8 之前的版本,值和对象才是一等公民,要写一个高阶函数可能还需要把函数包在对象中才行。[fn:4] 62 | 63 | 幸好 JavaScript 中的函数是一等函数,所谓一等,就是说跟值一样都是一等公民,所有值能到的地方,都可以替换成函数。例如,可以跟值一样作为别的函数的参数,可以被别的函数想值一样返回,而这个“别的函数”叫做 /高阶函数/ 。 64 | 65 | **** 函数作为参数 66 | 67 | 函数作为参数最典型的应用要数 map 了,想必如果没有使用过 Underscore,也或多或少会用过 ECMAScript 5 中 Array 的 map 方法吧。map 简单将一个数组转换为另一个数组。 68 | 69 | #+BEGIN_SRC js 70 | [1, 2, 3, 4].map(function(x) { 71 | return ++x; 72 | }); 73 | #+END_SRC 74 | 75 | #+RESULTS: 76 | : Please install 'z' first! 77 | : hehe 78 | : undefined 79 | 80 | 可以看到函数 =function(x){return x++}= 是作为参数被传入 Array 的 =map= 方法中。map 是函数式编程最常见的标志性函数,想想在 ECMAScript 5 出来之前应该怎么做类似的事情: 81 | 82 | #+BEGIN_SRC js 83 | var array = [1, 2, 3, 4]; 84 | var result = []; 85 | for (var i in array){ 86 | result.push(++i); 87 | } 88 | 89 | #+END_SRC 90 | 91 | 这段命令式的代码跟利用 map 的函数式代码解决问题的方式和角度是完全不同的。命令式需要操心所有的过程,如何遍历以及如何组织结果数据。而 map 由于将遍历,操作以及结果数据的组织的过程封装至 Array 中,从而参数化了最核心过程。而这里的核心过程就是 map 的参数里的匿名函数中的过程,也是我们真正关心的主要逻辑。 92 | 93 | **** 函数作为返回值 94 | 95 | 函数作为返回值的用法可能在 JavaScript 中会更为常见。而且在不同场景下被返回的函数又有着不同的名字。 96 | 97 | ***** 柯里化 98 | 99 | 我们把一个多参的函数变成一次只能接受一个参数的函数的过程叫做柯里化。如: 100 | 101 | #+BEGIN_SRC js 102 | var curriedSum = curry(sum) 103 | var sum5 = curriedSum(5) 104 | var sum5and4 = sum5(4) //=> 9 105 | sum5and4(3) // => 12 106 | #+END_SRC 107 | 108 | 当然柯里化这样做的目的非常简单,可以部分的配置函数,然后可以继续使用这些配置过的函数。当然,我会在第四章函数组合那里更详细的解释为什么要柯里化,在这之前闲不住的读者可以先猜猜为什么要把柯里化放函数组合那一章。 109 | 110 | ***** thunk 111 | 112 | thunk(槽)[fn:5] 是指有一些操作不被立即执行,也就是说准备好一个函数,但是不执行,默默等待着合适的时候被合适的人调用。我实在想不出能比下图这个玩意更能解释 thunk 的了。 在下一章,你会见到如何用 thunk 实现惰性序列。 113 | 114 | #+CAPTION: thunk 像是一个封装好待执行的容器 115 | [[./images/thunk.png]] 116 | 117 | **** 越来越函数式的ES6 118 | ECMAScript 6[fn:1]终于正式发布了,新的规范有非常的新特性,其中不少借鉴自其他函数式语言的特性,给 JavaScript 语言添加了不少函数式的新特性。 119 | 120 | #+BEGIN_QUOTE 121 | 虽然浏览器厂商都还没有完全实现 ES6 的所有规范,但是其实我们是可以通过一些中间编译器使用大部分的 ES6 的新特性,如 122 | 123 | *Babel* 124 | 125 | 这是目前支持 ES6 实现最多的编译器了,没有之一。 主要是 Facebook 在维护,因此也可以编译 Facebook 的 React。这也是目前[fn:2]能实现尾递归优化的唯一编译器。不过关于尾递归只能优化尾子递归,相互递归的优化还没有实现。 126 | 127 | *Traceur* 128 | 129 | Google 出的比较早得一个老牌编译器,支持的 ES6 也不少了。但是从 github 上来看似乎已经没有 babel 活跃了。 130 | 131 | 当然,除了这些也可以直接使用 FireFox。作为 ES6 规范的主要制定者之一的 Mozilla 出的 Firefox 当然也是浏览器中实现 ES6 标准最多的。 132 | #+END_QUOTE 133 | 134 | **** 箭头函数 135 | 136 | 这是 ES6 发布的一个新特性,虽然 Firefox 支持已久了,不算什么新东西,但是标准化之后还是比较令人激动的。 /箭头函数/ 也被叫做 /肥箭头/ (fat arrow)[fn:6],大致是借鉴自 CoffeeScript 或者 Scala 语言。箭头函数是提供词法作用域的匿名函数。 137 | 138 | ***** 声明一个箭头函数 139 | 140 | 你可以通过两种方式定义一个箭头函数: 141 | #+BEGIN_EXAMPLE 142 | ([param] [, param]) => { 143 | statement 144 | } 145 | // 或者 146 | param => expression 147 | #+END_EXAMPLE 148 | 149 | 表达式可以省略块(block)括号,而多行语句则需要用块括号括起来。 150 | 151 | ***** 为什么要用箭头函数 152 | 153 | 虽然看上去跟以前的匿名函数没有什么区别,我们可以对比旧的匿名函数是如何写一个使数组中数字都乘 2 的函数. 154 | #+BEGIN_SRC js 155 | var a = [1, 2, 3, 4, 5]; 156 | a.map(function(x){ return x*2 }); 157 | #+END_SRC 158 | 159 | 而使用箭头函数会变成: 160 | #+BEGIN_SRC js 161 | a.map(x => x*2); 162 | #+END_SRC 163 | 164 | 使用箭头函数可以少写 function 和 return 以及块括号,从而让我们其实更关心的转换关系变得更明显。略去没用的长的函数关键字,其实可以让代码更简洁更可读。特别是在传入高阶函数作为参数的时候, ~map(x=>x*2)~ 更形象和突出的表达了核心变换逻辑。 165 | 166 | ***** COMMENT 词法绑定 167 | 168 | 如果你觉得这种简化的语法糖还不足以说服你改变匿名函数的写法,那么想想以前写匿名函数中的经常需要 =var self=this= 的苦恼吧。 169 | 170 | #+BEGIN_SRC js -n -r 171 | var Multipler = function(inc){ 172 | this.inc = inc; 173 | } 174 | Multipler.prototype.multiple = function(numbers){ 175 | var self = this; // <= (ref:selfthis) 176 | return numbers.map(function(number){ 177 | return self.inc * number; // <= (ref:self) 178 | }); 179 | } 180 | new Multipler(2).multiple([1, 2, 3, 4]); // => [ 2, 4, 6, 8 ] 181 | #+END_SRC 182 | 183 | #+RESULTS: 184 | 185 | - [[(selfthis)][第(selfthis)行]] 中使用 =self= 保持了指向 Multipler 实例的 this 引用的缓存。 186 | - [[(self)][第(self)行]]使用 =self= 引用 =Multipler= 的实例,而此时的 =this= 应该指向 =numbers= 的元素。 187 | 188 | 这样做很怪不是吗,因此经常出现在各种面试题中,让你猜猜 =this= 到底是谁。或者让你去修正 =this= 绑定,方法如此之多,但是不管是使用 EcmaScript 5 的 =bind= ,还是 =map= 的第三个参数来保证 =this= 的绑定不会出错,都逃脱不了要手动修正 =this= 绑定的命运。 189 | 190 | #+BEGIN_SRC js 191 | ... 192 | return numbers.map(function(number){ 193 | return self.inc * number; // <= (ref:self) 194 | }.bind(this)); 195 | ... 196 | #+END_SRC 197 | 198 | 那么如果用箭头函数就不会存在上述问题: 199 | #+BEGIN_SRC js 200 | Multipler.prototype.multiple = function(numbers){ 201 | return numbers.map(number => number * this.inc); 202 | }; 203 | 204 | new Multipler(2).multiple([1, 2, 3, 4]);// => [ 2, 4, 6, 8 ] 205 | #+END_SRC 206 | 207 | #+RESULTS: 208 | 209 | 现在,箭头函数里面的 this 绑定的是外层函数的 this 值,不会受到运行时上下文的影响。[fn:7]而是从词法上就能轻松确定 this 的绑定。不需要 ~var self=this~ 了是不是确实方便了许多,不仅不会再被各种怪异的面试题坑了,还让代码更容易推理。 210 | 211 | **** 尾递归优化 212 | 213 | Clojure 能够通过 =recur= 函数对 /尾递归/ 进行优化,但是 ES5 的 JavaScript 实现是不会对尾递归进行任何优化,很容易出现 /爆栈/ 的现象。但是 ES6 的标准已经发布了对尾递归优化的支持,下来我们能做的只是等各大浏览器厂商的实现了。 214 | 215 | 不过在干等原生实现的同时,我们也可以通过一些中间编译器如 Babel,把 ES6 的代码编译成 ES5 标准 JavaScript,而在 Babel 编译的过程就可以把尾递归优化成循环。 216 | 217 | 218 | **** Destructure 219 | 220 | 在解释 Destructure[fn:8]之前,先举个生动的例子,比如吃在奥利奥的是时候,我的吃法是这样的: 221 | 222 | 1. 掰成两片,一片是不带馅的,一份是带馅的 223 | 2. 带馅的一半沾一下牛奶 224 | 3. 舔掉中间夹心的馅 225 | 4. 合起来吃掉 226 | 227 | 如果写成代码,大致应该是这样的: 228 | #+BEGIN_SRC js 229 | var orea = ["top","middle","bottom"]; 230 | var top = orea.shift(), middleAndBottom = orea; // <1> 231 | var wetMiddleAndBottom = dipMilk(middleAndBottom); // <2> 232 | var bottom = lip(wetMiddleAndBottom); // <3> 233 | eat([top,bottom]); // <4> 234 | #+END_SRC 235 | 236 | 注意那个诡异的 =shift= ,如果用 destructure 会写得稍微优雅一些: 237 | #+BEGIN_SRC js 238 | var [top, ...middleAndBottom] = ["top", "middle", "bottom"]; // <1> 239 | var wetMiddleAndBottom = dipMilk(middleAndBottom); // <2> 240 | var bottom = lip(wetMiddleAndBottom); // <3> 241 | eat([top,bottom]); // <4> 242 | #+END_SRC 243 | 244 | 有没有觉得我掰奥利奥的姿势变酷了许多?这就是 destructure,给定一个特定的模式 =[top, ...middleAndBottom]= ,让数据 =["top","middle","bottom"]= 按照该模式匹配进来。同样的,我将会专门在第6章介绍模式匹配这个概念,虽然它不是 Clojure 的重要概念,但是确实 Scala 或 Haskell 的核心所在。不过可以放心的是,你也不必在此之前先学习 Scala 和 Haskell,我还是会用最流行的 JavaScript 来介绍模式匹配。 245 | 246 | #+CAPTION: 我觉得这个玩具可以特别形象的解释模式匹配这个概念 247 | [[./images/patten-matching.jpg]] 248 | 249 | ** 作为函数式语言 JavaScript 还差些什么 250 | 251 | 作为多编程范式的语言,原型链支持的当然是面向对象编程,然而却同时支持一等函数的 JavaScript 也给函数式编程带来了无限的可能。之所以说可能是因为 JavaScript 本身对于函数式的支持还是非常局限的,为了让 JavaScript 全面支持函数式编程还需要非常多的第三方库的支持。下面我们来列一列到底 JavaScript 比起纯函数式语言,到底还差些什么? 252 | 253 | *** 不可变数据结构 254 | 255 | 首先需要支持的当然是不可变(immutable)数据结构,意味着任何操作都不会改变该数据结构的内容。JavaScript 中除了原始类型其他都是可变的(mutable)。相反,Clojure 的所有数据结构都是不可变的。 256 | 257 | #+BEGIN_QUOTE 258 | JavaScript 一共有6种原始类型(包括 ES6 新添加的 Symbol 类型),它们分别是 Boolean,Null,Undefined,Number String 和 Symbol。 除了这些原始类型,其他的都是 Object,而 Object 都是可变的。 259 | #+END_QUOTE 260 | 261 | 比如 JavaScript 的 Array 是可变的: 262 | #+BEGIN_SRC js 263 | var a = [1, 2, 3]; 264 | a.push(4); 265 | 266 | #+END_SRC 267 | 268 | =a= 的引用虽然没有变,但是内容确发生了变化。 269 | 270 | 而 Clojure 的 Vector 类型则行为刚好相反: 271 | #+BEGIN_SRC clojure 272 | (def a [1 2 3]) 273 | (conj a 4) ;; => [1 2 3 4] 274 | a ;; => [1 2 3] 275 | #+END_SRC 276 | 277 | 对 =a= 的操作并没有改变 =a= 的内容,而是 =conj= 操作返回 的改变后的新列表。在接下来的第二章你将会看到 Clojure 是如何实现不可变数据结构的。 278 | 279 | *** 惰性求值 280 | 281 | 惰性(lazy)指求值的过程并不会立刻发生。比如一些数学题(特别是求极限的)我们可能不需要把所有表达式求值才能得到最终结果,以防在算过程中一些表达式能被消掉。所以惰性求值是相对于及早求值(eager evaluation)的。 282 | 283 | 比如大部分语言中,参数中的表达式都会被先求值,这也称为 /应用序/ 语言。比如看下面这样一个 JavaScript 的函数: 284 | #+BEGIN_SRC js 285 | wholeNameOf(getFirstName(), getLastName()) 286 | #+END_SRC 287 | =getFirstName= 与 =getLastName= 会依次执行,返回值作为 =wholeNameOf= 函数的参数, =wholeNameOf= 最后被调用。 288 | 289 | 另外,对于数组操作时,大部分语言也同样采用的是应用序。 290 | #+BEGIN_SRC js 291 | map(function(x){return ++x}, [1, 2, 3, 4]); 292 | #+END_SRC 293 | 294 | 所以,这个表达式立刻会返回结果 ~[1,2,3,4]~ 。 295 | 296 | 当然这并不是说 Javascript 语言使用应用序有问题,但是没有提供惰性序列的支持就是 JavaScript 的不对了。如果 map 后发现其实我们只需要前 10 个元素时,去计算所有元素就显得是多余的了。 297 | 298 | *** 函数组合 299 | 300 | 面向对象通常被比喻为名词,而函数式编程是动词。面向对象抽象的是对象,对于对象的的描述自然是名词。面向对象把所有操作和数据都封装在对象内,通过接受消息做相应的操作。比如,对象 Kitty 和 Pussy,它们可以接受“打招呼”的消息,然后做相应的动作。而函数式的抽象方式刚好相反,是把动作抽象出来,比如就是一个函数“打招呼”,而参数,则是作为数据传入的 Kitty 或者 Pussy,是完全透明的。比如 Kitty 进入函数“打招呼”时,出来的应该是一只 /Hello Kitty/ 。 301 | 302 | 面向对象可以通过继承和组合在对象之间分享一些行为或者说属性,函数式的思路就是通过 *组合* 已有函数形成一个新的函数。JavaScript 语言虽然支持高阶函数,但是并没有一个原生的利于组合函数产生新函数的方式。关于函数组合的技巧,会在第四章作详细的解释,而这些强大的函数组合方式却往往被类似 underscore 库的光芒掩盖掉。 303 | 304 | *** 尾递归优化 305 | 306 | Clojure 的数据结构都是不可变的,除了使用数据结果本身的方法进行遍历,另外的循环手段自然只能是递归了。但是在没尾递归优化的 JavaScript 中就不会那么愉快了。 307 | 308 | 在 JavaScript 中可能会经常看到这样的代码: 309 | #+BEGIN_SRC js 310 | var a = [1, 2, 3, 4] 311 | var b = [4, 3, 2, 1] 312 | for (var i = 0; i < 4; i++){ 313 | a[i]+=b[i] 314 | } 315 | console.log(a); 316 | // => [5,5,5,5] 317 | #+END_SRC 318 | 319 | 如果使用 Clojure 硬要做类似的事情通常只能使用 reduce 解决,代码会变成这样: 320 | 321 | #+BEGIN_SRC clojure 322 | (loop [a [1 2 3 4] 323 | b [4 3 2 1] 324 | i (dec (count a))] 325 | (if (< i 0) a 326 | (recur (update a i #(+ % (get b i))) b (dec i)))) 327 | #+End_src 328 | 329 | #+RESULTS: 330 | | 5 | 5 | 5 | 5 | 331 | 332 | recur 看起来跟 for 循环非常类似,其实它是尾递归,如果把 loop 写成一个函数: 333 | 334 | #+BEGIN_SRC clojure 335 | (defn zipping-add [a b i] 336 | (if (< i 0) a 337 | (recur (update a i #(+ % (get b i))) b (dec i)))) 338 | (zipping-add [1 2 3 4] [4 3 2 1] 3) 339 | #+END_SRC 340 | 341 | #+Results: 342 | : #'user/zipping-add[5 5 5 5] 343 | 344 | 事实上效果是一样的,但是如果把 =recur= 想象成是 =zipping-add= ,明显能看出 =zipping-add= 是一个尾递归函数。 345 | 346 | 因此反过来看,若是要把尾递归换成循环是多么容易的一件事情,关键的是需要让解释器识别出来尾递归。 347 | 348 | 但是这不是 Clojure 的风格,亦不是函数式的风格。递归应该被认为是比较低级别的操作,像这种高级别的操作还是应该优先使用 map,reduce 来解决。 349 | 350 | #+BEGIN_SRC clojure 351 | (map #(+ %1 %2) [1 2 3 4] [4 3 2 1]) 352 | #+END_SRC 353 | 354 | Clojure 的 map 是个神奇的函数,若是给多个向量,他做的事情会相当于先 zip 成一个向量,再把向量的元素 apply 到组合子上。这样完全不需要循环和变量,得到了一段不需要循环和变量的简洁的代码。 355 | 但是,在写低级别的一些代码的时候,递归还是强有力的武器,而且尾递归优化能带来更好的性能,在第五章我会更详细的介绍不可变数据结构以及递归。 356 | 357 | ** Underscore 你错了 358 | 359 | 如果提到 JavaScript 的函数式库,可能会联想到 Underscore[fn:9]。Underscore 的官网解释是这样的: 360 | #+BEGIN_QUOTE 361 | Underscore 提供了100多个函数,不仅有常见的函数式小助手: map,filter,invoke,还有更多的一些额外的好处…… 362 | #+END_QUOTE 363 | 364 | 我就懒得翻译完了,重点是这句话里面的“函数式小助手”,这点我实在不是很同意。 365 | 366 | *** 跟大家都不一样的 map 函数 367 | 368 | 比如 map 这个函数式编程中比较常见的函数,我们来看看看 *函数式语言* 中都是怎么做 map 的: 369 | 370 | *Clojure:* 371 | #+BEGIN_SRC clojure 372 | (map inc [1 2 3]) 373 | #+END_SRC 374 | 375 | 其中 =inc= 是一个给数字加一的函数。 376 | *Haskell:* 377 | #+BEGIN_SRC haskell 378 | map (1+) [1,2,3] 379 | #+END_SRC 380 | 381 | 同样 =(1+)= 是一个函数,可以给数字进行加一操作。 382 | 383 | 这是非常简单的 map 操作,应用函数 =inc=, =(1+)= 到数组 中的每一个元素。同样的事情我们试试用 Underscore 来实现一下: 384 | #+BEGIN_SRC js 385 | _.map([1,2,3], function(x){return x+1}) 386 | #+END_SRC 387 | 388 | 感觉到有什么变化了吗?有没有发现参数的顺序完全不同了?好吧,你可能要说这并不是什么问题啊?不就是 map 的 api 设计得不太一样么?也没有必要保持所有的语言的 map 都是一样的吧? 389 | 390 | 在回答这个问题之前,我想再举几个例子,因为除了 Underscore,JavaScript 的函数式库还有很多很多: 391 | 392 | [[http://ramdajs.com/][*ramdajs*]]: 393 | #+BEGIN_SRC js 394 | R.map(function(x){return x+1}, [1,2,3]) 395 | #+END_SRC 396 | 397 | [[http://functionaljs.com/][*functionaljs:*]] 398 | #+BEGIN_SRC js 399 | fjs.map(function(x){return x+1}, [1,2,3]) 400 | #+END_SRC 401 | 402 | 应该不需要再多的例子了,不管怎么样看,underscore 的 map 是否都略显另类了呢?跟别的语言不一样就算了,跟其他 JavaScript 的函数式库都不一样的话,是不是有些说不过去了。 我猜 underscore 同学估计现在有种高考出来跟同学对答案,发现自己的答案跟别人的完全不一样的心情。 403 | 404 | 好吧,Underscore 先别急着认错,大家都这么做,肯定不是偶然。但是原因就说来话长了,我将会在第四章详细解释其他函数式语言/库为什么都跟 Underscore 不一样。[fn:10] 405 | 406 | 当然我可不会选一个“另类”的库来阐述函数式编程。[fn:11]我将像编程世界中最好的书《计算机程序的构造与解释》一样,我选择用 lisp 语言来阐述函数式编程概念,而用目前最流行的语言 —— JavaScript [fn:12]来实践函数式。当然我也不会真的用老掉牙的 scheme,因为所有前端开发者都应该知道,前端最唾弃的就是使用久的东西[fn:13],这样一来 Clojure 这门全新的现代 lisp 方言显然是最好的选择。 407 | 408 | *** ClojureScript 409 | 410 | Clojure 是跑着 JVM 上的lisp 方言,而 ClojureScript 是能编译成 JavaScript 的 Clojure。但是请不要把 ClojureScript 与 CoffeeScript,LiveScript,TypeScript做比较,就像每一行 Clojure 代码不能一一对应到 Java 代码一样,你可能很难像 CoffeeScript 对应 JavaScript 一样能找到 ClojureScript 与其编译出来的 JavaScript 的对应关系。 411 | 412 | #+caption: 各种编译成 JavaScript 的函数式语言 413 | [[./images/everyscript.png]] 414 | 415 | 不管怎么样,ClojureScript 把 Clojure 带到了前端确实是非常令人激动的一件事情。就跟前端程序员能在后端写 JavaScript 一样,Clojure 程序员终于能在前端也能找到自己熟悉的编程姿势。但是如同 Clojure 于 Java 的交互一样(或者更坏), ClojureScript 与 JavaScript 及JavaScript 的库的交互并不是那么容易,或者可以说,不那么优雅。而且前端开发者可能并不能很快的适应 lisp 语言,项目(特别是开源项目)的维护不能只靠懂 clojure 的少数开发者,所以如果能用最受欢迎的 JavaScript,又还能使用到 Clojure 的所有好处,那将再好不过了。幸运的是,Clojure 的持久性数据结构被 David Nolen[fn:14]移植到了原生 JavaScript —— [[https://github.com/swannodette/mori][mori]]。 416 | 417 | *** Mori 418 | 419 | 由于是移植的,所有的数据结构以及操作数据结构的函数都是 ClojureScript 保持一致,而且是作为 JavaScript 库,可以在原生 JavaScript 的代码中使用。显然 mori 是最适合用于前端函数式实践的库,当然也是本书为什么说是 Clojure 风格的函数式 JavaScript 的原因了。 420 | 421 | 选择 mori 的另一原因是因为它特别区别于其他的函数式库的地方——它使用 ClojureScript 的数据结构。也就是说从根本上消除了 JavaScript 可变的数据结构模型,更利于我们的进行函数式编程。 422 | 423 | #+BEGIN_QUOTE 424 | 为了保持从风格上更类似于 Clojure,以及迁移 Clojure 中的一些 macro,本书中也使用了我写的一系列的 macro —— [[http://ru-lang.org][ru-lang]][fn:15]。更多的关于 macro 的讨论我会放到第五章。 425 | #+END_QUOTE 426 | 427 | 当然,选择 mori 并不说明它是工程的上函数式类库的最佳选择,facebook 活跃维护的 Immutable.js 也是不错的选择。但是在这里,mori 确实是能将 Clojure 编程思想蔓延到 JavaScript 中的最好桥梁。 428 | 429 | * Footnotes 430 | 431 | [fn:15] http://ru-lang.org 432 | 433 | [fn:2] 至少在本书写到这一行之前是这样的。 434 | 435 | [fn:1] 也被叫做 ECMAScript 2015,本书中会简称为 ES6。 436 | 437 | [fn:3] 当然逻辑式编程并不是本书的重点,也不会展开深入介绍,如果有兴趣,可以联系出版社让我在写一本。 438 | 439 | [fn:4] 事实上,JavaScript 或者 Scala 其实也是通过把函数作为一种特殊的对象,来把函数变成一等公民。不过,在使用上基本感觉不到函数是对象。而在 Clojure 中,函数确确实实就是一等公民,因为所有 lisp 语言都一样,代码即是数据。 440 | 441 | [fn:5] thunk 的翻译“槽”来自《计算机程序的构造与解释》,但是我个人倾向不做翻译,因为很难从“槽”这一个字中获取到足够多的解释。 442 | 443 | [fn:6] 相对于廋箭头(thin arrow)。 444 | 445 | [fn:7] 正如我说的本书不是 JavaScript 的教科书,所以关于动态绑定和词法绑定,这里不会做太多的解释。简单的解释就是词法绑定可以从词法分析(通俗的说就是肉眼直接能看出来)判断出来绑定的值,而相反动态绑定需要根据运行时上下文决定。 446 | 447 | [fn:8] 同样的,我读的中文技术书太少,倾向于不翻译此类专业名词。翻译错了反而体会不出来原词的意思。这里明显 structure 是构造,前面加 de 词根,就是构造的反过程。 448 | 449 | [fn:9] Underscore在github上的收藏量已经超过一万五了,无疑是JavaScript最流行的库之一。 450 | 451 | [fn:10] 当然我并不是第一个发现 Undersocre 奇怪的人,13年一次js大会上就有人提出了这个话题 https://www.youtube.com/watch?v=m3svKOdZijA。 452 | 453 | [fn:11] 虽然 Michael Fogus 的《函数式 JavaScript 编程》中就是用 Underscore。 454 | 455 | [fn:12] 根据 github 的报告 https://github.com/blog/2047-language-trends-on-github。 456 | 457 | [fn:13] 前端社区发展特别奇怪,不管是什么库,过一段时间就有类似的库出来,把前一个的缺点列一遍,大家都开始用新的,而唾弃旧的库。作者很好奇什么时候 React 会开始被唾弃。 458 | 459 | [fn:14] ClojureScript 作者。 460 | 461 | -------------------------------------------------------------------------------- /book/zh/第七章.org: -------------------------------------------------------------------------------- 1 | * Monadic编程 2 | 3 | 这是 Haskell 弄出来的神秘玩意,但你可能没有在意,其实在前端世界早都被玩腻了。 4 | 5 | ** 链式调用 6 | 7 | 说到 /链式调用/ ,用过 jQuery 或是 Underscore 的人再熟悉不过了。虽然我一直在强调函数组合如何复用性更好,更持续可组合,但是其实这两者之间并不对立,反而应该是可以互相结合的两种非常好的模式。 8 | 9 | 作为前端比较流行的模式,我们其实甚至不需要借助于任何库也可以进行简单的链式调用,比如 ES5 的 Array: 10 | 11 | #+BEGIN_SRC js 12 | [1,2,3,4] 13 | .map(n=>n+1) 14 | .filter(n=>n%2**0) 15 | .reduce((acc, n)=>acc+n); 16 | #+END_SRC 17 | 18 | 由于 Array 的 map,filter 都仍然返回 Array,所以很自然的,可以链式的继续调用 Array 的方法。 19 | 20 | 当然,如果做这种运算我肯定是更推荐使用 Transducer 来做,将会更高效而且易于组合再利用。相比之下,链式调用其实有着更为适用的场景,那就是 ES6 标准中新加入的 Promise。 21 | 22 | *** Promise 23 | 24 | Promise 跟语义上一样,表示将来会发生但是还没有发生的事情。在不同的编程语言中名字略有不同,但都是同一个东西。只是,Promise 在 JavaScript 中是非阻塞的,也就是你不能直接 await 一个 Promise, 这样会使得主线程被阻塞。 25 | 26 | 而在 Clojure 中,可以 27 | 28 | 1. 定义一个 promise 29 | 2. 非阻塞的开启另一个线程给 promise 递一个值 30 | 3. 阻塞的从 promise 把值取出来, =@= 相当于 await 31 | 32 | #+BEGIN_SRC clojure 33 | (def fancy-promise (promise)) ; 1 34 | (future (Thread/sleep 1000) 35 | (deliver fancy-promise (+ 1 1))) ;2 36 | (print "waiting...") 37 | @fancy-promise ;2 38 | ;=> waiting... 39 | ;一秒后 => 2 40 | #+END_SRC 41 | 42 | 但是在资源有限的前端 JavaScript,宝贵的主线程只有一个,是不能让你能阻塞的去取一个 promise 的值的。 43 | 44 | #+BEGIN_SRC js 45 | var defered = when.defer(); 46 | setTimeout(_=>defered.resovle(1+1), 1000); 47 | defered.promise.then(_=>console.log(_)); 48 | #+END_SRC 49 | 50 | 你会发现 JavaScript 的 promise 略有不同。首先,在 JavaScript 中不能开一个线程去等上1秒,再给 promise 递一个值。前端只能依靠浏览器的 timer 线程去计算时间,1 秒后回调递值的函数;其次,当然也不能阻塞主线程来等待 promise 的值,只能通过传入回调函数给 promise 的 then 方法,来接受 promise 中的值并做处理。因为 JavaScript 引擎只有一个线程,这意味着如果我们阻塞的等待 promise 的值: 51 | 52 | #+BEGIN_SRC js 53 | // 伪代码 54 | ... 55 | setTimeout(_=>defered.resovle(1+1), 1000); 56 | await defered.promise 57 | #+END_SRC 58 | 59 | 那么 =defered.resovle(1+1)= 永远不会被执行。因为 JavaScript 是事件循环的并发模型[fn:2],意味着事件循环会不停的从消息队列中拿任务。所以这几行代码大概的执行过程是这样的: 60 | 61 | 1. setTimeout 的执行分派了浏览器 timer 去计时。 62 | 2. promise.then 的执行给 promise 挂了一个回调,当 promise 满足时调用。 63 | 3. 代码都执行完了, 消息队列里此时没有消息。 64 | 4. 1秒后 timer 把 =defered.resovle(1+1)= 放入消息队列。 65 | 5. =defered.resovle(1+1)= 随即促发 promise.then 上的回调。 66 | 67 | 所以如果 2 被阻塞,意味着 4 放回到消息队列的任务永远也轮询不上,因为当 event loop 还正阻塞在 2,而 2 却在等 5 的结果。 68 | 69 | 因此,前后端 JavaScript 的所有的 IO 操作(timer,XHR,worker)才被设计成为非阻塞的。 70 | 71 | 回到我们的问题上来,我们不知道 promise 什么时候有值,也不能阻塞的等待 promise 有值,所以只能是把回调函数给 promise,什么时候有值了再回调这个函数。而这个回调函数需要通过 promise 的 then 方法传给 promise。跟使用 jQuery 一样,then 返回的还是一个 promise,因此我们可以继续调用 then 方法给更多的的回调函数。 72 | 73 | 当然,除了传入一个接受 promise 内的值,返回另一个值(或不返回)的函数,还可以传入一个返回 Promise 的函数: 74 | 75 | #+BEGIN_SRC js 76 | when.promise(resolve=>setTimeout(_=>resolve(1),1000)) 77 | .then(value=>when.promise(resovle=>setTimeout(_=>resolve(value+1),1000))) 78 | .then(_=>console.log(_)) 79 | #+END_SRC 80 | 81 | 两秒后打印出来的是2,而不是一个 promise。注意这里用的是更简洁的创建 promise 的方法,直接给 promise 的参数传入一个 resolve 函数,告诉 promise 什么时候能 resolve。 至于为什么最终打印的不是 promise,而是 promise 的值,我想看完 7.3 节 Monad 自然会明白。 82 | 83 | *** 高阶 Promise 84 | 85 | 不像普通的操作值,可以通过高阶函数轻松的操作整个数组: 86 | 87 | #+BEGIN_SRC js 88 | var data = [1, 2, 3, 4]; 89 | 90 | var double = data.map(_=>_*2); 91 | 92 | console.log(double); // [2,4,6,8] 93 | #+END_SRC 94 | 95 | 如果是一个 promise 的数组,在想做 map 或者 filter 之类的用高阶函数来操作数组似乎就遇到了麻烦。 96 | 97 | #+BEGIN_SRC js 98 | var data = [when(1), when(2), when(3), when(4)]; 99 | 100 | var double = data.map(_=>_*2); 101 | 102 | console.log(double); // [NaN,NaN,NaN,NaN] 103 | #+END_SRC 104 | 105 | 当然,想着都不可能会工作。这里期望的结果应该是 promise 级别的 map,而不仅仅是把函数 map 到值上。选择使用 when 正是因为它提供更强大的 promise,不但有操作单个 promise 的方法,还提供通过 map,reduce 等高阶函数轻松的操作 promise 的数组。 只需要做小小的改动, 使用 =when.map= 方法就好了: 106 | 107 | #+BEGIN_SRC js 108 | var double = when.map(data, _=>_*2); 109 | 110 | double.then(_=>console.log(_)); // [2,4,6,8] 111 | #+END_SRC 112 | 113 | 114 | ** Monad 115 | 116 | 我为什么要在链式调用之后介绍 Monad,这个来自 Haskell 的这个看似令人费解的东西呢?虽然说当你问一个 Haskell 的人如何理解 Monad 时,他们经常会让你先了解下 Category Theory,但其实并没有这个必要。Crockford 在解释 Monad 的时候就说过: 117 | 118 | #+BEGIN_QUOTE 119 | 吃墨西哥鸡卷前要先学会墨西哥语吗? —— Crockford 120 | #+END_QUOTE 121 | 122 | 所以,我也并不打算在这里讲晦涩的范畴论(Catergory Theory),当然如果感兴趣的话网上有大把的资料。不过,你有可能会在本节结束时能理解下面这句话: 123 | #+BEGIN_QUOTE 124 | Monad 就是自函子范畴上的一个幺半群。 125 | #+END_QUOTE 126 | 127 | 那么,在了解 monad 之前,我们先来聊聊函子(Functor)。 128 | 129 | *** 函子(Functor) 130 | 131 | Functor 简单的来说是可以被 map over 的类型. 但是什么叫 map over? 132 | 133 | 比如 Array 就可以说是可以被 map over 的类型,来看看 Haskell 中如何解释的: 134 | 135 | #+BEGIN_EXAMPLE 136 | ghci > :t fmap 137 | fmap :: (a -> b) -> fa -> f b 138 | #+END_EXAMPLE 139 | 140 | 这里的 =fmap= 就是 map over 在 Functor 上的函数。这个函数只干一个事情, 141 | ~(a -> b) -> f a -> f b~ 这个类型标记告诉了我们,一个 a 到 b 的映射在接收 a 的 Functor 之后,返回一个 b 的 Functor。 142 | 143 | 跟所有的数学定义一样,这个太抽象,我们来具体化一下以便理解。例如我现有有一个 number 类型的 a,那么我可以定义一个由 number 到 string 的映射,也就是接受 a 为参数的函数 f,使得 f(a) 返回 string 类型 b。 144 | 145 | #+BEGIN_SRC js 146 | var f = x => x + 'b'; 147 | var a = 2; 148 | typeof a == 'number'; 149 | var b = f(a); // => 2b 150 | typeof b == 'string' 151 | #+END_SRC 152 | 153 | #+caption: 映射值的函数 154 | [[./images/func-value.png]] 155 | 156 | 那么如果 a 存在一个容器 fa 中,显然我们不能简单的通过 f(fa) 就能得到一个 b 类型的容器 fb。制作一个容器很简单,只需要创建一个对象就好了,这里我们把这个对象叫 Just 好了。 157 | 158 | #+BEGIN_SRC js 159 | function Just(val){ 160 | this.value = val; 161 | } 162 | var fa = new Just(a); 163 | f(fa) 164 | // => [object Object]b 165 | #+END_SRC 166 | 167 | 显然这时候映射 f 是不工作的,输出的是一个奇怪的结果,而不是期望的包含 b 的容器 fb,也就是 Just(b)。 168 | 169 | 要实现这种映射其实也非常简单,只需要容器 Just 能告诉映射 f 如何应用到容器内的值就好了,而这种如何将函数应用到自己内部值的方法,就是 fmap,而实现了 fmap 的容器,就叫做 Functor。 170 | 171 | #+BEGIN_SRC js 172 | Just.prototype.fmap = function(f) { return new Just(f(this.value))} 173 | Just.prototype.of = function(value) { 174 | return new Just(value) 175 | } 176 | var fa = Just.of(a); 177 | var fb = fa.fmap(f); // => Object { value: "2b" } 178 | fb instanceof Just; // => true 179 | #+END_SRC 180 | 181 | 可以看到通过 fmap,得到了期望的在容器 Just 中的值 "2b"。其中的 =of= 方法是为了在构造一个 Just 容器时代码更可读一些。 182 | 183 | #+caption: Functor 可以通过 map 把函数映射应用到容器内的值 184 | [[./images/functor.png]] 185 | 186 | 而刚刚的 Just 就是 Functor,由于它实现了 fmap,所以就可以说是可以 map over 了,自然就成为了 Functor。 187 | 188 | 所以照这个逻辑,那么我只需要做一点点手脚,那么 Array 也会变成 Functor 了: 189 | 190 | #+BEGIN_SRC js 191 | Array.prototype.fmap = Array.prototype.map 192 | #+END_SRC 193 | 194 | 等一下,出自 Category 理论,难道就没有写什么 Functor 的定理什么的? 195 | 196 | 当然,由于出自完整的理论,这里也顺便提一下 Functor 具有以下性质: 197 | 198 | 1. fa.fmap(x=>x) 恒等于 fa 199 | 2. fa.fmap(x=>f(g(x))) 恒等于 fa.fmap(g).fmap(f) 200 | 201 | 我们先来测试一下 Array: 202 | #+BEGIN_SRC js 203 | [1,2,3].fmap(x=>x); // => [1,2,3] 204 | var f = x=>x+2 205 | var g = x=>x*2 206 | [1,2,3].fmap(x=>f(g(x))); // => [4,6,8] 207 | [1,2,3].fmap(g).fmap(f) // => [4,6,7] 208 | #+END_SRC 209 | 210 | 看上去是没有什么问题的,我就不充分证明了,但是这个定理在后面的理解会是非常有帮助的。 211 | 212 | *** Applicative Functor 213 | 现在,我们已经有 fmap 可以把一个函数应用到一个 Functor 上,接着来看看下一个概念——Applicative Functor(下面简称 Applicative)。有了 Functor 之后,我们可以直接给容易加一组映射,但是,函数既然是一等的,那么值能放到容器中,函数是不是也可以放到容器里呢? 214 | 215 | #+BEGIN_SRC js 216 | var fplus2 = Just.of(x=>x+2); 217 | fplus2(Just.of(3)); 218 | #+END_SRC 219 | 显然在一个含有函数的容器是不能直接应用到任何容器或者值的,fmap 显然也不能帮我们把装有函数的容器打开。这时候 Applicative 就出来解决这种问题了。 220 | 221 | #+BEGIN_SRC js 222 | Just.prototype.ap = function(container){ 223 | container.fmap(this.value); 224 | } 225 | fplus2.ap(Just.of(3)) // => 5 226 | #+END_SRC 227 | 228 | 没有想到,只是这么简单就又实现了 Applicative 吧。 现在知道为什么我先介绍 Functor 了吧。所以现在看来,所有的 Applicative 都也是 Functor,因为需要实现 fmap 方法。 229 | 230 | 所以来看下 Applicative 的类型注解 =f (a -> b) -> f a -> f b= ,一个 a 到 b 映射的容器,接收 a 的容器返回 b 的容器,那么这个容器就是 Applicative。 231 | 232 | 同样的,Applicative 也具备一些性质: 233 | 234 | 1. 恒等性: =a.of(x=>x).ap(value)= 恒等于 value 235 | 2. 同态: =a.of(f).op(a.of(x))= 等于 =a.of(f(x))= 236 | 3. 交换律: =u.ap(a.of(y))= 等于 =a.of(f=>f(y)).ap(u)= 237 | 238 | 刚好试一下我们的新 Just 有没有符合这些性质: 239 | 240 | #+BEGIN_SRC js 241 | Just.of(x=>x).ap(3) //=> 3 242 | Just.of(x=>x+1).ap(Just.of(2)) == Just.of((x=>x+1)(2)) // true 243 | Just.of(x=>y).ap(Just.of(3)) == Just.of(f=>f(3)).ap(Just.of(x=>y)) //true 244 | #+END_SRC 245 | 246 | 如果把容器看成是 lift 的话,那么把坐电梯下来(把容器剥开,这里既是去掉所有的 Just.of 和 ap),他们其实保持着函数与值同样的关系: 247 | 248 | #+BEGIN_SRC js 249 | (x=>x)(3) //=> 3 250 | (x=>x+1)(2) == (x=>x+1)(2) // true 251 | (x=>y)(3) == (f=>f(3))(x=>y) //true 252 | #+END_SRC 253 | 254 | *** 含幺半群(Monoid) 255 | 256 | 前面说了两个抽象的 Functor 与 Applicative,分别是两种不同等级的 lift 操作,前者 lift 了值,后者 lift 函数,但都是为了 lift 或者说抽象函数应用,例如 =f(x)= 。 257 | 258 | 那么值之间的二元操作该如何抽象或者说 lift 呢?比如简单的值之间的算术运算加法: 259 | 260 | #+BEGIN_SRC js 261 | 1 + 2 //=> 3 262 | #+END_SRC 263 | 264 | 这并不是想 =f(x)= 的简单一元操作,这里如果把 =+= 号理解成函数的话,这将是二元操作: 265 | 266 | #+BEGIN_SRC js 267 | add(1, 2) //=> 3 268 | #+END_SRC 269 | 270 | 那么这里需要抽象三个东西,或者说把三个值放到容器中,或者所如何把容器中的两个值叠加起来? 271 | 272 | 这样看来,无论是 Functor 或者 Applicative 都没有办法把这些盒子撮合到一起,变成一个含有3的盒子 =Just.of(3)= 。 273 | 274 | 于是,我们需要把它们抽象成 Monoid,先别着急问 Monoid 的概念是什么,等我举完这个例子先。这里用 Just 来做算术运算不太合适,请允许我换一个语义更好一些的容器比如 Sum。这里我们要做的 大概是像这样叠加两个容器: 275 | 276 | #+BEGIN_SRC js 277 | Sum.of(1).append(Sum.of(2)) // => Sum.of(3) 278 | #+END_SRC 279 | 280 | 所以重点是这个 append 方法,实现上应该要把两个盒子内的值做加法: 281 | 282 | #+BEGIN_SRC js 283 | Sum.prototype.append = function(anotherSum){ 284 | return anotherSum.fmap(value=>this.value+value); 285 | } 286 | #+END_SRC 287 | 288 | 有意思的是,加法会存在一个类似 identity 的元素 empty,使得任何数加 empty 值完全不变,因此加法的 emtpy 就是 0。那么当我抽象到容器之后,应该符合同样的规则,Sum 应该也存在一个 empty,使得 =Sum.of(any).append(Sum.empty)= 任然是 any。 289 | 290 | #+BEGIN_SRC js 291 | Sum.prototype.empty = Sum.of(0); 292 | #+END_SRC 293 | 294 | 没有错,那就是 Sum.of(0),我们管这个 empty 叫做 _幺元_(identity element). 好了这个简单的例子讲完也实现了,现在可以来定义 Monoid 了,现在这个 Sum 就是 Monoid, 因为他 295 | 296 | 1. 存在一个二元操作返回值仍然是 Sum 297 | 2. 有一个幺元,满足 ~Sum.empty.append(Sum.of(2)) == Sum.of(2)~ 298 | 3. 满足结合律 ~Sum.of(1).append(Sum.of(2)).append(Sum.of(3)) == Sum.of(2).append(Sum.of(2).append(Sum.of(3)))~ 299 | 300 | 对于 Monoid 的实现非常简单,可能并没有人会关系实现,而更重要的是,为什么要满足这些条件,这些公理能带来什么好处。 301 | 302 | #+caption: monoid 只是抽象到容器的二元操作 303 | [[./images/monoid.png]] 304 | 305 | 想一想我们要叠加多个数,可以这样: 306 | 307 | #+BEGIN_SRC js 308 | 1+2+3+4+5 309 | #+END_SRC 310 | 311 | 当然有更高雅的 reduce 方式: 312 | 313 | #+BEGIN_SRC js 314 | [1,2,3,4,5].reduce((_1,_2)=>_1+_2) 315 | #+END_SRC 316 | 317 | 好了,那么抽象到容器,我们是不是可以干同样的事情? 318 | #+BEGIN_SRC js 319 | [Sum.of(1), Sum.of(2), Sum.of(3), Sum.of(4), Sum.of(5)].reduce((_1,_2)=>_1.append(_2)) 320 | #+END_SRC 321 | 322 | 323 | *** Monad 324 | 好了,有了包在容器中的函数或值之后,我们可以 fmap 一个函数到容器,我们还可以直接应用含有函数的容器到另一个容器,我们还可以像函数组合一样组合这些容器们。但是,还记得与函数组合(compose)一起介绍的管道(pipeline)吗?通过管道,可以让函数组合的顺序更符合阅读习惯,更适合一眼就能看出数据的流向。激动人心的时刻到了,Monad 这个被解释的过于抽象的东西,我要说其实就是容器界的管道。 325 | 326 | 如果我告诉你在前一节其实已经见过 Monad 了(如果不小心跳过了上一节,赶紧回去补一补),你会信吗? 327 | 328 | #+BEGIN_SRC js 329 | Promise(resolve=>0) 330 | .then(x=>x+1) // 1 331 | .then(x=>2/x) // 2 332 | #+END_SRC 333 | 334 | 当然,这个并不是 Monad,这是简单的 pipe,等同于: 335 | #+BEGIN_SRC js 336 | mori.pipeline(0, x=>x+1, x=>2/x) 337 | #+END_SRC 338 | 339 | 正常的函数组合的顺序应该是这样的: 340 | 341 | #+BEGIN_SRC js 342 | (x=>2/x)((x=>x+1)(0)) 343 | #+END_SRC 344 | 345 | 运用我们前面学过的 Applicative Functor,来把这些都装容器里试试,首先,来把函数组合装容器中,继续使用我们刚刚实现的 Just: 346 | 347 | #+BEGIN_SRC js 348 | Just.of(x=>2/x).ap(Just.of(x=>x+1).ap(Just.of(0))) 349 | #+END_SRC 350 | 351 | 本来看函数的嵌套调用都已经有些头晕了,这回全部包了层容器,更是摸不着头脑了。试试用 Functor 实现: 352 | 353 | #+BEGIN_SRC js 354 | Just.of(0).map(x=>x+1).map(x=>2/x) 355 | #+END_SRC 356 | 357 | 似乎已经非常像 pipeline,以及可以链式编程的 Promise 了,但是现在问题来了,如果我再 map 一个函数,而且这时输入变成了 Just.of(1) 358 | 359 | #+BEGIN_SRC js 360 | Just.of(1).fmap(x=>x+1).fmap(x=>2/x).fmap(x=>x*x) 361 | #+END_SRC 362 | 363 | 你会看到直接抛异常了,因为输入改成 =Just.of(1)= 之后,当数据通过第一个函数返回的是 0,也就是当 =Just.of(0)= 再 =map(x=>2/x)= 相当于除零运算,当然是异常。 364 | 365 | 好,这时候如果能让我避免异常,可以在 map 里的函数加 try catch,或者 if else 验证输入条件。但是,最后的那个 map 怎么办?如果是 try catch 没有返回,意味着后面的 x*x 自然也会出错,如果是 if else,那么 else 里面应该返回什么呢? 366 | 367 | 如果能返回一个容器,map 任何函数到它都还是它自己,似乎就可以忽略掉一旦出错后面的一大堆 map 了。 368 | 369 | #+BEGIN_SRC js 370 | function Nothing(){}; 371 | Nothing.prototype.fmap = function(){ 372 | return this; 373 | } 374 | var nothing = new Nothing; 375 | #+END_SRC 376 | 377 | 问题是,Just 不管怎么 fmap 都是 Just,如何能让它变成 Nothing 呢? 378 | 379 | #+BEGIN_SRC js 380 | Just.of(1).fmap(x=>x+1).fmap(x=>{ 381 | if(x==0) return nothing; 382 | return Just.of(2/x); 383 | }) 384 | #+END_SRC 385 | 386 | 没有错,这段代码返回的会是一个在容器里的容器, =Just.of(Nothing)= 或者 =Just.of(Just.of(2/x))= ,所以我们还需要修改后面 map 中的函数程序才能正常工作: 387 | 388 | #+BEGIN_SRC js 389 | Just.of(1).fmap(x=>x+1).fmap(x=>{ 390 | if(x==0) return nothing; 391 | return Just.of(2/x); 392 | }).fmap(x=>x.fmap(y=>y*y)) 393 | #+END_SRC 394 | 395 | 这显然越改越复杂了,现实包一层,后面的 fmap 再拆掉前面包的这一层容器,那我们何必要多包一层呢?不如直接不包,比如将包和拆包放到 flatmap 方法中。 396 | 397 | #+BEGIN_SRC js 398 | Just.prototype.flat = function(){ 399 | return this.value 400 | } 401 | Just.prototype.flatmap = function(f){ 402 | this.fmap(f).flat(); 403 | } 404 | Nothing.prototype.flatmap = Nothing.prototype.fmap 405 | #+END_SRC 406 | 407 | 当然 ,这样我们能写出一个更优雅的带容器的 pipeline 了: 408 | 409 | #+BEGIN_SRC js 410 | Just.of(1).flatmap(x=>Just.of(x+1)).flatmap(x=>{ 411 | if(x==0) return nothing; 412 | return Just.of(2/x); 413 | }).flatmap(x=>Just.of(x*x)); 414 | // => nothing 415 | #+END_SRC 416 | 417 | *** TODO Monad 就是自函子范畴上的一个幺半群 418 | 但是举了这么些例子,怎么一点也看不出来 monad 与 monoid 的关系呢?倒是很明确 Monad 是个比较特殊的 Functor。 419 | 420 | 我们需要进一步的抽象才能解释这句话,首先,回顾前面 Monoid 的知识,比如那个 Sum 的 Monoid。 421 | 422 | #+BEGIN_SRC js 423 | Sum.of(1).append(Sum.of(2)).append(Sum.of(0)) // => Sum.of(3) 424 | #+END_SRC 425 | 426 | 这个 Monoid 很明显,它的二元操作是 =append= ,幺元是 =Sum.of(0)= ,范畴是 =Sum= 。 为了更明显我们可以降一个维度(范畴),把 =Sum.of(1)= 降成 =1= 427 | 428 | (1 + 2 + 0) 429 | 430 | 但是 Monad 哪来的二元操作啊?一个 =flat= ,一个 =fmap= ,都是一元操作啊? 431 | 432 | 如果我们降一个维度, 到只有数字的维度上, 433 | 434 | #+BEGIN_EXAMPLE 435 | (1.2).3 = 1.(2.3) 436 | #+END_EXAMPLE 437 | 438 | 二元操作 =+= 可以使得这个等式成立。 439 | 而幺元 0,又可以使得 440 | 441 | #+BEGIN_EXAMPLE 442 | 0.2 = 2 = 2.0 443 | #+END_EXAMPLE 444 | 在二元操作 =+= 上成立。 445 | 446 | 所以我们得到的 monoid 为 (数字集合,二元操作 =+= ,幺元 0)。 447 | 448 | 同样的,上升到函子的范畴上[fn:3],注意是自函子 *范畴* 上的幺半群,就代表的是函子范畴而不是函子实例是幺半群,所以以 Maybe 为例,就需要符合: 449 | 450 | #+BEGIN_EXAMPLE 451 | (Maybe.Maybe).Maybe = Maybe.(Maybe.Maybe) 452 | #+END_EXAMPLE 453 | 454 | 能找到一个二元操作使得上式成立?而不是: 455 | 456 | #+BEGIN_EXAMPLE 457 | (Just.of(1).append(Just.of(2))).append(Just.of(3)) = Just.of(1).append(Just.of(2).append(Just.of(3))) 458 | #+END_EXAMPLE 459 | 460 | 这个二元操作就是我们刚实现的 Just Monad 的 flat。我们很容易可以把 flat 代入到式子变换成以下格式, 461 | #+BEGIN_EXAMPLE 462 | flat(flat(Maybe.Maybe).Maybe) = flat(Maybe.flat(Maybe.Maybe)) 463 | #+END_EXAMPLE 464 | 465 | 其中的 =.= 是两个类型组合在一起。如同函数的组合是 ~f.g x= f(g(x))~ ,在函子范畴上的组合就是 Maybe 类型的值,在一个 Maybe 类型的容器中: 466 | 467 | #+BEGIN_EXAMPLE 468 | Maybe.Maybe = Maybe[Maybe] 469 | #+END_EXAMPLE 470 | 471 | 再代入一遍,就非常清晰结合律的等式是成立的了: 472 | 473 | #+BEGIN_EXAMPLE 474 | flat(flat(Maybe[Maybe])[Maybe]) = flat(Maybe[flat(Maybe[Maybe])]) 475 | #+END_EXAMPLE 476 | 477 | 大声念出来应该就是: 478 | flat 一个容器为 =flat(Maybe[Maybe])= 类型,其值类型为 =Maybe= 得到的类型等于, 479 | flat 一个 Maybe 类型,其内值类型为 =flat(Maybe[Maybe])= 所得到的类型。 480 | 481 | 听起来比较像绕口令,仔细看看等式就能理解了。另外一个 monoid 的法则是需要有一个幺元,满足: 482 | 483 | #+BEGIN_EXAMPLE 484 | ?.Maybe = Maybe = Maybe.? 485 | #+END_EXAMPLE 486 | 487 | 我们很容易能猜到把 Maybe 代入就是我的解,因此任何 Maybe 类型都是幺元: 488 | 489 | #+BEGIN_SRC js 490 | flat(M[M]) = M = flat(M[M]) 491 | #+END_SRC 492 | 493 | *所以,flat 就像 moniod 里的 append 一样,但是它并不连接值或是容器,而是连接函子组合,让函子在不同范畴间变换* 494 | 495 | 496 | #+caption: Monad 是 Functor 类型的 Monoid 497 | [[./images/monad.png]] 498 | 499 | 到这里,我可以告诉你现在的 Just 就是 Monad 了, 它是 Functor 的加强,把 fmap 的结果铺平(flat)。同时又是 Applicative 的加强, Applicative 让我们可以用一般函数作用到在容器中的值,而 Monad 让我们可以把一个容器中的值传入一个接收值的函数中,并返回同样的容器。 500 | 501 | ** 走钢丝 502 | 503 | 如果用简单的 Monad 来表示薛定谔猫就再简单不过了,比如往盒子在加放射性原子,如果猫活着,就是绿巨猫, 504 | 如果猫是死的,那就是绿巨死猫。 505 | #+BEGIN_SRC js 506 | Box.of(cat).flatmap(cat=>{ 507 | var hulkcat = radioactive(cat) 508 | if(hulkcat=='dead') 509 | return BloodyBox.of(hulkcat) 510 | return Box.of(hulkcat) 511 | } 512 | #+END_SRC 513 | 514 | 所以可以说我们最终得到的若不是(either)含有绿巨猫,就是含有一只绿巨死猫的血淋淋的盒子。这个例子过于简单,我们来一个更实际的例子。 515 | 516 | *** 皮尔斯走钢丝 517 | 皮尔斯决定要辞掉他的工作改行试着走钢丝。他对走钢丝还挺在行的。不过仍有个小问题,就是鸟会停在他拿的平衡竿上。他们会飞过来停一小会儿,然后再飞走。这样的情况在两边的鸟的数量一样时并不是个太大的问题。但有时候,所有的鸟都会想要停在同一边,皮尔斯就失去了平衡,就会让他从钢丝上掉下去。 518 | 519 | 我们假设两边的鸟差异在三个之内的时候,皮尔斯仍能保持平衡。 520 | 521 | 522 | **** 一般解法 523 | 首先看看不用 Monad 怎么解: 524 | 525 | #+BEGIN_SRC js 526 | var landLeft = function(n, pole){ 527 | return [pole[0]+n, pole[1]]; 528 | } 529 | var landRight = function(n, pole){ 530 | return m.reverse(landLeft(n, m.reverse(pole))); 531 | } 532 | var result = m.pipeline([0,0], 533 | m.partial(landLeft, 1), 534 | m.partial(landRight,1), 535 | m.partial(landLeft, 2)); 536 | console.log(result); 537 | // => [3 1] 538 | #+END_SRC 539 | 540 | 对了,还差一个判断皮尔斯是否掉下来的操作. 541 | 542 | #+BEGIN_SRC js 543 | var landLeft = eweda.curry(function(n, pole){ 544 | if(pole=='dead') return pole; 545 | if(Math.abs(pole[0]-pole[1]) > 3) 546 | return 'dead'; 547 | return [pole[0]+n, pole[1]]; 548 | }); 549 | var landRight = eweda.curry(function(n, pole){ 550 | if(pole=='dead') return pole; 551 | return landLeft(n, eweda.reverse(pole)); 552 | }); 553 | var result = eweda.pipe(landLeft(10), landRight(1), landRight(8))([0,0]); 554 | console.log(result); 555 | // => dead 556 | 557 | #+END_SRC 558 | 559 | **** 现在来试试用 Either 560 | 我们先把皮尔斯放进 Either 容器里让他走钢丝,这样皮尔斯的状态只有打开 Either 容器 561 | 才能看见。假设 Right 是装着活着的皮尔斯的容器,Left 是装死了得皮尔斯的容器。 562 | 563 | #+BEGIN_SRC js 564 | var land = function(lr, n, pole){ 565 | pole[lr] = pole[lr] + n; 566 | if(Math.abs(pole[0]-pole[1]) > 3) { 567 | return new Left("dead when land " + n + " became " + pole); 568 | } 569 | return new Right(pole); 570 | } 571 | 572 | var landLeft = n=>m.partial(land,0,n); 573 | var landRight = n=>m.partial(land,1,n); 574 | #+END_SRC 575 | 576 | 现在落鸟后会返回一个 Either,Right 或者 Left。 577 | 578 | 偷看容器内东西的函数可以是这样的,类似于 fmap,但是这里可以分别对 Either 左右值应用不同的函数: 579 | 580 | #+BEGIN_SRC js 581 | var stillAlive = function(x){ 582 | console.log(x) 583 | } 584 | var dead = function(x){ 585 | console.log('皮尔斯 ' + x); 586 | } 587 | either(dead, stillAlive, landLeft(2, [0,0])) 588 | #+END_SRC 589 | 590 | 好像越来越接近了,但是这里只落了一次鸟,如果我要落好几次呢。这就需要实现 Either 的 flatmap 方法,让小鸟不停的落到皮尔斯的杆子上: 591 | 592 | #+BEGIN_SRC js 593 | Left.prototype.flatmap = function(fn){return this;}; 594 | Right.prototype.flatmap = function(fn){ 595 | return fn(this.value) 596 | } 597 | #+END_SRC 598 | 599 | 当然,应用左右函数的 either 函数实现起来也非常简单: 600 | 601 | #+BEGIN_SRC js 602 | either = function(left, right, Either){ 603 | if(Either.constructor.name == 'Right') 604 | return Either.fmap(right) 605 | else 606 | return Either.fmap(left) 607 | } 608 | #+END_SRC 609 | 610 | 611 | 我们来试试工作不工作: 612 | 613 | #+BEGIN_SRC js 614 | var walkInLine = new Right([0,0]); 615 | eitherDeadOrNot = walkInLine.flatmap(landLeft(2)) 616 | .flatmap(landRight(5)) 617 | either(dead, stillAlive, eitherDeadOrNot) 618 | // => [2,5] <1> 619 | eitherDeadOrNot = walkInLine.flatmap(landLeft(2)) 620 | .flatmap(landRight(5)) 621 | .flatmap(landLeft(3)) 622 | .flatmap(landLeft(10) 623 | .flatmap(landRight(10))) 624 | 625 | either(dead, stillAlive, eitherDeadOrNot) 626 | // => "皮尔斯dead when land 10 became 15,5" <2> 627 | #+END_SRC 628 | 果然跟预期的一样: 629 | 1. 在所有小鸟的差值不大于三时,我们可以偷看到皮尔斯还是或者的 630 | 2. 但是如果一旦大于 3,皮尔斯的就已经死掉,不管之后再怎么落鸟,死的皮尔斯状态不会再变化。 631 | 632 | 633 | ** Monad 在 JavaScript 中的应用 634 | 你知道 ES6 有个新的 类型 635 | [[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#Browser_compatibility][Promise]] 636 | 吗, 如果不知道, 想必也听过 jQuery 的 =$.ajax= 吧, 但如果你没听过 promise, 637 | 说明你没有认真看过他的返回值: 638 | #+BEGIN_SRC js 639 | var aPromise = $.ajax({ 640 | url: "https://api.github.com/users/jcouyang/gists" 641 | dataType: 'jsonp' 642 | }) 643 | aPromise /*** 644 | => Object { state: .Deferred/r.state(), 645 | always: .Deferred/r.always(), 646 | then: .Deferred/r.then(), 647 | promise: .Deferred/r.promise(), 648 | pipe: .Deferred/r.then(), 649 | done: b.Callbacks/p.add(), 650 | fail: b.Callbacks/p.add(), 651 | progress: b.Callbacks/p.add() } 652 | ***/ 653 | 654 | #+END_SRC 655 | 656 | 657 | 我们看到返回了好多 =Deferred= 类型的玩意, 我们来试试这玩意有什么用 658 | #+BEGIN_SRC js 659 | anotherPromise = aPromise.then(_ => _.data.forEach(y=> console.log(y.description))) 660 | /* => 661 | Object { state: .Deferred/r.state(), 662 | always: .Deferred/r.always(), 663 | then: .Deferred/r.then(), 664 | promise: .Deferred/r.promise(), 665 | pipe: .Deferred/r.then(), 666 | done: b.Callbacks/p.add(), 667 | fail: b.Callbacks/p.add(), 668 | progress: b.Callbacks/p.add() } 669 | 670 | "connect cisco anyconnect in terminal" 671 | "为什么要柯里化(curry)" 672 | "批量获取人人影视下载链接" 673 | ...... 674 | ,*/ 675 | 676 | #+END_SRC 677 | 678 | 看见没有,它又返回了同样一个东西,而且传给 then 679 | 的函数可以操作这个对象里面的值。这个对象其实就是 Promise 了。 680 | 为什么说这是 Monad 呢?来试试再实现一次“走钢丝”。 681 | 682 | *** Promise 版本的走钢丝 683 | 下面我们开始使用 ES6 标准的 Promise。 684 | 同样的,我们需要一个接受普通值返回容器的落鸟函数: 685 | #+BEGIN_SRC js 686 | var land = function(lr, n, pole){ 687 | pole[lr] = pole[lr] + n; 688 | if(Math.abs(pole[0]-pole[1]) > 3) { 689 | return new Promise((resovle,reject)=>reject("dead when land " + n + " became " + pole)); 690 | } 691 | return new Promise((resolve,reject)=>resolve(pole)); 692 | } 693 | var landLeft = n=>m.partial(land,0,n) 694 | var landRight = n=>m.partial(land,1,n) 695 | #+END_SRC 696 | 697 | 然后就可以开始往皮尔斯的杆子上落鸟了。 698 | 699 | #+BEGIN_SRC js 700 | Promise.all([0,0]) 701 | .then(landLeft(2)) 702 | .then(landRight(3)) // => Array [ 2, 3 ] 703 | .then(landLeft(10)) 704 | .then(landRight(10)) 705 | .then(_=>console.log(_),_=>console.log(_)) 706 | // => "dead when land 10 became 12,3" 707 | #+END_SRC 708 | 709 | 最后一个 then 相当于 either 函数,之前的每一个 then 相当于 flatmap, 710 | 711 | *** When 712 | 713 | 虽然已经是 ES6 的标准,但是 Promise 只提供非常基本的支持,通常我们可以使用更实用的库入 [[https://github.com/cujojs/when][when]] 或者 [[https://documentup.com/kriskowal/q/][q]]。 这里我简单的介绍一下更为丰富的 when。 714 | 715 | **** lift 716 | 717 | 还记得之前讲 Applicative 时提到的 lift,可以把一个函数装到容器中,而 when.lift 就是把一般函数提升(lift)成一个含有函数的 Pormise。既然变成一个 lifted 的函数,当然,就像 Applicative 一样,可以应用到一个含有值的容器上,也就是类似 =when.lift(f).ap(aPromise)= 。更方便的,直接调用就等同于调用 ap 方法: 718 | 719 | #+BEGIN_SRC js 720 | var readFile = function(){ 721 | return when.promise(function(resolve){ 722 | setTimeout(_=>resolve("i am a file"), 3000); 723 | }) 724 | } 725 | 726 | var print = function(text){ 727 | console.log(text); 728 | return "printed"; 729 | } 730 | when.lift(print)(readFile()) 731 | .then(_=>console.log(_)) 732 | // 三秒后 => "i am a file" 733 | // => printed 734 | #+END_SRC 735 | 736 | 跟 Applicative 一样,当吧函数也提升到容器层面,那么含有函数的容器应用到一个含有值的容器,返回的当然是一个包含结果的容器。所以这里会看到应用函数 print 到 readFile() 会打印出 readFile() Promise 内的值,也就是三秒后 resolve 的 “i am a file”,同时,print 的返回值 “printed” 会被包在容器 Promise 中,从而还可以通过 then 打印出 print Promise 的值。 737 | 738 | **** when.map 739 | 这又是另一个特别方便的方法,在我们可以 lift 函数来操作 Promise 的时候,如果需要操作多个 Promise 的时候,很容易想到只要 map 这个 lift 的函数到 Promise 数组就好了。 740 | 741 | #+BEGIN_SRC js 742 | [readFile(),readFile()].map(when.lift(print)) 743 | #+END_SRC 744 | 745 | when 提供更便捷的方式来 map 一个普通函数到 Promise 数组,那就是 when.map: 746 | 747 | #+BEGIN_SRC js 748 | when.map([readFile(),readFile()], print) 749 | #+END_SRC 750 | 751 | 跟之前的代码效果完全相同,只是 when.map 自动的把函数 lift 起来再 map 到数组中的 Promise 上。 752 | 753 | 当然 when 还有非常多强大的方法这里就不一一介绍了,我的目的是通过比较有用的一些库中的 Monad 应用,说明 Monad 并非只是晦涩难懂的范畴论中的概念,而在实际应用中其实是非常常用而且功能强大的。 754 | 755 | ** Reactive 编程 756 | 757 | 上节提到了提供 Promise 的库 when,还有一些丰富的方法,当然 Promise 只能算是一种 Monad,在 JavaScript 的世界中还有非常多的 Monad,而我特别想要介绍跟 when 同样出自 cujojs 的 [[https://github.com/cujojs/most][most]]。 758 | 759 | most 的 github 页面写得非常清楚,只有三个词——Monadic reactive streams,还是老习惯,如果看不懂这三个词,请先看完例子我再解释。 760 | 761 | *** 流(stream) 762 | 763 | 如果说数组是空间维度,那么流则是时间维度版本的数组。比如我们有一个数组,需要 reduce 一下再打印出结果,是非常容易做到的: 764 | 765 | #+BEGIN_SRC js 766 | [1,2,3,4].reduce((acc,x)=>acc+x) 767 | #+END_SRC 768 | 769 | 那么问题是,我们操作在一个空间上已经存在的数组,是非常容易的,但是如果我们的输入是随着时间变化的,该如何处理? 770 | 771 | 而在前端世界,这种情况非常常见,比如一个简单的用户输入框 input,同样的,我想得到输的总和,似乎是有些棘手的一件事情,只是,对于函数式编程来说,对于状态的保存就非常头疼。当然如果不考虑函数式,弄一个全局变量来保存 acc 也是最直接的思路了。 772 | 773 | #+BEGIN_SRC js 774 | var acc = 0; 775 | $('input').onChange(_=>acc+=_) 776 | #+END_SRC 777 | 778 | 这样每次在 input 中修改数字,都会加入到 acc 中。 779 | 780 | 而不可变的函数式应该如何解决这种问题呢? 781 | 782 | 下面开始用 most: 783 | 784 | #+BEGIN_SRC js 785 | most.fromEvent('input', document.querySelector('input')) 786 | .reduce((acc,x)=>acc+x) 787 | .then(_=>console.log(_)) 788 | #+END_SRC 789 | 790 | 而这样的一组随时间变化的输入,就变成了输入流,使用 reactive programming 的技巧,我们可以像操作空间上的数组一样操作流,这就是 reactive programming,另外如果还符合 monad 的一些公理,就会变成 monadic reactive programming。 791 | 792 | *** Functor 793 | 每个 most 的流都是一个 functor,因此我们可以 map 一个函数到流上。 794 | 795 | #+BEGIN_SRC js 796 | most.from([1,2,3,4]) //<1> 797 | .map(_=>_*2) 798 | .observe(_=>console.log(_)); //<2> 799 | // -2-4-6-8-> 800 | #+END_SRC 801 | 802 | 这段代码会依次输出 =2 4 6 8= 。 803 | <1> most.from 会从一个数组生成一个 most 流,跟之前的 most.fromEvent 生成一个输入流一样。 804 | <2> observe 用于观察流内的数据,每次流的数据变化,都会触发 obse 上的回调。 805 | 806 | *** Applicative 807 | 808 | 不仅如此,most 还是 Applicative Functor,希望之前的概念还记得,Applicative 可以把含有函数的容器应用到另一个含有值的容器上,所以上例可以用 Applicative 这样做: 809 | 810 | #+BEGIN_SRC js 811 | most.of(_=>_*2) 812 | .ap(most.from([1,2,3,4])) 813 | .observe(_=>console.log(_)) 814 | #+END_SRC 815 | 816 | 除了使用 Applicative 之外,我们还可以把函数 lift 起来,这样在使用上跟一般的函数就没有什么区别了,只是现在 lifted 的函数可以操作 most 流。[fn:1] 817 | 818 | #+BEGIN_SRC js 819 | var multiple2 = function(x){return x*2}; 820 | var lifedMultiple2 = most.lift(multiple2); 821 | lifedMultiple2(most.of(3)) 822 | .observe(_=>console.log(_)) 823 | #+END_SRC 824 | 825 | *** Monad 826 | 827 | 当然,most 的流同时也是 Monad,因此可以方便的 flatmap 一个返回 stream 的函数。 828 | 829 | #+BEGIN_SRC js 830 | most.from([1, 2]) 831 | .flatMap(x=>most.periodic(x * 1000).take(5).constant(x)) 832 | .observe(_=>console.log(_)); 833 | #+END_SRC 834 | 835 | 思考一下如果是一个数组 =[1,2]= ,比如 flatMap =x=>[x*2]= 会得到一个展开的数组 =[2,4]= ,而不是 =[[2],[4]]= 。 同样的,flatMap 一个流,得到应该是 flat 过的流,那么这里产生的两个流, =1-1-1-1-1= ,和 =2---2---2---2---2= ,想象一下要把两个流展开放到一个流里,空间的元素放到数组中是可以按空间排列,那么元素放到流中则是应该按照时间排列,我们做一个简单的对齐: 836 | 837 | #+BEGIN_EXAMPLE 838 | 1-1-1-1-1 839 | 2- -2- -2--2--2 840 | 841 | 1 1 1 842 | 2-1-2-1-2--2--2 843 | #+END_EXAMPLE 844 | 845 | 其中每一个 =-= 代表一秒,所以输出会是 =12-1-12-1-12--2--2= 。数字之间没有 =-= 代表会同时打印,因此有可能会出现 2 在 1 前的可能,其实应该是同时的。 846 | 847 | 正是因为流可以是 Monadic 的,又可以响应式的在流的内容变化时做出相应动作,因此叫做 Monadic Reactive Stream。 848 | 849 | *** Object.observe 850 | 851 | ES7 的标准草案中给 Object 添加了一个新的方法(但是又被撤回了),可以监控一个对象的变化,从而做出对应的相应。 这一标准目前只在 Chrome 36 以上版本实现。 852 | 853 | #+BEGIN_SRC js 854 | var obj = {foo: 'bar'}; 855 | Object.observe(obj, console.log.bind(console)); 856 | obj.foo = 'bear;' 857 | // => {name: "foo", object: [Object], oldValue: "bar", type: "update"} 858 | #+END_SRC 859 | 860 | 可以看到当我修改 obj 的时候,所产生的变化会被答应到控制台,给出了比较有用的新值和旧值。 861 | 862 | * Footnotes 863 | 864 | [fn:3] 基本上我们说的函子都是自函子,自函子是 map 到自己范畴上的函子。 865 | 866 | [fn:2] 具体的并发模型会在第8章详细介绍。 867 | 868 | [fn:1] 虽然不知道为什么官网并没有推荐(deprecated) 使用 lift,反倒我觉得是用 lift 更适合函数的重用。 869 | -------------------------------------------------------------------------------- /book/zh/第二章.org: -------------------------------------------------------------------------------- 1 | * COMMENT install 2 | #+BEGIN_SRC emacs-lisp 3 | (require 'ob-dot) 4 | #+END_SRC 5 | 6 | #+RESULTS: 7 | : ob-dot 8 | 9 | * 集合 10 | 11 | 本章主要介绍 Clojure 的集合数据结构。这是个无聊但是又很重要的章节, 可以说函数式编程最基本最重要的就是集合操作。本章会涉及: 12 | 1. 一些集合类型。 13 | 2. 如何操作集合。 14 | 3. 一些组合子。 15 | 4. 什么是以及为什么要惰性求值。 16 | 5. 如何在 JavaScript 中使用 Clojure 的数据结构。 17 | 18 | 当然,已经熟悉 Clojure 的读者自然可以略过本章。 19 | 20 | 上一章大致提到了 JavaScript 的数据结构都是可变的数据结构。也就是说一些操作会改变数据结构中的数据内容。当然 JavaScript 原生的 Number 和 String 本身就是不可变的,而且不会有太多的操作,所以本章主要是介绍 *集合* 数据结构。而且,集合也是函数式编程中最常使用的数据结构。 21 | 22 | #+BEGIN_QUOTE 23 | 本章开始会使用 mori,想在本机使用 mori 非常简单,如果使用 node,只需要 =npm install mori= 然后 =var mori = require('mori')= 引入即可。如想要在浏览器中使用,可以使用 [[https://cdnjs.cloudflare.com/ajax/libs/mori/0.3.2/mori.js][cdnjs]]。[fn:7] 24 | #+END_QUOTE 25 | ** 集合的使用 26 | *** 向量(vector) 27 | 28 | 向量是带有索引(index)的一组数据。跟 JavaScript 的 Array 非常像,但是区别在于 29 | 30 | - 向量是不可变(immutable)数据结构。 31 | - 向量是持久性(persistent)数据结构。 32 | 33 | 这里两个概念听起来很相似,但是其实有一点点区别。 34 | 35 | /不可变/ 指的是一旦被创建,就再也不能改变。比如我创建一个向量 Alice,那么不管发生什么,判等 Alice 的话只需要简单的等号,因为 Alice 的内容不可能被改变,所以完全不需要深入判等,只需要简单的比较 Alice 引用的是否还是原来的对象。 36 | 37 | 而 /持久性/ 是“改变”不可变数据结构的一种方式,每当尝试去修改一个不可变数据结构的时候,其实会返回一个建立在旧的数据的基础上的新数据结构。本节只会涉及不可变性,下一节会通过介绍向量的持久性数据结构,加深对持久性的认识。 38 | 39 | 下面我将介绍如何使用向量数据结构,当然,我不是深入细节,只是为了显示不可变数据结构与 JavaScript 原生可变数据结构的区别。 40 | 41 | **** 创建向量 42 | 43 | 使用 Clojure 可以字面的(lieral)创建一个向量,或者用 vector 函数,效果都是一样的。 44 | 45 | #+BEGIN_SRC clojure 46 | [1 2 3 4] 47 | (vector 1 2 3 4) 48 | #+END_SRC 49 | 50 | #+RESULTS: 51 | | 1 | 2 | 3 | 4 | 52 | 53 | 54 | 其中,方括号用于字面创建向量,而圆括号表示调用了 =vector= 函数,参数列表为 ~1 2 3 4~ 。 55 | 56 | 使用 mori 创建向量也非常的类似: 57 | #+BEGIN_SRC js 58 | console.log(1+1) 59 | #+END_SRC 60 | 61 | #+RESULTS: 62 | 63 | #+BEGIN_SRC js 64 | mori.vector(1,2,3,4) 65 | // => [1 2 3 4] 66 | #+END_SRC 67 | 68 | #+RESULTS: 69 | : [object Object] 70 | 71 | #+BEGIN_QUOTE 72 | 从 JavaScript 或其他语言转换 clojure 或 lisp 的语法非常简单,只需要将括号 “(” 向左移一个函数名,逗号去掉即可。因此,mori 的 api 则正是这个过程的反转。 73 | #+END_QUOTE 74 | 75 | 如果是在 node 的 repl 中就可以看见类似 Clojure 字面定义 vector 的输出 =[1 2 3 4]= 。 76 | 77 | **** 获取向量中的元素 78 | 79 | 通常在 JavaScript Array 中我们获取元素通常会通过 =somearray[0]= 直接用索引获取,当然使用 vector 也非常类似,只不过需要使用 get 方法: 80 | 81 | #+BEGIN_SRC js 82 | var vec = mori.vector(1,2,3,4) 83 | mori.get(vec, 0) 84 | #+END_SRC 85 | 86 | #+RESULTS: 87 | 88 | 当然,为了更符合 JavaScript 的习惯,我们还可以使用 vector 的成员变量 get 获取元素,但是需要使用 mori 的另一个版本 conjs[fn:8]: 89 | #+BEGIN_SRC js 90 | require('con.js').vector(1,2,3,4).get(0) 91 | #+END_SRC 92 | 93 | **** 添加元素 94 | 95 | Clojure 中所有集合的添加操作都可以通过 conj 函数,conj全称 conjoin。对于不同数据结构的 conj 操作可能添加的方向是不一样的,但不管怎么样,conj 都会选择最容易的添加(即复杂度最低)的方向添加数据。而在向量数据结构中,conj 的方向是往尾部添加元素: 96 | 97 | #+BEGIN_SRC js 98 | mori.conj(vec, 5) //=> [1 2 3 4 5] 99 | vec // => [1 2 3 4] 100 | #+END_SRC 101 | 102 | 这很像 JavaScript Array 的 push 方法,但是值得注意的是,push (以及其他数组操作)是一个可变操作(mutation operation),也就是说,push 会改变 Array 中的数据。 103 | 104 | #+BEGIN_SRC js 105 | var array = [1,2,3,4] 106 | array.push(5) // => 5 107 | array // => [1,2,3,4,5] 108 | #+END_SRC 109 | 110 | 注意看 push 的返回值是添加的数据,而 push 之后的 array 变化成添加过的数据的数组了。 111 | 112 | **** 弹出元素 113 | 114 | 弹出元素跟 Array 一样都是使用 pop,当然所有 Clojure 的数据结构都是不可变的,弹出也不例外。所以弹出会返回一个新的“删除”尾部元素的向量,而原来的向量保持不变: 115 | 116 | #+BEGIN_SRC js 117 | mori.pop(vec) //=> [1 2 3] 118 | vec // => [1 2 3 4] 119 | #+END_SRC 120 | 121 | **** 首个元素及剩余元素 122 | 123 | 另外在函数式编程中,特别是递归的时候,经常会把列表分为首元素,和剩余(rest)元素集合。 124 | 125 | #+BEGIN_SRC js 126 | mori.first(vec) //=> 1 127 | mori.rest(vec) // => (2 3 4) 128 | #+END_SRC 129 | 130 | 注意看 rest 返回的是圆括号,为什么变成圆括号了呢?我会在最后一节做详细的解释。 131 | 132 | **** 获取子向量(subvec) 133 | 134 | subvec 操作返回一个持久性的子向量,比如: 135 | 136 | #+BEGIN_SRC js 137 | mori.subvec(vec, 1) // => [2 3 4] 138 | mori.subvec(vec, 1, 2) //=> [2] 139 | vec // [1 2 3 4] 140 | #+END_SRC 141 | 142 | #+BEGIN_QUOTE 143 | 看到这里,可能细心的读者会发现向量的所有操作都是不可变的,不管如何操作该向量,用于会返回一个新的向量而不是修改原有向量。这样每次都返回一个新的数据结构,听起来像是又拷贝了一份再做操作,效率不是会很低吗?这个问题会在下节解释持久性数据结构的时候得到解答。 144 | #+END_QUOTE 145 | 146 | *** Map 147 | 148 | 虽然想只介绍 vector 就好了,但是 ES6 的把 Map 纳入了标准,这里顺便介绍一下 Map 对应的 Clojure 的数据结构好了。在 Map 还没有被所有浏览器厂商实现之前,绝大多数情况下我们在写 JavaScript 时会使用 Object 来当做 Map 使用。当然,到底是使用 Map 还是 Object 并不是本书的重点,不管是 Map 还是 Object,重点是他们仍然是可变的。 149 | 150 | #+BEGIN_SRC js 151 | var map = new Map(); 152 | map.set(0, "零"); // => {0:"零"} 153 | map.set(1, "壹"); // => {0:"零",1:"壹"} 154 | #+END_SRC 155 | 156 | =map= 实例的内容在不同的地方值有可能发生改变。同样的,Clojure 提供不可变的 Map 数据结构, =hash-map= 。同样的,我们都可以通过 mori 在 JavaScript 中使用到 Clojure 的 =hash-map= 。 157 | 158 | 我们可以简单的使用 =mori.hashMap= 创建一个 ClojureScript 的 hashmap 159 | 实例,同样的,所有操作都不会改变原来的不可变对象。 160 | #+BEGIN_SRC js 161 | var m0 = mori.hashMap("零", 0, "壹", 1); 162 | // => {"零" 0, "壹" 1} 163 | 164 | mori.get(m0, "零"); // => 0 165 | 166 | var m1 = mori.assoc(m0, mori.vector(1,2), 2); 167 | // m1 = {"零" 0, "壹" 1, [1 2] 2} 168 | m0 // => {"零" 0, "壹" 1} 169 | 170 | mori.get(m1, m.vector(1,2)); // => 2 171 | #+END_SRC 172 | 173 | =m0= 永远是 =m0= 。 其中 =mori.assoc= 是更新操作,有意思的是,assoc 操作也同样可以用在 =vector= 上。 174 | 175 | #+BEGIN_SRC js 176 | mori.assoc(mori.vector(1,2,3),1,8) // => [1 8 3] 177 | #+END_SRC 178 | 179 | 跟 =vector= 一样,也可以用 =conj= 操作连接 hashmap: 180 | #+BEGIN_SRC js 181 | mori.conj(m0, mori.vector("foo", "bar")) // => {"零" 0, "壹" 1, "foo" "bar"} 182 | #+END_SRC 183 | 184 | *** 函数组合子 185 | 借用函数组合子这个词来代表集合上的一些通用方法,如 map, filter, reduce。更详细的组合子定义可以在 [[http://stackoverflow.com/questions/7533837/explanation-of-combinators-for-the-working-man][stackoverflow]][fn:5] 上找到非常好的解释。先不用去管具体定义,下面我会简单列举一些函数式编程,特别是 Clojure 编程中经常会使用到的一些函数组合子。 186 | 187 | **** map 188 | 189 | map 把参数中的函数应用到集合中每一个元素上,并返回函数返回的元素组成的新集合。 190 | 191 | 简单的说,比如要把一包奥利奥变成馅被舔掉的奥利奥,曾经的你可能会命令式的这样做: 192 | #+BEGIN_SRC js 193 | const oreaPack = [101,101,101,101]; 194 | const lip = (oreo)=>11; 195 | let lipedOreoPack = []; 196 | for (let oreo of oreoPack){ 197 | lipedOreoPack.push(lip(oreo)); 198 | } 199 | #+END_SRC 200 | 201 | 先定义一个空 Array,在把一个一个元素应用上函数 =lip= 的结果塞进去。 202 | 203 | 但是 JavaScript 的 Array 其实自带了一个方法叫做 =map= ,可以让你一行就做了同样的事情: 204 | 205 | #+BEGIN_SRC js 206 | let lipedOreoPack = oreoPack.map(lip); 207 | #+END_SRC 208 | 209 | 这样做不但变得更为简洁,隐藏了遍历和修改 Array 的细节,同时还省去了局部变量 ~let lipedOreoPack = []~ 。 引入和修改局部变量给程序带来太多的状态,而且引入和修改的时间也是影响状态变化的因素,这正是是函数式编程专门要消除的。 210 | 211 | 当然,如果使用 mori,它提供了一个更为抽象的 map 函数: 212 | #+BEGIN_SRC js 213 | mori.map(lip, oreoPack) 214 | #+END_SRC 215 | 216 | 不但可以 map 一个 JavaScript Array, 当 oreaPack 是 vector,或者是一个序列[fn:10],一个集合时,都同样适用。[fn:9] 217 | 218 | 相比起 mori 的 map 函数,Array 的 map 方法似乎更符合我们的阅读习惯,不过我会在第 4 章解释什么情况更适合哪种情况。 但在本章我会一直使用 Clojure 的组合子使用习惯。 219 | 220 | **** filter 221 | 222 | filter 接收一个 /谓词函数/ (predicate function),用于判断哪些元素应该保留,哪些应该被剔除掉。谓词函数顾名思义就是用作谓词的函数,谓词自然应该就是“是”,“等于”,“大于”,“属于”之类的词。 223 | 224 | 比如, =mori.isEven= 就是一个谓词函数,它用于判断一个数是否是偶数: 225 | 226 | #+BEGIN_SRC js 227 | mori.filter(mori.isEven, [1,2,3,4,5]); 228 | // => (2 4) 229 | #+END_SRC 230 | 231 | 同样的,Array 也有 filter 方法: 232 | #+BEGIN_SRC js 233 | [1,2,3,4,5].filter(x=>x%2==0); 234 | #+END_SRC 235 | 236 | **** reduce 237 | 238 | 前面都是集合内容的转换,而使用 reduce 则方便的可以将集合规约成值,比如我们很容易的可以用 reduce 写一个 sum 函数: 239 | #+BEGIN_SRC js 240 | mori.reduce((a,b)=>a+b, 0, [1,2,3,4,5]) 241 | // => 15 242 | #+END_SRC 243 | 其中,第一个函数描述如何进行规约,第二个函数是规约的初始值,最后是集合。 244 | 245 | 跟 map 一样,reduce 适用各种集合类型,因此我们也可以去 reduce 一个 Map。 246 | #+BEGIN_SRC js 247 | mori.reduce((acc, [key, val])=>(mori.update(acc, key, val)), 248 | mori.hashMap(), 249 | {a: 1, b: 2, c: 3}) 250 | // => {a:2, b:3, c:4} 251 | #+END_SRC 252 | 253 | 这个例子中, =reduce= 把参数 ={a: 1, b: 2, c: 3}= 看成是一个向量的序列 =([a 1] [b 2] [c 3])= 。所以 =reduce= 函数的第二个参数 =[key, val]= 刚好 destructure 了序列中的向量,分别得到键和值。 254 | 255 | 以上三个非常基本的组合子函数都存在于 EcmaScript 5 标准中的 Array。但是 Object 或者 Map 并没有提供这些组合子。而以下的一些函数更是没有在 ES5 甚至是 ES6 的标准中,但是他们却是很多函数式语言的必备函数。 256 | 257 | **** take,takeWhile,drop,dropWhile 258 | 259 | 这些函数都大部分会应用于惰性序列。 260 | take 会经常用于从一个集合中取出一部分,比如: 261 | #+BEGIN_SRC js 262 | var s = mori.range(); // 无限序列 263 | 264 | mori.take(10, s); // => (0 1 2 3 4 5 6 7 8 9) 265 | #+END_SRC 266 | 267 | 注意 s 是从 0 开始的无限整数序列,当使用 take 取出前 10 个时,会得到包含着前10个整数的序列。更多关于惰性的话题会在第5节继续。 268 | 269 | 不像 take 直接接收需要获取的连续的元素数量,takeWhile 给了我们机会动态的计算是否需要将元素获取出来。所以,takeWhile 第一个参数是一个谓词函数: 270 | 271 | #+BEGIN_SRC js 272 | const futuramaCast = [ 273 | { 274 | name: "Turanga Leela", 275 | date: "2974-12-3", 276 | },{ 277 | name: "Philip Fry", 278 | date: "1974-8-14", 279 | },{ 280 | name: "Hubert J. Farnsworth", 281 | date: "2841-4-9", 282 | },{ 283 | name: "Bender Bending Rodríguez, Sr.", 284 | date: "2996" 285 | },{ 286 | name: "Amy Wong", 287 | date: "2978-5-4", 288 | } 289 | ] 290 | const notFromFuture = (who)=>Date.parse(who.date) ({name: "Philip Fry", date: "1974-8-14"}) 293 | #+END_SRC 294 | 295 | 例子中我们从 =futuramaCast=[fn:11] 中挑选出不是未来的人,当然结果是 Fry 没错,但是需要注意的是,区别于 filter,返回值会是一个序列(注意是圆括号)。 296 | 297 | 与 take 和 takeWhile 相反的,drop 和 dropWhile 也会经常用到,当然结果是刚好互补的: 298 | #+BEGIN_SRC js 299 | mori.dropWhile(notFromFuture, futuramaCast) 300 | // => TODO 301 | #+END_SRC 302 | 303 | ** 持久性数据结构 304 | 305 | 大概对集合中的向量与 hashMap,以及集合的常用组合子 简单的做了介绍,应该还记得介绍向量时提到的效率问题吗?我们来以向量为例,深入研究一下向量的数据结构到底是怎样的,又是如何做到持久性和不可变性,同时还保证效率的? 306 | 307 | 首先在解释向量的数据结构之前,我想再普及一下什么是持久性数据结构和不可变性。 308 | 309 | 持久性是指数据结构在被操作的时候永远保持着前一版本。不可变性是说明不管是什么,在被创建之后就再也不能改变。所以持久性约束的是操作,而不可变性约束的是数据。好了,概念的东西就说到这,我们来举个例子,比如我们最熟悉的链表数据结构: 310 | 311 | 还是前面那个例子,假设数组和向量的数据结构都是链表。 312 | 313 | 那么,如果我要往中添加一项: 314 | 315 | #+CAPTION: TODO 持久化数据的增加操作 316 | [[./images/persistent-conj.png]] 317 | 318 | #+caption: 非持久化数据的增加 319 | [[./images/mutable-push.png]] 320 | 321 | #+BEGIN_QUOTE 322 | ⚠️前方高能预警,一大波 Clojure 源代码来袭。 323 | #+END_QUOTE 324 | 325 | *** 向量的持久性数据结构 326 | 327 | 当然,Clojure 的向量数据结构并不是简单的链表,而是 Rich Hickey 发明的树形数据结构。官方文档也提到了向量的所有操作的复杂度都是 O(log_{32}N),但为什么是32呢。回忆一下二分查找是多少,log_{2}N,而二分查找类似于一颗平衡二叉树,那么猜想 log_{32}N 复杂度应该是一个32叉的平衡树才对。 328 | 329 | 好吧,偷看了一眼源代码,确实证明这个猜想是对的。[fn:1] 330 | 331 | #+BEGIN_SRC java 332 | Node(AtomicReference edit){ 333 | this.edit = edit; 334 | this.array = new Object[32]; 335 | } 336 | #+END_SRC 337 | 338 | 通过这个结构体明显确定是每一个节点有 32 叉的树型结构。我们继续往下看我们关心的问题:如何持久化的? 339 | 340 | 源代码一直往下翻直到 217 行,会看到 cons 方法[fn:2],而且这是 IPersistentVector 接口里的方法,这应该就是添加元素了。 341 | 342 | #+BEGIN_SRC java -n -r 343 | public PersistentVector cons(Object val){ 344 | int i = cnt 345 | if(cnt - tailoff() < 32) // <= 1 (ref:tailoff) 346 | { 347 | Object[] newTail = new Object[tail.length + 1]; 348 | System.arraycopy(tail, 0, newTail, 0, tail.length); 349 | newTail[tail.length] = val; 350 | return new PersistentVector(meta(), cnt + 1, shift, root, newTail); 351 | } 352 | //full tail, push into tree 353 | Node newroot; 354 | Node tailnode = new Node(root.edit,tail); 355 | int newshift = shift; 356 | //overflow root? 357 | if((cnt >>> 5) > (1 << shift)) // <= 2 (ref:overflow) 358 | { 359 | newroot = new Node(root.edit); 360 | newroot.array[0] = root; 361 | newroot.array[1] = newPath(root.edit,shift, tailnode); 362 | newshift += 5; 363 | } 364 | else // <= 3 365 | newroot = pushTail(shift, root, tailnode); 366 | return new PersistentVector(meta(), cnt + 1, newshift, newroot, new Object[]{val}); 367 | } 368 | 369 | #+END_SRC 370 | 371 | #+BEGIN_SRC dot :file ./images/tailoff.png :exports results 372 | digraph { 373 | node [shape=plaintext, fontsize=16]; 374 | vec 375 | node [shape=record, height=.1]; 376 | vec -> node0 377 | {rank=same; node4;node5;node6;node7;} 378 | node0[label = " root | tail"]; 379 | node1[label="|"]; 380 | node2[label="|"]; 381 | node3[label="|"]; 382 | node4[label=" 1| 2",color=orange]; 383 | node5[label=" 3| 4",color=orange]; 384 | node6[label=" 5| 6",color=orange]; 385 | node7[label=" 7| "]; 386 | node0:f0 -> node1; 387 | node0:f1 -> node7; 388 | node1:f0 -> node2; 389 | node1:f1 -> node3; 390 | node2:f0 -> node4; 391 | node2:f1 -> node5; 392 | node3:f0 -> node6; 393 | subgraph "tailoff" { 394 | "node4";node6;node5; 395 | } 396 | } 397 | #+END_SRC 398 | 399 | #+caption: tailoff 的区域 400 | #+RESULTS: 401 | [[file:./images/tailoff.png]] 402 | 403 | 很明显这段代码里有三个分支,不要着急,我们一个一个看一下: 404 | 1. 可以看到 [[(tailoff)][第(tailoff)行]] 中的 cnt 应该就是当前向量的长度,tailoff 往前找一下会发现是抹掉二进制后五位,也就是除掉最后一片叶子的大小。所以,这个分支是处理当最后一片叶子不完整时的情况。如果是二叉树的话,就是非满二叉树的情况。 405 | 2. 如果不满足 1 自然就是子树的叶子都是满的情况,但是满叶子的情况又分两种,如果是比完全树多一片满的叶子,再加一个叶子就溢出了。 406 | 3. 剩下是没有溢出的情况。 407 | 408 | 409 | 下面我们再仔细看看如何处理这三种情况。 410 | 411 | *** 最后一片叶子不完整 412 | 413 | 这种情况是第一个分支, 一共才 4 行代码,我们不妨仔细读读。 414 | 415 | #+BEGIN_SRC java -n -r 416 | Object[] newTail = new Object[tail.length + 1]; // <= 1 417 | System.arraycopy(tail, 0, newTail, 0, tail.length); // <= 2 418 | newTail[tail.length] = val; // <= 3 419 | return new PersistentVector(meta(), cnt + 1, shift, root, newTail); // <= 4 420 | #+END_SRC 421 | 422 | #+BEGIN_QUOTE 423 | System.arraycopy 的 API 是: 424 | #+BEGIN_SRC java 425 | public static void arraycopy(Object src, //拷贝源 426 | int srcPos, // 拷贝开始的索引 427 | Object dest, // 拷贝目标地址 428 | int destPos, // 目标起始索引 429 | int length) // 拷贝长度 430 | #+END_SRC 431 | #+END_QUOTE 432 | 433 | 1. 创建一个比尾部多1的对象数组 =newTail= 。 434 | 2. 拷贝尾叶子数组到新创建的对象数组 =newTail= 。 435 | 3. 最后一个元素赋值为需要添加的值。 436 | 4. 最后一步很重要,创建一个新的 =PersistentVector= 并把 =tail= 设置成 =newTail= 。 437 | 438 | 所以以下列代码为例,我们很容易想象这种情况下添加元素的过程。 439 | 440 | #+BEGIN_QUOTE 441 | 注意,由于画32叉树实在是太长了太难看了,因此这里我画成二叉树,只是为了表示如何插入元素的过程。当然读者应该不介意把它“脑补”成32叉的吧。 442 | #+END_QUOTE 443 | 444 | #+BEGIN_SRC js 445 | var vec = mori.vector(1,2,3,4,5,6,7) 446 | var vec2 = mori.conj(vec, 8) 447 | #+END_SRC 448 | 449 | #+BEGIN_SRC dot :file ./images/vecconj8.png :exports results 450 | digraph { 451 | node[shape=record] 452 | newrank=true; 453 | 454 | subgraph cluster_level1{ 455 | style=dotted; 456 | vec2[shape=plaintext] 457 | node0[label = " root | tail"]; 458 | node1[label="|"]; 459 | node2[label="|"]; 460 | node3[label="|"]; 461 | node4[label=" 1| 2"]; 462 | node5[label=" 3| 4"]; 463 | node6[label=" 5| 6"]; 464 | node7[label=" 7| "]; 465 | vec2 -> node0; 466 | } 467 | subgraph cluster_level2{ 468 | style=dotted 469 | vec3[shape=plaintext] 470 | node8[label=" root| tail"]; 471 | node9[label="|"]; 472 | node11[label=" 7| 8",color=green]; 473 | vec3 -> node8; 474 | } 475 | {rank=same; node0;node8;} 476 | {rank=same; node7;node11;node4;node5;node6;} 477 | node0:f0 -> node1; 478 | node0:f1 -> node7; 479 | node1:f0 -> node2; 480 | node1:f1 -> node3; 481 | node2:f0 -> node4; 482 | node2:f1 -> node5; 483 | node3:f0 -> node6; 484 | node8:f0 -> node9; 485 | node9:f0 -> node2; 486 | node9:f1 -> node11; 487 | node7 -> node11[label=copy,style=dotted] 488 | } 489 | #+END_SRC 490 | 491 | #+CAPTION: 向 vec 添加新元素 8 492 | #+RESULTS: 493 | [[file:./images/vecconj8.png]] 494 | 495 | 细心的读者会发现,新的 =vec2.root= 还是指向旧的 =vec.root= ,只是 =vec2.tail= 为 =vec1.tail= 的拷贝再加上新的元素而已。这个操作应该是 O(1) 才对。没有错,这种情况下添加元素确实效率是 O(1)。但是再想想, =vec2= 不像是一颗连贯的树啊,tail 指到了一个完全分离的数组拷贝上。 496 | 497 | 带着问题我们继续来看如果我再 conj 一个元素会发生什么? 498 | #+BEGIN_SRC js 499 | var vec3 = mori.conj(vec2, 9) 500 | #+END_SRC 501 | 502 | *** 所有叶子完整且叶子个数不大于完全树的叶子个数 503 | 504 | 这时就会进入到这个分支了,现在 =vec2= 的所有叶子都满了,按正常的思路我们需要创建一个新的叶子节点来放我们的新元素 7。我们来看看 Clojure 是怎么做的: 505 | 506 | #+BEGIN_SRC java -n -r 507 | Node newroot; 508 | Node tailnode = new Node(root.edit,tail); // (ref:tailnode) 509 | int newshift = shift; // (ref:newshift) 510 | ... 511 | newroot = pushTail(shift, root, tailnode); // (ref:newroot) 512 | return new PersistentVector(meta(), cnt + 1, newshift, newroot, new Object[]{val}) // (ref:vector) 513 | #+END_SRC 514 | 515 | 也只有四行代码,我们来仔细读一下: 516 | 1. 第[[(tailnode)]]行 创建一个节点,节点的数组指向当前的 tail,也就是 vec2.tail 517 | 2. 第[[(newshift)]]行 不是很重要,表示二进制移多少位,对应到树里面就是可以判断当前在树的第几层 518 | 3. 第[[(newroot)]]行的 pushTail 非常关键,如果你继续看 pushTail 的实现的话,大致意思就是从 vec2.root开始克隆 tail 一侧的节点,直到最后指向 tailnode 节点。 519 | 4. 最后一行没有什么好解释的,vec3.tail 指向只包含7的新数组。 520 | 521 | #+BEGIN_SRC dot :file ./images/vecconj9.png :exports results 522 | digraph { 523 | node[shape=record] 524 | newrank=true; 525 | subgraph cluster_level1{ 526 | style=dotted; 527 | vec2[shape=plaintext] 528 | node0[label = " root | tail"]; 529 | node1[label="|"]; 530 | node2[label="|"]; 531 | node3[label="|"]; 532 | node4[label=" 1| 2"]; 533 | node5[label=" 3| 4"]; 534 | node6[label=" 5| 6"]; 535 | node7[label=" 7| 8"]; 536 | vec2 -> node0; 537 | } 538 | subgraph cluster_level2{ 539 | style=dotted 540 | vec3[shape=plaintext] 541 | node8[label=" root| tail"]; 542 | node9[label="|"]; 543 | node10[label="|"]; 544 | node11[label=" 9|",color=green]; 545 | vec3 -> node8; 546 | } 547 | {rank=same; node0;node8;} 548 | {rank=same; node7;node11} 549 | node0:f0 -> node1; 550 | node0:f1 -> node7; 551 | node1:f0 -> node2; 552 | node1:f1 -> node3; 553 | node2:f0 -> node4; 554 | node2:f1 -> node5; 555 | node3:f0 -> node6; 556 | node8:f0 -> node9; 557 | node8:f1 -> node11; 558 | node9:f0 -> node2; 559 | node9:f1 -> node10; 560 | node10:f0 -> node6; 561 | node10:f1 -> node7; 562 | } 563 | #+END_SRC 564 | 565 | #+caption: 在满叶子的情况下添加元素9 566 | #+RESULTS: 567 | [[file:./images/vecconj9.png]] 568 | 569 | 这时候我们再添加 10: 570 | #+BEGIN_SRC js 571 | var vec4 = mori.conj(vec3, 10) 572 | #+END_SRC 573 | 574 | 应该还是第一种情况,有叶子不满,那么我们再添加 11 会怎么样呢? 575 | #+BEGIN_SRC js 576 | var vec5 = mori.conj(vec4, 11) 577 | #+END_SRC 578 | 579 | *** 所有叶子完整且叶子个数大于完全树的叶子个数 580 | 581 | 如果是向量元素总数大于一颗完全树的所有叶子,而且所有叶子是完整的,那再往 vec4中添加元素就是这种情况了。 582 | 583 | #+BEGIN_SRC java 584 | newroot = new Node(root.edit); 585 | newroot.array[0] = root; // <= 1 586 | newroot.array[1] = newPath(root.edit,shift, tailnode); // <= 2 587 | newshift += 5; // <= 3 588 | return new PersistentVector(meta(), cnt + 1, newshift, newroot, new Object[]{val}); // <= 4 589 | #+END_SRC 590 | 591 | 这种情况下代码也不太多,需要看的也就是四行代码: 592 | 593 | 1. 创建一个新的节点,左子树指向 vec4.root 594 | 2. 第二颗子树为新创建的 path,path 直通到 vec4.tail 595 | 3. 树的高度加1 596 | 4. vec5.tail指向新的对象数组,vec5.root 指向 1 创建的新的节点 597 | 598 | #+BEGIN_SRC dot :file ./images/vecconj11.png :exports results 599 | digraph { 600 | node[shape=record] 601 | newrank=true; 602 | subgraph cluster_level1{ 603 | style=dotted; 604 | vec4[shape=plaintext] 605 | node0[label = " root | tail"]; 606 | node1[label="|"]; 607 | node2[label="|"]; 608 | node3[label="|"]; 609 | node4[label=" 1| 2"]; 610 | node5[label=" 3| 4"]; 611 | node6[label=" 5| 6"]; 612 | node7[label=" 7| 8"]; 613 | node11[label=" 9| 10"]; 614 | vec4 -> node0; 615 | } 616 | subgraph cluster_level2{ 617 | style=dotted 618 | vec5[shape=plaintext] 619 | node8[label=" root| tail"]; 620 | node9[label="|"]; 621 | node10[label="|"]; 622 | node12[label="|"]; 623 | 624 | node13[label=" 11|",color=green]; 625 | vec5 -> node8; 626 | } 627 | {rank=same; node0;node9;} 628 | {rank=same; node7;node11;node13} 629 | node0:f0 -> node1; 630 | node0:f1 -> node11; 631 | node1:f0 -> node2; 632 | node1:f1 -> node3; 633 | node2:f0 -> node4; 634 | node2:f1 -> node5; 635 | node3:f0 -> node6; 636 | node3:f1 -> node7; 637 | node8:f0 -> node9; 638 | node8:f1 -> node13; 639 | node9:f0 -> node1; 640 | node9:f1 -> node10; 641 | node10:f0 -> node12; 642 | node12:f0 -> node11; 643 | } 644 | #+END_SRC 645 | 646 | #+caption: 添加11 647 | #+RESULTS: 648 | [[file:./images/vecconj11.png]] 649 | 650 | 好了,看到这里,我们已经看到了 Clojure 的向量数据结构完整的添加元素的过程。我们可以看到整个过程并没有做全部数据的拷贝,而只是最多 log_{32}N次,也就是树的高度次的拷贝。总体来说复杂度应该是非常可观的,因为一个 6 层的 32 叉树已经能存放 10亿(1,073,741,824)个元素了,而10亿个元素的添加操作最多也只是 O(6*32),效率是非常不错的。 651 | 652 | 既然学会了看 Clojure 的源码,下来更新元素和弹出元素的过程可以留给读者研究了。类似的,效率也是O(log_{32}N)。 653 | 654 | ** 不可变性 655 | 656 | 在函数式世界里,所有东西在被创建出来之后都应该是不可变的,换句话说, 比如说我买了一个巨无霸汉堡,这汉堡会一直在那里,不对变两个巨无霸,不会变迷你 霸,不会发霉,也不会过期。这个巨无霸不管在任何时候,都是最初服务员给我递上来的那一个色香味恒定的巨无霸。 657 | 658 | *** 致命魔术 659 | 660 | #+BEGIN_QUOTE 661 | ⚠️ 本小节严重剧透,好奇心强的读者请看完电影再回来接着看。 662 | #+END_QUOTE 663 | 664 | 如果你看过克里斯托弗·诺兰的电影《致命魔术》(The Prestige),应该会对里面的安吉尔[fn:3]用特斯拉给的神秘装置复制自己来完成瞬间移动的魔术。虽然安吉尔不停的杀死自己确实做法极端,但是完全又印证了片中开头和结束解释的变魔术的三个步骤: 665 | 666 | 1. 让你看一个小鸟 667 | 2. 让小鸟 *“消失”* 668 | 3. 再把小鸟变 *“回来”* (这也是最难的步骤) 669 | 670 | 注意到“消失”和“回来”我都加了引号,因为小鸟是真的“消失”,而”回来“的其实是另一只几乎一样的小鸟。 671 | 672 | #+CAPTION: 电影《致命魔术》海报 673 | [[./images/The-Prestige.png]] 674 | 675 | 回到我们的话题上来,那么可变操作就像是让小鸟消失再回来,其实永远都找不回来消失的那只小鸟了。 676 | 677 | #+BEGIN_SRC js 678 | var magic = function(cage){ 679 | cage[0] = {name:‘翠花’} 680 | } 681 | var birdInACage = [{name:’tweety’}] 682 | magic(birdInACage) 683 | birdInACage// => [{name:‘翠花’}] 684 | #+END_SRC 685 | 686 | 可以看到,经过 magic 函数后,tweety 就消失了,笼子里只有翠花,而这只被 =magic= 变没有的 tweety,不久之后就可能会被 JavaScript 的 GC(垃圾回收器)铲走。 687 | 688 | 但是,函数式编程并不喜欢魔术[fn:12],就像博登在台上把小鸟“变回来”时,台下的小朋友哭着说我要原来那只小鸟一样。函数式编程时,我们更希望在不论何时都可以找回来原来那只小鸟。 689 | 690 | 因此,我们需要一种神奇的模式把 twetty 隐藏起来。 691 | #+BEGIN_SRC js 692 | var anotherBirdInTheCage = magic(birdInACage) 693 | function magic(birdInCage){ 694 | return birdInCage.map(function(bird){return bird.name='翠花'}) 695 | } 696 | anotherBirdInTheCage// => [{name:‘翠花’}] 697 | birdInACage // => [{name:'tweety'}] 698 | #+END_SRC 699 | 700 | 太好了,twetty 没有“消失”,只是多了一只叫做翠花的小鸟。 701 | 702 | 虽然可变性 给我们编程带来了一些便利,这可能是因为我们的真实世界的所有东西都是可变的,这非常符合我们真实环境的思维方式。但是,这种可变性也能带来类似现实世界一样不可预测性的问题,有可能在不经意间会给我带来一些困扰,而却很难推理出产生这种困扰的原因。 703 | 704 | *** 引用透明性 705 | 706 | 由于所有的对象都是可变的,就像现实世界一样,对象之间靠消息通信,而通过各种消息发来发去之后谁也不知道在某一时间这些对象的状态都是些什么。然而对象的行为又可能依赖于其他对象的状态。这样依赖,如果想推测一个对象某个时间的行为,可能需要先确定其所有有消息通信相关的对象这时的状态,以及通信所发生的先后顺序。 707 | 708 | 在函数式编程中有个概念叫 /引用透明性/ (Referential Transparent),引用透明是指函数对于相同输入一等返回相同的输出,因此如果将其替换成他的输出,也不会影响程序的结果。所以通常纯函数都是引用透明的。而越透明,说明代码越容易推理(reason about)。 709 | 710 | 写过前端 JavaScript 的人都应该非常清楚前端代码是非常难推理的,光看一段代码片段很难推测出其行为。通常,自由变量越多,行为越不确定,而前端的 /自由变量/[fn:6] 太多太多: 711 | 712 | 1. DOM:不管是谁都可以修改。 713 | 2. 全局变量:谁都可以改。 714 | 3. Event:事件是全局的,如果你句柄函数如果不纯会导致结果无法预测,因为谁都可以在任何时候触发相应的事件。 715 | 4. 持久化的数据:比如 localStorage, cookie 之类的,依赖他们比依赖全局变量还难以预测。 716 | 717 | 而通常 JavaScript 或前端一些框架,都或多或少的依赖于这些因素。 718 | 719 | 有意思的是最近火热的 facebook 的 UI 库 ReactJS 就相对更容易推理。因为它使用了单向数据流状态机模型,VirtualDOM 的使用很好的隔离开了 DOM 的状态。React 的成功也充分的诠释了面向对象和函数式编程的完美结合。正常一个 React 控件是这样工作的: 720 | 721 | #+BEGIN_SRC dot :file images/react-flow.png :exports results 722 | digraph{ 723 | { rank = same; "初始化";"属性更新";"状态更新"; } 724 | 数据 -> 属性更新-> VirtualDOM 725 | 初始化 -> VirtualDOM 726 | 状态更新 -> VirtualDOM 727 | VirtualDOM -> DOM [label=diff] 728 | DOM -> 状态更新 [label=用户事件, style=dotted] 729 | } 730 | #+END_SRC 731 | 732 | #+caption: React 控件隔离变化 733 | #+RESULTS: 734 | [[file:images/react-flow.png]] 735 | 736 | 所以,React 的模型为更高内聚的模型[fn:4],只有当自己的属性和状态发生变化时,才会重新的返回该状态和属性下的 *全新* 控件。注意是全新的,不同于传统的修改 DOM 的可变性模型,React 的任何操作都是返回全新控件的不可变操作,就像操作 vector 一样,不会去修改,而是再建一个新的。而且,React 把所有可变的部分都隔离了,所有的可变的因素如,用户事件,数据变化,其他上下游控件的影响,都隔离在状态和属性之外。这样做使得我们的控件行为更简单,容易推理,也容易测试。就像接受两个参数(状态,属性)的函数,给定这两个参数 ,那么返回的控件一定是一样的。而可变的 DOM,也被 VirtualDOM 隔离了。所以完全可以把所有 React 的控件编写的像纯函数一样。因此,也可以像纯函数一样轻松的把一个组件替换掉,轻松解耦了组件之间的关系。 737 | 738 | *** 线程不安全 739 | 740 | 前端 JavaScript 虽然说是单线程的,但是基于事件循环的并发模型一样会遇到多线程的线程安全问题。线程不安全是指一个值会被多个线程中的操作同时修改。带来的问题是你很难预测以及重现这个值在某个时间到底是什么。 解决线程安全通常会用到互斥锁,原子操作等等,这些方式大大的增加编程和测试的难度。 741 | 742 | 在前端即使没有多线程同样会遇到一样的问题,比如在期望线程安全的一个事物操作中,某个值突然被修改了: 743 | 744 | #+BEGIN_SRC js 745 | // 假设收钱比如使用第三方支付宝之类的, 这里假设100ms之后知道支付成功,然后调用回调函数 746 | function charge(order,callback){ 747 | setTimeout(callback.bind(this,order), 100) 748 | } 749 | // 假设熊孩子喝牛奶只需要99ms(可能熊孩子是闪电侠) 750 | function drinkMilkThenChange(order){ 751 | setTimeout(order.push({name:'R2D2',price:99999}), 752 | 99) 753 | } 754 | // 打印发票 755 | function printReceipt(order){console.log(order)} 756 | // 熊孩子买了两个东西 757 | var order = [{name:'kindle',price:99}, {name:'drone', price:299}]; 758 | // 熊孩子结账 759 | charge(order, printReceipt) 760 | // 熊孩子喝了杯牛奶后过来修改订单 761 | drinkMilkThenChange(order) 762 | // 这时熊孩子发票上有三个东西 763 | // [{name:'kindle',price:99}, {name:'drone', price:299}, {name: 'R2D2', 99999}] 764 | #+END_SRC 765 | 766 | 这里到底发生了什么?单线程也不安全吗?难道要给 order 加锁吗? 这里的 setTimeout 都是写死的多少秒,如果是真实代码多几个熊孩子而且发 ajax 请求不确定回调时间之类的,你永远猜不到最后打印出来的发票上有些什么。 767 | 768 | 首先,让我来解释一下这里到底发生了什么。使用多线程的思路的话,charge 应该是个 io 操作,通常需要 fork 一个线程来做,这样就不阻塞主线程。于是 printReceipt 就是运行在 fork 出来的另一个线程,意味着我在主线程的操作修改到了子线程依赖的值,导致了线程不安全。 769 | 770 | 但是 JavaScript 在单线程的运行环境下如何做到线程不安全?单线程,说的是 JavaScript 运行的主线程,但是浏览器可以有若干线程处理这样的 IO 操作,也就是维护传说中的 /事件循环/ 。就拿刚才简单的 setTimeout 为例,其实是另一个线程在100毫秒之后把回调函数放入到事件循环的队列中。 771 | 772 | 所以解决方式是加锁吗? 在每次收钱之前,把订单锁上: 773 | 774 | #+BEGIN_SRC js 775 | function charge(order,callback){ 776 | Object.freeze(order); 777 | setTimeout(callback.bind(this,order), 100) 778 | } 779 | drinkMilkThenChange(order) 780 | // Uncaught TypeError: Cannot assign to read only property 'length' of [object Array] 781 | #+END_SRC 782 | 783 | 当然加锁可以解决,但是更容易而且无需考虑是多线程的方式则是简单的使用不可变数据结构。简单的把 order 的类型改成 vector 就可以了: 784 | 785 | #+BEGIN_SRC js 786 | function charge(order,callback){ 787 | setTimeout(callback.bind(this,order), 100) 788 | } 789 | function drinkMilkThenChange(order){ 790 | setTimeout(mori.conj(order,{name:'R2D2',price:99999}), 791 | 99) 792 | } 793 | var order = mori.vector({name:'kindle',price:99}, {name:'drone', price:299}) 794 | function printReceipt(order){console.log(order.toString())} 795 | charge(order, printReceipt) 796 | drinkMilkThenChange(order) 797 | // [#js {:name "kindle", :price 99} #js {:name "drone", :price 299}] 798 | #+END_SRC 799 | 800 | 不可变性保证了不管是主线程代码还是回调函数,拿到的值都能一直保持不变,所以不再需要关心会出现线程安全问题。 801 | 802 | ** 惰性序列 803 | 804 | 还记得介绍向量时这个怪怪的返回吗? 805 | #+BEGIN_SRC js 806 | mori.rest(vec) // => (2 3 4) 807 | #+END_SRC 808 | 809 | 我明明是取一个向量的尾部,为什么返回的不是方括号的向量,而是圆括号呢? 810 | 811 | 这个圆括号代表惰性序列(lazy sequence),当然,我接着要来定义 /惰性/ 和 /序列/ 。 812 | 813 | 这一章既介绍了集合 API 又读了 Clojure 源代码,实在是太无聊了,我自己都快写不下去了,所以我们不妨先暂停一下,来一个十分生动的故事稍微提提神。 814 | 815 | *** 改良吃奥利奥法 816 | 817 | 还是吃奥利奥这件事情,如果你已经忘了,我们来回顾一下之前的吃法: 818 | 819 | 1. 掰成两片,一片是不带馅的,一份是带馅的。 820 | 2. 带馅的一半沾一下牛奶。 821 | 3. 舔掉中间夹心的馅。 822 | 4. 合起来吃掉。 823 | 824 | 这是吃一个奥利奥的方法,我要把这个步骤写下来(这个故事的设定是我的记忆力极差,不写下来我会忘了该怎么吃)。既然学过 map 函数,我们试试要怎么将我的吃法 map 到一整包奥利奥上。首先封装一下如何吃一个奥利奥的步骤: 825 | 826 | #+BEGIN_SRC js 827 | function lipMiddle(oreo){ 828 | var wetOreo = dipMilk(oreo); 829 | var [top, ...middleBottom] = wetOreo; 830 | var bottom = lip(middleBottom); 831 | return [top, bottom]; 832 | } 833 | eat(lipMiddle(oreo)); 834 | #+END_SRC 835 | 836 | 然后我们开始吃整包奥利奥(underscore 版吃法): 837 | 838 | #+BEGIN_SRC js 839 | var _ = require('underscore') 840 | var oreoPack = _.range(10).map(function(x){return ["top","middle","bottom"]}) 841 | var wetOreoPack = _.map(oreoPack,lipMiddle); 842 | _.each(wetOreoPack, eat) 843 | #+END_SRC 844 | 845 | 1. 按照吃奥利奥步骤,我挨个舔掉一整包奥利奥的馅,然后放回袋子里。 846 | 2. 一个一个吃掉舔过的湿湿的奥利奥。 847 | 848 | 问题是,我其实并不知道自己能不能吃完整包,但是按照这种吃法的话, 我会打开并且着急的把所有奥利奥都沾了下牛奶,把馅舔掉,又塞回了袋子里。 849 | 850 | 假如我吃了两块就发现吃不下去了,我把袋子封好,然后困得不行去睡觉了。过了两天打开袋子发现我的奥利奥全发霉了。于是开始抱怨为什么当初不吃的要手贱去沾一下牛奶,太浪费了不是吗。 851 | 852 | 我是个特别抠门的人,于是开始苦思冥想到底吃奥利奥的方式哪里有问题。 853 | 854 | 很明显我不应该贪心的先吃掉整包奥利奥的馅,我应该吃多少就舔多少奥利奥的馅。但是问题是,我怎么知道我要吃多少呢? 855 | 856 | 又经过一番又一番的苦思冥想,我终于想到了在不知道能吃多少块的情况下怎样完美的吃一包奥利奥(mori 版吃法): 857 | 858 | 1. 把吃的步骤写成10长小条(假设一包有十块奥利奥) 859 | 2. 把小条依次贴到每块奥利奥上 860 | 3. 待吃的时候每拿出来一个,按照奥利奥上的小条的步骤开始吃 861 | 4. 完美! 862 | 863 | 写成代码该是长这样的: 864 | #+BEGIN_SRC js 865 | var oreoPack = mori.repeat(["top","middle","bottom"]); 866 | var wetOreoPack = mori.map(lipMiddle,oreoPack); 867 | // 条都塞好了,现在该吃了,假设我吃3块 868 | mori.each(eat, mori.take(3, wetOreoPack)); 869 | #+END_SRC 870 | 871 | 故事就这么圆满的结束了!于是公主和王子...... 872 | 873 | 等等,这个实现怎么看着跟前面 underscore 的实现没有什么两样,到底是在哪里把小条塞进去的? 874 | 875 | *** 惰性求值 VS 及早求值 876 | 877 | 那么现在我们来看看 mori 是如何把小条塞进去的。在这之前,我们再来看看 underscore 版本的实现,细心的读者会发现我还没有实现 lip 函数,这个函数具体如何去舔奥利奥我们并不是很关心,暂且简单的打印出来点东西好了: 878 | 879 | #+BEGIN_SRC js 880 | function lip(oreo){ 881 | console.log("舔了一下") 882 | return oreo 883 | } 884 | function dipMilk(orea){ 885 | console.log("沾一下牛奶") 886 | return oreo 887 | } 888 | #+END_SRC 889 | 890 | 那么, map 我的吃奥利奥方式到整包奥利奥的时候会发生什么呢? 891 | #+BEGIN_SRC js 892 | var wetOreoPack = _.map(oreoPack,lipMiddle); 893 | // => " 沾一下牛奶" “舔了一下” 这两句话被打印10次 894 | #+END_SRC 895 | 896 | 而同样的 mori 版本的 map 却什么也不会打印出来: 897 | #+BEGIN_SRC js 898 | var wetOreoPack = mori.map(lipMiddle,oreoPack) // 无打印信息 899 | #+END_SRC 900 | 901 | 为什么会什么都没打印,难道没 map 上吗?当然不是,map 是成功的,但是 mori 的 map 不会真对每一块奥利奥都执行我的吃奥利奥流程 lipMiddle,它只会在奥利奥上贴上一张描述如何吃奥利奥的流程的小条。因此,什么也不会返回,相当于我把整包奥利奥打开,贴上小条,再放回原位,封好袋子。 902 | 903 | #+caption: 惰性吃奥利奥法 904 | [[./images/lazy-oreo.png]] 905 | 906 | 好了,生动的故事真的要圆满结束了,如果这个故事都听明白了的话,再加上几个学术名词,我想我已经解释完什么是惰性和为什么要使用惰性了。故事中的小条,叫做 /thunk/ (我在第一章提过),而这种贴过条的序列,叫做 /惰性序列/ ,对应的 map 操作方式,叫 /惰性求值/ 。 Underscore 的这种立即执行的 map 方式,叫做 /及早求值/ 。 907 | 908 | *** 惰性求值的实现 909 | 910 | 在了解这一大堆名词之后,我们来进一步研究如何具体实现一个惰性的数据结构。我将继续以吃奥利奥为例子,解释如何实现这个惰性的 map。 911 | 912 | 之前见到的 =mori.map(lipMiddle,oreoPack)= 没有打印出任何信息,按照我的例子的说法是因为“map 只把操作的过程写成小条贴到饼干上”。那么,具体是如何把过程贴到这包奥利奥里的呢? 913 | 914 | 只要是涉及到实现,我必然要贴源代码,因为没有什么文档会比代码更真实。首先我们大眼看一下 map 的实现: 915 | 916 | #+BEGIN_SRC clojure -n -r 917 | ([f coll] 918 | (lazy-seq ;; <= 1 (ref:lazyseq) 919 | (when-let [s (seq coll)] 920 | (if (chunked-seq? s) ;; <= 2 (ref:chunkseq) 921 | (let [c (chunk-first s) 922 | size (int (count c)) 923 | b (chunk-buffer size)] 924 | (dotimes [i size] 925 | (chunk-append b (f (.nth c i)))) 926 | (chunk-cons (chunk b) (map f (chunk-rest s)))) 927 | (cons (f (first s)) (map f (rest s))))))) ;; <= 3 (ref:cons) 928 | #+END_SRC 929 | 930 | 1. [[(lazyseq)][第(lazyseq)行]]中的 lazy-seq 的 macro,其实就是用来 new 一个新的 LazySeq 实例(源码在往上翻几页,在658行)。 931 | 2. 第一个分支处理 chunked-seq 类型的序列,返回一个包含两个元素的序列 =(chunk b)= 和 =(map f (chunk-rest s))= 。 932 | 3. 另外一个分支则处理普通序列,可以看出来返回一个包含两个元素的序列 =(f (first s))= 和 =(map f (rest s))= 。 933 | 934 | 两种分支其实返回的都差不多,都是两个元素, 而第二个元素都是递归的再次调用 =map= 。我们先别看第一个分支,看看第二个简单分支。重要的是,所有的过程都放在一个叫 =lazy-seq= 的 macro 中。如果我们把 =(map lipMiddle oreoPack)= 代换展开的话会得到: 935 | 936 | #+BEGIN_SRC clojure 937 | (lazy-seq (cons (lipMiddle (first oreoPack) (map lipMiddle (rest oreoPack))))) 938 | #+END_SRC 939 | 940 | 其中 =lazy-seq= 做的事情就是阻止 =(cons...)= 被求值,硬生生的把序列从 /应用序/ 变成 /正则序/ 。回到我们的例子,这样一来, =map= 其实就是创建了一个 =lazy-seq= 的对象或者容器,容器内的序列其实还没有被求值。所以在 =map= 之后不会有任何的打印信息,因为所有的东西其实都还没有被求值,也就是我例子中说的,只是给奥利奥贴上了写满过程的小条而已。 941 | 这个例子中,就是在吃奥利奥的时候,我们才真正需要进行这么一个吃奥利奥的过程。所以当我从一包奥利奥中拿一个准备吃的时候,我需要按照条上的过程操作一遍: 942 | 943 | #+BEGIN_SRC clojure 944 | (take 1 (map lipMiddle oreoPack)) 945 | #+END_SRC 946 | 947 | 那么 =lazy-seq= 中的序列会被求值,意味着,两个元素都会被求值 948 | 949 | #+BEGIN_SRC clojure 950 | (cons lipedOreo (map lipMiddle (rest oreoPack)))) 951 | #+END_SRC 952 | 953 | =(lipMiddle (first oreoPack)= 求值得到 =lipedOreo= 而 =(map lipMiddle (rest oreoPack)= 求值变成又一个 =lazy-seq= 954 | #+BEGIN_SRC clojure 955 | (lazy-seq (cons (lipMiddle (first (rest oreoPack))) (map lipMiddle (rest (rest oreoPack))))) 956 | #+END_SRC 957 | 958 | 以此类推,需要吃第二块奥利奥时,同样的再对上式 =lazy-seq= 容器中的序列求值。 959 | 960 | 961 | * Footnotes 962 | 963 | [fn:12] 程序员更是不喜欢别人代码中的魔术。 964 | 965 | [fn:11] Futurama 是我最爱看的动画片,作者同另一部我最爱看的动画片《辛普森一家》一样都是 Matt Groening。 966 | 967 | [fn:10] 我会在第四节专门介绍序列。 968 | 969 | [fn:9] 这让我想起了“抽象屏障”这个词。 970 | 971 | [fn:8] conjs(https://github.com/jcouyang/conjs) 是我 fork 过来的一个特别版本,不仅对 JavaScript 使用习惯做了改进,而且还加入了 core.async。 972 | 973 | [fn:7] 更多具体的安装和使用步骤可以参考 mori 的 README https://github.com/swannodette/mori 974 | 975 | [fn:6] 第四章会有专门的章节解释自由变量这个词。 976 | 977 | [fn:5] http://stackoverflow.com/questions/7533837/explanation-of-combinators-for-the-working-man 978 | 979 | [fn:4] 也有人认为 React 是紧耦合,不妨再仔细看看我画这张图。 980 | 981 | [fn:1] 源代码在 https://github.com/clojure/clojure/blob/36d665793b43f62cfd22354aced4c6892088abd6/src/jvm/clojure/lang/PersistentVector.java 第34行。 982 | 983 | [fn:2] 按 lisp 语言的传统来说 cons(construct) 代表的是组成包含一个头(car)和一个尾(cdr)的结构体,主要用于创建序列 list,在 Clojure 中就是 sequence。 984 | 985 | [fn:3] 休杰克曼Hugh Jackman饰,大家更熟悉的休杰克曼应该是X战警(X-MEN)里的金刚狼 986 | 987 | 988 | 989 | -------------------------------------------------------------------------------- /book/zh/第八章.org: -------------------------------------------------------------------------------- 1 | #+OPTIONS: :makeindex nil 2 | #+LANGUAGE: zh-CN 3 | * COMMENT Import 4 | #+BEGIN_SRC emacs-lisp 5 | (require 'ob-ditaa) 6 | #+END_SRC 7 | 8 | #+RESULTS: 9 | : ob-ditaa 10 | 11 | * 并发编程 12 | 并发编程一直是令人头疼的编程方式,直到 Clojure 和 Go 的出现,彻底改变了我们并发编程的方式。而对于单线程的 JavaScript,基于事件循环的 并发模型也一直困扰着我们,到底能从 Clojure 学些什么,可以使我们的前端 并发编程之路更顺畅一些呢?本章将带你熟悉: 13 | 14 | 1. 什么是并发? 15 | 2. JavaScript 的并发模型。 16 | 3. CSP 并发模型。 17 | 4. 前端实践中如何使用 CSP。 18 | 19 | ** 什么是并发 20 | 在介绍 CSP 之前首先有两个概念需要强调一下,那就是并发与并行。 为了便于理解,我会结合现实生活举一个例子。 21 | 22 | 假设我正在上班写代码,老板过来拍着肩膀说明天要发布,加个班吧。于是我发个短信给老婆说晚点回,发完以后继续敲代码。那么请问,发短信和敲代码两个任务是 *并发* 还是并行 ? 23 | 24 | 但如果我还特别喜欢音乐,所以我边听音乐边敲代码,那么写代码和听音乐两个任务是并发还是 *并行* ? 25 | 26 | 为了不侮辱读者的智商,我就不公布答案了。所以说: 27 | - 并发是为了解决如何管理需要同时运行的多个任务。就例子来说就是我要决定的是到底先发短信,还是先写代码,还是写两行代码,发两个字短信呢?对于计算机来说,也就是线程管理。 28 | - 并行是要解决如何让多个任务同时运行。例子中的我享受音乐与写代码所用到的大脑区域可能并不冲突,因此可以让它们同时运行。对于计算机来说,就需要多个 CPU(核)或者集群来实现并行计算。 29 | 30 | 并行与并发的最大区别就是后者任务之间是互相阻塞的,任务不能同时进行,因此在执行一个任务时就得阻塞另外一个任务。 31 | 32 | *** 异步与多线程 33 | 所以说到并发,如果不是系统编程,我们大多关心的只是多线程并发编程。因为进程调度是需要操作系统更关心的事情。 34 | 35 | 继续敲代码这个例子,假如我现在能 fork 出来一只手发来短信,但是我还是只有一个脑袋,在发短信的时候我的脑子还是只能集中在 36 | 如何编一个理由向老婆请假,而另外两只手只能放在键盘上什么也干不了,直到短信发出去,才能继续写代码。 37 | 38 | 所以多线程开销大至需要长出(fork)一只手,结束后缩回去(join),但是这 些代价并没有带来时间上的好处,发短信时其它两只手其实是闲置(阻塞)着的。 39 | 40 | #+BEGIN_SRC ditaa :file images/multithread.png :exports results 41 | 42 | 43 | +---+ +---+ +---+ 44 | Thread A ----+a1 +-+a2 +-+a3 +------------------> 45 | +-+-+ +-+-+ +-+-+ 46 | : | : 47 | | : +-----+ 48 | | | | 49 | v v v 50 | +---+ +---+ +---+ +---+ +---+ 51 | CPU ----+a1 +-+a2 +-+b1 +-+a3 +-+b2 +------> 52 | +---+ +---+ +-^-+ +---+ +-^-+ 53 | : | 54 | +-----------+ | 55 | | +-----------------+ 56 | : : 57 | +-+-+ +-+-+ 58 | Thread B ----+b1 +-+b2 +------------------------> 59 | +---+ +-+-+ 60 | 61 | #+END_SRC 62 | 63 | #+caption: 线程任务到达 CPU 的不确定顺序 64 | #+RESULTS: 65 | [[file:images/multithread.png]] 66 | 67 | #+INDEX: 事件驱动模型 68 | 因此,另外一种更省资源的处理并发的方式就出现了——异步编程,或者叫 /事件驱动模型/ 。对的,就是我们在 JavaScript 代码里经常发的 Ajax 那个异步。 69 | 70 | 比如我还是两只手,我发完短信继续就敲代码了,这时,老婆给我回了一条短信,那我放下手中的活,拿起手机看看,老牌居然说了“同意”,于是就安心的放下手机继续敲代码了。 71 | 72 | 注意这段动作与之前多线程的区别,相对于多线程的场景下 fork 了第三只手在敲代码时一直呆呆的握着手机,异步编程并不需要增加胳膊,资源利用率更高一些。 73 | 74 | 那么你就要问了,你是怎么知道手机响的,还不是要开一个线程让耳朵监听着。对的,但是异步只需要很少的有限个线程就好了。比如我有十个手机 75 | 要发给十个老婆,我还是两个线程,相比如果是多线程的话我要 fork 出来十只手,却是会省了不少的资源的[fn:4]。 76 | 77 | 所以 JavaScript 的并发模型就是这么实现的,有一个专门 78 | 的事件循环([[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/EventLoop][event loop]])线程,就如同我们的耳朵,不停的检查消息队列中是否还有待执行的任务。 79 | 80 | *** JavaScript 的并发模型 81 | JavaScript 的并发模型主要基于事件循环,运行 JavaScript 代码其实就是从 event loop 里面取任务,队列中任务的来源为函数调用栈与事件绑定。 82 | - 每写一个函数 =f()= ,都会被加到消息队列中,运行该任务直到调用栈全部弹空。 83 | - 而像 =setTimeout(somefunction,0)= 其实是 注册一个事件句柄(event handler), timer 会在“0毫秒”后“立刻”往队列加入 =somefunction= (如果不是 0,则是 n 长时间后加入队列) 84 | 85 | #+BEGIN_SRC ditaa :file images/event-loop-model.png :exports results 86 | +---+ +---+ +---+ 87 | Functions----+a1 +-+a2 +-+a3 +------------------> 88 | +-+-+ +-+-+ +-+-+ 89 | : | : 90 | | : | 91 | | | | 92 | v v v 93 | +---+ +---+ +---+ +---+ +---+ 94 | Queue for----+a1 +-+a2 +-+a3 +-+b1 +-+b2 +------> 95 | Event loop +---+ +---+ +---+ +-^-+ +-^-+ 96 | | | 97 | +-----------------+ | 98 | | +-----------------+ 99 | : : 100 | +-+-+ +-+-+ 101 | Callbacks----+b1 +-+b2 +------------------------> 102 | +---+ +-+-+ 103 | #+END_SRC 104 | 105 | #+caption: callback 会加到消息队列末尾 106 | #+RESULTS: 107 | [[file:images/event-loop-model.png]] 108 | 109 | #+BEGIN_SRC javascript 110 | function a(){ 111 | console.log('a'); 112 | } 113 | function b(){ 114 | console.log('b'); 115 | } 116 | function timeout(){ 117 | console.log('timeout'); 118 | } 119 | setTimeout(timeout,0); 120 | a(); 121 | b(); 122 | // => "a" 123 | // => "b" 124 | // => "timeout" 125 | #+END_SRC 126 | 127 | 这个例子中的 =timeout= 函数并没有在 =a= 或 =b= 之前被调用,因为当时的消息队列应该是这样的(处理顺序从左至右) 128 | 129 | #+BEGIN_SRC ditaa :file images/message-queue.png :exports results 130 | +-----------+-----+-----+--------+ 131 | out <- |setTimeout |a |b |timeout | <- in 132 | +-----------+-----+-----+--------+ 133 | #+END_SRC 134 | 135 | #+RESULTS: 136 | [[file:images/message-queue.png]] 137 | 138 | 现在,我们可以用的并发模型来再实现一下我们最开始的加班写代码的例子: 139 | 140 | #+BEGIN_SRC javascript 141 | let keepDoing = (something, interval) => { 142 | return setInterval(()=>console.log(something), interval); 143 | }; 144 | let notify = function(read, callback, yesno){ 145 | console.log('dinglingling') 146 | setTimeout(read.bind(callback), 2000) 147 | 148 | }; 149 | let meSendingText = function(callback) { 150 | console.log('Me sending text'); 151 | notify(wifeReadingText, callback) 152 | } 153 | let wifeReadingText = function(callback){ 154 | console.log('my wife sending text'); 155 | notify(callback, null, 'yes') 156 | }; 157 | 158 | let working = keepDoing('typing',1000); 159 | let meReadingText = function(msg) { 160 | if(msg!='ok') clearInterval(work); 161 | console.log('I\'m reading text'); 162 | } 163 | 164 | meSendingText((msg)=>{ 165 | if(msg!='ok') clearInterval(work); 166 | else 167 | console.log('continue working'); 168 | }); 169 | 170 | #+END_SRC 171 | 172 | 173 | 其中 =notify= 负责往事件循环上放一个任务,当老婆读了短信,并 =notify= 我读回信之后,两秒后短信发到了我的手机上,手机(包含快来阅读短信句柄)的铃声通过我的耳朵传到我的脑回路中,触发我开始读短信。 174 | 175 | 使用事件循环回调的形式看起来还挺高效的,而且 JavaScript 编程中我们也一直也是这么用的。但是当异步调用多了之后,就会出现 /回调地狱/ (Callback Hell)的现象,为什么说是 *地狱* 呢, 可以想象一下前面例子中如果我有十个老婆,要向 五个老婆发短信申请加班,而且都同意后才能继续工作,该是如何实现呢? 176 | #+INDEX 回调地狱 177 | #+BEGIN_SRC js 178 | meSendingText(wife1Reading, (msg)=>{ 179 | if(msg=='yes') 180 | metSendingText(wife2Reading, (msg)=>{ 181 | if(msg=='yes') 182 | metSendingText(wife3Reading, (msg)=>{ 183 | if(msg=='yes') 184 | metSendingText(wife4Reading, (msg)=>{ 185 | if(msg=='yes') 186 | metSendingText(wife5Reading, (msg)=>{ 187 | if(msg=='yes') 188 | console.log('continue working') 189 | }) 190 | }) 191 | }) 192 | }) 193 | }) 194 | 195 | #+END_SRC 196 | 197 | 只要有一个异步函数要回调,那么所有依赖于这个异步函数结束的函数都得放到该函数的回调内。这是个比地狱还要深的回调地狱。 198 | 于是前段时间特别火的 Promise,似乎能够缓解一下回调地狱的窘境。但是,Promise 并不是专门用来消除回调地狱的,Promise 更有意义的应该是在于 Monadic 编程。对于回调地狱,Promise 能做的也只是把这些回调平铺开而已。 199 | #+INDEX: Monadic 200 | #+BEGIN_QUOTE 201 | 从乘坐手扶电梯下回调地狱,变成了乘坐直梯下回调地狱。 202 | #+END_QUOTE 203 | 204 | #+BEGIN_SRC js 205 | meSendingText(wife1Reading) 206 | .then(()=>meSendingText(wife2Reading)) 207 | .then(()=>meSendingText(wife3Reading)) 208 | .then(()=>meSendingText(wife4Reading)) 209 | .then(()=>meSendingText(wife5Reading)) 210 | #+END_SRC 211 | 212 | 当然,如果是使用 Monadic 编程方式来解决这种问题的话,其实也可以变得非常优雅而且函数式,读者可以尝试用 =when= 实现一下(请回到第七章,如果你忘了 =when= 是什么)。 213 | 214 | 但是本章,我要强调的是一种更有意思的异步编程方式 CSP。 215 | 216 | ** 通信顺序进程(CSP) 217 | 通信顺序进程(Communicating Sequential Processes), 是计算机科学中用于一种描述并发系统中交互的形式语言,简称 CSP,来源于C.A.R Hoare 1978年的论文。没错了,Hoare就是发明(让我们熟悉的大学算法课纠结得快要挂科的) 快排算法的那位计算机科学家了。 218 | 219 | CSP 由于最近 Go 语言的兴起突然复活,[[http://talks.golang.org/2012/concurrency.slide#1][Go]] 给自己的 CSP 实现起名叫 /goroutines and channels/ [fn:3],由于实在是太好用了,Clojure 也加入了 220 | CSP 的阵营,弄了一个包叫做 /core.async/ 。 221 | 222 | CSP 的概念非常简单, 想象一下事件循环,类似的: 223 | 224 | 1. CSP 把这个事件循环的消息队列转换成一个数据队列,并且把这个队列叫做 /channel/ 225 | 2. 任务等待队列中的数据 226 | 227 | #+BEGIN_SRC ditaa :file images/csp.png :exports results 228 | +----+ +----+ 229 | Process A ----+ +-+ +---------> 230 | +----+ +----+ 231 | : put 232 | +-->+----+ 233 | Channel ----------+data+-------> 234 | +----+ 235 | : take 236 | +=---+ +->+----+ 237 | Process B ----+ +-----------+ +-----> 238 | +----+ +----+ 239 | 240 | #+END_SRC 241 | 242 | #+CAPTION: CSP 中的 Channel 243 | #+RESULTS: 244 | [[file:images/csp.png]] 245 | 246 | 247 | 这样就成功的把任务和异步数据成功从回调地狱中分离开来。还是刚才发短信的例子,我们来用 CSP 实现一遍: 248 | 249 | #+BEGIN_SRC clojure -r 250 | (def working (chan)) 251 | (def texting (chan)) 252 | 253 | (defn boss-yelling [] 254 | (go-loop [no 1] 255 | (! working (str "bose say: work " no)) 257 | (recur (+ no 1)))) 258 | 259 | (defn wife-texting [] (ref:wife) 260 | (go-loop [] 261 | (! texting "wife say: come home!") 263 | (recur))) 264 | 265 | (defn reading-text [] (ref:reading) 266 | (go-loop [] 267 | (println (JS Bin 282 | 283 | - 可以看出 boss yelling,wife texting,me working 和 reading text 四个任务是 *并发* 进行的 284 | - 所有任务都相互没有依赖,之间完全没有 callback,没有哪个任务是另一个任务的 callback。 而且他们都只依赖于 =working= 和 =texting= 这两个channel 285 | - 其中的 =go-loop= 神奇的地方是,它循环获取channel中的数据,当队列空时,它的状态会变成 parking,并没有阻塞线程,而是保存当前状态,继续去试另一个 =go= 语句。 286 | - 拿 =work= 来说, =(! texting "wife say: come home!")= 是往 channel texting 中加数据,如果 channel 已满,则也切到 parking 状态。 288 | 289 | ** 使用 generator 实现 CSP [fn:2] 290 | 在看明白了 Clojure 是如何使用 channel 来解耦我的问题后,再回过头来看 JavaScript 如何实现类似的 CSP 编程呢? 291 | 292 | 先理一下我们都要实现些什么: 293 | - go block:当然是需要这样的个block,只有在这个 block 内我们可以自如的切换状态。 294 | - channel:用来存放消息 295 | - timeout:一个特殊的 channel,在规定时间内关闭 296 | - take (!):同样的,往 channe 中发消息,也会决定下一个状态是什么。 298 | 299 | 当然,首先要实现的当然是最重要的 go block,但是在这之前,让我们看看实现 go block 的前提 ES6 的一个的新标准—— /generator/ 。 300 | 301 | *** Generator 302 | [[http://blog.dev/javascript/essential-ecmascript6.html#sec-9][ES6 终于支持了Generator]],目前Firefox与Chrome都已经实现。[fn:1] Generator 在每次被调用时返回 =yield= 后边表达式的值,并保存状态,下次调用时继续运行。 303 | 304 | 这种功能听起来刚好符合上例中神奇的 parking 的行为,于是,我们可以试试用 generator 来实现刚刚 Clojure 的 CSP 版本。 305 | 306 | *** Go Block 307 | go block 其实就是一个状态机,generator 为状态机的输入,根据不同的输入使得状态机状态转移。所以实现 go block 其实就是: 308 | - 一个函数 309 | - 可以接受一个 [[(generator)][generator]] 310 | - 如果 generator 没有下一步,则结束 311 | - 如果该步的返回值状态为 park,[[(parking)][那么就是什么也不做, 过一会继续尝试新的输入]] 312 | - 如果为 continue,[[(continue)][就接着去 generator]] 取下一输入 313 | #+BEGIN_SRC javascript -r 314 | function go_(machine, step) { 315 | while(!step.done) { 316 | var arr = step.value(), 317 | state = arr[0], 318 | value = arr[1]; 319 | switch (state) { 320 | case "park": 321 | setTimeout(function() { go_(machine, step); },0); (ref:parking) 322 | return; 323 | case "continue": 324 | step = machine.next(value); (ref:continue) 325 | break; 326 | } 327 | } 328 | } 329 | 330 | function go(machine) { 331 | var gen = machine(); (ref:generator) 332 | go_(gen, gen.next()); 333 | } 334 | #+END_SRC 335 | 336 | *** timeout 337 | timeout 是一个类似于 thread sleep 的功能,想让任务能等待个一段时间再执行, 338 | 只需要在 =go_= 中加入一个 timeout 的 =case= 就好了。 339 | #+BEGIN_SRC javascript 340 | ... 341 | case 'timeout': 342 | setTimeout(function(){ go_(machine, machine.next());}, value); 343 | return; 344 | ... 345 | #+END_SRC 346 | 347 | 如果状态是 timeout,那么等待 =value= 那么长的时间再继续运行 generator。 348 | 349 | 另外,当然还需要一个返回 timeout channel 的函数: 350 | #+BEGIN_SRC javascript 351 | function timeout(interval){ 352 | var chan = [interval]; 353 | chan.name = 'timeout'; 354 | return chan; 355 | } 356 | #+END_SRC 357 | 358 | 每次使用 timeout 都会生成一个新的 channel,但是 channel 内只有一个消息,就是 timeout 的 毫秒数。 359 | 360 | *** take ! 382 | 当 generator 往 channel 中 put 消息 383 | - 如果 channel 空,则将消息放入,状态变为 continue 384 | - 如果 channel 非空,则进入 parking 状态 385 | 386 | #+BEGIN_SRC javascript 387 | function put(chan, val) { 388 | return function() { 389 | if(chan.length === 0) { 390 | chan.unshift(val); 391 | return ["continue", null]; 392 | } else { 393 | return ["park", null]; 394 | } 395 | }; 396 | } 397 | #+END_SRC 398 | 399 | *** JavaScript CSP 版本的例子 400 | 有了 go block 这个状态机以及使他状态转移表之后,终于可以原原本本的将之前的 clojure 的例子翻译成 JavaScript 了。 401 | #+BEGIN_SRC javascript 402 | function boss_yelling(){ 403 | go(function*(){ 404 | for(var i=0;;i++){ 405 | yield take(timeout(1000)); 406 | yield put(work, "boss say: work "+i); 407 | } 408 | }); 409 | } 410 | 411 | function wife_texting(){ 412 | go(function*(){ 413 | for(;;){ 414 | yield take(timeout(4000)); 415 | yield put(text, "wife say: come home"); 416 | } 417 | }); 418 | } 419 | 420 | function working(){ 421 | go(function*(){ 422 | for(;;){ 423 | var task = yield take(work); 424 | console.log(task, "me working"); 425 | } 426 | }); 427 | } 428 | 429 | function texting(){ 430 | go(function*(){ 431 | for(;;){ 432 | var read = yield take(text); 433 | console.log(read, "me ignoring"); 434 | } 435 | }); 436 | } 437 | boss_yelling(); 438 | wife_texting(); 439 | working(); 440 | texting(); 441 | #+END_SRC 442 | 443 | 是不是决定跟 Clojure 的例子非常相似呢?注意每一次 yield 都是操作 go block 这个状态机,因此就这个例子来说,我们可以跟踪一下它的状态转移过程,这样可能会对这个简单的 go block 状态机有更深得理解。 444 | 445 | 1. 首先看 =boss_yelling= 这个 go 状态机,当操作为 =take(timeout(1000))= 时,状态会切换到 =timeout= 这样状态机会停一个 1000 毫秒。 446 | 2. 其他的状态机会继续运行,接下来应该就到 =wife_texting= ,同样的这个状态机也会停 4000秒 447 | 3. 现在轮到 =working= ,但是 work channel 中并没有任何的消息,所以也进入 parking 状态。 448 | 4. 同样 =texting= 状态机也进入 parking 状态。 449 | 450 | 直到 1000 毫秒后, =boss_yelling= timeout 451 | 452 | 1. =bose_yelling= 状态机继续运行,往 work channel 中放了一条消息。 453 | 2. =working= 状态机得以继续运行,打印消息。 454 | 455 | 此时没有别的状态机的状态可以变化,又过了 1000 毫秒, =working= 还会继续打印,直到第 4000 毫秒, =wife_texting= timeout,状态机继续运行,往 text channel 添加了一条消息。这时状态机 =texting= 的状态才从 parking 切到 continue,开始打印消息。 456 | 457 | 以此类推,就会得到这样的结果: 458 | #+BEGIN_EXAMPLE 459 | "boss say: work 0" 460 | "me working" 461 | "boss say: work 1" 462 | "me working" 463 | "boss say: work 2" 464 | "me working" 465 | "boss say: work 3" 466 | "me working" 467 | "boss say: work 4" 468 | "me working" 469 | "wife say: come home" 470 | "me ignoring" 471 | "boss say: work 5" 472 | "me working" 473 | ... 474 | #+END_EXAMPLE 475 | 476 | 477 | ** 在前端实践中使用 CSP 478 | 479 | 之前的实验性的代码只是为了说明 CSP 的原理和实现思路之一,更切合实际的,我们可以通过一些库来使用到 Clojure 的 core.async。这里我简单的介绍一下我从 ClojureScript 的 core.async 移植过来的 conjs[fn:5]。 480 | 481 | *** 使用移植的 core.async 482 | 由于 go block 在 Clojure 中是用 macro 生成状态机来实现的,要移植过来困难不小,因此这里我只将 core.async 的 channel 移植了过来,但是是以接受回调函数的方式。 483 | #+BEGIN_SRC js 484 | const _ = require('con.js'); 485 | const {async} = _; 486 | var c1 = async.chan() 487 | var c2 = async.chan() 488 | 489 | async.doAlts(function(v) { 490 | console.log(v.get(0)); // => c1 491 | console.log(_.equals(c1, v.get(1))) // => true 492 | },[c1,c2]); 493 | 494 | async.put$(c1, 'c1'); 495 | async.put$(c2, 'c2'); 496 | #+END_SRC 497 | 498 | 有意思的是,我顺带实现了 Promise 版本的 core.async,会比回调要稍微更方便一些。 499 | 500 | #+BEGIN_SRC js 501 | async.alts([c1,c2]) 502 | .then((v) => { 503 | console.log(v.get(0)); // => c1 504 | console.log(_.equals(c1, v.get(1))) // => true 505 | }) 506 | async.put(c1, 'c1').then(_=>{console.log('put c1 into c1')}) 507 | async.put(c2, 'c2').then(_=>{console.log('put c2 into c2')}) 508 | #+END_SRC 509 | 510 | 虽然把 channel 能移植过来,但是缺少 macro 原生支持的 JavaScript 似乎对 go block 也无能为力,除非能有 generator 的支持。 511 | 512 | *** 使用 ES7 中的异步函数 513 | 由于在实践中我们经常会使用到 babel 来将 ES6 规范的代码编译成 ES5 的代码。所以顺便可以将 ES7 的开关打开,这样我们就可以使用 ES7 规范中的一个新特性—— async 函数。 使用 async 函数实现我们之前的例子估计代码并不会有大的变化,让我们使用 async 函数和 channel 实现一下 go 经典的乒乓球小例子。 514 | 515 | #+BEGIN_SRC js -n -r 516 | let _ = require('con.js'); 517 | let {async} = _; 518 | 519 | async function player(name, table) { 520 | while (true) { 521 | var ball = await table.take(); (ref:take) 522 | ball.hits += 1; 523 | console.log(name + " " + ball.hits); 524 | await async.timeout(100).take(); 525 | table.put(ball); 526 | } 527 | } 528 | 529 | (async function () { 530 | var table = async.chan(); 531 | 532 | player("ping", table); 533 | player("pong", table); 534 | 535 | await table.put({hits: 0}); 536 | await async.timeout(1000).take(); 537 | table.close(); 538 | })(); 539 | #+END_SRC 540 | 当把球 ={hist:0}= 放到 =table= channel 上的时候,阻塞在第[[(take)][(take)]]行 =take= 的 player ping 会先接到球,player ping 击完球 100ms 之后,球又回到了 =table= channel。之后 player pong 之间来回击球知道 table 在 1000ms 后被关闭。 541 | 542 | 所以我们运行代码后看到的间断性的 100ms 的打印出: 543 | #+BEGIN_EXAMPLE 544 | pong 1 545 | ping 2 546 | pong 3 547 | ping 4 548 | pong 5 549 | ping 6 550 | pong 7 551 | ping 8 552 | pong 9 553 | ping 10 554 | pong 11 555 | ping 12 556 | #+END_EXAMPLE 557 | 558 | 通过 async/await,结合 conjs 的 channel, 真正让我们写出了 Clojure core.async 风格的代码。利用 CSP 异步编程的方式,我们可以用同步的思路,去编写实际运行时异步的代码。这样做不仅让我们的代码更好推理,更符合简单的命令式思维方式,也更容易 debug 和做异常处理。 559 | 560 | 561 | * Footnotes 562 | 563 | [fn:5] 源代码在 http://github.com/jcouyang/conjs,可以简单的通过 =npm install con.js= 安装。 564 | 565 | [fn:4] 这里的多线程是相对用户而言,也就是这个例子中的我,而用手机发短信(这种 I/O 操作)是如何给基站发送消息的,占用了多少线程来做我们并不关心。 566 | 567 | [fn:3] /goroutine/ 名字取自 /coroutine/ (协程),由于是 go 的实现,所以叫 goroutine 了。 568 | 569 | [fn:1] Chrome有一个 feature toggle 可以打开部分 es6 功能. 打开 =chrome://flags/#enable-javascript-harmony= 设置为 =true= 570 | 571 | [fn:2] 里面的go的实现来自 http://swannodette.github.io/2013/08/24/es6-generators-and-csp/ 572 | -------------------------------------------------------------------------------- /emacs.el: -------------------------------------------------------------------------------- 1 | (require 'color-theme) 2 | (color-theme-initialize) 3 | (color-theme-gtk-ide) 4 | (require 'clojure-mode) 5 | (clojure-font-lock-setup) 6 | (require 'org) 7 | (require 'ox-latex) 8 | (require 'htmlize) 9 | (setq make-backup-files nil) 10 | (setq book-path (expand-file-name "book")) 11 | (setq org-html-validation-link nil) 12 | (setq org-confirm-babel-evaluate nil) 13 | (add-to-list 'org-latex-classes 14 | '("tufte" "\\documentclass[11pt,twoside,openright,a5paper]{tufte-book}" 15 | ("\\chapter{%s}" . "\\chapter*{%s}") 16 | ("\\section{%s}" . "\\section*{%s}") 17 | ("\\subsection{%s}" . "\\subsection*{%s}") 18 | )) 19 | (setq tex-compile-commands '(("xelatex %r"))) 20 | (setq tex-command "xelatex") 21 | (setq-default TeX-engine 'xelatex) 22 | 23 | (setq org-latex-pdf-process 24 | '("xelatex -interaction nonstopmode -output-directory %o %f" 25 | "xelatex -interaction nonstopmode -output-directory %o %f" 26 | "xelatex -interaction nonstopmode -output-directory %o %f")) 27 | 28 | (setq locate-command "mdfind") 29 | (setenv "PATH" (concat (getenv "PATH") ":/usr/local/share/npm/bin:/usr/local/bin:/usr/texbin")) 30 | (setq exec-path (append exec-path '("/usr/local/bin" "/usr/texbin"))) 31 | (custom-set-variables 32 | '(org-publish-timestamp-directory 33 | (convert-standard-filename "public/.org-timestamps/"))) 34 | (setq postamble (with-temp-buffer 35 | (insert-file-contents "html/postamble.html") 36 | (buffer-string))) 37 | (setq header (with-temp-buffer 38 | (insert-file-contents "html/header.html") 39 | (buffer-string))) 40 | (defun set-org-publish-project-alist () 41 | "Set publishing projects for Orgweb and Worg." 42 | (interactive) 43 | (setq org-publish-project-alist 44 | `(("html" 45 | ;; Directory for source files in org format 46 | :base-directory ,book-path 47 | :base-extension "org" 48 | :html-doctype "html5" 49 | :html-head ,header 50 | :html-html5-fancy t 51 | :html-postamble ,postamble 52 | ;; HTML directory 53 | :publishing-directory "public" 54 | :publishing-function org-html-publish-to-html 55 | :recursive t 56 | :headline-levels 5 57 | ;; :with-sub-superscript nil 58 | :section-numbers 3 59 | :makeindex t 60 | :exclude ".*" 61 | :include ("index.org" "zh/index.org") 62 | :html-head-include-default-style nil 63 | ) 64 | ("pdf" 65 | ;; Directory for source files in org format 66 | :base-directory ,book-path 67 | :base-extension "org" 68 | :publishing-directory "public/pdf" 69 | :publishing-function org-latex-publish-to-pdf 70 | :headline-levels 5 71 | ;; :with-sub-superscript nil 72 | :section-numbers 3 73 | :makeindex t 74 | :exclude ".*" 75 | :include ("index.org" "zh/index.org") 76 | ) 77 | 78 | ;; where static files (images, pdfs) are stored 79 | ("blog-static" 80 | :base-directory ,book-path 81 | :base-extension "css\\|js\\|png\\|jpg\\|gif\\|mp3\\|ogg\\|swf\\|woff2\\|woff" 82 | :publishing-directory "public" 83 | :recursive t 84 | :publishing-function org-publish-attachment 85 | ) 86 | ("blog" :components ("book-notes" "book-static")) 87 | ))) 88 | (set-org-publish-project-alist) 89 | -------------------------------------------------------------------------------- /html/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /html/postamble.html: -------------------------------------------------------------------------------- 1 |

Author: %a Follow @jcouyang 2 |

3 |

Modified: %C

4 |

Generated by: %c

5 |

<Publish> with _(:з」∠)_ by OrgPress

6 | Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License. 7 | 8 | 9 | 10 | 11 | 19 | 20 | 30 | 31 | 32 |
33 | 46 | 47 | --------------------------------------------------------------------------------