├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README.md ├── ROUTES.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ ├── assets │ │ ├── img │ │ │ ├── twitter-bird-dark-bgs.png │ │ │ └── type-lattice.png │ │ ├── jquery-cookie.js │ │ ├── scaffold.css │ │ ├── scaffold.js │ │ ├── show-hint.css │ │ └── show-hint.js │ └── markdown │ │ ├── data-and-control-flow │ │ ├── for-expressions.md │ │ ├── methods-side-effects-and-unit.md │ │ ├── pattern-matching.md │ │ ├── type-inference.md │ │ └── variable-declarations-and-expressions.md │ │ ├── design-patterns │ │ ├── design-patterns.md │ │ └── monads.md │ │ ├── generics-and-functional-programming │ │ ├── functional-programming.md │ │ └── introduction-to-generics.md │ │ ├── getting-started │ │ └── background.md │ │ └── object-oriented-programming │ │ ├── abstract-and-sealed-classes.md │ │ ├── apply-unapply-and-case-classes.md │ │ ├── classes-and-objects.md │ │ ├── path-dependent-types.md │ │ └── traits.md ├── scala │ └── com │ │ └── twitter │ │ ├── scaffold │ │ ├── Document.scala │ │ ├── Flags.scala │ │ ├── Interpreter.scala │ │ ├── InterpreterSupervisor.scala │ │ ├── Scaffold.scala │ │ └── markdown │ │ │ ├── HtmlRenderer.scala │ │ │ └── TextRenderer.scala │ │ └── spray │ │ └── package.scala └── twirl │ └── com │ └── twitter │ └── scaffold │ ├── index.scala.html │ ├── main.scala.html │ └── markdown.scala.html └── test └── scala └── com └── twitter └── scaffold ├── ContentSpec.scala ├── InterpreterSpec.scala └── ScaffoldSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | lib_managed 2 | target 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.10.2 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | [![Build Status](https://travis-ci.org/twitter/scala_school2.png?branch=master)](https://travis-ci.org/twitter/scala_school2) 4 | 5 | ## Workflow 6 | 7 | We use GitHub's normal [fork-and-pull](https://help.github.com/articles/using-pull-requests#fork--pull) collaboration model, which usually works like this: 8 | 9 | ### Initial setup 10 | 11 | 1. [Fork](https://help.github.com/articles/fork-a-repo) the [main Scala School 2 repository](https://github.com/twitter/scala_school2) from the Twitter account to your personal account. 12 | 13 | 2. Clone your fork from GitHub to your local development machine: 14 | 15 | ```shell 16 | git clone git@github.com:/scala_school2.git 17 | ``` 18 | 19 | 3. Your local repository will have `origin` bound to your remote repository on GitHub, where it was cloned from. You'll also want to be able to pull directly from the upstream Twitter repository, so: 20 | 21 | ```shell 22 | git remote add upstream git@github.com:twitter/scala_school2.git 23 | ``` 24 | 25 | ### For each pull request 26 | 27 | 1. Pull the latest changes from `upstream/master` (Twitter's repo) into your local `master` branch: 28 | 29 | ```shell 30 | git checkout master 31 | git pull upstream master 32 | ``` 33 | 34 | 2. Do your work on a feature branch: 35 | 36 | ```shell 37 | git checkout -b 38 | ... 39 | git commit 40 | ``` 41 | 42 | 3. Push the new branch to `origin` (your personal GitHub remote): 43 | 44 | ```shell 45 | git push -u origin 46 | ``` 47 | 48 | 4. The new branch will then appear in your GitHub repository, along with a helpful suggestion to "compare & pull request." [Do it](https://help.github.com/articles/using-pull-requests)! It should probably reference an issue number in our [issue tracker](https://github.com/twitter/scala_school2/issues?state=open), like: `fix #123: teach all the scalas`. 49 | 50 | ## Rapid Development 51 | 52 | Since the server doesn't automatically pick up code or resource changes on the fly when using `sbt run`, it's typically handiest to use the [sbt-revolver](https://github.com/spray/sbt-revolver) plugin instead; this is already configured in the project. From within the `sbt` console, invoke: 53 | 54 | ```shell 55 | ~re-start 56 | ``` 57 | 58 | This will kick the server every time you save a change to disk, which should take just a couple seconds. Leave the console running in the background while you work, and if you notice that changes you're making aren't showing up, check the console for compile errors. 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: target/start --port $PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ### OMG spoilers 2 | > 3 | > This project has not yet been publicly announced. Not that there's anything particularly scary here, but it's still full of [hipster ipsum](http://hipsteripsum.me/) and isn't ready for primetime. For now: the first rule of Scala School is **you do not talk about Scala School**. Don't share links here just yet; we'll announce soon. 4 | 5 | # Scala School 2 6 | 7 | The goal of Scala School 2 is to provide organized, interactive, reference-quality material for learning the Scala language. It is implemented as a self-contained, locally running web server, which serves book-style material along with live, editable and runnable code snippets. This is intended both for self-paced study as well as for classroom-style lecture and group exercise. 8 | 9 | We aim to eventually provide this as a high availability public service, hosted by Twitter, for people everywhere to learn Scala. Currently, however, this is a *very bad idea* for technical reasons: the underlying Scala interpreter is not sandboxed (see issues [#6](https://github.com/twitter/scala_school2/issues/6) and [#7](https://github.com/twitter/scala_school2/issues/7)). So, for now, please download the project and run it locally. 10 | 11 | ## Running 12 | 13 | `sbt run` will start an HTTP server on port 8080. 14 | 15 | ## License 16 | 17 | All original code in Scala School 2 is provided under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html), with the exception of lesson content (`src/main/resources/markdown`), provided under the [Creative Commons (CC BY 3.0)](http://creativecommons.org/licenses/by/3.0/legalcode) license. 18 | 19 | ## Shout Outs 20 | 21 | * Mathias Doenitz ([@sirthias](https://twitter.com/sirthias)), Johannes Rudolph ([@virtualvoid](https://twitter.com/virtualvoid)) and contributors to: 22 | * The **[Spray](http://spray.io/)** REST/HTTP toolkit for [Akka](http://akka.io/) (Apache 2 license) 23 | * The **[Twirl](https://github.com/spray/twirl)** SBT plugin, providing simple stand-alone use of the [Play 2.0 template engine](http://www.playframework.com/documentation/2.0/ScalaTemplates) (Apache 2 license) 24 | * The **[Revolver](https://github.com/spray/sbt-revolver)** SBT plugin, to make development not suck (Apache 2 license) 25 | * The **[Pegdown](https://github.com/sirthias/pegdown)** [Markdown](http://daringfireball.net/projects/markdown/) processor (Apache 2 license) 26 | * Marijn Haverbeke ([@marijnh](https://twitter.com/marijnjh)) and contributors to the **[CodeMirror](http://codemirror.net/)** text editor component (MIT license) 27 | * Mark Otto ([@mdo](https://twitter.com/mdo)), Jacob Thornton ([@fat](https://twitter.com/fat)) and contributors to the **[Bootstrap](http://twitter.github.io/bootstrap/)** framework (Apache 2 license) 28 | * John Resig ([@jeresig](https://twitter.com/jeresig)), Dave Methvin ([@davemethvin](https://twitter.com/davemethvin)) and contributors to the **[jQuery](http://jquery.com/)** JavaScript library (MIT license) 29 | * Jan Kovařík ([@jankovarik](https://twitter.com/jankovarik)) for **[GLYPHICONS](http://glyphicons.com/)** (CC BY 3.0) 30 | * Alexandar Farkas and contributors to **[html5shiv](https://github.com/aFarkas/html5shiv)** (MIT and GPL2 licensed) 31 | 32 | ## Real Talk, Special Thanks 33 | 34 | * Marko Gargenta ([@markogargenta](https://twitter.com/MarkoGargenta)), Saša Gargenta ([@agargenta](https://twitter.com/agargenta)) and Rob Nikzad from **[Marakana](http://marakana.com/)**, the best open-source technology training company in the world, who incubated the initial portions of the Scala School 2 content for customized, on-location training. 35 | * Marius Eriksen ([@marius](https://twitter.com/marius)), Larry Hosken ([@lahosken](https://twitter.com/lahosken)), Steve Jensen ([@stevej](https://twitter.com/stevej)), Jeff Sarnat ([@eigenvariable](https://twitter.com/eigenvariable)) and many others at Twitter for their work on the several incarnations of Scala School preceding this. 36 | * Martin Odersky ([@odersky](https://twitter.com/odersky)), Paul Phillips ([@extempore2](https://twitter.com/extempore2)), Iulian Dragos ([@jaguarul](https://twitter.com/jaguarul)), Philipp Haller ([@philippkhaller](https://twitter.com/philippkhaller)), Adriaan Moors ([@adriaanm](https://twitter.com/adriaanm)) and contributors to the **[Scala](http://www.scala-lang.org/)** programming language. 37 | * The many members of the greater Scala community who come together and speak at conferences every year, sharing their diverse and often conflicting viewpoints and experiences, to influence our understanding and appreciation for this powerful language. Fair warning: if we ever write a book, some of y'all are getting called out by name for your contributions. 38 | -------------------------------------------------------------------------------- /ROUTES.md: -------------------------------------------------------------------------------- 1 | # Routes 2 | 3 | * **/assets/{rest}** 4 | 5 | * **GET**: serve static resources 6 | 7 | * **/** 8 | 9 | * **GET**: render index (twirl) 10 | 11 | * **/{rest}** 12 | 13 | * **GET**: render lesson (twirl + markdown) 14 | 15 | * **/interpreter** 16 | 17 | * **POST**: Create a new sub-resource representing a session. 18 | 19 | Should respond with 201 CREATED, or 403 FORBIDDEN if somebody's spamming. 20 | * **/interpreter/{id}** 21 | 22 | * **POST**: send code to be interpreted. 23 | 24 | Should respond with 200 OK normally, 400 BAD REQUEST if the code is broken, 403 FORBIDDEN if the user does something malicious like `System.exit`, or 404 NOT FOUND if the session id doesn't exist. 25 | 26 | * **DELETE**: kill the session. 27 | 28 | Should return 204 NO CONTENT normally, 404 NOT FOUND if the session id doesn't exist. 29 | 30 | * **/interpreter/{id}/completions/{string}** 31 | * **GET**: submit a partial string identifier for autocompletions. 32 | 33 | Should respond with 200 OK (media type application/json) containing an array of completions, or 404 NOT FOUND if the session id doesn't exist. 34 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "scaffold" 2 | 3 | organization := "com.twitter" 4 | 5 | version := "1.0-SNAPSHOT" 6 | 7 | scalaVersion := "2.10.4" 8 | 9 | resolvers += "spray repo" at "https://repo.spray.io" 10 | 11 | libraryDependencies ++= Seq( 12 | "com.typesafe.akka" %% "akka-actor" % "2.2.0-RC1" % "compile", 13 | "io.spray" % "spray-caching" % "1.2-M8" % "compile", 14 | "io.spray" % "spray-can" % "1.2-M8" % "compile", 15 | "io.spray" % "spray-httpx" % "1.2-M8" % "compile", 16 | "io.spray" %% "spray-json" % "1.2.5" % "compile", 17 | "io.spray" % "spray-routing" % "1.2-M8" % "compile", 18 | "org.pegdown" % "pegdown" % "1.4.0" % "compile", 19 | "org.scala-lang" % "scala-compiler" % "2.10.4" % "compile", 20 | "org.webjars" % "bootstrap" % "2.3.2" % "runtime", 21 | "org.webjars" % "codemirror" % "3.14" % "runtime", 22 | "org.webjars" % "html5shiv" % "3.6.2" % "runtime", 23 | "org.webjars" % "jquery" % "2.0.2" % "runtime", 24 | "com.typesafe.akka" %% "akka-testkit" % "2.2.0-RC1" % "test", 25 | "io.spray" % "spray-testkit" % "1.2-M8" % "test", 26 | "org.scalatest" %% "scalatest" % "1.9.1" % "test" 27 | ) 28 | 29 | fork := true 30 | 31 | seq(Revolver.settings: _*) 32 | 33 | seq(Twirl.settings: _*) 34 | 35 | seq(com.typesafe.sbt.SbtStartScript.startScriptForClassesSettings: _*) 36 | 37 | Twirl.twirlImports := Seq("com.twitter.scaffold.Document", "Document._") 38 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.9 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "spray repo" at "https://repo.spray.io" 2 | 3 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2") 4 | 5 | addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0") 6 | 7 | addSbtPlugin("com.typesafe.sbt" % "sbt-start-script" % "0.10.0") 8 | -------------------------------------------------------------------------------- /src/main/resources/assets/img/twitter-bird-dark-bgs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter/scala_school2/dcaa1e5515af1faed5c6f6632589632dec7af004/src/main/resources/assets/img/twitter-bird-dark-bgs.png -------------------------------------------------------------------------------- /src/main/resources/assets/img/type-lattice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter/scala_school2/dcaa1e5515af1faed5c6f6632589632dec7af004/src/main/resources/assets/img/type-lattice.png -------------------------------------------------------------------------------- /src/main/resources/assets/jquery-cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.3.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD. Register as anonymous module. 11 | define(['jquery'], factory); 12 | } else { 13 | // Browser globals. 14 | factory(jQuery); 15 | } 16 | }(function ($) { 17 | 18 | var pluses = /\+/g; 19 | 20 | function raw(s) { 21 | return s; 22 | } 23 | 24 | function decoded(s) { 25 | return decodeURIComponent(s.replace(pluses, ' ')); 26 | } 27 | 28 | function converted(s) { 29 | if (s.indexOf('"') === 0) { 30 | // This is a quoted cookie as according to RFC2068, unescape 31 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 32 | } 33 | try { 34 | return config.json ? JSON.parse(s) : s; 35 | } catch(er) {} 36 | } 37 | 38 | var config = $.cookie = function (key, value, options) { 39 | 40 | // write 41 | if (value !== undefined) { 42 | options = $.extend({}, config.defaults, options); 43 | 44 | if (typeof options.expires === 'number') { 45 | var days = options.expires, t = options.expires = new Date(); 46 | t.setDate(t.getDate() + days); 47 | } 48 | 49 | value = config.json ? JSON.stringify(value) : String(value); 50 | 51 | return (document.cookie = [ 52 | config.raw ? key : encodeURIComponent(key), 53 | '=', 54 | config.raw ? value : encodeURIComponent(value), 55 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 56 | options.path ? '; path=' + options.path : '', 57 | options.domain ? '; domain=' + options.domain : '', 58 | options.secure ? '; secure' : '' 59 | ].join('')); 60 | } 61 | 62 | // read 63 | var decode = config.raw ? raw : decoded; 64 | var cookies = document.cookie.split('; '); 65 | var result = key ? undefined : {}; 66 | for (var i = 0, l = cookies.length; i < l; i++) { 67 | var parts = cookies[i].split('='); 68 | var name = decode(parts.shift()); 69 | var cookie = decode(parts.join('=')); 70 | 71 | if (key && key === name) { 72 | result = converted(cookie); 73 | break; 74 | } 75 | 76 | if (!key) { 77 | result[name] = converted(cookie); 78 | } 79 | } 80 | 81 | return result; 82 | }; 83 | 84 | config.defaults = {}; 85 | 86 | $.removeCookie = function (key, options) { 87 | if ($.cookie(key) !== undefined) { 88 | // Must not alter options, thus extending a fresh object... 89 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 90 | return true; 91 | } 92 | return false; 93 | }; 94 | 95 | })); 96 | -------------------------------------------------------------------------------- /src/main/resources/assets/scaffold.css: -------------------------------------------------------------------------------- 1 | /* Scaffold 2 | -------------------------------------------------- */ 3 | 4 | .CodeMirror { 5 | background-color: #fff; 6 | border: 1px solid #ddd; 7 | height: auto; 8 | margin: 15px 0; 9 | font-size: 14pt; 10 | -webkit-border-radius: 4px; 11 | -moz-border-radius: 4px; 12 | border-radius: 4px; 13 | -webkit-box-shadow: none !important; 14 | -moz-box-shadow: none !important; 15 | box-shadow: none !important; 16 | } 17 | 18 | .CodeMirror-gutters { 19 | box-shadow: none !important; 20 | padding: 0 5px !important; 21 | -webkit-box-shadow: none !important; 22 | -moz-box-shadow: none !important; 23 | box-shadow: none !important; 24 | } 25 | 26 | .CodeMirror-scroll { 27 | overflow-x: auto !important; 28 | overflow-y: hidden !important; 29 | } 30 | 31 | .btn-group { 32 | position: absolute; 33 | bottom: -3px; 34 | right: -1px; 35 | opacity: 0.5; 36 | } 37 | 38 | .btn { 39 | position: absolute; 40 | bottom: -1px; 41 | right: -1px; 42 | z-index: 10; 43 | -webkit-border-radius: 4px 0 4px 0; 44 | -moz-border-radius: 4px 0 4px 0; 45 | border-radius: 4px 0 4px 0; 46 | } 47 | 48 | pre.output { 49 | position: relative; 50 | overflow: hidden; 51 | } 52 | 53 | pre.output.error { 54 | color: #b94a48; 55 | background-color: #f2dede 56 | } 57 | 58 | pre.output div.output { 59 | min-height: 20px; 60 | } 61 | 62 | code { 63 | font-size: smaller !important; 64 | } 65 | 66 | .brand img { 67 | width: 20px; 68 | height: 20px; 69 | vertical-align: bottom; 70 | } 71 | 72 | h4 { 73 | margin-bottom: 5px !important; 74 | } 75 | 76 | .dropdown-toggle { 77 | padding: 10px 10px !important; 78 | } 79 | 80 | /* Body and structure 81 | -------------------------------------------------- */ 82 | 83 | body { 84 | position: relative; 85 | padding-top: 40px; 86 | } 87 | 88 | /* Code in headings */ 89 | h3 code { 90 | font-size: 14px; 91 | font-weight: normal; 92 | } 93 | 94 | 95 | 96 | /* Sections 97 | -------------------------------------------------- */ 98 | 99 | /* padding for in-page bookmarks and fixed navbar */ 100 | section { 101 | padding-top: 30px; 102 | } 103 | section > .page-header, 104 | section > .lead { 105 | color: #5a5a5a; 106 | } 107 | section > ul li { 108 | margin-bottom: 5px; 109 | } 110 | 111 | /* Separators (hr) */ 112 | .scaffold-separator { 113 | margin: 40px 0 39px; 114 | } 115 | 116 | /* Faded out hr */ 117 | hr.soften { 118 | height: 1px; 119 | margin: 70px 0; 120 | background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 121 | background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 122 | background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 123 | background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 124 | border: 0; 125 | } 126 | 127 | 128 | 129 | /* Jumbotrons 130 | -------------------------------------------------- */ 131 | 132 | /* Base class 133 | ------------------------- */ 134 | .jumbotron { 135 | position: relative; 136 | padding: 40px 0; 137 | color: #fff; 138 | text-align: center; 139 | text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075); 140 | background: #020031; /* Old browsers */ 141 | background: -moz-linear-gradient(45deg, #020031 0%, #6d3353 100%); /* FF3.6+ */ 142 | background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353)); /* Chrome,Safari4+ */ 143 | background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* Chrome10+,Safari5.1+ */ 144 | background: -o-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* Opera 11.10+ */ 145 | background: -ms-linear-gradient(45deg, #020031 0%,#6d3353 100%); /* IE10+ */ 146 | background: linear-gradient(45deg, #020031 0%,#6d3353 100%); /* W3C */ 147 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ 148 | -webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 149 | -moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 150 | box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); 151 | } 152 | .jumbotron h1 { 153 | font-size: 80px; 154 | font-weight: bold; 155 | letter-spacing: -1px; 156 | line-height: 1; 157 | } 158 | .jumbotron p { 159 | font-size: 24px; 160 | font-weight: 300; 161 | line-height: 1.25; 162 | margin-bottom: 30px; 163 | } 164 | 165 | /* Link styles (used on .masthead-links as well) */ 166 | .jumbotron a { 167 | color: #fff; 168 | color: rgba(255,255,255,.5); 169 | -webkit-transition: all .2s ease-in-out; 170 | -moz-transition: all .2s ease-in-out; 171 | transition: all .2s ease-in-out; 172 | } 173 | .jumbotron a:hover { 174 | color: #fff; 175 | text-shadow: 0 0 10px rgba(255,255,255,.25); 176 | } 177 | 178 | /* Download button */ 179 | .masthead .btn { 180 | padding: 19px 24px; 181 | font-size: 24px; 182 | font-weight: 200; 183 | color: #fff; /* redeclare to override the `.jumbotron a` */ 184 | border: 0; 185 | -webkit-border-radius: 6px; 186 | -moz-border-radius: 6px; 187 | border-radius: 6px; 188 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 189 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 190 | box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 191 | -webkit-transition: none; 192 | -moz-transition: none; 193 | transition: none; 194 | } 195 | .masthead .btn:hover { 196 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 197 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 198 | box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 199 | } 200 | .masthead .btn:active { 201 | -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 202 | -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 203 | box-shadow: inset 0 2px 4px rgba(0,0,0,.1), 0 1px 0 rgba(255,255,255,.1); 204 | } 205 | 206 | 207 | /* Masthead (docs home) 208 | ------------------------- */ 209 | .masthead { 210 | padding: 70px 0 80px; 211 | margin-bottom: 0; 212 | color: #fff; 213 | } 214 | .masthead h1 { 215 | font-size: 120px; 216 | line-height: 1; 217 | letter-spacing: -2px; 218 | } 219 | .masthead p { 220 | font-size: 40px; 221 | font-weight: 200; 222 | line-height: 1.25; 223 | } 224 | 225 | /* Textual links in masthead */ 226 | .masthead-links { 227 | margin: 0; 228 | list-style: none; 229 | } 230 | .masthead-links li { 231 | display: inline; 232 | padding: 0 10px; 233 | color: rgba(255,255,255,.25); 234 | } 235 | 236 | /* Social proof buttons from GitHub & Twitter */ 237 | .scaffold-social { 238 | padding: 15px 0; 239 | text-align: center; 240 | background-color: #f5f5f5; 241 | border-top: 1px solid #fff; 242 | border-bottom: 1px solid #ddd; 243 | } 244 | 245 | /* Quick links on Home */ 246 | .scaffold-social-buttons { 247 | margin-left: 0; 248 | margin-bottom: 0; 249 | padding-left: 0; 250 | list-style: none; 251 | } 252 | .scaffold-social-buttons li { 253 | display: inline-block; 254 | padding: 5px 8px; 255 | line-height: 1; 256 | *display: inline; 257 | *zoom: 1; 258 | } 259 | 260 | /* Subhead (other pages) 261 | ------------------------- */ 262 | .subhead { 263 | text-align: left; 264 | border-bottom: 1px solid #ddd; 265 | } 266 | .subhead h1 { 267 | font-size: 60px; 268 | } 269 | .subhead p { 270 | margin-bottom: 20px; 271 | } 272 | .subhead .navbar { 273 | display: none; 274 | } 275 | 276 | 277 | 278 | /* Marketing section of Overview 279 | -------------------------------------------------- */ 280 | 281 | .marketing { 282 | text-align: center; 283 | color: #5a5a5a; 284 | } 285 | .marketing h1 { 286 | margin: 60px 0 10px; 287 | font-size: 60px; 288 | font-weight: 200; 289 | line-height: 1; 290 | letter-spacing: -1px; 291 | } 292 | .marketing h2 { 293 | font-weight: 200; 294 | margin-bottom: 5px; 295 | } 296 | .marketing p { 297 | font-size: 16px; 298 | line-height: 1.5; 299 | } 300 | .marketing .marketing-byline { 301 | margin-bottom: 40px; 302 | font-size: 20px; 303 | font-weight: 300; 304 | line-height: 1.25; 305 | color: #999; 306 | } 307 | .marketing-img { 308 | display: block; 309 | margin: 0 auto 30px; 310 | max-height: 145px; 311 | } 312 | 313 | 314 | 315 | /* Footer 316 | -------------------------------------------------- */ 317 | 318 | .footer { 319 | text-align: center; 320 | padding: 30px 0; 321 | margin-top: 70px; 322 | border-top: 1px solid #e5e5e5; 323 | background-color: #f5f5f5; 324 | } 325 | .footer p { 326 | margin-bottom: 0; 327 | color: #777; 328 | } 329 | .footer-links { 330 | margin: 10px 0; 331 | } 332 | .footer-links li { 333 | display: inline; 334 | padding: 0 2px; 335 | } 336 | .footer-links li:first-child { 337 | padding-left: 0; 338 | } 339 | 340 | 341 | 342 | /* Special grid styles 343 | -------------------------------------------------- */ 344 | 345 | .show-grid { 346 | margin-top: 10px; 347 | margin-bottom: 20px; 348 | } 349 | .show-grid [class*="span"] { 350 | background-color: #eee; 351 | text-align: center; 352 | -webkit-border-radius: 3px; 353 | -moz-border-radius: 3px; 354 | border-radius: 3px; 355 | min-height: 40px; 356 | line-height: 40px; 357 | } 358 | .show-grid [class*="span"]:hover { 359 | background-color: #ddd; 360 | } 361 | .show-grid .show-grid { 362 | margin-top: 0; 363 | margin-bottom: 0; 364 | } 365 | .show-grid .show-grid [class*="span"] { 366 | margin-top: 5px; 367 | } 368 | .show-grid [class*="span"] [class*="span"] { 369 | background-color: #ccc; 370 | } 371 | .show-grid [class*="span"] [class*="span"] [class*="span"] { 372 | background-color: #999; 373 | } 374 | 375 | 376 | 377 | /* Mini layout previews 378 | -------------------------------------------------- */ 379 | .mini-layout { 380 | border: 1px solid #ddd; 381 | -webkit-border-radius: 6px; 382 | -moz-border-radius: 6px; 383 | border-radius: 6px; 384 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.075); 385 | -moz-box-shadow: 0 1px 2px rgba(0,0,0,.075); 386 | box-shadow: 0 1px 2px rgba(0,0,0,.075); 387 | } 388 | .mini-layout, 389 | .mini-layout .mini-layout-body, 390 | .mini-layout.fluid .mini-layout-sidebar { 391 | height: 300px; 392 | } 393 | .mini-layout { 394 | margin-bottom: 20px; 395 | padding: 9px; 396 | } 397 | .mini-layout div { 398 | -webkit-border-radius: 3px; 399 | -moz-border-radius: 3px; 400 | border-radius: 3px; 401 | } 402 | .mini-layout .mini-layout-body { 403 | background-color: #dceaf4; 404 | margin: 0 auto; 405 | width: 70%; 406 | } 407 | .mini-layout.fluid .mini-layout-sidebar, 408 | .mini-layout.fluid .mini-layout-header, 409 | .mini-layout.fluid .mini-layout-body { 410 | float: left; 411 | } 412 | .mini-layout.fluid .mini-layout-sidebar { 413 | background-color: #bbd8e9; 414 | width: 20%; 415 | } 416 | .mini-layout.fluid .mini-layout-body { 417 | width: 77.5%; 418 | margin-left: 2.5%; 419 | } 420 | 421 | 422 | 423 | /* Misc 424 | -------------------------------------------------- */ 425 | 426 | /* Make tables spaced out a bit more */ 427 | h2 + table, 428 | h3 + table, 429 | h4 + table, 430 | h2 + .row { 431 | margin-top: 5px; 432 | } 433 | 434 | /* Responsive docs 435 | -------------------------------------------------- */ 436 | 437 | /* Utility classes table 438 | ------------------------- */ 439 | .responsive-utilities th small { 440 | display: block; 441 | font-weight: normal; 442 | color: #999; 443 | } 444 | .responsive-utilities tbody th { 445 | font-weight: normal; 446 | } 447 | .responsive-utilities td { 448 | text-align: center; 449 | } 450 | .responsive-utilities td.is-visible { 451 | color: #468847; 452 | background-color: #dff0d8 !important; 453 | } 454 | .responsive-utilities td.is-hidden { 455 | color: #ccc; 456 | background-color: #f9f9f9 !important; 457 | } 458 | 459 | /* Responsive tests 460 | ------------------------- */ 461 | .responsive-utilities-test { 462 | margin-top: 5px; 463 | margin-left: 0; 464 | list-style: none; 465 | overflow: hidden; /* clear floats */ 466 | } 467 | .responsive-utilities-test li { 468 | position: relative; 469 | float: left; 470 | width: 25%; 471 | height: 43px; 472 | font-size: 14px; 473 | font-weight: bold; 474 | line-height: 43px; 475 | color: #999; 476 | text-align: center; 477 | border: 1px solid #ddd; 478 | -webkit-border-radius: 4px; 479 | -moz-border-radius: 4px; 480 | border-radius: 4px; 481 | } 482 | .responsive-utilities-test li + li { 483 | margin-left: 10px; 484 | } 485 | .responsive-utilities-test span { 486 | position: absolute; 487 | top: -1px; 488 | left: -1px; 489 | right: -1px; 490 | bottom: -1px; 491 | -webkit-border-radius: 4px; 492 | -moz-border-radius: 4px; 493 | border-radius: 4px; 494 | } 495 | .responsive-utilities-test span { 496 | color: #468847; 497 | background-color: #dff0d8; 498 | border: 1px solid #d6e9c6; 499 | } 500 | 501 | 502 | 503 | /* Sidenav for Docs 504 | -------------------------------------------------- */ 505 | 506 | .scaffold-sidenav { 507 | width: 228px; 508 | margin: 30px 0 0; 509 | padding: 0; 510 | background-color: #fff; 511 | -webkit-border-radius: 6px; 512 | -moz-border-radius: 6px; 513 | border-radius: 6px; 514 | -webkit-box-shadow: 0 1px 4px rgba(0,0,0,.065); 515 | -moz-box-shadow: 0 1px 4px rgba(0,0,0,.065); 516 | box-shadow: 0 1px 4px rgba(0,0,0,.065); 517 | } 518 | .scaffold-sidenav > li > a { 519 | display: block; 520 | width: 190px \9; 521 | margin: 0 0 -1px; 522 | padding: 8px 14px; 523 | border: 1px solid #e5e5e5; 524 | } 525 | .scaffold-sidenav > li:first-child > a { 526 | -webkit-border-radius: 6px 6px 0 0; 527 | -moz-border-radius: 6px 6px 0 0; 528 | border-radius: 6px 6px 0 0; 529 | } 530 | .scaffold-sidenav > li:last-child > a { 531 | -webkit-border-radius: 0 0 6px 6px; 532 | -moz-border-radius: 0 0 6px 6px; 533 | border-radius: 0 0 6px 6px; 534 | } 535 | .scaffold-sidenav > .active > a { 536 | position: relative; 537 | z-index: 2; 538 | padding: 9px 15px; 539 | border: 0; 540 | text-shadow: 0 1px 0 rgba(0,0,0,.15); 541 | -webkit-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); 542 | -moz-box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); 543 | box-shadow: inset 1px 0 0 rgba(0,0,0,.1), inset -1px 0 0 rgba(0,0,0,.1); 544 | } 545 | /* Chevrons */ 546 | .scaffold-sidenav .icon-chevron-right { 547 | float: right; 548 | margin-top: 2px; 549 | margin-right: -6px; 550 | opacity: .25; 551 | } 552 | .scaffold-sidenav > li > a:hover { 553 | background-color: #f5f5f5; 554 | } 555 | .scaffold-sidenav a:hover .icon-chevron-right { 556 | opacity: .5; 557 | } 558 | .scaffold-sidenav .active .icon-chevron-right, 559 | .scaffold-sidenav .active a:hover .icon-chevron-right { 560 | background-image: url(/assets/bootstrap/img/glyphicons-halflings-white.png); 561 | opacity: 1; 562 | } 563 | .scaffold-sidenav.affix { 564 | top: 40px; 565 | } 566 | .scaffold-sidenav.affix-bottom { 567 | position: absolute; 568 | top: auto; 569 | bottom: 270px; 570 | } 571 | 572 | 573 | 574 | /* Responsive 575 | -------------------------------------------------- */ 576 | 577 | /* Desktop large 578 | ------------------------- */ 579 | @media (min-width: 1200px) { 580 | .scaffold-container { 581 | max-width: 970px; 582 | } 583 | .scaffold-sidenav { 584 | width: 258px; 585 | } 586 | .scaffold-sidenav > li > a { 587 | width: 230px \9; /* Override the previous IE8-9 hack */ 588 | } 589 | } 590 | 591 | /* Desktop 592 | ------------------------- */ 593 | @media (max-width: 980px) { 594 | /* Unfloat brand */ 595 | body > .navbar-fixed-top .brand { 596 | float: left; 597 | margin-left: 0; 598 | padding-left: 10px; 599 | padding-right: 10px; 600 | } 601 | 602 | /* Inline-block quick links for more spacing */ 603 | .quick-links li { 604 | display: inline-block; 605 | margin: 5px; 606 | } 607 | 608 | /* When affixed, space properly */ 609 | .scaffold-sidenav { 610 | top: 0; 611 | width: 218px; 612 | margin-top: 30px; 613 | margin-right: 0; 614 | } 615 | 616 | .subhead h1, 617 | .subhead .lead { 618 | margin-right: 0; 619 | } 620 | 621 | } 622 | 623 | /* Tablet to desktop 624 | ------------------------- */ 625 | @media (min-width: 768px) and (max-width: 979px) { 626 | /* Remove any padding from the body */ 627 | body { 628 | padding-top: 0; 629 | } 630 | /* Widen masthead and social buttons to fill body padding */ 631 | .jumbotron { 632 | margin-top: -20px; /* Offset bottom margin on .navbar */ 633 | } 634 | /* Adjust sidenav width */ 635 | .scaffold-sidenav { 636 | width: 166px; 637 | margin-top: 20px; 638 | } 639 | .scaffold-sidenav.affix { 640 | top: 0; 641 | } 642 | } 643 | 644 | /* Tablet 645 | ------------------------- */ 646 | @media (max-width: 767px) { 647 | /* Remove any padding from the body */ 648 | body { 649 | padding-top: 0; 650 | } 651 | 652 | /* Widen masthead and social buttons to fill body padding */ 653 | .jumbotron { 654 | padding: 40px 20px; 655 | margin-top: -20px; /* Offset bottom margin on .navbar */ 656 | margin-right: -20px; 657 | margin-left: -20px; 658 | } 659 | .masthead h1 { 660 | font-size: 90px; 661 | } 662 | .masthead p, 663 | .masthead .btn { 664 | font-size: 24px; 665 | } 666 | .marketing .span4 { 667 | margin-bottom: 40px; 668 | } 669 | .scaffold-social { 670 | margin: 0 -20px; 671 | } 672 | 673 | /* Sidenav */ 674 | .scaffold-sidenav { 675 | width: auto; 676 | margin-bottom: 20px; 677 | } 678 | .scaffold-sidenav.affix { 679 | position: static; 680 | width: auto; 681 | top: 0; 682 | } 683 | 684 | /* Unfloat the back to top link in footer */ 685 | .footer { 686 | margin-left: -20px; 687 | margin-right: -20px; 688 | padding-left: 20px; 689 | padding-right: 20px; 690 | } 691 | .footer p { 692 | margin-bottom: 9px; 693 | } 694 | } 695 | 696 | /* Landscape phones 697 | ------------------------- */ 698 | @media (max-width: 480px) { 699 | /* Remove padding above jumbotron */ 700 | body { 701 | padding-top: 0; 702 | } 703 | 704 | /* Change up some type stuff */ 705 | h2 small { 706 | display: block; 707 | } 708 | 709 | /* Downsize the jumbotrons */ 710 | .jumbotron h1 { 711 | font-size: 45px; 712 | } 713 | .jumbotron p, 714 | .jumbotron .btn { 715 | font-size: 18px; 716 | } 717 | .jumbotron .btn { 718 | display: block; 719 | margin: 0 auto; 720 | } 721 | 722 | /* center align subhead text like the masthead */ 723 | .subhead h1, 724 | .subhead p { 725 | text-align: center; 726 | } 727 | 728 | /* Do our best to make tables work in narrow viewports */ 729 | table code { 730 | white-space: normal; 731 | word-wrap: break-word; 732 | word-break: break-all; 733 | } 734 | 735 | /* Tighten up footer */ 736 | .footer { 737 | padding-top: 20px; 738 | padding-bottom: 20px; 739 | } 740 | } -------------------------------------------------------------------------------- /src/main/resources/assets/scaffold.js: -------------------------------------------------------------------------------- 1 | !function ($) { 2 | 3 | var 4 | submitButtonTemplate = $(''), 5 | clearButtonTemplate = $(''), 6 | resetButtonTemplate = $(''), 7 | buttonGroupTemplate = $('
'), 8 | outputTemplate = $(''), 9 | interpreterCookie = 'scaffold-interpreter'; 10 | 11 | function autoComplete(editor) { 12 | var Pos = CodeMirror.Pos, cur = editor.getCursor(), cur_token = editor.getTokenAt(cur); 13 | var tokens = editor.getLine(cur.line).split(/[^a-zA-Z0-9._]/); 14 | var token = tokens[tokens.length - 1]; 15 | if (cur_token.string == ".") cur_token.start++; 16 | withInterpreter(function(interpreter) { 17 | $.ajax({ 18 | type: 'POST', 19 | url: interpreter + "/completions", 20 | data: token, 21 | }).done(function (result) { 22 | var hints = {list: result, from: Pos(cur.line, cur_token.start), to: Pos(cur.line, cur_token.end)}; 23 | CodeMirror.showHint(editor, hints); 24 | }).fail(function (xhr) { 25 | if (xhr.status === 404) { 26 | withNewInterpreter(function (interpreter) { 27 | autoComplete(editor); 28 | }); 29 | } else { 30 | console.log("Unexpected failure") 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | function withInterpreter(fn) { 37 | var interpreter = $.cookie(interpreterCookie); 38 | if (interpreter === undefined) { 39 | withNewInterpreter(fn); 40 | } else { 41 | fn(interpreter); 42 | } 43 | } 44 | 45 | function withNewInterpreter(fn) { 46 | $.ajax({ 47 | type: 'POST', 48 | url: '/interpreter' 49 | }).done(function (_, _, xhr) { 50 | var interpreter = xhr.getResponseHeader('location'); 51 | $.cookie(interpreterCookie, interpreter); 52 | fn(interpreter); 53 | }).fail(function() { 54 | console.log("Creating new cookie failed") 55 | }) 56 | } 57 | 58 | function deleteInterpreter() { 59 | var interpreter = $.cookie(interpreterCookie); 60 | if (interpreter !== undefined) { 61 | $.removeCookie(interpreterCookie); 62 | $.ajax({ 63 | type: 'DELETE', 64 | url: interpreter 65 | }).done(function () { 66 | var outputs = $('pre.output'); 67 | outputs.addClass('hidden').removeClass('error'); 68 | $('div.output', outputs).text(''); 69 | }); 70 | } 71 | } 72 | 73 | function evaluate(expression, output, retry) { 74 | withInterpreter(function(interpreter) { 75 | $.ajax({ 76 | type: 'POST', 77 | url: interpreter, 78 | data: expression, 79 | }).done(function (result) { 80 | output.removeClass('hidden').removeClass('error'); 81 | $('div.output', output).text(result); 82 | }).fail(function (xhr) { 83 | if (xhr.status === 400) { 84 | output.removeClass('hidden').addClass('error'); 85 | $('div.output', output).text(xhr.responseText); 86 | } else if (xhr.status === 404) { 87 | if (retry) { 88 | console.log("Retry failed"); 89 | } else { 90 | withNewInterpreter(function (interpreter) { 91 | evaluate(expression, output, true); 92 | }); 93 | } 94 | } else { 95 | console.log("Unexpected failure") 96 | } 97 | }); 98 | }); 99 | } 100 | 101 | $(function() { 102 | CodeMirror.commands.autocomplete = autoComplete 103 | $('textarea').each(function (_, e) { 104 | var 105 | cm = CodeMirror.fromTextArea(e, { 106 | autoCloseBrackets: true, 107 | lineNumbers: true, 108 | matchBrackets: true, 109 | smartIndent: false, 110 | tabSize: 2, 111 | theme: "solarized light", 112 | mode: "text/x-scala", 113 | extraKeys: {"Ctrl-Space": "autocomplete"} 114 | }), 115 | container = $(cm.getWrapperElement()), 116 | submitButton = submitButtonTemplate.clone(), 117 | clearButton = clearButtonTemplate.clone(), 118 | resetButton = resetButtonTemplate.clone(), 119 | buttonGroup = buttonGroupTemplate.clone().append(clearButton).append(resetButton), 120 | output = outputTemplate.clone().append(buttonGroup); 121 | 122 | function evaluateThis() { 123 | evaluate(cm.getValue(), output, false); 124 | } 125 | cm.addKeyMap({ 'Ctrl-Enter': evaluateThis }); 126 | submitButton.click(evaluateThis); 127 | 128 | clearButton.click(function() { 129 | output.addClass('hidden').removeClass('error'); 130 | $('div.output', output).text(''); 131 | }); 132 | 133 | resetButton.click(deleteInterpreter); 134 | 135 | container.append(submitButton); 136 | container.after(output); 137 | }); 138 | 139 | $('.scaffold-sidenav').affix({ 140 | offset: { 141 | top: function() { return $(window).width() <= 980 ? 290 : 210 }, 142 | bottom: 270 143 | } 144 | }); 145 | }); 146 | 147 | }(window.jQuery); -------------------------------------------------------------------------------- /src/main/resources/assets/show-hint.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-hints { 2 | position: absolute; 3 | z-index: 10; 4 | overflow: hidden; 5 | list-style: none; 6 | 7 | margin: 0; 8 | padding: 2px; 9 | 10 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 11 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 12 | box-shadow: 2px 3px 5px rgba(0,0,0,.2); 13 | border-radius: 3px; 14 | border: 1px solid silver; 15 | 16 | background: white; 17 | font-size: 90%; 18 | font-family: monospace; 19 | 20 | max-height: 20em; 21 | overflow-y: auto; 22 | } 23 | 24 | .CodeMirror-hint { 25 | margin: 0; 26 | padding: 0 4px; 27 | border-radius: 2px; 28 | max-width: 19em; 29 | overflow: hidden; 30 | white-space: pre; 31 | color: black; 32 | cursor: pointer; 33 | } 34 | 35 | .CodeMirror-hint-active { 36 | background: #08f; 37 | color: white; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/assets/show-hint.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | CodeMirror.showHint = function(cm, hints) { 5 | // We want a single cursor position. 6 | if (cm.somethingSelected()) return; 7 | if (cm.state.completionActive) cm.state.completionActive.close(); 8 | 9 | var completion = cm.state.completionActive = new Completion(cm); 10 | CodeMirror.signal(cm, "startCompletion", cm); 11 | return completion.showHints(hints); 12 | }; 13 | 14 | function Completion(cm) { 15 | this.cm = cm; 16 | this.options = {}; 17 | this.widget = this.onClose = null; 18 | } 19 | 20 | Completion.prototype = { 21 | close: function() { 22 | if (!this.active()) return; 23 | 24 | if (this.widget) this.widget.close(); 25 | if (this.onClose) this.onClose(); 26 | this.cm.state.completionActive = null; 27 | CodeMirror.signal(this.cm, "endCompletion", this.cm); 28 | }, 29 | 30 | active: function() { 31 | return this.cm.state.completionActive == this; 32 | }, 33 | 34 | pick: function(data, i) { 35 | var completion = data.list[i]; 36 | if (completion.hint) completion.hint(this.cm, data, completion); 37 | else this.cm.replaceRange(getText(completion), data.from, data.to); 38 | this.close(); 39 | }, 40 | 41 | showHints: function(data) { 42 | if (!data || !data.list.length || !this.active()) return this.close(); 43 | 44 | if (this.options.completeSingle != false && data.list.length == 1) 45 | this.pick(data, 0); 46 | else 47 | this.showWidget(data); 48 | }, 49 | 50 | showWidget: function(data) { 51 | this.widget = new Widget(this, data); 52 | CodeMirror.signal(data, "shown"); 53 | 54 | var completion = this, finished; 55 | var closeOn = this.options.closeCharacters || /[\s()\[\]{};:>,]/; 56 | var startPos = this.cm.getCursor(), startLen = this.cm.getLine(startPos.line).length; 57 | 58 | function done() { 59 | if (finished) return; 60 | finished = true; 61 | completion.close(); 62 | completion.cm.off("cursorActivity", activity); 63 | CodeMirror.signal(data, "close"); 64 | } 65 | function isDone() { 66 | if (finished) return true; 67 | if (!completion.widget) { done(); return true; } 68 | } 69 | function activity() { 70 | var pos = completion.cm.getCursor(), line = completion.cm.getLine(pos.line); 71 | if (pos.line != startPos.line || line.length - pos.ch != startLen - startPos.ch || 72 | pos.ch < startPos.ch || completion.cm.somethingSelected() || 73 | (pos.ch && closeOn.test(line.charAt(pos.ch - 1)))) 74 | completion.close(); 75 | } 76 | this.cm.on("cursorActivity", activity); 77 | this.onClose = done; 78 | } 79 | }; 80 | 81 | function getText(completion) { 82 | if (typeof completion == "string") return completion; 83 | else return completion.text; 84 | } 85 | 86 | function buildKeyMap(options, handle) { 87 | var baseMap = { 88 | Up: function() {handle.moveFocus(-1);}, 89 | Down: function() {handle.moveFocus(1);}, 90 | PageUp: function() {handle.moveFocus(-handle.menuSize());}, 91 | PageDown: function() {handle.moveFocus(handle.menuSize());}, 92 | Home: function() {handle.setFocus(0);}, 93 | End: function() {handle.setFocus(handle.length);}, 94 | Enter: handle.pick, 95 | Tab: handle.pick, 96 | Esc: handle.close 97 | }; 98 | var ourMap = options.customKeys ? {} : baseMap; 99 | function addBinding(key, val) { 100 | var bound; 101 | if (typeof val != "string") 102 | bound = function(cm) { return val(cm, handle); }; 103 | // This mechanism is deprecated 104 | else if (baseMap.hasOwnProperty(val)) 105 | bound = baseMap[val]; 106 | else 107 | bound = val; 108 | ourMap[key] = bound; 109 | } 110 | if (options.customKeys) 111 | for (var key in options.customKeys) if (options.customKeys.hasOwnProperty(key)) 112 | addBinding(key, options.customKeys[key]); 113 | if (options.extraKeys) 114 | for (var key in options.extraKeys) if (options.extraKeys.hasOwnProperty(key)) 115 | addBinding(key, options.extraKeys[key]); 116 | return ourMap; 117 | } 118 | 119 | function Widget(completion, data) { 120 | this.completion = completion; 121 | this.data = data; 122 | var widget = this, cm = completion.cm, options = completion.options; 123 | 124 | var hints = this.hints = document.createElement("ul"); 125 | hints.className = "CodeMirror-hints"; 126 | this.selectedHint = 0; 127 | 128 | var completions = data.list; 129 | for (var i = 0; i < completions.length; ++i) { 130 | var elt = hints.appendChild(document.createElement("li")), cur = completions[i]; 131 | var className = "CodeMirror-hint" + (i ? "" : " CodeMirror-hint-active"); 132 | if (cur.className != null) className = cur.className + " " + className; 133 | elt.className = className; 134 | if (cur.render) cur.render(elt, data, cur); 135 | else elt.appendChild(document.createTextNode(cur.displayText || getText(cur))); 136 | elt.hintId = i; 137 | } 138 | 139 | var pos = cm.cursorCoords(options.alignWithWord !== false ? data.from : null); 140 | var left = pos.left, top = pos.bottom, below = true; 141 | hints.style.left = left + "px"; 142 | hints.style.top = top + "px"; 143 | // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. 144 | var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); 145 | var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); 146 | var box = hints.getBoundingClientRect(); 147 | var overlapX = box.right - winW, overlapY = box.bottom - winH; 148 | if (overlapX > 0) { 149 | if (box.right - box.left > winW) { 150 | hints.style.width = (winW - 5) + "px"; 151 | overlapX -= (box.right - box.left) - winW; 152 | } 153 | hints.style.left = (left = pos.left - overlapX) + "px"; 154 | } 155 | if (overlapY > 0) { 156 | var height = box.bottom - box.top; 157 | if (box.top - (pos.bottom - pos.top) - height > 0) { 158 | overlapY = height + (pos.bottom - pos.top); 159 | below = false; 160 | } else if (height > winH) { 161 | hints.style.height = (winH - 5) + "px"; 162 | overlapY -= height - winH; 163 | } 164 | hints.style.top = (top = pos.bottom - overlapY) + "px"; 165 | } 166 | (options.container || document.body).appendChild(hints); 167 | 168 | cm.addKeyMap(this.keyMap = buildKeyMap(options, { 169 | moveFocus: function(n) { widget.changeActive(widget.selectedHint + n); }, 170 | setFocus: function(n) { widget.changeActive(n); }, 171 | menuSize: function() { return widget.screenAmount(); }, 172 | length: completions.length, 173 | close: function() { completion.close(); }, 174 | pick: function() { widget.pick(); } 175 | })); 176 | 177 | if (options.closeOnUnfocus !== false) { 178 | var closingOnBlur; 179 | cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); 180 | cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); 181 | } 182 | 183 | var startScroll = cm.getScrollInfo(); 184 | cm.on("scroll", this.onScroll = function() { 185 | var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); 186 | var newTop = top + startScroll.top - curScroll.top; 187 | var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop); 188 | if (!below) point += hints.offsetHeight; 189 | if (point <= editor.top || point >= editor.bottom) return completion.close(); 190 | hints.style.top = newTop + "px"; 191 | hints.style.left = (left + startScroll.left - curScroll.left) + "px"; 192 | }); 193 | 194 | CodeMirror.on(hints, "dblclick", function(e) { 195 | var t = e.target || e.srcElement; 196 | if (t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} 197 | }); 198 | CodeMirror.on(hints, "click", function(e) { 199 | var t = e.target || e.srcElement; 200 | if (t.hintId != null) widget.changeActive(t.hintId); 201 | }); 202 | CodeMirror.on(hints, "mousedown", function() { 203 | setTimeout(function(){cm.focus();}, 20); 204 | }); 205 | 206 | CodeMirror.signal(data, "select", completions[0], hints.firstChild); 207 | return true; 208 | } 209 | 210 | Widget.prototype = { 211 | close: function() { 212 | if (this.completion.widget != this) return; 213 | this.completion.widget = null; 214 | this.hints.parentNode.removeChild(this.hints); 215 | this.completion.cm.removeKeyMap(this.keyMap); 216 | 217 | var cm = this.completion.cm; 218 | if (this.completion.options.closeOnUnfocus !== false) { 219 | cm.off("blur", this.onBlur); 220 | cm.off("focus", this.onFocus); 221 | } 222 | cm.off("scroll", this.onScroll); 223 | }, 224 | 225 | pick: function() { 226 | this.completion.pick(this.data, this.selectedHint); 227 | }, 228 | 229 | changeActive: function(i) { 230 | i = Math.max(0, Math.min(i, this.data.list.length - 1)); 231 | if (this.selectedHint == i) return; 232 | var node = this.hints.childNodes[this.selectedHint]; 233 | node.className = node.className.replace(" CodeMirror-hint-active", ""); 234 | node = this.hints.childNodes[this.selectedHint = i]; 235 | node.className += " CodeMirror-hint-active"; 236 | if (node.offsetTop < this.hints.scrollTop) 237 | this.hints.scrollTop = node.offsetTop - 3; 238 | else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) 239 | this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; 240 | CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); 241 | }, 242 | 243 | screenAmount: function() { 244 | return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; 245 | } 246 | }; 247 | })(); -------------------------------------------------------------------------------- /src/main/resources/markdown/data-and-control-flow/for-expressions.md: -------------------------------------------------------------------------------- 1 | # For-Expressions 2 | 3 | Transforming collections in style 4 | 5 | # `while`- and `for`-loops 6 | 7 | We've all written this code in _some_ language at least once: 8 | 9 | var i = 0 10 | while (i < 10) { 11 | println("I'M AWESOME") 12 | i = i + 1 13 | } 14 | 15 | or more succinctly: 16 | 17 | for (i <- 0 until 10) 18 | println("I'M AWESOME") 19 | 20 | # Scaling up and out 21 | 22 | Fantastic. Now let's say you're trying to do something marginally more useful: 23 | 24 | import collection.mutable.Buffer 25 | 26 | val quietWords = List("let's", "transform", "some", "collections") 27 | val noisyWords = Buffer.empty[String] 28 | for (i <- 0 until quietWords.size) 29 | noisyWords += quietWords(i).toUpperCase 30 | 31 | There are a few truly awful things about this code: 32 | 33 | * Explicitly indexing into the list is prone to bugs; you're likely to have an off-by-one error or accidentally use the wrong index variable in the wrong collection. 34 | * The time complexity of this algorithm is O(n2), because indexing into a `List` is a linear-time operation, not constant-time (note: if we used a more appropriate collection type, such as `Vector`, this wouldn't be an issue). 35 | * It's not nearly DRY enough: `noisyWords` appears two times and `quietWords` appears three times. 36 | 37 | So we can do better: 38 | 39 | val noisyWords = Buffer.empty[String] 40 | for (word <- quietWords) 41 | noisyWords += word.toUpperCase 42 | 43 | No more explicit indexing, and we're now linear time, but... 44 | 45 | # `for`-expressions 46 | 47 | The mutable `Buffer`-based approach is still dissatisfying. We've been doing very well treating control flow structures as value-producing expressions so far, so why not here? 48 | 49 | val noisyWords = for (word <- quietWords) yield 50 | word.toUpperCase 51 | 52 | Aha, immutable and DRY. The `for-yield` expression indicates _transformation_: it directly produces a new collection, where each element is transformed from a corresponding element in the original collection. The behavior varies depending on the type of the original collection, but in this case where you start with a `List[String]`, the `for-yield` expression produces a `List[String]` in the order you'd expect. In many other programming languages, this is referred to as "list comprehensions." 53 | 54 | > #### Note for Java refugees 55 | > The `yield` keyword has nothing to do with Java's `Thread.yield()` method. In fact, since `yield` is a reserved keyword in Scala, if you want to call that method, you have to write ``Thread.`yield`()`` instead. Surrounding the keyword in backticks forces it to be parsed as an identifier. 56 | 57 | One nice thing about `for`-loops and `for`-expressions is that they can contain multiple _generators_, producing a similar effect to nested loops: 58 | 59 | val salutations = for (hello <- List("hello", "greetings"); world <- List("world", "interwebs")) yield 60 | "%s %s!".format(hello, world) 61 | 62 | This syntax starts to get a bit clunky, the more generators you have, so Scala provides an alternative syntax: 63 | 64 | val salutations = for { 65 | hello <- List("hello", "greetings") 66 | world <- List("world", "interwebs") 67 | } yield "%s %s!".format(hello, world) 68 | 69 | This is a bit confusing-looking at first, since the braces look like they're delineating a block of statements. It's not difficult to get used to, though, and it's the recommended style when you have multiple generators. 70 | 71 | # Assignments and filters 72 | 73 | You can also directly assign bindings (equivalent to `val`s) inside the `for { ... }`, not directly generated from elements in a collection: 74 | 75 | val salutations = for { 76 | hello <- List("hello", "greetings") 77 | world <- List("world", "interwebs") 78 | salutation = "%s %s!".format(hello, world) // assignment here! 79 | } yield salutation 80 | 81 | ... which isn't necessarily useful by itself, but becomes very handy when you need to _filter_ some of the results: 82 | 83 | val salutations = for { 84 | hello <- List("hello", "greetings") 85 | world <- List("world", "interwebs") 86 | salutation = "%s %s!".format(hello, world) 87 | if salutation.length < 20 // tl;dr 88 | } yield salutation 89 | -------------------------------------------------------------------------------- /src/main/resources/markdown/data-and-control-flow/methods-side-effects-and-unit.md: -------------------------------------------------------------------------------- 1 | # Methods, Side Effects and Unit 2 | 3 | Defining methods, best practices for type inference, and identifying side effects with Unit. 4 | 5 | # Methods: `def` 6 | 7 | Method `def`s are syntactically uniform with `var` and `val` declarations. The only difference is that `def`s can take parameters: 8 | 9 | def add(a: Int, b: Int): Int = a + b 10 | 11 | The type ascription is in the same place, right before the `=`. Also note: no braces or `return` statement are needed; the body of the `add` method is just an expression. Now, how about type inference? 12 | 13 | def add(a, b) = a + b 14 | 15 | We can guess that `a` and `b` might be `Int`, but that's not the only type for which a `+` method is defined. They could be `String`s, for example, so the compiler can't guess. Type ascriptions are _always_ required for method parameters (note: this is enforced by the language syntax, not the type checker), but you _can_ usually omit the method's result type ascription: 16 | 17 | def add(a: Int, b: Int) = a + b 18 | 19 | Since we know `a` and `b` are both `Int`, the expression `a + b` must be `Int`, and therefore the method can be inferred to produce `Int`. The same rules you saw previously, about least upper bounds, apply here as well: 20 | 21 | import util.Random._ 22 | def contrived(a: Int, b: String) = if (nextBoolean()) a else b 23 | 24 | # Named and default arguments 25 | 26 | When calling a method, you don't have to pass the arguments in the order they're declared in the method parameter list; you can use named arguments instead: 27 | 28 | def formatUser(userId: Long, userName: String, realName: String): String = 29 | "%s <%d>: %s".format(userName, userId, realName) 30 | 31 | formatUser( 32 | realName = "Dan Rosen", 33 | userName = "drosen", 34 | userId = 31337 35 | ) 36 | 37 | > #### Note for C refugees 38 | > Don't use terrible names for your method parameters. In Scala, those names are part of your public API, not just implementation detail. 39 | 40 | When calling a method that takes many parameters (many of which might have the same type), it's nice to be explicit at the call sites about which parameter is bound to which argument. This provides useful documentation for anybody reading the code, and some degree of future-proofing against API changes. 41 | 42 | Named arguments also allow us to provide defaults: 43 | 44 | def formatUser( 45 | userId: Long = 0, 46 | userName: String = "unknown", 47 | realName: String = "Unknown" 48 | ): String = 49 | "%s <%d>: %s".format(userName, userId, realName) 50 | 51 | formatUser(userName = "drosen") 52 | 53 | # Explicit type ascriptions 54 | 55 | Even though you can usually omit method result type ascriptions, most of the time it's good practice to include them, because: 56 | 57 | * They serve as useful API documentation, saving other developers the hassle of trying to do type inference on your code in their heads. 58 | * The type inference algorithm assumes you don't make mistakes. It occasionally infers a type you didn't intend, due to a mistake in your code. Usually this will cause a compile error, as the resulting program fails to typecheck, but occasionally---surprisingly---compilation will succeed. This is bad. 59 | 60 | There is also a situation where you _must_ provide a method result type: recursive methods. 61 | 62 | def fib(n: Int) = if (n == 0 || n == 1) 1 else fib(n - 1) + fib(n - 2) 63 | 64 | Have some sympathy for the compiler: in order to infer the result type of `fib`, it looks at the method body and determines that it must first infer the result type of `fib`. Turtles all the way down. 65 | 66 | # Side-effecting methods and `Unit` 67 | 68 | How about methods, or statements, that don't produce any value? 69 | 70 | println("hello") // what's the result type of println? 71 | 72 | In C-influenced languages, the result type would be `void`, representing the _absence_ of a value; but the notion that some methods can produce values and some can't adds some degree of inconsistency and complexity to these languages. Scala, along with other functional languages such as Haskell and ML, introduces a type called `Unit` which has a single value: `()`. You can't actually do anything interesting with `()`---in that respect it's very similar to `void`---but its presence comes in handy when you start using higher-order functions. 73 | 74 | Some more examples of the `Unit` type and `()` value: 75 | 76 | // no effect or value 77 | val a = { } 78 | 79 | // local side effect only 80 | val b = { 81 | var x = 0 82 | x = x + 1 83 | } 84 | 85 | // no "else" clause 86 | val c = if (nextBoolean()) 42 87 | -------------------------------------------------------------------------------- /src/main/resources/markdown/data-and-control-flow/pattern-matching.md: -------------------------------------------------------------------------------- 1 | # Pattern Matching 2 | 3 | Branching on steroids. 4 | 5 | # `match` expressions and literal patterns 6 | 7 | Old and busted branching: 8 | 9 | def monthName(n: Int): String = 10 | if (n == 1) "January" 11 | else if (n == 2) "February" 12 | else if (n == 3) "March" 13 | // ... 14 | else "Unknown" 15 | 16 | New hotness branching: 17 | 18 | def monthName(n: Int): String = n match { 19 | case 1 => "January" 20 | case 2 => "February" 21 | case 3 => "March" 22 | // ... 23 | } 24 | 25 | Wait, this looks suspiciously like `switch-case` from C. But notice that there's no `break` statement needed, to prevent falling through to subsequent cases (sorry, no [Duff's device](http://en.wikipedia.org/wiki/Duff's_device) in Scala). If you want multiple cases to correspond to a single output, there's a very convenient syntax: 26 | 27 | def daysInMonth(n: Int): Int = n match { 28 | case 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31 29 | case 4 | 6 | 9 | 11 => 30 30 | case 2 => 28 31 | } 32 | 33 | The values appearing on the left hand side of the `=>` are called "literal patterns." 34 | 35 | > #### Exercise: type inference 36 | > What if the expressions on the right hand side of the `=>` are of different types? 37 | 38 | # Variable and typed patterns 39 | 40 | Pattern matching is more powerful than `switch-case`, because it can introduce new `val` bindings. First, a simple example: what happens if you pass an unmatched value to `monthName` or `daysInMonth`? 41 | 42 | monthName(13) 43 | daysInMonth(13) 44 | 45 | > #### Exercise: fix it 46 | > Use a variable or wildcard to provide a "default" case. 47 | 48 | You can also combine this with type ascriptions to provide safe down-casting: 49 | 50 | def whatIs(a: Any): String = a match { 51 | case n: Int => 52 | val evenOrOdd = if (n % 2 == 0) "even" else "odd" 53 | "the %s number %d".format(evenOrOdd, n) 54 | case s: String => 55 | val englishOrNot = if (s forall { '\u0020' to '\u007F' contains _ }) "probably english" else "" 56 | "the %s string %s".format(englishOrNot, s) 57 | case _ => 58 | "something else" 59 | } 60 | 61 | This can be written differently with _pattern guards_ (which sort of look like `if-else` expressions, but aren't): 62 | 63 | def whatIs(a: Any): String = a match { 64 | case n: Int if n % 2 == 0 => 65 | "the even number " + n 66 | case n: Int => 67 | "the odd number " + n 68 | case s: String if s forall { '\u0020' to '\u007F' contains _ } => 69 | "the probably english string " + s 70 | case s: String => 71 | "the string " + s 72 | case _ => 73 | "something else" 74 | } 75 | 76 | Notice that the first pattern to match, in order from top to bottom, wins. 77 | 78 | # Extractor patterns 79 | 80 | Pattern matching is most compelling when used to _extract_ features from various data structures. For example: 81 | 82 | def whatIs(a: Any): String = a match { 83 | case (x, y, z) => 84 | "a Tuple3 containing %s, %s and %s".format(x, y, z) 85 | case List(x, y, z, _*) => 86 | "a List containing three or more elements, starting with: %s, %s and %s".format(x, y, z) 87 | case List((x1, y1), (x2, y2)) => 88 | "a List containing exactly two Tuple2s: %s -> %s, %s -> %s".format(x1, y1, x2, y2) 89 | case _ => 90 | "something else" 91 | } 92 | 93 | > #### Exercise: moar! 94 | > This example composes two different kinds of patterns. Identify them, and expand the example to include literal and typed patterns as well. 95 | 96 | # Exception handling 97 | 98 | This code probably behaves badly: 99 | 100 | import util.Random._ 101 | val s = if (nextBoolean()) "42" else "i like pretty flowers" 102 | val n = s.toInt 103 | 104 | An _exception_ unwinds the call stack, from the frame in which it's thrown to the nearest enclosing `catch` block. When you anticipate the possibility of one or more kinds of exceptions (for example, because you're validating possibly malicious user input, or because you're trying to connect to a possibly unavailable server), you can pattern match against those exceptions: 105 | 106 | def defensiveToInt(s: String): Int = try { 107 | s.toInt 108 | } catch { 109 | case _: NumberFormatException => 0 110 | } 111 | 112 | Note that, just like all our other control flow so far, `try-catch` is an expression that produces a value and runtime and has a type at compile time. The static type is the LUB of the types of the `try` block and all of the `case` clauses. 113 | 114 | > #### Exercise: verbose mode 115 | > Print the stack trace when an exception is caught, and catch more kinds of exceptions (e.g. `NullPointerException`). Note: just as in Java, `Throwable` is the mother of all exception types. 116 | 117 | --- 118 | 119 | > #### Combo Bonus: `finally` 120 | > It's often important to close resources, such as input/output streams, database connections and sockets. What does this look like, and how does it affect the static type of the corresponding `try-catch`? 121 | 122 | --- 123 | 124 | > #### Mega Combo Bonus: `Option` 125 | > If `defensiveToInt` produces the value `0`, we don't know whether it was because the input was the string `"0"` or a non-number. Scala has a data type called `Option` which disambiguates; use it. 126 | 127 | # Yo dawg, I herd u like patterns... 128 | 129 | So I put some patterns in your `for`-expressions so you can match while you loop. Remember this example? 130 | 131 | val salutations = for { 132 | hello <- List("hello", "greetings") 133 | world <- List("world", "interwebs") 134 | } yield "%s %s!".format(hello, world) 135 | 136 | In this example, `hello` and `world` are actually both variable patterns, and in fact the left hand side of the `<-`s in `for`-expressions are always some sort of pattern. This example shows an extractor pattern matching a `Tuple2`, with two variable patterns inside: 137 | 138 | val quietNumbers = Map(1 -> "one", 2 -> "two", 3 -> "three") 139 | val noisyOddNumbers = for { 140 | (key, value) <- quietNumbers // extractor pattern! 141 | if key % 2 == 1 142 | } yield key -> value.toUpperCase 143 | 144 | > #### Exercise: reinvent the wheel 145 | > `Map` has two methods, `keys` and `values`, which can be easily reimplemented using a `for`-expression and some patterns you've seen earlier... 146 | 147 | This trick even applies to plain `val` declarations: 148 | 149 | val (headKey, headValue) = quietNumbers.head 150 | 151 | Pattern matching is one of the most powerful constructs provided by Scala, so it's often good engineering practice to model your problem domain by decomposition into cases. This becomes especially compelling when paired with [case classes](/object-oriented-programming/apply-unapply-and-case-classes), which you'll see in the next lesson. 152 | -------------------------------------------------------------------------------- /src/main/resources/markdown/data-and-control-flow/type-inference.md: -------------------------------------------------------------------------------- 1 | # Type Inference 2 | 3 | More typing with less typing. 4 | 5 | # Basic type inference 6 | 7 | In the previous section, we kept type ascriptions on all `var` and `val` declarations for clarity, like: 8 | 9 | val n: Int = 1 10 | 11 | But it's pretty obvious in this case what the type of `n` is. In many cases, you can omit the type ascriptions: 12 | 13 | val n = 1 + 1 14 | val s = "hello, type inference" 15 | 16 | Scala is statically typed, so the compiler has to infer appropriate types for `n` and `s` from context. In this case, the static types of `n` and `s` are inferred from the types of the expressions on the right hand side of the assignments. 17 | 18 | We can go further... 19 | 20 | import util.Random._ 21 | val n = if (nextBoolean()) 1 else 0 22 | val s = if (nextBoolean()) "less filling" else "tastes great" 23 | 24 | # Least upper bounds 25 | 26 | What type is inferred here, if the types don't quite line up? 27 | 28 | val weird1 = if (nextBoolean()) 1 else 'a' 29 | val weird2 = if (nextBoolean()) 1 else true 30 | val weird3 = if (nextBoolean()) 1 else "tastes great" 31 | 32 | We have to look at Scala's **type lattice** (informally, a class hierarchy where all classes have both a common superclass---like `Object` in Java---as well as a common subclass) to find the closest common ancestor of the two sub-expression types: 33 | 34 | ![lol](/assets/img/type-lattice.png) 35 | 36 | The `AnyVal` types correspond to the JVM's primitive types, and the `AnyRef` types correspond to the JVM's object and array types. Note that the grey arrows in this diagram don't represent actual subclass relationships, just convertibility ("weak conformance"). So the LUB of `1: Int` and `'a': Char` is `Int` by weak conformance, the LUB of `1: Int` and `true: Boolean` is `AnyVal`, and the LUB of `1: Int` and `"tastes great": String` is `Any`. 37 | 38 | We can do the same thing with classes: 39 | 40 | class Base 41 | class A extends Base 42 | class B extends Base 43 | val obviousAlready = if (nextBoolean()) new A else new B 44 | 45 | > #### Exercise: break things 46 | > What happens if you explicitly ascribe the wrong types? 47 | -------------------------------------------------------------------------------- /src/main/resources/markdown/data-and-control-flow/variable-declarations-and-expressions.md: -------------------------------------------------------------------------------- 1 | # Variables and Expressions 2 | 3 | Declaring variables and values, executing statements and composing expressions. 4 | 5 | # Mutable variables: `var` 6 | 7 | You can declare and assign a variable in Scala using the `var` keyword: 8 | 9 | var n: Int = 1 + 1 10 | 11 | This can be read as "define a variable `n` of type `Int` with the initial value `1 + 1`." The syntax here is a bit more verbose at first than in C-influenced languages (e.g. `int n = ...`, `var n = ...` or just `n = ...`), because we explicitly state we're defining a variable with a particular type. We're not usually this pedantic---we can usually leave off the _type ascription_, `: Int`---but we'll keep it here for clarity, for now. 12 | 13 | `var`s are mutable, so we can reassign `n`: 14 | 15 | n = n + 1 16 | 17 | It turns out that `Int` is not the only data type in Scala. We also have 21st century `String` technology; but don't cross the streams: 18 | 19 | var s: String = "hello, scala" 20 | 21 | n = s // shouldn't work 22 | s = n // shouldn't work either 23 | 24 | Scala is strongly typed: it's not legal to assign a `String` to an `Int` or vice-versa. It's also statically typed: the type checker runs at compile time, rather than at run time. 25 | 26 | > #### Exercise: explicit conversions 27 | > How do you convert a `String` to an `Int`? How do you convert an `Int` to a `String`? 28 | 29 | # Statements vs. expressions 30 | 31 | Imperative-style programming, as in most procedural and object-oriented languages since the 1950s, is all about the sequential execution of **statements**. The execution of a statement has some effect on the system's state (e.g. assigning a value to a variable) and different code paths can execute predicated on the current state. For example: 32 | 33 | import util.Random._ 34 | 35 | var n: Int = 0 36 | if (nextBoolean()) { 37 | println("randomly true") 38 | n = n + 1 39 | } else { 40 | println("randomly false") 41 | n = n - 1 42 | } 43 | println(n) 44 | 45 | We're accustomed to thinking about control-flow structures like `if-else` as imperative statements, that produce the net effect of the statements within them. In that style, it's not always obvious what that net effect might be. In Scala, it's more conventional to use control-flow structures as **expressions**, which have meaningful types at compile time and directly produce values at runtime. For example: 46 | 47 | n = if (nextBoolean()) n + 1 else n - 1 48 | 49 | Look ma, no side effects! `n + 1` and `n - 1` are both expressions of type `Int`, so the `if-else` expression will also have type `Int`. You can keep the `println` statements if you want to, by using **block expressions**: 50 | 51 | n = 52 | if (nextBoolean()) { 53 | println("randomly true") 54 | n + 1 55 | } else { 56 | println("randomly false") 57 | n - 1 58 | } 59 | 60 | The big deal about expressions is that they compose cleanly: 61 | 62 | var s: String = 63 | if (n % 2 == 0) 64 | "n is even: %d, half is: %d".format(n, n / 2) 65 | else 66 | "n is odd: %d, randomly fixing it: %d".format(n, if (nextBoolean()) n + 1 else n - 1) 67 | 68 | > #### Exercise: "feel the burn" 69 | > Rewrite this code in imperative style. 70 | 71 | # Immutable values: `val` 72 | 73 | Given that expressions compose, there isn't nearly as much need for mutable variables as there would be in an imperative style. It's typically easiest to compute values by composing expressions rather than sequencing effects. So we use `val` most of the time: 74 | 75 | val n: Int = 1 + 1 76 | 77 | `val`s are immutable: 78 | 79 | n = n + 1 // shouldn't work 80 | 81 | > #### Protip: use `val` 82 | > Sometimes `var` is the right thing. Most of the time it isn't. -------------------------------------------------------------------------------- /src/main/resources/markdown/design-patterns/design-patterns.md: -------------------------------------------------------------------------------- 1 | # Design Patterns 2 | 3 | Small batch keytar cillum ennui fugiat. Aliquip forage chillwave, fap tattooed small batch fashion axe pitchfork in fixie scenester next level sapiente quinoa pariatur. 4 | 5 | 6 | # Sartorial authentic fap tousled 7 | 8 | Irony bespoke literally, Schlitz mixtape anim consectetur. Freegan pitchfork pug, esse mustache tote bag fashion axe YOLO incididunt duis quinoa letterpress vero ugh. Anim photo booth ullamco Godard, pitchfork chillwave gastropub meggings single-origin coffee banh mi meh ea cred dreamcatcher. Nesciunt plaid semiotics dolor, reprehenderit readymade sustainable. 9 | 10 | Cardigan intelligentsia exercitation disrupt. Scenester chambray deserunt hoodie. Odio ullamco Carles, church-key placeat delectus shoreditch tattooed DIY pop-up meh. Incididunt nisi wayfarers, ennui photo booth mustache twee vinyl eu. Schlitz letterpress Carles gentrify selfies. Etsy put a bird on it hashtag, stumptown quis 3 wolf moon tempor. Truffaut Bushwick chillwave wayfarers swag, iPhone dreamcatcher authentic exercitation selvage aliqua. 11 | -------------------------------------------------------------------------------- /src/main/resources/markdown/design-patterns/monads.md: -------------------------------------------------------------------------------- 1 | # Monads 2 | 3 | Messenger bag ennui single-origin coffee magna, High Life dolor butcher actually. 4 | 5 | # Street art tote bag placeat 6 | 7 | Typewriter voluptate fingerstache you probably haven't heard of them wolf wayfarers commodo helvetica. Locavore farm-to-table ethnic Marfa semiotics street art. Odd Future pickled asymmetrical tattooed, shabby chic Portland mollit voluptate actually deep v kogi. Carles duis excepteur qui chillwave. Forage sint vegan pitchfork tumblr, assumenda try-hard eiusmod. Sustainable anim four loko, Carles in butcher placeat narwhal craft beer asymmetrical magna. 8 | 9 | Small batch brunch post-ironic ugh. Dreamcatcher sapiente freegan, vegan ex Marfa commodo chillwave master cleanse. Aliqua ea intelligentsia Echo Park shabby chic, blue bottle id kogi. Assumenda anim ethnic, 90's vero keytar banh mi vegan dreamcatcher cornhole twee gentrify non. Odd Future flexitarian yr sapiente farm-to-table intelligentsia sriracha, artisan 3 wolf moon. Veniam freegan fixie brunch ethnic, ethical literally put a bird on it squid mollit disrupt artisan shabby chic pug post-ironic. Farm-to-table consequat officia jean shorts PBR. 10 | -------------------------------------------------------------------------------- /src/main/resources/markdown/generics-and-functional-programming/functional-programming.md: -------------------------------------------------------------------------------- 1 | # Functional Programming 2 | 3 | Semiotics 8-bit fixie, sustainable pork belly anim vero gastropub blog deserunt. 4 | 5 | # Church-key salvia authentic 6 | 7 | Artisan officia minim food truck commodo thundercats blog aesthetic tofu pork belly bicycle rights fanny pack. Actually organic Brooklyn hoodie, Williamsburg butcher Tonx tumblr. Authentic pug quis DIY, helvetica culpa assumenda. Chillwave hella wolf helvetica deep v, Etsy irure Cosby sweater. Deserunt Pinterest mixtape aliqua. Dolor consectetur 90's, actually banjo four loko in flexitarian direct trade tote bag. 8 | 9 | Excepteur raw denim sapiente DIY mumblecore Carles lo-fi eiusmod, sriracha magna organic mlkshk nisi. Nostrud fanny pack wolf Vice vinyl ethnic. Consequat four loko tempor put a bird on it synth. Delectus before they sold out artisan, selvage hella placeat cardigan gluten-free church-key occupy fap. Carles Pinterest ennui, literally authentic odio DIY laborum next level aliquip flexitarian Wes Anderson. Kitsch gluten-free thundercats freegan, forage Schlitz sartorial McSweeney's salvia chillwave polaroid craft beer nostrud ut raw denim. Excepteur exercitation nisi, sint before they sold out blog Cosby sweater Bushwick esse you probably haven't heard of them scenester incididunt Terry Richardson Neutra nesciunt. 10 | -------------------------------------------------------------------------------- /src/main/resources/markdown/generics-and-functional-programming/introduction-to-generics.md: -------------------------------------------------------------------------------- 1 | # Introduction to Generics 2 | 3 | Laboris forage whatever Cosby sweater nostrud organic, culpa gentrify nulla Austin Neutra authentic. Scenester nisi street art Schlitz, cred single-origin coffee VHS. 4 | 5 | # Master cleanse delectus 6 | 7 | Williamsburg Pinterest, veniam wolf ullamco. Mumblecore plaid street art salvia, art party freegan selfies artisan. Biodiesel photo booth wayfarers, viral Wes Anderson gentrify readymade duis anim esse hella DIY excepteur fashion axe VHS. American apparel in chambray, pitchfork literally direct trade umami aliquip. Scenester synth non, shabby chic VHS aliqua viral. 8 | 9 | Brooklyn Pinterest magna vero small batch. Godard nihil sint, cillum intelligentsia before they sold out photo booth sriracha. Yr Austin Terry Richardson pug, Carles minim Bushwick twee Cosby sweater eu Williamsburg odio occaecat. Plaid voluptate dolor, deep v nisi aliquip nesciunt lo-fi biodiesel Terry Richardson tattooed officia. Synth esse butcher, aliquip fap master cleanse gluten-free Vice. Keytar nesciunt helvetica mumblecore, do voluptate Neutra single-origin coffee. Commodo butcher 8-bit quinoa. 10 | -------------------------------------------------------------------------------- /src/main/resources/markdown/getting-started/background.md: -------------------------------------------------------------------------------- 1 | # Background 2 | 3 | Scala's ancestry and development 4 | 5 | # Models of computation 6 | 7 | * **1936**: [Alonzo Church](http://en.wikipedia.org/wiki/Alonzo_Church) develops the **[lambda calculus](http://en.wikipedia.org/wiki/Lambda_calculus)** as a means of formally expressing algorithmic solutions to problems. In its simplest form, variables (such as `x`) and functions (called "lambda abstractions," such as `λx.t`) are the only kinds of entity defined by the lambda calculus, and function application (such as `f x`) is the only operation. A "program" written in the lambda calculus is evaluated algebraically, by repeatedly binding, reducing and discarding terms as appropriate until no more algebraic simplifications are possible. 8 | 9 | Meanwhile, in independent pursuit of the same goal as Church, [Alan Turing](http://en.wikipedia.org/wiki/Alan_Turing) describes the a-machine---now known eponymously as the **[Turing machine](http://en.wikipedia.org/wiki/Turing_machine)**---consisting of a theoretical infinite length storage tape, a read-write "head" that can move left and right along the tape, and a finite state "controller" which operates the head. A "program" in this model is evaluated imperatively, by performing the state transitions prescribed in the controller until it reaches a halt state. Turing also describes a **[universal machine](http://en.wikipedia.org/wiki/Universal_Turing_machine)**: a Turing machine that can efficiently emulate any other Turing machine by interpreting instructions encoded on its tape. 10 | 11 | * **1939**: [J. B. Rosser](http://en.wikipedia.org/wiki/J._Barkley_Rosser) asserts that Church's and Turing's computational models---as well as a third model involving recursive functions developed by Rosser and [Stephen Kleene](http://en.wikipedia.org/wiki/Stephen_Cole_Kleene)---have equivalent expressive power. That is, if the solution to a problem can be implemented in any one of the three formal models, it can be implemented in the other two as well. 12 | 13 | * **1945**: [John von Neumann](http://en.wikipedia.org/wiki/John_von_Neumann) describes in an unfinished, unpublished draft paper the architecture of a **[stored-program computer](http://en.wikipedia.org/wiki/Stored_program_computer)**. This architecture resembles the Universal Turing Machine: it accepts arbitrary programs as input, and its execution model is based on reading and writing to storage locations. This architecture was subsequently realized in the [EDVAC](http://en.wikipedia.org/wiki/EDVAC), and despite Turing's influence (and independent implementation, [ACE](http://en.wikipedia.org/wiki/Automatic_Computing_Engine)), became known as the **[von Neumann architecture](http://en.wikipedia.org/wiki/Von_Neumann_architecture)**. 14 | 15 | # Major language families 16 | 17 | With programmable computers being built, the question becomes how to program them. 18 | 19 | * **1957**: [John Backus](http://en.wikipedia.org/wiki/John_Backus) and his team at IBM develop the first version of the **[FORTRAN](http://en.wikipedia.org/wiki/Fortran)** programming language, the first popular high-level language. The semantics of FORTRAN so closely resemble von Neumann's architecture, with variables abstractly representing mutable storage locations, that FORTRAN is considered the first of what would eventually become a very large family of [von Neumann languages](http://en.wikipedia.org/wiki/Von_Neumann_programming_languages). 20 | 21 | A less industrially successful, yet far more academically influential language in this family is **[ALGOL](http://en.wikipedia.org/wiki/ALGOL)**, designed initially in 1958 by a committee of 13 computer scientists including Backus, to improve on FORTRAN. ALGOL is generally considered the grandparent of [Dennis Ritchie](http://en.wikipedia.org/wiki/Dennis_Ritchie)'s [C](http://en.wikipedia.org/wiki/C_\(programming_language\)) language and its many descendants. 22 | 23 | * **1958**: [John McCarthy](http://en.wikipedia.org/wiki/John_McCarthy_\(computer_scientist\)) creates the **[LISP](http://en.wikipedia.org/wiki/Lisp_\(programming_language\))** programming language, the first popular programming language based more closely on Church's lambda calculus than on the von Neumann architecture. Functions are defined as lambda abstractions, such as `(lambda (x) (+ x 1))`, and programs are evaluated algebraically by variable substitution. LISP is considered the first of a relatively small family of [functional languages](http://en.wikipedia.org/wiki/Functional_programming). 24 | 25 | Ironically, despite Backus's major contribution in FORTRAN---which may have cemented the industrial success of the von Neumann style and which garnered Backus the [ACM Turing Award](http://en.wikipedia.org/wiki/Turing_Award) in 1977---his award lecture is titled "[Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs](http://www.stanford.edu/class/cs242/readings/backus.pdf)." He argues in this lecture that the imperative style doesn't allow for composability, reuse or algebraic reasoning about program fragments, and presents a new language called [FP](http://en.wikipedia.org/wiki/FP_\(programming_language\)) embodying functional principles. 26 | 27 | # Type systems, polymorphism 28 | 29 | Compilers for FORTRAN and many other emerging languages include some amount of type checking, to prove basic well-formedness properties of programs before they are executed. These early type systems are overly conservative, however---rejecting programs that would otherwise run without issue---so refinement is needed. 30 | 31 | * **1967**: [Ole-Johan Dahl](http://en.wikipedia.org/wiki/Ole-Johan_Dahl) and [Kristen Nygaard](http://en.wikipedia.org/wiki/Kristen_Nygaard) publish "Class and Subclass Declarations," building on ALGOL and other research from the 1960s, to define **[Simula 67](http://en.wikipedia.org/wiki/Simula)**. Simula introduces [subtype polymorphism](http://en.wikipedia.org/wiki/Subtyping) via [virtual methods](http://en.wikipedia.org/wiki/Virtual_function). 32 | 33 | For example, given a base class `Widget` defining a virtual `draw` method, it's possible to write a method that draws entire user interfaces without any knowledge about which concrete subclasses, such as `Button`, `Label` or `Container`, might exist. However, this technique discards specific type information, so for example, it's not possible to express the requirement that all widgets in a container must have the same type. 34 | 35 | * **1973**: [Robin Milner](http://en.wikipedia.org/wiki/Robin_Milner) creates the **[ML](http://en.wikipedia.org/wiki/ML_\(programming_language\))** functional programming language. ML's type system introduces [parametric polymorphism](http://en.wikipedia.org/wiki/Parametric_polymorphism), in which a type can be constructed by binding its type parameters, the same way a function is evaluated by binding its value parameters. 36 | 37 | For example, `'a container` might denote a container widget with children of some type variable `'a`, which can be bound to any type: `button container` is a container of buttons, `label container` is a container of labels, and the two are considered different types. However, there's no way of expressing a heterogeneous `widget container`, and in fact the `container` data type can't assume that the type parameter `'a` is a `widget` at all. 38 | 39 | * **1985**: [Luca Cardelli](http://en.wikipedia.org/wiki/Luca_Cardelli) and [Peter Wegner](http://en.wikipedia.org/wiki/Peter_Wegner) recognize that subtype polymorphism and parametric polymorphism both have their respective strengths and weaknesses, and introduce a new notion of **[bounded](http://en.wikipedia.org/wiki/Bounded_quantification)** polymorphism. Cardelli, et al., later formalize this in [System F<:](http://en.wikipedia.org/wiki/System_F-sub) in 1991. 40 | 41 | This system permits data type declarations such as `Container[A <: Widget]`, asserting that any concrete type bound to `A` must be a subtype of `Widget`. For example, a `Container[Button]` would be accepted and a `Container[Flower]` would be rejected. Additionally, a `Container[Widget]` may be heterogeneous, containing a mix of `Button`, `Label` and even other `Container` instances. 42 | 43 | # From Wirth to Odersky 44 | 45 | * **1966**: [Niklaus Wirth](http://en.wikipedia.org/wiki/Niklaus_Wirth) starts publishing a steady stream of programming language research, based initially on ALGOL, evolving to [Algol W](http://en.wikipedia.org/wiki/Algol_W), [Euler](http://en.wikipedia.org/wiki/Euler_\(programming_language\)) and [Pascal](http://en.wikipedia.org/wiki/Pascal_\(programming_language\)), and then on to [Modula](http://en.wikipedia.org/wiki/Modula), [Modula-2](http://en.wikipedia.org/wiki/Modula-2), [Oberon](http://en.wikipedia.org/wiki/Oberon_\(programming_language\)) and [Oberon-2](http://en.wikipedia.org/wiki/Oberon-2_\(programming_language\)). Many of the ideas explored by these languages, such as separate compilation, access modifiers and reflection, are considered core to modern object-oriented programming. 46 | 47 | * **1991**: [Martin Odersky](http://en.wikipedia.org/wiki/Martin_Odersky), a doctoral student of Wirth's at ETH Zürich, submits his dissertation, "[A New Approach to Formal Language Definition and its Application to Oberon](http://e-collection.library.ethz.ch/eserv/eth:37765/eth-37765-01.pdf)." This research introduces a formalism called CADET (Calculus on Derivation Trees) to specify the static semantics of programming languages (called "context-dependent syntax" in this work, though not to be confused with "context-sensitive grammar"). CADET itself is not especially influential, but the experience serves as Odersky's TODO 48 | 49 | # From Java to Scala 50 | 51 | As years pass, the PL research community makes substantial additional progress both in programming models, accounting for the availability of multi-processor and distributed systems, and in type systems, allowing more useful proofs about programs to be expressed or even inferred. Very little of this research, however, is incorporated into languages popularly used in industry. 52 | 53 | * **1996**: [Patrick Naughton](http://en.wikipedia.org/wiki/Patrick_Naughton), [James Gosling](http://en.wikipedia.org/wiki/James_Gosling) and Mike Sheridan at Sun Microsystems, in pursuit of a better environment for systems programming at Sun, develop the **[Java](http://en.wikipedia.org/wiki/Java_\(programming_language\))** programming language and virtual machine. 54 | 55 | Java is a von Neumann style language, strongly influenced by Simula (by way of [Modula-3](http://en.wikipedia.org/wiki/Modula-3) and [Smalltalk](http://en.wikipedia.org/wiki/Smalltalk)). In other words, as an imperative language it lacks useful functional combinators; and as a Simula derivative, its type system is already about 30 years old on its release date. 56 | 57 | * **1997**: [Martin Odersky](http://en.wikipedia.org/wiki/Martin_Odersky) and [Phil Wadler](http://en.wikipedia.org/wiki/Philip_Wadler), well aware of Java 1.0's potential for success despite its many shortcomings compared to the state of the art in research, develop the **[Pizza](http://en.wikipedia.org/wiki/Pizza_\(programming_language\))** programming language. Pizza is a superset of Java which compiles to Java source, adding F-bounded parametric polymorphism ("[generics](http://en.wikipedia.org/wiki/Generics_in_Java)") and higher-order functions ("lambdas"), as well as a powerful control-flow construct called pattern-matching. 58 | 59 | Odersky and Wadler continue work on Pizza in collaboration with [Gilad Bracha](http://en.wikipedia.org/wiki/Gilad_Bracha) and Dave Stoutamire of Sun, releasing **[GJ](http://en.wikipedia.org/wiki/Generic_Java)** (Generic Java) in 1998. GJ compiles directly to Java bytecode, and a stripped-down version of the GJ compiler replaces `javac` in the JDK 1.3 release. In 2004, with the release of Java 5, generics finally become part of the language standard; lambdas are scheduled for Java 8 in 2014. 60 | 61 | * **2000**: Odersky, frustrated by Java's constraints, develops a new, minimalistic language on the JVM called **Funnel**, based on the [join-calculus](http://en.wikipedia.org/wiki/Join-calculus). Despite being hosted on the JVM, however, the language lacks a means of reusing existing Java libraries, and its design is so Spartan that it's later described by Odersky as "not very pleasant to use in practice." 62 | 63 | * **2003**: Starting over, with lessons learned from Funnel and GJ, Odersky develops Scala. 64 | 65 | # Scala overview 66 | 67 | Scala is an attempt to unify TODO 68 | 69 | -------------------------------------------------------------------------------- /src/main/resources/markdown/object-oriented-programming/abstract-and-sealed-classes.md: -------------------------------------------------------------------------------- 1 | # Abstract and Sealed Classes 2 | 3 | Creating enumerations and algebraic data types from primitive, object-oriented building blocks. 4 | 5 | # Enumerations: `sealed` 6 | 7 | > #### Note: `scala.Enumeration` 8 | > There is an abstract class in Scala's standard library called [`Enumeration`](http://www.scala-lang.org/api/current/index.html#scala.Enumeration). Check it out, if you like; it's terribad. Fortunately there's no reason to ever use it. 9 | 10 | There are a few common use cases for _subtype polymorphism_---generic programming based on superclass-subclass relationships---and inheritance in Scala. The first we'll look at here is building enumerations: 11 | 12 | abstract class RequestMethod 13 | object RequestMethod { 14 | case object DELETE extends RequestMethod 15 | case object GET extends RequestMethod 16 | case object HEAD extends RequestMethod 17 | case object OPTIONS extends RequestMethod 18 | case object POST extends RequestMethod 19 | case object PUT extends RequestMethod 20 | } 21 | 22 | abstract class ResponseStatus(val code: Int) 23 | object ResponseStatus { 24 | case object Ok extends ResponseStatus(200) 25 | case object NotModified extends ResponseStatus(304) 26 | case object EnhanceYourCalm extends ResponseStatus(420) 27 | } 28 | 29 | So far so good: you declare an abstract class, and some number of case objects extending it. But if the compelling purpose for enumerations is to provide an exhaustive set of possible values, we have two problems: 30 | 31 | * What's to stop anybody from extending the same abstract base class? Yes, you could make the base class constructors `private` and define all subclasses in the companion object as we've done here. But we still have another problem... 32 | * When you're pattern matching over the case objects, how do you know you've covered all of them? Alternatively, when you add a case object to the enumeration, how do you know what client code needs to be updated? For example: 33 | 34 | import RequestMethod._ 35 | def isSafe(method: RequestMethod): Boolean = method match { 36 | case GET => true 37 | case POST => false 38 | } 39 | 40 | This code obviously fails to match the majority of declared `RequestMethod` objects, but maybe it's not always so obvious. The solution is to mark the base classes as `sealed`. 41 | 42 | > #### Exercise: `sealed` 43 | > Do it, and observe the difference when you define `isSafe`. 44 | 45 | This limits subclassing from that base class to be allowed only within the same file, and allows the compiler to verify that pattern match expressions are exhaustive. 46 | 47 | # Abstract methods and `override` 48 | 49 | Another common use case for _subtype polymorphism_ is to implement so-called "algebraic data types," (more accurately known as sum types; also known as discriminated unions, tagged variants, etc.) which can be considered a more general form of enumerations. For example: 50 | 51 | sealed abstract class Message { 52 | def headers: Map[String, String] 53 | def entity: String 54 | } 55 | 56 | object Message { 57 | case class Request( 58 | method: RequestMethod = RequestMethod.GET, 59 | headers: Map[String, String] = Map.empty, 60 | entity: String = "" 61 | ) extends Message 62 | 63 | case class Response( 64 | status: ResponseStatus = ResponseStatus.Ok, 65 | headers: Map[String, String] = Map.empty, 66 | entity: String = "" 67 | ) extends Message 68 | } 69 | 70 | Here, instead of singleton case objects, we use case classes because there will be many instances of each case (each with its own data). Pattern matching over these is epic win, but that's beside the point: the base class here has method _declarations_ with no implementations. These methods are abstract. 71 | 72 | Hey, wait a minute! Those abstract `def`s in the base class are being implemented as `val`s in the case classes! Yes, this is legal and encouraged. In general, the choice of whether to implement a member as a `def` or a `val` is regarded as an implementation detail, which shouldn't make any difference to code referencing that member. This is known as the "uniform access principle." 73 | 74 | > #### Exercise: overriding 75 | > Try providing a concrete, default implementation for `Message.entity`, to see how it affects the subclasses. 76 | -------------------------------------------------------------------------------- /src/main/resources/markdown/object-oriented-programming/apply-unapply-and-case-classes.md: -------------------------------------------------------------------------------- 1 | # Apply, Unapply and Case Classes 2 | 3 | Dropping boilerplate like Kanye drops the mic. 4 | 5 | # The `apply` method 6 | 7 | So far, we have three different ways to construct values: 8 | 9 | // literals 10 | val n = 1 11 | val s = "hello" 12 | 13 | // constructors 14 | val nvi = new NotVeryInteresting 15 | 16 | // magic? 17 | val list = List(1, 2, 3) 18 | val map = Map(1 -> "one", 2 -> "two") 19 | 20 | The third category there just uses a method on the companion object named `apply`, which is treated as syntactically special by Scala: if the parser sees parameters being passed to an object, rather than a method, it translates that to a call to the object's `apply` method. This implies you can write your own: 21 | 22 | class Recipe(val ingredients: List[String], val directions: List[String]) 23 | object Recipe { 24 | def apply(ingredients: List[String], directions: List[String]): Recipe = 25 | new Recipe(ingredients, directions) 26 | } 27 | 28 | In cases like this, the `apply` method's special treatment provides little more than syntactic sugar, allowing you to leave off the `new` keyword when instantiating objects; not especially compelling. In cases like `List` or `Map` it's a bit more convenient, because you can't actually instantiate those with `new`; they aren't concrete classes. And spoiler alert: this is a big part of how Scala implements "first-class" functions. 29 | 30 | # The `unapply` method 31 | 32 | There's another bit of magic we haven't unraveled yet: 33 | 34 | def whatIs(a: Any): String = a match { 35 | case 1 => "the number one" // literal pattern 36 | case nvi: NotVeryInteresting => "nothing interesting" // typed variable pattern 37 | case List(x, y) => "a List containing %s and %s".format(x, y) // magic? 38 | } 39 | 40 | This works because of some additional plumbing called `unapply`. which is somewhat more mind-bending than `apply`, but very powerful. In extremely rough terms, the translation the compiler applies above (ignoring the first two cases) is: 41 | 42 | def whatIs(a: Any): String = { 43 | val resultOpt: Option[(Any, Any)] = List.unapply(a) 44 | if (resultOpt.isDefined) { 45 | val result = resultOpt.get 46 | val x = result._1 47 | val y = result._2 48 | "a List containing %s and %s".format(x, y) 49 | } else { 50 | throw new MatchError(a) 51 | } 52 | } 53 | 54 | > #### Exercise: lies! 55 | > The above code doesn't actually compile, because in the case of `List` and most other collections, the real translation involves a slightly different method called `unapplySeq`. Figure out how that works. 56 | 57 | This again implies you can write your own `unapply` (or `unapplySeq`) for your own classes: 58 | 59 | class Recipe(val ingredients: List[String], val directions: List[String]) 60 | object Recipe { 61 | def apply(ingredients: List[String], directions: List[String]): Recipe = 62 | new Recipe(ingredients, directions) 63 | 64 | def unapply(recipe: Recipe): Option[(List[String], List[String])] = 65 | if (recipe eq null) None 66 | else Some((recipe.ingredients, recipe.directions)) 67 | } 68 | 69 | This is a fairly conventional implementation of `unapply`. 70 | 71 | > #### Note: object equality 72 | > We've been using `==` so far to determine value equality, but in the example above we use `eq` instead. This is testing for _reference equality_. You can do this on any object extending `AnyRef`. Incidentally, note that `==` doesn't yet work the way we'd want it to for `Recipe`... we'll fix that shortly. 73 | 74 | Now that `Recipe` has a suitable `unapply` method, you can do things like: 75 | 76 | val pbj = new Recipe( 77 | ingredients = List("peanut butter", "jelly", "bread"), 78 | directions = List("put the peanut butter and jelly on the bread")) 79 | 80 | val baconPancakes = new Recipe( 81 | ingredients = List("bacon", "pancakes"), 82 | directions = List("take some bacon", "put it in a pancake")) 83 | 84 | def containsNuts(recipe: Recipe): Boolean = recipe match { 85 | case Recipe(ingredients, _) => ingredients exists { _ containsSlice("nut") } 86 | } 87 | 88 | def isSimple(recipe: Recipe): Boolean = recipe match { 89 | case Recipe(_, List(_)) => true 90 | case _ => false 91 | } 92 | 93 | > #### Boss Level Exercise: custom extractors 94 | > It's extremely common to see hairy `if-else` style code, where the predicates determining which branch should be chosen are inseparable from the logic that happens in each branch. Custom extractors help to tease that spaghetti code apart into smaller, composable bits. For a simple example, you saw [before](/data-and-control-flow/pattern-matching#exception-handling) how to safely parse an `Int` from a `String`... Capture that logic in a `ContainsInt` extractor, for great good. 95 | 96 | # Case classes 97 | 98 | In most cases, when you're defining data types to model a problem domain, you just want all this stuff to work. Your implementations for `apply` and `unapply`, not to mention many others like `equals`, `hashCode`, `toString` and `copy`, will be pretty mechanical boilerplate. Scala tends to be very good about not forcing you to do repetitive, mechanical work, and this is one of the best examples: 99 | 100 | case class Recipe(ingredients: List[String], directions: List[String]) 101 | 102 | > #### Exercise: play around 103 | > Create a few instances of this new case class and see how it behaves compared to the old implementations. 104 | 105 | When you create a case class, sane implementations of all of the methods above are synthesized for you by the compiler. The fact that it's so little effort to create, maintain and debug case classes should _strongly_ encourage you to create rich domain models using them. 106 | -------------------------------------------------------------------------------- /src/main/resources/markdown/object-oriented-programming/classes-and-objects.md: -------------------------------------------------------------------------------- 1 | # Classes and Objects 2 | 3 | Defining classes, declaring constructors and members, and singleton objects. 4 | 5 | # Declaring and instantiating a `class` 6 | 7 | Nothing special here: 8 | 9 | class NotVeryInteresting 10 | val nvi = new NotVeryInteresting 11 | 12 | We've defined our own data type, `NotVeryInteresting`, which is a subtype of `AnyRef`. It contains no data of its own, but we can instantiate it with `new`. Not very interesting, though... How about classes with some `var` members: 13 | 14 | class Recipe { 15 | var ingredients: List[String] = _ 16 | var directions: List[String] = _ 17 | } 18 | 19 | class Cookbook { 20 | var recipes: Map[String, Recipe] = _ 21 | } 22 | 23 | > #### Exercise: default field values 24 | > What are the default values of these fields? What if we had an `Int` field? What if they were `val`s (like they ought to be)? How would you improve this code? 25 | 26 | Slightly more interesting. Now given these definitions, we can construct instances: 27 | 28 | val recipe = new Recipe 29 | recipe.ingredients = List("peanut butter", "jelly", "bread") 30 | recipe.directions = List("put the peanut butter and jelly on the bread") 31 | 32 | val cookbook = new Cookbook 33 | cookbook.recipes = Map("peanut butter and jelly sandwich" -> recipe) 34 | 35 | # Primary and auxiliary constructors 36 | 37 | All classes have a _primary constructor_, which is a special method that's always invoked during object instantiation. The primary constructor---informally speaking---is just the body of the class, so in our example so far, the primary constructors initialize the declared fields `ingredients`, `directions` and `recipes`. 38 | 39 | > #### Exercise: logging 40 | > As with any other method `def`, you can put arbitrary code in primary constructors. Insert code into the `Recipe` and `Cookbook` constructors to log object instantiations. 41 | 42 | You can also create _auxiliary constructors_ for a class: 43 | 44 | class Recipe { 45 | var ingredients: List[String] = _ 46 | var directions: List[String] = _ 47 | 48 | def this(ingredients: List[String], directions: List[String]) = { 49 | this() 50 | this.ingredients = ingredients 51 | this.directions = directions 52 | } 53 | } 54 | 55 | class Cookbook { 56 | var recipes: Map[String, Recipe] = _ 57 | 58 | def this(recipes: Map[String, Recipe]) = { 59 | this() 60 | this.recipes = recipes 61 | } 62 | } 63 | 64 | This is starting to look somewhat Java-esque, but at least we can instantiate objects in a single statement now: 65 | 66 | val recipe = new Recipe( 67 | ingredients = List("peanut butter", "jelly", "bread"), 68 | directions = List("put the peanut butter and jelly on the bread")) 69 | val cookbook = new Cookbook( 70 | recipes = Map("peanut butter and jelly sandwich" -> recipe)) 71 | 72 | Trouble is, the `Recipe` and `Cookbook` classes are still mutable (which is generally terrible), and far more boilerplate-ish. What we really want is, the primary constructor should take parameters: 73 | 74 | class Recipe(ingredients: List[String], directions: List[String]) { 75 | val ingredients: List[String] = ingredients 76 | val directions: List[String] = directions 77 | } 78 | 79 | class Cookbook(recipes: Map[String, Recipe]) { 80 | val recipes: Map[String, Recipe] = recipes 81 | } 82 | 83 | > #### Exercise: OH NOES 84 | > That doesn't compile. Explain why. 85 | 86 | The right way to do this turns out to be much, much simpler: 87 | 88 | class Recipe(val ingredients: List[String], val directions: List[String]) 89 | class Cookbook(val recipes: Map[String, Recipe]) 90 | 91 | Just declare that the constructor parameters are `val`s! 92 | 93 | > #### Exercise: instance methods 94 | > Implement `def printThis(): Unit = ...` for `Recipe` and `Cookbook`. 95 | 96 | # Singleton objects 97 | 98 | Problem: you need a good way to declare constants and utility methods. 99 | 100 | In some older, "impure" object-oriented languages like C++, it's possible to declare variables and methods outside the scope of a class. This suggests a clean separation: it's unlikely you'd ever make a coding error because of confusion between a class instance member and a "global." The drawback here is that the language needs to provide some other mechanism for modularization of globals (e.g. `namespace`). 101 | 102 | In newer, "pure" object-oriented languages, such as Java and C#, _everything_ is a member of a class. So conversely, these languages need to provide some other mechanism (e.g. `static`) to distinguish instance members from non-instance members. In certain cases, it's easy to confuse these. 103 | 104 | Scala's approach is much more similar to Ruby's `module` concept: just like you can declare a `class`, which provides a template for instantiating an arbitrary number of objects, you can also declare an `object`, which is a singleton with its own distinct type. For example: 105 | 106 | object Recipes { 107 | val pbj = new Recipe( 108 | ingredients = List("peanut butter", "jelly", "bread"), 109 | directions = List("put the peanut butter and jelly on the bread")) 110 | val baconPancakes = new Recipe( 111 | ingredients = List("bacon", "pancakes"), 112 | directions = List("take some bacon", "put it in a pancake")) 113 | } 114 | 115 | It's not legal here to instantiate a `new Recipes`. 116 | 117 | > #### Exercise: lazy objects 118 | > Singleton `object`s are lazily initialized. How lazy? 119 | 120 | # Companion objects 121 | 122 | When a class and an object are defined with the same name, in the same source file, they're called "companions." Companion objects are tremendously useful, as demonstrated by their prevalence in the [Scala standard library](http://www.scala-lang.org/api). For example: 123 | 124 | class Recipe private (val ingredients: List[String], val directions: List[String]) 125 | object Recipe { 126 | val pbj = new Recipe( 127 | ingredients = List("peanut butter", "jelly", "bread"), 128 | directions = List("put the peanut butter and jelly on the bread")) 129 | val baconPancakes = new Recipe( 130 | ingredients = List("bacon", "pancakes"), 131 | directions = List("take some bacon", "put it in a pancake")) 132 | 133 | def make(ingredients: List[String], directions: List[String]): Option[Recipe] = 134 | if (ingredients.isEmpty || directions.isEmpty) 135 | None 136 | else 137 | Some(new Recipe(ingredients, directions)) 138 | } 139 | 140 | The `Recipe` class constructor here is `private`---other code can't directly instantiate a `new Recipe`---but because the `Recipe` object is a companion, it can (this is somewhat similar to `friend` in C++). 141 | 142 | > #### Note: access modifiers 143 | > This is the first time we've seen `private` (on a primary constructor, no less)! You can annotate fields and methods to be `private` as well. There's also `protected`, but no `public`: everything is publicly accessible by default. This is actually pretty reasonable, considering we generally prefer `val` and immutable data structures, unlike in most other object-oriented languages where `private` encapsulation is absolutely necessary to prevent unwanted mutation. 144 | -------------------------------------------------------------------------------- /src/main/resources/markdown/object-oriented-programming/path-dependent-types.md: -------------------------------------------------------------------------------- 1 | # Path-Dependent Types 2 | 3 | Est selfies ad hoodie deserunt artisan, mlkshk shabby chic keffiyeh. 4 | 5 | # Mumblecore gastropub ea Wes Anderson tattooed aliquip. 6 | 7 | McSweeney's do fugiat, Williamsburg post-ironic irure eu wayfarers ennui placeat leggings tumblr 90's. Pinterest gluten-free et fap, Portland Tonx locavore Echo Park. Polaroid craft beer consequat master cleanse wayfarers, commodo letterpress hella kale chips esse blue bottle do american apparel. Odd Future pork belly vero Cosby sweater. Direct trade irure tempor synth, post-ironic kale chips nisi ullamco. Placeat kogi organic culpa salvia ugh, bespoke iPhone. 8 | 9 | Forage shoreditch craft beer direct trade fashion axe. Aliqua selvage fingerstache, beard forage in cillum occaecat bespoke swag master cleanse. Synth Cosby sweater 3 wolf moon, post-ironic butcher leggings banh mi mixtape DIY excepteur incididunt. Disrupt Portland dolore, next level sartorial mixtape id Banksy polaroid semiotics aliquip sunt hoodie. Before they sold out meggings leggings, cornhole banjo officia plaid bespoke ennui keffiyeh wolf iPhone gentrify. Est Tonx voluptate cliche, whatever viral lomo. Lomo 90's cliche brunch pug banjo placeat 8-bit. 10 | -------------------------------------------------------------------------------- /src/main/resources/markdown/object-oriented-programming/traits.md: -------------------------------------------------------------------------------- 1 | # Traits 2 | 3 | Hey, calm down you two, new Shimmer is a floor wax _and_ a dessert topping! 4 | 5 | # Composing classes 6 | 7 | Let's say we want to define some simple "services" that accept requests and produce responses: 8 | 9 | import Message._ 10 | 11 | abstract class Service { 12 | def apply(request: Request): Response 13 | } 14 | 15 | class HelloService extends Service { 16 | def apply(request: Request): Response = 17 | Response(status = ResponseStatus.Ok, entity = "hello") 18 | } 19 | 20 | class EchoService extends Service { 21 | def apply(request: Request): Response = 22 | Response(status = ResponseStatus.Ok, entity = request.entity) 23 | } 24 | 25 | Reasonably clean design, so far: these services do exactly what their names suggest, and no more. But now say that we'd like to add some simple metrics functionality, without polluting the code as it stands. The traditional object-oriented design pattern for this is to create a [decorator](http://en.wikipedia.org/wiki/Decorator_pattern): 26 | 27 | final class Counter { 28 | private[this] var mutableCount: Int = 0 29 | def increment(): Unit = mutableCount += 1 30 | def value(): Int = mutableCount 31 | } 32 | 33 | class CountingFilter(service: Service) extends Service { 34 | private[this] val counter: Counter = new Counter 35 | 36 | def apply(request: Request): Response = { 37 | counter.increment() 38 | service(request) 39 | } 40 | 41 | def value(): Int = counter.value() 42 | } 43 | 44 | val countingHelloService = new CountingFilter(new HelloService) 45 | val countingEchoService = new CountingFilter(new EchoService) 46 | 47 | Not bad so far. Now let's say we want to add logging as well: 48 | 49 | final class Logger(owner: String) { 50 | def log(message: Any): Unit = 51 | println("%s <%s>: %s".format(owner, new java.util.Date, message)) 52 | } 53 | 54 | class LoggingFilter(service: Service) extends Service { 55 | private[this] val logger: Logger = 56 | new Logger(service.getClass.getName) 57 | 58 | def apply(request: Request): Response = { 59 | logger.log(request) 60 | val response = service(request) 61 | logger.log(response) 62 | response 63 | } 64 | } 65 | 66 | val loggingCountingHelloService = new LoggingFilter(countingHelloService) 67 | val loggingCountingEchoService = new LoggingFilter(countingEchoService) 68 | 69 | Now things are starting to get clumsy: we're composing these decorators in some arbitrary order, and only the outermost decorator's methods are exposed to us. In this example, if you want to get the current value of the `Counter` attached to `HelloService`, you'd better have held onto that `countingHelloService` reference before feeding it to the `LoggingFilter`: 70 | 71 | class CountingLoggingFilter(service: Service) extends Service { 72 | private[this] val countingService = new CountingFilter(service) 73 | private[this] val loggingService = new LoggingFilter(countingService) 74 | 75 | def apply(request: Request): Response = loggingService(request) 76 | def value(): Int = countingService.value() 77 | } 78 | 79 | The effort here is obviously not going to scale. The more classes we want to mix and match like this, the more of these forwarding shims we have to write. Despite trying to observe the object-oriented design principle of [preferring composition over inheritance](http://en.wikipedia.org/wiki/Composition_over_inheritance) we've hit a wall. Inheritance is looking justifiably appealing, not merely for the convenience of code reuse, but for the fact that the service we're building _is a_ counting, logging service. Alas, the JVM doesn't allow us to inherit from both `HelloService`, `CountingFilter` and `LoggingFilter`. 80 | 81 | # Composing `traits` 82 | 83 | Relief comes from the realization that the process of constructing these forwarding shims is completely mechanical, something the compiler ought to be good at. Starting with the `Counter` and `Logger` classes: 84 | 85 | trait Counter { 86 | private[this] var mutableCount: Int = 0 87 | protected[this] def increment(): Unit = mutableCount += 1 88 | def value(): Int = mutableCount 89 | } 90 | 91 | trait Logger { 92 | protected[this] def log(message: Any): Unit = 93 | println("%s <%s>: %s".format( 94 | getClass.getName, 95 | new java.util.Date, 96 | message)) 97 | } 98 | 99 | When we rework these to be `trait`s, it signals to the compiler that _at least two_ classes should be synthesized for each: one containing the concrete behavior, and another containing forwarding methods. 100 | 101 | > #### Note for Java refugees 102 | > If you're familiar with `interface`s in Java, those play a role here as well in the compiled bytecode. For a given trait, an interface is generated containing all of its methods. The class containing the forwarding methods implements that interface. If any of those methods are abstract in the Scala trait, they will be abstract in the resulting compiled class as well. 103 | 104 | class CountingFilter(service: Service) extends Service with Counter { 105 | def apply(request: Request): Response = { 106 | increment() 107 | service(request) 108 | } 109 | } 110 | 111 | class LoggingFilter(service: Service) extends Service with Logger { 112 | def apply(request: Request): Response = { 113 | log(request) 114 | val response = service(request) 115 | log(response) 116 | response 117 | } 118 | } 119 | 120 | > #### Exercise: conflicts? 121 | > What happens if you try to mix in multiple traits that conflict in some way? 122 | 123 | The `CountingFilter` now _is a_ `Counter`, and the `LoggingFilter` now _is a_ `Logger`. But we haven't solved our real problem yet, which is that the filters themselves still don't compose nicely; they need to be traits as well. 124 | 125 | # Mind == Blown: `abstract override` 126 | 127 | The crux here is that each filter is really meant to extend some underlying service---either `HelloService`, `EchoService` or something else---and if we were weaving a filter together with a service individually by hand, we'd have the filter override its superclass `apply` method to augment the service's behavior. However, assuming we have no prior knowledge of where each filter will be used, the only thing we know for sure about its superclass is that it's some abstract `Service`. 128 | 129 | trait CountingFilter extends Service with Counter { 130 | abstract override def apply(request: Request): Response = { 131 | increment() 132 | super.apply(request) 133 | } 134 | } 135 | 136 | trait LoggingFilter extends Service with Logger { 137 | abstract override def apply(request: Request): Response = { 138 | log(request) 139 | val response = super.apply(request) 140 | log(response) 141 | response 142 | } 143 | } 144 | 145 | > #### Spoiler Alert 146 | > This is one of the wackiest things (in a good way, mostly) in all of Scala. 147 | 148 | The `abstract override` combo indicates that you're overriding behavior in _some_ concrete superclass, but _you don't know_ what that superclass actually is yet. Putting it all together: 149 | 150 | val loggingCountingHelloService = 151 | new HelloService with CountingFilter with LoggingFilter 152 | val loggingCountingEchoService = 153 | new EchoService with CountingFilter with LoggingFilter 154 | 155 | # Linearization algorithm 156 | 157 | Lomo mlkshk Brooklyn esse swag Tonx 3 wolf moon. Narwhal tattooed Truffaut, occupy sriracha dolore beard tempor gastropub master cleanse fashion axe street art blue bottle. Delectus mumblecore VHS, culpa Tonx odio est Etsy Odd Future Vice lomo messenger bag nisi. American apparel roof party helvetica, et biodiesel disrupt pour-over. Delectus chillwave ut lo-fi try-hard single-origin coffee. Neutra wolf Godard ethical next level. Quis YOLO tattooed, skateboard salvia Bushwick cillum Portland occupy. 158 | 159 | Vegan hella incididunt deep v, esse nesciunt yr brunch. Art party mixtape bicycle rights dreamcatcher, readymade fugiat qui master cleanse intelligentsia gastropub non deep v lomo. Try-hard bespoke messenger bag, nesciunt in chillwave excepteur aliqua cray cred ethnic scenester 3 wolf moon tempor biodiesel. VHS aliquip authentic, letterpress disrupt meh Terry Richardson nulla before they sold out 3 wolf moon yr accusamus. Twee officia sartorial, 90's bicycle rights High Life kitsch cornhole squid hoodie Godard banjo YOLO. Banh mi aliqua VHS, disrupt ex bitters pitchfork farm-to-table authentic. Tousled Brooklyn Austin enim. 160 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/scaffold/Document.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold 2 | 3 | import twirl.api._ 4 | import org.pegdown._ 5 | import ast._ 6 | 7 | case class Document(title: HeaderNode, lead: Seq[Node], sections: Seq[(HeaderNode, Seq[Node])]) 8 | 9 | object Document { 10 | 11 | def renderHtml(nodes: Node*): Html = Html((new markdown.HtmlRenderer).toHtml(nodes)) 12 | def renderText(nodes: Node*): String = (new markdown.TextRenderer).toText(nodes) 13 | def renderHref(nodes: Node*): String = renderText(nodes: _*).replaceAll("\\W+", "-").toLowerCase 14 | 15 | def render(name: String): Option[Html] = for { 16 | text <- load(name) 17 | document <- parse(text) 18 | } yield html.markdown(document) 19 | 20 | private[this] val / = sys.props("file.separator") 21 | private def load(name: String): Option[Array[Char]] = 22 | getClass.getResourceAsStream(/ + "markdown" + / + name + ".md") match { 23 | case null => None 24 | case stream => Some(io.Source.fromInputStream(stream).toArray) 25 | } 26 | 27 | private def parse(text: Array[Char]): Option[Document] = { 28 | import collection.JavaConverters.asScalaBufferConverter 29 | import collection.mutable.Buffer 30 | 31 | val root = new PegDownProcessor(Extensions.ALL).parseMarkdown(text) 32 | val sections = Buffer.empty[(HeaderNode, Buffer[Node])] 33 | 34 | root.getChildren.asScala foreach { 35 | case node: HeaderNode if node.getLevel == 1 => 36 | sections += node -> Buffer.empty[Node] 37 | case node => 38 | sections.lastOption match { 39 | case Some((_, buf)) => buf += node 40 | case None => 41 | } 42 | } 43 | 44 | sections match { 45 | case (title, lead) +: tail => Some(Document(title, lead, tail)) 46 | case _ => None 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/scaffold/Flags.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold 2 | 3 | case class Flags(interface: String = "localhost", port: Int = 8080) 4 | 5 | object Flags { 6 | def apply(args: Seq[String]): Flags = { 7 | @annotation.tailrec 8 | def go(flags: Flags, args: Seq[String]): Flags = args match { 9 | case ("-p" | "--port") +: port +: tail => 10 | go(flags.copy(port = port.toInt), tail) 11 | case unknown +: tail => 12 | throw new IllegalArgumentException("Unknown flag: %s".format(unknown)) 13 | case _ => 14 | flags 15 | } 16 | go(Flags(), args) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/scaffold/Interpreter.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold 2 | 3 | import akka.actor.{ Actor, Props } 4 | 5 | class Interpreter extends Actor { 6 | import Interpreter._ 7 | 8 | import java.io.ByteArrayOutputStream 9 | import scala.tools.nsc._ 10 | import scala.tools.nsc.interpreter._ 11 | 12 | private[this] val interpreter = { 13 | val settings = new Settings 14 | settings.usejavacp.value = true 15 | new IMain(settings) 16 | } 17 | 18 | private[this] val completion = new JLineCompletion(interpreter) 19 | 20 | def receive = { 21 | case Complete(expression) => 22 | val result = completion.topLevelFor(Parsed.dotted(expression, expression.length) withVerbosity 4) 23 | sender ! Completions(result) 24 | case Interpret(expression) => 25 | val out = new ByteArrayOutputStream 26 | val result = Console.withOut(out) { interpreter.interpret(expression) } 27 | val response = result match { 28 | case Results.Success => Success(out.toString) 29 | case Results.Error | Results.Incomplete => Failure(out.toString) 30 | } 31 | sender ! response 32 | } 33 | } 34 | 35 | object Interpreter { 36 | 37 | val props = Props[Interpreter] 38 | 39 | // requests 40 | case class Interpret(expression: String) 41 | case class Complete(expression: String) 42 | 43 | // responses 44 | case class Success(output: String) 45 | case class Failure(output: String) 46 | case class Completions(results: Seq[String]) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/scaffold/InterpreterSupervisor.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold 2 | 3 | import akka.actor._ 4 | import collection.mutable.Map 5 | import concurrent.duration.FiniteDuration 6 | 7 | class InterpreterSupervisor(idleTimeout: FiniteDuration) extends Actor { 8 | import InterpreterSupervisor._ 9 | implicit def dispatcher = context.system.dispatcher 10 | 11 | private[this] class SupervisedInterpreter(val interpreter: ActorRef, var supervisor: Cancellable) 12 | private[this] val interpreters = Map.empty[String, SupervisedInterpreter] 13 | 14 | private[this] def supervisor(id: String): Cancellable = 15 | context.system.scheduler.scheduleOnce(idleTimeout, self, Destroy(id)) 16 | 17 | def receive = { 18 | case Create => 19 | val id = randomId() 20 | val interpreter = context.actorOf(Interpreter.props, s"interpreter-$id") 21 | interpreters += id -> new SupervisedInterpreter(interpreter, supervisor(id)) 22 | sender ! Created(id) 23 | 24 | case Destroy(id) => 25 | interpreters.remove(id) match { 26 | case Some(supervisedInterpreter) => 27 | supervisedInterpreter.interpreter ! PoisonPill 28 | supervisedInterpreter.supervisor.cancel() 29 | sender ! Destroyed 30 | case None => 31 | sender ! NoSuchInterpreter 32 | } 33 | 34 | case Request(id, request) => 35 | interpreters.get(id) match { 36 | case Some(supervisedInterpreter) => 37 | supervisedInterpreter.interpreter.forward(request) 38 | supervisedInterpreter.supervisor.cancel() 39 | supervisedInterpreter.supervisor = supervisor(id) 40 | case None => 41 | sender ! NoSuchInterpreter 42 | } 43 | } 44 | 45 | } 46 | 47 | object InterpreterSupervisor { 48 | def props(idleTimeout: FiniteDuration) = Props(classOf[InterpreterSupervisor], idleTimeout) 49 | 50 | def randomId(): String = util.Random.alphanumeric.take(32).mkString 51 | 52 | // requests 53 | case object Create 54 | case class Destroy(id: String) 55 | case class Request(id: String, request: Any) 56 | 57 | // responses 58 | case class Created(id: String) 59 | case object Destroyed 60 | case object NoSuchInterpreter 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/scaffold/Scaffold.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold 2 | 3 | import akka.actor._ 4 | import akka.event.Logging.InfoLevel 5 | import akka.io.IO 6 | import concurrent.duration._ 7 | import com.twitter.spray._ 8 | import spray.can.Http 9 | import spray.http._ 10 | import spray.http.StatusCodes._ 11 | import spray.httpx.SprayJsonSupport._ 12 | import spray.httpx.TwirlSupport._ 13 | import spray.json.DefaultJsonProtocol._ 14 | import spray.routing._ 15 | import spray.routing.directives._ 16 | import spray.util._ 17 | 18 | trait ScaffoldService extends HttpService with CachingDirectives { 19 | 20 | def interpreters: ActorRef 21 | private[this] def requestCache = routeCache() 22 | 23 | import Interpreter._ 24 | import InterpreterSupervisor._ 25 | 26 | def assetsRoute = 27 | pathPrefix("assets") { 28 | cache(requestCache) { 29 | getFromResourceDirectory("META-INF/resources/webjars") ~ 30 | getFromResourceDirectory("assets") 31 | } 32 | } 33 | 34 | def markdownRoute = 35 | get { 36 | cache(requestCache) { 37 | path(Slash) { 38 | complete { html.index() } 39 | } ~ 40 | path(Rest) { 41 | Document.render(_) match { 42 | case Some(html) => complete { html } 43 | case None => reject 44 | } 45 | } 46 | } 47 | } 48 | 49 | def interpreterRoute = 50 | path("interpreter") { 51 | post { 52 | Create -!> interpreters -!> { 53 | case Created(id) => created(s"/interpreter/$id") 54 | } 55 | } 56 | } ~ 57 | path("interpreter" / Segment) { id => 58 | (post & entity(as[String])) { expression => 59 | Request(id, Interpret(expression)) -!> interpreters -!> { 60 | case Success(message) => 61 | complete { message } 62 | case Failure(message) => 63 | respondWithStatus(BadRequest) { complete { message } } 64 | case NoSuchInterpreter => 65 | complete { NotFound } 66 | } 67 | } ~ 68 | delete { 69 | Destroy(id) -!> interpreters -!> { 70 | case Destroyed => 71 | complete { NoContent } 72 | case NoSuchInterpreter => 73 | complete { NotFound } 74 | } 75 | } 76 | } ~ 77 | path("interpreter" / Segment / "completions") { id => 78 | (post & entity(as[String])) { expression => 79 | Request(id, Complete(expression)) -!> interpreters -!> { 80 | case Completions(results) => 81 | complete { results } 82 | case NoSuchInterpreter => 83 | complete { NotFound } 84 | } 85 | } 86 | } 87 | 88 | def route = assetsRoute ~ markdownRoute ~ interpreterRoute 89 | } 90 | 91 | class Scaffold(val interpreters: ActorRef) extends Actor with ScaffoldService { 92 | override val actorRefFactory = context 93 | override def receive = runRoute(logRequestResponse("scaffold", InfoLevel) { route }) 94 | } 95 | 96 | object Scaffold extends App { 97 | implicit val system = ActorSystem("scaffold-system") 98 | 99 | def props(interpreters: ActorRef) = Props(classOf[Scaffold], interpreters) 100 | 101 | val interpreters = system.actorOf(InterpreterSupervisor.props(10 minutes), "interpreter-supervisor") 102 | val scaffold = system.actorOf(props(interpreters), "scaffold") 103 | val flags = Flags(args) 104 | 105 | { 106 | // pre-warm the interpreter 107 | val warm = system.actorOf(Interpreter.props) 108 | warm ! Interpreter.Interpret("1 + 1") 109 | warm ! PoisonPill 110 | } 111 | 112 | IO(Http) ! new Http.Bind( 113 | listener = scaffold, 114 | endpoint = new java.net.InetSocketAddress(flags.port), 115 | backlog = 100, 116 | options = Nil, 117 | settings = None 118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/scaffold/markdown/HtmlRenderer.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold.markdown 2 | 3 | import org.pegdown._ 4 | import ast._ 5 | 6 | class HtmlRenderer extends ToHtmlSerializer(new LinkRenderer) { 7 | def toHtml(nodes: Seq[Node]): String = { 8 | nodes foreach { _.accept(this) } 9 | printer.getString 10 | } 11 | 12 | override def visit(node: BlockQuoteNode): Unit = { 13 | printIndentedTag(node, "div", "class" -> "alert alert-block alert-info") 14 | } 15 | 16 | override def visit(node: VerbatimNode): Unit = { 17 | printTag(node, "textarea") 18 | } 19 | 20 | private def printIndentedTag(node: SuperNode, tag: String, attributes: (String, String)*) = { 21 | printer.println().print('<').print(tag) 22 | for ((name, value) <- attributes) 23 | printer.print(' ').print(name).print('=').print('"').print(value).print('"') 24 | printer.print('>').indent(+2) 25 | 26 | visitChildren(node) 27 | printer.indent(-2).println().print('<').print('/').print(tag).print('>') 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/scaffold/markdown/TextRenderer.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold.markdown 2 | 3 | import org.pegdown._ 4 | import ast._ 5 | 6 | class TextRenderer extends Visitor { 7 | private[this] val printer = new Printer 8 | 9 | def toText(nodes: Seq[Node]): String = { 10 | nodes foreach { _.accept(this) } 11 | printer.getString 12 | } 13 | 14 | def visit(node: AbbreviationNode): Unit = visitChildren(node) 15 | def visit(node: AutoLinkNode): Unit = visit(node: TextNode) 16 | def visit(node: BlockQuoteNode): Unit = visitChildren(node) 17 | def visit(node: BulletListNode): Unit = visitChildren(node) 18 | def visit(node: CodeNode): Unit = visit(node: TextNode) 19 | def visit(node: DefinitionListNode): Unit = visitChildren(node) 20 | def visit(node: DefinitionNode): Unit = visitChildren(node) 21 | def visit(node: DefinitionTermNode): Unit = visitChildren(node) 22 | def visit(node: ExpImageNode): Unit = visitChildren(node) 23 | def visit(node: ExpLinkNode): Unit = visitChildren(node) 24 | def visit(node: HeaderNode): Unit = visitChildren(node) 25 | def visit(node: HtmlBlockNode): Unit = visit(node: TextNode) 26 | def visit(node: InlineHtmlNode): Unit = visit(node: TextNode) 27 | def visit(node: ListItemNode): Unit = visitChildren(node) 28 | def visit(node: MailLinkNode): Unit = visit(node: TextNode) 29 | def visit(node: OrderedListNode): Unit = visitChildren(node) 30 | def visit(node: ParaNode): Unit = visitChildren(node) 31 | def visit(node: QuotedNode): Unit = visitChildren(node) 32 | def visit(node: ReferenceNode): Unit = visitChildren(node) 33 | def visit(node: RefImageNode): Unit = visitChildren(node) 34 | def visit(node: RefLinkNode): Unit = visitChildren(node) 35 | def visit(node: RootNode): Unit = visitChildren(node) 36 | def visit(node: SimpleNode): Unit = printer print { 37 | import SimpleNode.Type._ 38 | node.getType match { 39 | case Apostrophe => "'" 40 | case Ellipsis => "..." 41 | case Emdash => "---" 42 | case Endash => "--" 43 | case _ => "" 44 | } 45 | } 46 | def visit(node: SpecialTextNode): Unit = visit(node: TextNode) 47 | def visit(node: StrongEmphSuperNode): Unit = visitChildren(node) 48 | def visit(node: TableBodyNode): Unit = visitChildren(node) 49 | def visit(node: TableCaptionNode): Unit = visitChildren(node) 50 | def visit(node: TableCellNode): Unit = visitChildren(node) 51 | def visit(node: TableColumnNode): Unit = visitChildren(node) 52 | def visit(node: TableHeaderNode): Unit = visitChildren(node) 53 | def visit(node: TableNode): Unit = visitChildren(node) 54 | def visit(node: TableRowNode): Unit = visitChildren(node) 55 | def visit(node: VerbatimNode): Unit = visit(node: TextNode) 56 | def visit(node: WikiLinkNode): Unit = visit(node: TextNode) 57 | 58 | def visit(node: TextNode): Unit = printer print node.getText 59 | def visit(node: SuperNode): Unit = visitChildren(node) 60 | def visit(node: Node): Unit = {} 61 | 62 | private def visitChildren(node: SuperNode): Unit = { 63 | import collection.JavaConverters.asScalaBufferConverter 64 | node.getChildren.asScala foreach { _.accept(this) } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/spray/package.scala: -------------------------------------------------------------------------------- 1 | package com.twitter 2 | 3 | package object spray { 4 | 5 | import akka.actor.{ Actor, ActorRef, ActorRefFactory, Props } 6 | import _root_.spray.http.HttpHeaders.Location 7 | import _root_.spray.http.StatusCodes.Created 8 | import _root_.spray.routing.{ Directives, RequestContext, Route } 9 | import Directives._ 10 | 11 | class Continuation(run: PartialFunction[Any, Route], ctx: RequestContext) extends Actor { 12 | def receive = run andThen { 13 | case route => 14 | try route(ctx) 15 | catch { case util.control.NonFatal(e) => ctx.failWith(e) } 16 | finally context.stop(self) 17 | } 18 | } 19 | object Continuation { 20 | def props(run: PartialFunction[Any, Route], ctx: RequestContext): Props = 21 | Props(classOf[Continuation], run, ctx) 22 | } 23 | 24 | class AndThen(message: Any, ref: ActorRef, factory: ActorRefFactory) { 25 | def -!>(run: PartialFunction[Any, Route])(ctx: RequestContext): Unit = { 26 | val continuation = factory actorOf Continuation.props(run, ctx) 27 | ref.tell(message, continuation) 28 | } 29 | } 30 | 31 | implicit class MessageOps(message: Any)(implicit factory: ActorRefFactory) { 32 | def -!>(ref: ActorRef): AndThen = new AndThen(message, ref, factory) 33 | } 34 | 35 | def created(location: String): Route = 36 | respondWithSingletonHeader(Location(location)) { 37 | complete { Created } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/twirl/com/twitter/scaffold/index.scala.html: -------------------------------------------------------------------------------- 1 | @main(title = "Scala School 2") { 2 |
3 |
4 |

Scala School 2

5 |

Learn You a Scala for Great Good

6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 |

Lomo fashion axe authentic blog kitsch master cleanse letterpress, hella Pinterest YOLO cupidatat deep v readymade. Duis art party PBR, pop-up beard cornhole asymmetrical sustainable shoreditch placeat Odd Future delectus pitchfork. Excepteur deserunt sint, ex magna twee disrupt laborum qui quinoa shabby chic freegan bicycle rights organic. Et before they sold out cray fixie. Proident aliquip vero, salvia fashion axe blue bottle mollit. Mixtape Vice bicycle rights, single-origin coffee helvetica wayfarers ethnic tattooed raw denim reprehenderit actually 90's lomo blog. Non pork belly four loko minim.

14 | 15 |

Before they sold out laboris organic yr, nostrud Vice street art literally non raw denim messenger bag dolore Wes Anderson photo booth bespoke. Kale chips cray cred, lomo esse before they sold out reprehenderit master cleanse organic 8-bit flannel church-key. American apparel gentrify literally labore minim. Master cleanse exercitation cardigan, velit flannel Godard tote bag next level eu. Sapiente mlkshk direct trade skateboard, commodo chambray intelligentsia dolor jean shorts accusamus voluptate kale chips occupy enim. Kitsch Truffaut ennui, occaecat four loko 90's chambray photo booth voluptate letterpress butcher McSweeney's placeat Godard. Church-key thundercats street art in occaecat sunt.

16 | 17 |

Carles hoodie vero keytar, art party incididunt photo booth before they sold out intelligentsia minim try-hard veniam fap delectus. Cray trust fund excepteur, vegan twee beard locavore bitters gentrify duis cornhole Echo Park odio aute. Readymade viral aliqua, placeat ut organic banjo. Banh mi flexitarian selfies +1 seitan kitsch scenester readymade, roof party kale chips adipisicing viral. Craft beer Odd Future sunt, leggings actually magna culpa labore fixie sint typewriter whatever tofu kogi pariatur. McSweeney's selvage roof party selfies squid, stumptown adipisicing. Quis mollit Bushwick Cosby sweater, typewriter Brooklyn DIY ennui cillum stumptown pariatur selfies.

18 |
19 |
20 |
    21 |
  1. Data and Control Flow
  2. 22 |
  3. Object-Oriented Programming
  4. 23 |
  5. Introduction to Generics
  6. 24 |
  7. Functional Programming
  8. 25 |
  9. Design Patterns
  10. 26 |
  11. Monads
  12. 27 |
28 |
29 |
30 | 31 |
32 | } -------------------------------------------------------------------------------- /src/main/twirl/com/twitter/scaffold/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 79 | 80 | @content 81 | 82 |
83 |
84 |

Designed and built with all the love in the world by @@mergeconflict.

85 |
86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/twirl/com/twitter/scaffold/markdown.scala.html: -------------------------------------------------------------------------------- 1 | @(document: Document) 2 | 3 | @import collection.JavaConverters.asScalaBufferConverter 4 | @import org.pegdown.ast._ 5 | 6 | @main("Scala School 2 - " + renderText(document.title)) { 7 |
8 |
9 | @renderHtml(document.title) 10 | @renderHtml(document.lead: _*) 11 |
12 |
13 | 14 |
15 | 20 |
21 | 22 |
23 |
24 |
25 | @for((title, content) <- document.sections) { 26 |
27 | 28 | @renderHtml(content: _*) 29 |
30 | } 31 |
32 |
33 |
34 | } 35 | -------------------------------------------------------------------------------- /src/test/scala/com/twitter/scaffold/ContentSpec.scala: -------------------------------------------------------------------------------- 1 | package io.utils 2 | 3 | import java.io.File 4 | import org.scalatest.WordSpec 5 | import org.scalatest.matchers.MustMatchers 6 | import com.twitter.scaffold.Document 7 | 8 | // Testing documentation 9 | // http://doc.scalatest.org/1.9.1/org/scalatest/WordSpec.html 10 | // http://doc.scalatest.org/1.9.1/org/scalatest/matchers/MustMatchers.html 11 | 12 | class ContentSpec extends WordSpec with MustMatchers { 13 | 14 | private[this] val / = sys.props("file.separator") 15 | private[this] def markdownRoot() = new File(getClass().getResource(/ + "markdown").getPath) 16 | private[this] def allMarkdownFiles() = { 17 | def allFilesInDirectory(root: File, prefix: String): Seq[String] = root.listFiles() flatMap { 18 | case file if file.isDirectory => 19 | allFilesInDirectory(file, prefix + / + file.getName) 20 | case file if file.getName.endsWith(".md") => 21 | Seq(prefix + / + file.getName.stripSuffix(".md")) 22 | } 23 | allFilesInDirectory(markdownRoot, "") 24 | } 25 | 26 | "A resource directory" must { 27 | "not be empty" in { 28 | allMarkdownFiles() must not be 'empty 29 | } 30 | 31 | "have valid markdown resources" in { 32 | allMarkdownFiles() foreach { Document.render } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/com/twitter/scaffold/InterpreterSpec.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold 2 | 3 | import akka.actor.{ ActorRef, ActorSystem, PoisonPill, Props } 4 | import akka.pattern.ask 5 | import akka.testkit.{ ImplicitSender, TestActorRef, TestKit } 6 | import akka.util.Timeout 7 | import concurrent.Await 8 | import concurrent.duration._ 9 | import org.scalatest.{ BeforeAndAfterAll, BeforeAndAfterEach, WordSpec } 10 | import org.scalatest.matchers.MustMatchers 11 | import util.{Try, Success, Failure} 12 | 13 | class InterpreterSpec 14 | extends WordSpec 15 | with MustMatchers 16 | with BeforeAndAfterEach { 17 | 18 | implicit val timeout: Timeout = 10 seconds 19 | private[this] var testInterpreter:ActorRef = _ 20 | 21 | implicit val system = ActorSystem("IntepreterSpec") 22 | import system._ 23 | 24 | def interpretShouldSucceed(request: String, response: String) { 25 | val responseFuture = testInterpreter ? Interpreter.Interpret(request) 26 | Await.result(responseFuture, 10 seconds) match { 27 | case Interpreter.Success(resp) => resp must include (response) 28 | case _ => assert(false, "Interpreter did not return Success") 29 | } 30 | } 31 | 32 | def interpretShouldFail(request: String, response: String) = { 33 | val responseFuture = testInterpreter ? Interpreter.Interpret(request) 34 | Await.result(responseFuture, 10 seconds) match { 35 | case Interpreter.Failure(resp) => resp must include (response) 36 | case _ => assert(false, "Interpreter did not return Failure") 37 | } 38 | } 39 | 40 | override def beforeEach() { 41 | testInterpreter = TestActorRef( new Interpreter() ) 42 | } 43 | 44 | override def afterEach() { 45 | testInterpreter ! PoisonPill 46 | } 47 | 48 | "The Interpreter" should { 49 | "succeed" when { 50 | "adding two integers" in { 51 | interpretShouldSucceed("1 + 1", "Int = 2") 52 | } 53 | 54 | "add two strings" in { 55 | interpretShouldSucceed("\"foo\" + \"bar\"", "foobar") 56 | } 57 | 58 | "retrieving a previously set value" in { 59 | interpretShouldSucceed("val p = 7", "Int = 7") 60 | interpretShouldSucceed("p", "Int = 7") 61 | } 62 | 63 | "reassigning a variable" in { 64 | interpretShouldSucceed("var m = 8654", "Int = 8654") 65 | interpretShouldSucceed("m = 900", "Int = 900") 66 | } 67 | 68 | "importing a package" in { 69 | interpretShouldSucceed("import scala.util.matching.Regex", "import scala.util.matching.Regex") 70 | interpretShouldSucceed("val x = new Regex(\"happy\")", "scala.util.matching.Regex = happy") 71 | } 72 | 73 | "defining a class" in { 74 | interpretShouldSucceed("class Bar(val x: String) { def printTheThing { println(\"hdtweyhdfhdfg\") } }", "") 75 | interpretShouldSucceed("val z = new Bar(\"hello\")", "Bar = Bar@") 76 | interpretShouldSucceed("z.x", "String = hello") 77 | interpretShouldSucceed("z.printTheThing", "hdtweyhdfhdfg") 78 | } 79 | 80 | "defining a case class" in { 81 | interpretShouldSucceed("case class Foo(bar: Int)", "") 82 | interpretShouldSucceed("val q = new Foo(12)", "Foo(12)") 83 | } 84 | } 85 | 86 | "report an error" when { 87 | "assigning a String to an Int" in { 88 | interpretShouldFail("val x: Int = \"booo\"", "required: Int") 89 | } 90 | 91 | "reassigning a value" in { 92 | interpretShouldSucceed("val d: Int = 5", "Int = 5") 93 | interpretShouldFail("d = 9", "error: reassignment to val") 94 | } 95 | 96 | "reassigning an Int to a String" in { 97 | interpretShouldSucceed("var x = \"hey\"", "String = hey") 98 | interpretShouldFail("x = 5", "required: String") 99 | } 100 | 101 | "an exception is thrown" in { 102 | interpretShouldFail("throw new Exception", "java.lang.Exception") 103 | } 104 | 105 | "interpreting a System.exit()" ignore { 106 | interpretShouldFail("System.exit()", "java.lang.Exception") 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/scala/com/twitter/scaffold/ScaffoldSpec.scala: -------------------------------------------------------------------------------- 1 | package com.twitter.scaffold 2 | 3 | import concurrent.duration._ 4 | import org.scalatest.OptionValues._ 5 | import org.scalatest.WordSpec 6 | import org.scalatest.matchers.MustMatchers 7 | import spray.http.StatusCodes._ 8 | import spray.testkit.ScalatestRouteTest 9 | 10 | class ScaffoldSpec extends WordSpec with MustMatchers with ScalatestRouteTest with ScaffoldService { 11 | override def actorRefFactory = system 12 | override lazy val interpreters = system.actorOf(InterpreterSupervisor.props(10 minutes)) 13 | implicit val routeTestTimeout = RouteTestTimeout(10 seconds) 14 | 15 | "The assets route" should { 16 | "render a lesson" is (pending) 17 | } 18 | 19 | "The markdown route" should { 20 | "respond to GET /" in { 21 | Get("/") ~> sealRoute(markdownRoute) ~> check { 22 | status must be (OK) 23 | } 24 | } 25 | } 26 | 27 | "The interpreter" should { 28 | "respond Not Found for non-existent interpreter ids" when { 29 | "interpreting" in { 30 | Post("/interpreter/0", "1 + 1") ~> sealRoute(interpreterRoute) ~> check { 31 | status must be (NotFound) 32 | } 33 | } 34 | 35 | "deleting" in { 36 | Delete("/interpreter/0") ~> sealRoute(interpreterRoute) ~> check { 37 | status must be (NotFound) 38 | } 39 | } 40 | 41 | "autocompleting" in { 42 | Post("/interpreter/0/completions", "") ~> sealRoute(interpreterRoute) ~> check { 43 | status must be (NotFound) 44 | } 45 | } 46 | } 47 | 48 | "be able to create a new console" in { 49 | Post("/interpreter","") ~> sealRoute(interpreterRoute) ~> check { 50 | status must be (Created) 51 | header("Location") must be ('defined) 52 | } 53 | } 54 | 55 | "not share namespace with other valid consoles" in { 56 | Post("/interpreter","") ~> sealRoute(interpreterRoute) ~> check { 57 | val newInterpreterLocation = header("Location").value.value 58 | Post(newInterpreterLocation, "def myThingA = 1 + 1") ~> route ~> check { 59 | status must be (OK) 60 | 61 | Post("/interpreter","") ~> sealRoute(interpreterRoute) ~> check { 62 | val secondInterpreterLocation = header("Location").value.value 63 | Post(secondInterpreterLocation, "myThingA") ~> route ~> check { 64 | status must be (BadRequest) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | "not interpret code after being deleted" in { 72 | Post("/interpreter","") ~> sealRoute(interpreterRoute) ~> check { 73 | val newInterpreterLocation = header("Location").value.value 74 | Delete(newInterpreterLocation) ~> route ~> check { 75 | status must be (NoContent) 76 | 77 | Post(newInterpreterLocation, "1 + 1") ~> route ~> check { 78 | status must be (NotFound) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | --------------------------------------------------------------------------------