├── src ├── meta │ ├── epub.yaml │ ├── html.yaml │ ├── pdf.yaml │ └── metadata.yaml ├── css │ └── book.less ├── pages │ ├── links.md │ ├── solutions.md │ ├── foundation │ │ ├── target3.png │ │ ├── index.md │ │ ├── setup.md │ │ ├── properties.md │ │ ├── atoms.md │ │ ├── background.md │ │ └── implementation.md │ ├── random │ │ ├── distributions.pdf │ │ ├── brownian-motion.pdf │ │ ├── sierpinski-confection.pdf │ │ ├── index.md │ │ ├── implementation.md │ │ ├── random.md │ │ ├── examples.md │ │ └── sierpinski-confection.svg │ ├── animation │ │ ├── current-position.png │ │ ├── current-position.pxm │ │ ├── index.md │ │ ├── easing.scala │ │ ├── example.scala │ │ ├── interface.md │ │ ├── EventStream.scala │ │ ├── example.md │ │ ├── easing.md │ │ ├── notes.md │ │ ├── background.md │ │ └── implementation.md │ ├── typeclasses │ │ ├── index.md │ │ ├── ux.md │ │ ├── typeclasses.scala │ │ └── typeclasses.md │ └── index.md ├── raw │ └── random.tar.gz └── covers │ ├── epub-cover.ai │ ├── epub-cover.png │ ├── gumroad-cover.ai │ └── gumroad-cover.png ├── .sbtopts ├── go.sh ├── project └── plugins.sbt ├── docker-compose.yml ├── Gruntfile.coffee ├── .gitignore ├── package.json └── README.md /src/meta/epub.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ... -------------------------------------------------------------------------------- /src/meta/html.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ... -------------------------------------------------------------------------------- /.sbtopts: -------------------------------------------------------------------------------- 1 | -Dsbt.ivy.home=/source/.ivy2/ 2 | -------------------------------------------------------------------------------- /src/css/book.less: -------------------------------------------------------------------------------- 1 | @book-color: #e8515b; 2 | -------------------------------------------------------------------------------- /go.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker-compose run book bash 3 | -------------------------------------------------------------------------------- /src/pages/links.md: -------------------------------------------------------------------------------- 1 | [doodle]: https://github.com/underscoreio/doodle 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.3") 2 | -------------------------------------------------------------------------------- /src/meta/pdf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | papersize: "a4paper" 3 | geometry: "margin=.75in" 4 | ... -------------------------------------------------------------------------------- /src/pages/solutions.md: -------------------------------------------------------------------------------- 1 | \appendix 2 | 3 | # Solutions to Exercises {#solutions} 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/raw/random.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/raw/random.tar.gz -------------------------------------------------------------------------------- /src/covers/epub-cover.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/covers/epub-cover.ai -------------------------------------------------------------------------------- /src/covers/epub-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/covers/epub-cover.png -------------------------------------------------------------------------------- /src/covers/gumroad-cover.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/covers/gumroad-cover.ai -------------------------------------------------------------------------------- /src/covers/gumroad-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/covers/gumroad-cover.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | book: 4 | image: underscoreio/book:latest 5 | volumes: 6 | - .:/source 7 | -------------------------------------------------------------------------------- /src/pages/foundation/target3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/pages/foundation/target3.png -------------------------------------------------------------------------------- /src/pages/random/distributions.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/pages/random/distributions.pdf -------------------------------------------------------------------------------- /src/pages/random/brownian-motion.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/pages/random/brownian-motion.pdf -------------------------------------------------------------------------------- /src/pages/animation/current-position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/pages/animation/current-position.png -------------------------------------------------------------------------------- /src/pages/animation/current-position.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/pages/animation/current-position.pxm -------------------------------------------------------------------------------- /src/pages/random/sierpinski-confection.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/essential-scala-doodle-case-study/HEAD/src/pages/random/sierpinski-confection.pdf -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | #global module:false 2 | 3 | "use strict" 4 | 5 | ebook = require 'underscore-ebook-template' 6 | 7 | module.exports = (grunt) -> 8 | ebook(grunt, { 9 | dir: { 10 | page : "target/pages" 11 | } 12 | }) 13 | return -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | -------------------------------------------------------------------------------- /src/pages/foundation/index.md: -------------------------------------------------------------------------------- 1 | # Foundation 2 | 3 | In this section we implement the foundation for the rest of the library: the basic objects and methods for creating and drawing pictures. 4 | 5 | Before we get into this, we'll look at the code we'll be using and go through some background on the problem we're trying to solve. 6 | -------------------------------------------------------------------------------- /src/pages/typeclasses/index.md: -------------------------------------------------------------------------------- 1 | # Type Classes and Implicits 2 | 3 | In this section we'll explore uses of implicits. Most of our time will be spend implementing type classes, but we'll also look at how we can use implicits to provide a simpler user interface. 4 | 5 | You should base your work off the `feature/event` branch, or your equivalent if you have implemented the previous sections. 6 | -------------------------------------------------------------------------------- /src/pages/animation/index.md: -------------------------------------------------------------------------------- 1 | # Animation 2 | 3 | We are now going to add animations to our graphics library. To do this we will implement a library for processing streams of events. These systems are useful in any domain that deals with streams of data, such as finance and web analytics. 4 | 5 | We'll start, as we did in the previous chapter, with some background to the problem and play around with a better solution before we come to implement it ourselves. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "essesntial-scala-doodle-case-study", 3 | "description": "Extended Case Study for Essential Scala", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "bootstrap": "3.3.1", 7 | "coffeeify": "1.0.0", 8 | "jquery": "2.1.1", 9 | "uglifyify": "2.6.0", 10 | "underscore": "1.7.0", 11 | "underscore-ebook-template": "git+https://github.com/underscoreio/underscore-ebook-template.git#0.3.0" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/animation/easing.scala: -------------------------------------------------------------------------------- 1 | final case class EaseIn(get: Double => Double) extends AnyVal { 2 | def apply(t: Double) = get(t) 3 | /** Convert this `EaseIn` to an `EaseOut`. This implementation avoids accumulating error. */ 4 | def easeOut: EaseOut = EaseOut(get) 5 | } 6 | final case class EaseOut(get: Double => Double) extends AnyVal { 7 | def apply(t: Double) = 1.0 - get(1.0 - t) 8 | def easeIn: EaseIn = EaseIn(get) 9 | } 10 | 11 | object EaseIn { 12 | import scala.math._ 13 | 14 | val linear = EaseIn(t => t) 15 | val quadratic = EaseIn(t => t * t) 16 | val elastic = EaseIn(t => pow(2, -10 * t) * sin( (t - 0.3/4) * 2 * Pi / 0.3) + 1.0) 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/animation/example.scala: -------------------------------------------------------------------------------- 1 | // Framework to run the code 2 | final case class Vec(x: Double, y: Double) { 3 | def +(that: Vec): Vec = 4 | Vec(this.x + that.x, this.y + that.y) 5 | } 6 | 7 | 8 | // User input is a Key 9 | sealed trait Key 10 | final case object Up extends Key 11 | final case object Down extends Key 12 | final case object Left extends Key 13 | final case object Right extends Key 14 | 15 | // Velocity is represented as a two dimensional Vector 16 | def currentVelocity(previousVelocity: Vec, input: Key): Vec = 17 | input match { 18 | case Up => previousVelocity + Vec(0, 1) 19 | case Down => previousVelocity + Vec(0, -1) 20 | case Left => previousVelocity + Vec(-1, 0) 21 | case Right => previousVelocity + Vec(1, 0) 22 | } 23 | 24 | // Location is represented as a two dimensional Vector, by abuse of notation 25 | def currentLocation(previousLocation: Vec, velocity: Vec): Vec = 26 | previousLocation + velocity 27 | 28 | val input = List(Up, Up, Down, Down, Left, Right, Left, Right) 29 | 30 | val images: List[Vec] = 31 | input.scanLeft(Vec(0, 0)){ currentVelocity }. 32 | scanLeft(Vec(0, 0)){ currentLocation } 33 | -------------------------------------------------------------------------------- /src/meta/metadata.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Used by Pandoc to print covers: 3 | title: "Essential Scala: Doodle Case Study" 4 | author: "Noel Welsh and Dave Gurnell" 5 | date: "Pre-release Version, September 2016" 6 | copyright: "2015-2016" 7 | # Used by Pandoc to generate the PDF cover... 8 | # see src/css/book.css for HTML/ePub covers: 9 | coverColor: "E8515B" 10 | # Used by Grunt to name output files: 11 | filenameStem: "essential-scala-doodle-case-study" 12 | # Used by Grunt to create a ZIP of source code: 13 | exercisesRepo: null # "git@github.com:underscoreio/essential-scala-code.git" 14 | # Used by Grunt to assemble pandoc command line: 15 | pages: 16 | - index.md 17 | 18 | - foundation/index.md 19 | - foundation/setup.md 20 | - foundation/background.md 21 | - foundation/properties.md 22 | - foundation/atoms.md 23 | - foundation/implementation.md 24 | 25 | - random/index.md 26 | - random/random.md 27 | - random/implementation.md 28 | - random/examples.md 29 | 30 | - animation/index.md 31 | - animation/background.md 32 | - animation/example.md 33 | - animation/interface.md 34 | - animation/implementation.md 35 | - animation/easing.md 36 | 37 | - typeclasses/index.md 38 | - typeclasses/ux.md 39 | - typeclasses/typeclasses.md 40 | 41 | - links.md 42 | - solutions.md 43 | ... 44 | -------------------------------------------------------------------------------- /src/pages/typeclasses/ux.md: -------------------------------------------------------------------------------- 1 | ## Improving User Experience with Implicits 2 | 3 | In this section we'll look at a few improvements we can make to the `Image` API using implicits. 4 | 5 | ### Implicit Parameters 6 | 7 | It's a bit inconvenient to always explicitly pass a `Canvas` to the `draw` method on `Image`. Let's add some implicit magic to make the `Canvas` optional. Make the `Canvas` argument of `draw` an implicit parameter, and makes corresponding changes to the canvas implementations to make implicit values available. Hint: look in the objects `Java2DCanvas` and `HtmlCanvas`. 8 | 9 | 10 | ### Syntax 11 | 12 | The interface for animating an `EventStream[Image]` is a bit clunky. We didn't add an `animate` method to `EventStream` because event streams work with generic types, though it feels like this is where such a method should live. We ended up creating a method `animate` on an object, passing it both an `EventStream[Image]` and a `Canvas`. 13 | 14 | We can add `animate` as a method to only `EventStream[Image]` via the magic of implicit classes. In the package `doodle.syntax` (directory is `shared/src/main/scala/doodle/syntax`) add an implicit class called `EventStreamImageSyntax` following the conventions already in use in that package. Wire in your syntax to the package object in `package.scala` following the existing conventions. 15 | 16 | When you `import doodle.syntax.eventStreamImage._` you should now have an `animate` method available on any object of type `EventStream[Image]`. This method should accept a `Canvas` as an implicit parameter, just as we have done with `draw`. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Essential Scala Doodle Case Study 2 | 3 | Written by [Dave Gurnell](http://twitter.com/davegurnell) and 4 | [Noel Welsh](http://twitter.com/noelwelsh). 5 | Copyright [Underscore Consulting LLP](http://underscore.io), 2015-2017. 6 | 7 | Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. 8 | 9 | ## Overview 10 | 11 | This extended case study is a supplement to [Essential Scala][essential-scala] which shows how the patterns in Essential Scala can be used in a larger context. 12 | 13 | 14 | ## Building 15 | 16 | Essential Scala uses [Underscore's ebook build system][ebook-template]. 17 | 18 | The simplest way to build the book is to use [Docker Compose](http://docker.com): 19 | 20 | - install Docker Compose (`brew install docker-compose` on OS X; or download from [docker.com](http://docker.com/)); and 21 | - run `go.sh` (or `docker-compose run book bash` if `go.sh` doesn't work). 22 | 23 | This will open a `bash` shell running inside the Docker container which contains all the dependencies to build the book. From the shell run: 24 | 25 | - `npm install`; and then 26 | - `sbt`. 27 | 28 | Within `sbt` you can issue the commands `pdf`, `html`, `epub`, or `all` to build the desired version(s) of the book. Targets are placed in the `dist` directory: 29 | 30 | [ebook-template]: https://github.com/underscoreio/underscore-ebook-template 31 | [essential-scala]: http://underscore.io/books/essential-scala/ 32 | -------------------------------------------------------------------------------- /src/pages/animation/interface.md: -------------------------------------------------------------------------------- 1 | ## Interface 2 | 3 | We can start sketching an interface. Let's call our type `Stream[A]`, where the type variable indicates the type of elements that the event stream produces. 4 | 5 | Write down an interface that captures the examples we've seen so far. Are there any other methods you think we should include? 6 | 7 |
8 | This is a sufficient interface: 9 | 10 | ```scala 11 | sealed trait Stream[A] { 12 | def map[B](f: A => B): Stream[B] 13 | 14 | def scanLeft[B](seed: B)(f: (B,A) => B): Stream[B] 15 | 16 | def join[B](that: Stream[B]): EventStream[(A,B)] 17 | 18 | def runFold[B](zero: B)(f: (B, A) => B): B 19 | } 20 | ``` 21 | 22 | You might be tempted to add `flatMap`. 23 | 24 | ```scala 25 | def flatMap[B](f: A => Stream[B]): EventStream[B] 26 | ``` 27 | 28 | 29 | Let's think for a minute about what this means. The function `f` uses the input `A` to choose an `Stream[B]` to handle further processing, thereby possibly changing the downstream event processing network on every input. There are event stream systems that allow this but it is tricky to reason about. For these reasons we've chosen to not implement `flatMap` but we encourage you to implement it if you want an additional challenge. 30 | 31 | By the way, `scanLeft` is sometimes called `foldP`, meaning "fold over the past". This is the name you'll find in the "functional reactive programming" literature. 32 |
33 | 34 | The `Stream` interface is simpler than the `Image` interface, though we have new tools (type variables, functions) that we're using. The implementation, however, is more complex and there is much more variation in possible implementations. In the next section we'll look at one possible implementation, but we encourage you to explore your own ideas. 35 | -------------------------------------------------------------------------------- /src/pages/random/index.md: -------------------------------------------------------------------------------- 1 | # Random Art 2 | 3 | In this section we'll develop a library for generating random data. Why random data? In our context, it is useful for generating art, as depicted in [@fig:random:brownian-motion]. There are other uses, including 4 | 5 | - generating test data for *property based testing*; and 6 | - performing probabilistic inference on data, a subfield of statistics and machine learning. 7 | 8 | 9 | ![Brownian motion, studies of which led to Jean Perrin receiving the Nobel Prize in 1926, rendered for artistic effect.](src/pages/random/brownian-motion.pdf+svg){#fig:random:brownian-motion} 10 | 11 | 12 | ## Why Not `scala.util.Random`? 13 | 14 | The most straightforward way to generate random values in Scala is to use `scala.util.Random`. Here are some exampes 15 | 16 | ```tut:book 17 | val rng = scala.util.Random 18 | rng.nextDouble() 19 | rng.nextDouble() 20 | ``` 21 | 22 | Why would we not use to generate random data? What principles does it break? 23 | 24 |
25 | The methods on `scala.util.Random` return different values each time we call them. This means we cannot use substitution to reason about them. Concretely. 26 | 27 | ```tut:book 28 | val answer = scala.util.Random.nextDouble() 29 | answer + answer 30 | ``` 31 | 32 | is not the same as 33 | 34 | ```tut:book 35 | scala.util.Random.nextDouble() + scala.util.Random.nextDouble() 36 | ``` 37 | 38 | Once we lose substitution we lose other desirable properties like composition. 39 |
40 | 41 | ## A Plan 42 | 43 | Our solution to this problem is to do exactly what we did with `Image`: separate describing and running the computation. For `Image` this means we separate constructing the image (with `above`, `beside`, and so on) and drawing it on the screen. For our random data problem we will separate describing how to construct the random data, and actually randomly generating it. 44 | -------------------------------------------------------------------------------- /src/pages/foundation/setup.md: -------------------------------------------------------------------------------- 1 | ## Getting Setup 2 | 3 | The code we will build on can be found on [Github](https://github.com/underscoreio/doodle-case-study). Fork this repository so you have your own copy to make changes in. (If you're not experienced with Git and Github, it's a good idea to go through Github's documentation to get a better understanding of how it all works.) 4 | 5 | Now run `sbt`, Scala's standard build tool. If you don't have it installed already, you can run `sbt.sh` (Mac or Linux) or `sbt.bat` (Windows) as appropriate. You should see output like the below and arrive at a Scala prompt. 6 | 7 | ```bash 8 | Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=512m; support was removed in 8.0 9 | 10 | [info] Loading global plugins from /Users/noel/.sbt/0.13/plugins 11 | [info] Loading project definition from /Users/noel/dev/essential-scala/case-study-code/project 12 | [info] Set current project to doodle-case-study (in build file:/Users/noel/dev/essential-scala/case-study-code/) 13 | > 14 | ``` 15 | 16 | From this prompt we can give SBT commands. A good start is to enter `compile`, which compiles all the code we've already provided. This should do some work and finish without issue. 17 | 18 | A more interesting command is `run`. Try this now. A window with an interesting picture should pop up. Close the window and you should return to the SBT prompt. 19 | 20 | Our final exploration of SBT is to run the `console` command. This gets us to a Scala console where we can run Scala code directly. We can try exciting programs like `1 + 1`, but more interesting is to try 21 | 22 | ```scala 23 | val canvas = Java2DCanvas.canvas 24 | Spiral.draw(canvas) 25 | ``` 26 | 27 | This should pop up a window containing the same picture we saw before. 28 | 29 | We've done enough exploration of the code for now. Let's head back to the problem we're trying to solve. We'll be back in the code soon enough. 30 | -------------------------------------------------------------------------------- /src/pages/animation/EventStream.scala: -------------------------------------------------------------------------------- 1 | package doodle.event 2 | 3 | sealed trait Observer[A] { 4 | def observe(in: A): Unit 5 | } 6 | sealed trait EventStream[A] { 7 | def map[B](f: A => B): EventStream[B] 8 | def scanLeft[B](seed: B)(f: (B,A) => B): EventStream[B] 9 | } 10 | object EventStream { 11 | def fromCallbackHandler[A](handler: (A => Unit) => Unit) = { 12 | val stream = new Map[A,A](identity _) 13 | handler((evt: A) => stream.observe(evt)) 14 | stream 15 | } 16 | } 17 | private[event] sealed trait Node[A,B] extends Observer[A] with EventStream[B] { 18 | import scala.collection.mutable 19 | 20 | val observers: mutable.ListBuffer[Observer[B]] = 21 | new mutable.ListBuffer() 22 | 23 | def map[C](f: B => C): EventStream[C] = { 24 | val node = Map(f) 25 | observers += node 26 | node 27 | } 28 | 29 | def scanLeft[C](seed: C)(f: (C,B) => C): EventStream[C] = { 30 | val node = ScanLeft(seed, f) 31 | observers += node 32 | node 33 | } 34 | 35 | def join[C](that: EventStream[C]): EventStream[(B,C)] = { 36 | val node = Join[B,C]() 37 | this.map(b => node.updateLeft(b)) 38 | that.map(c => node.updateRight(c)) 39 | node 40 | } 41 | } 42 | final case class Map[A,B](f: A => B) extends Node[A,B] { 43 | def observe(in: A): Unit = { 44 | val output = f(in) 45 | observers.foreach(o => o.observe(output)) 46 | } 47 | } 48 | final case class ScanLeft[A,B](var seed: B, f: (B,A) => B) extends Node[A,B] { 49 | def observe(in: A): Unit = { 50 | val output = f(seed, in) 51 | seed = output 52 | observers.foreach(o => o.observe(output)) 53 | } 54 | } 55 | final case class Join[A,B]() extends Node[(A,B),(A,B)] { 56 | val state: MutablePair[Option[A],Option[B]] = new MutablePair(None, None) 57 | 58 | def observe(in: (A,B)): Unit = { 59 | observers.foreach(o => o.observe(in)) 60 | } 61 | 62 | def updateLeft(in: A) = { 63 | state.l = Some(in) 64 | state.r.foreach { r => this.observe( (in,r) ) } 65 | } 66 | 67 | def updateRight(in: B) = { 68 | state.r = Some(in) 69 | state.l.foreach { l => this.observe( (l,in) ) } 70 | } 71 | } 72 | 73 | private [event] class MutablePair[A,B](var l: A, var r: B) 74 | -------------------------------------------------------------------------------- /src/pages/typeclasses/typeclasses.scala: -------------------------------------------------------------------------------- 1 | import scala.language.higherKinds 2 | 3 | trait Functor[F[_]] { 4 | def map[A, B](fa: F[A])(f: A => B): F[B] 5 | } 6 | 7 | trait Monad[F[_]] extends Functor[F] { 8 | def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] 9 | def point[A](a: A): F[A] 10 | } 11 | 12 | trait Applicative[F[_]] extends Functor[F] { 13 | def zip[A, B](fa: F[A])(fb: F[B]): F[(A, B)] 14 | def point[A](a: A): F[A] 15 | } 16 | 17 | trait Scanable[F[_]] { 18 | def scanLeft[A,B](fa: F[A])(b: B)(f: (B,A) => B): F[B] 19 | } 20 | 21 | object ListInstances { 22 | implicit object list extends Functor[List] with Monad[List] with Applicative[List] with Scanable[List] { 23 | def map[A, B](fa: List[A])(f: A => B): List[B] = 24 | fa.map(f) 25 | def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = 26 | fa.flatMap(f) 27 | def point[A](a: A): List[A] = 28 | List(a) 29 | def zip[A, B](fa: List[A])(fb: List[B]): List[(A, B)] = 30 | fa.zip(fb) 31 | def scanLeft[A,B](fa: List[A])(b: B)(f: (B,A) => B): List[B] = 32 | fa.scanLeft(b)(f) 33 | } 34 | } 35 | 36 | object EventStreamInstances { 37 | implicit object eventStream extends Functor[EventStream] with Monad[EventStream] with Applicative[EventStream]with Scanable[EventStream] { 38 | def map[A, B](fa: EventStream[A])(f: A => B): EventStream[B] = 39 | fa.map(f) 40 | def point[A](a: A): EventStream[A] = 41 | EventStream.now(a) 42 | def zip[A, B](fa: EventStream[A])(fb: EventStream[B]): EventStream[(A, B)] = 43 | fa.zip(fb) 44 | def scanLeft[A,B](fa: EventStream[A])(b: B)(f: (B,A) => B): EventStream[B] = 45 | fa.scanLeft(b)(f) 46 | } 47 | } 48 | 49 | object IdInstances { 50 | type Id[A] = A 51 | 52 | implicit object list extends Functor[Id] with Monad[Id] with Applicative[Id] with Scanable[Id] { 53 | def map[A, B](fa: Id[A])(f: A => B): Id[B] = 54 | f(fa) 55 | def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] = 56 | f(fa) 57 | def point[A](a: A): Id[A] = 58 | a 59 | def zip[A, B](fa: Id[A])(fb: Id[B]): Id[(A, B)] = 60 | (fa, fb) 61 | def scanLeft[A,B](fa: Id[A])(b: B)(f: (B,A) => B): Id[B] = 62 | f(b,fa) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/pages/foundation/properties.md: -------------------------------------------------------------------------------- 1 | ## Properties of a Better System 2 | 3 | Now we know what is wrong with the imperative approach, let's think about what kind of properties should hold in a better system. The focus here is not on the kinds of pictures we can draw---if we can use gradient fills, for example---but on the process of constructing pictures. What should that be like? For example, if your combine two pictures in Doodle using, say, `image1 above image2`, you have a new picture that you can combine with other picture ad infinitum. 4 | 5 | Can you come up with a short description or a few terms that describe desirable properties and what they mean in the context of a drawing library? Take some time to think about this before reading our answer. 6 | 7 |
8 | A *compostional* library allows us to build larger pictures from smaller ones by composing them together. How could we compose images? We have already talked about layout as one possibility. We could define a new image as the composition of two images beside one another. You can imagine other layout operations to arrange images vertically, or to stack them on top of one another, and so on. We could also compose images using geometric transformations such as rotations and translation, or using styling such as fill color. 9 | 10 | Compostionality implies there is no global state. There are many closely related terms that all boil down to removing state: maintaining *substitution*, enabling *local reasoning*, *referential transparency*, or *purity*. 11 | 12 | *Closure* is another property implied by compositionality. This means there are operations that take two or more pictures and return an element of the same type. Closure allows us to apply operations indefinitely to build more complex pictures from simpler ones. 13 | 14 | Our library should allow operations in terms of a *local coordinate system* to make composition easier. 15 | 16 | Finally, we want an *expressive* library, a rather loosely defined term that we can take to mean we should write the minimal amount of code required to achieve our goal. There is a tradeoff between the expressivity of the library and the effort required to implement it. Since this is just a case study we will create a less expressive system than we might otherwise. 17 |
18 | -------------------------------------------------------------------------------- /src/pages/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This supplement to Essential Scala brings together the book's main concepts in a sizable case study. The goal is to develop a *two-dimensional vector graphics and animation* library similar to [Doodle][doodle]. Let us first describe the what vector graphics are, then why this makes a good case study, and finally describe the structure of the project. 4 | 5 | By vector graphics we mean images that are described in terms of lines and curves, rather than in terms of pixels. Vector graphics are independent of how they are drawn. They can be rendered equally well on a high resolution plotter or a tiny mobile screen. They can be arbitrarily transformed without losing information, unlike pixel graphics which distort when they are rotated, for example. This structure is something our library will heavily leverage. 6 | 7 | So why a case study vector graphics? There are a few reasons. Firstly, graphics are fun, and hopefully something you can enjoy even if you haven't worked with computer graphics before. Having a tangible output is of great benefit. We can directly draw on the screen the concepts we're working with! Finally, we'll see that graphics great vehicle for the concepts taught in Essential Scala, and we can wrap up most of the content in the course in this one case study. 8 | 9 | The case study is divided into three main sections: 10 | 11 | 1. Building the basic objects and methods for our library, which makes heavy use of algebraic data types and structural recursion. 12 | 2. Adding animations, which introduces sequencing abstractions such as `map` and `fold`. 13 | 3. Abstracting the rendering pipeline, which introduces type classes and touches briefly on some more advanced functional programming concepts. 14 | 15 | Each section asks you to implement part of the library. Supplementing this supplement is a code repository containing support code as well as working implementations for each exercise. There are many possible implementations for each exercise, each making different design tradeoffs. Our solution is just one of these, and you shouldn't take it to be any more correct than other solutions you may come up with. A primary goal of this supplement is to understand the tradeoffs different solutions make. 16 | 17 | To get the most from this case study *you have to do the work*. While you will get some value from reading through and looking at our solutions, you will get infinitely more if you attempt every exercise yourself first. 18 | 19 | Finally, we always enjoy talking to other programmers. If you have any comments or questions about this case study, do drop us an email at `noel@underscore.io` and `dave@underscore.io`. 20 | -------------------------------------------------------------------------------- /src/pages/animation/example.md: -------------------------------------------------------------------------------- 1 | ## A Small Example 2 | 3 | Let's experiment with a version of the system we'll be building. You can find this system on the `feature/event` branch. We're going to use it to make a little ball move around the screen in response to key presses. 4 | 5 | We start by converting the `Canvas` callbacks for animation frames and key presses into event streams. 6 | 7 | ```scala 8 | import doodle.backend.Key 9 | import doodle.core._ 10 | import doodle.event._ 11 | import doodle.jvm.Java2DCanvas 12 | 13 | val redraw = Canvas.animationFrameStream(canvas) 14 | val keys = Canvas.keyDownStream(canvas) 15 | ``` 16 | 17 | Now we're going to convert key presses into velocity. Velocity in a vector starting at (0, 0), and we'll increment or decrement it by one as appropriate on each key press. Additionally we going to limit the x and y components of velocity to be in the range -5 to 5. This stops the ball flying around the screen too quickly. 18 | 19 | ```scala 20 | val velocity = keys.scanLeft(Vec.zero)((key, prev) => { 21 | val velocity = 22 | key match { 23 | case Key.Up => prev + Vec(0, 1) 24 | case Key.Right => prev + Vec(1, 0) 25 | case Key.Down => prev + Vec(0, -1) 26 | case Key.Left => prev + Vec(-1, 0) 27 | case _ => prev 28 | } 29 | Vec(velocity.x.min(5).max(-5), velocity.y.min(5).max(-5)) 30 | } 31 | ) 32 | ``` 33 | 34 | Now we update the location of the ball by the velocity. Location starts at (0,0) and this time we're limiting it to be within a 600 by 600 screen. 35 | 36 | ```scala 37 | val location = velocity.scanLeft(Vec.zero){ (velocity, prev) => 38 | val location = prev + velocity 39 | Vec(location.x.min(300).max(-300), location.y.min(300).max(-300)) 40 | } 41 | ``` 42 | 43 | Finally we create the frames and render them. 44 | 45 | ```scala 46 | val ball = Circle(20) fillColor (Color.red) lineColor (Color.green) 47 | 48 | val frames = location.map(location => ball at location) 49 | Canvas.animate(Java2DCanvas.canvas, frames) 50 | ``` 51 | 52 | If you play with this code you'll find it has an annoying problem: it only updates the ball's position on key presses. What we really want is to the ball continually moving around the screen. We can achieve this by joining the `velocity` stream with the `redraw` stream. The resulting stream will have a value everytime there is a new value available on either `velocity` and `redraw`. Since `redraw` is updated 60 times a second (the screen refresh rate) this will give us a ball that moves around smoothly. The following redefinition of `location` is sufficient. 53 | 54 | ```scala 55 | val location = redraw.join(velocity).map{ case(ts, m) => m }. 56 | scanLeft(Vec.zero){ (velocity, prev) => 57 | val location = prev + velocity 58 | Vec(location.x.min(300).max(-300), location.y.min(300).max(-300)) 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /src/pages/random/implementation.md: -------------------------------------------------------------------------------- 1 | ## Implementation 2 | 3 | Here's the API we designed in the previous section. 4 | 5 | ```tut:book:silent 6 | sealed trait Random[A] { 7 | def run(rng: scala.util.Random): A = 8 | ??? 9 | 10 | def flatMap[B](f: A => Random[B]): Random[B] = 11 | ??? 12 | 13 | def map[B](f: A => B): Random[B] = 14 | ??? 15 | 16 | def zip[B](that: Random[B]): Random[(A,B)] = 17 | for { 18 | a <- this 19 | b <- that 20 | } yield (a, b) 21 | } 22 | object Random { 23 | val double: Random[Double] = 24 | ??? 25 | 26 | val int: Random[Int] = 27 | ??? 28 | 29 | /** Generate a value from a normal or Gaussian distribution. */ 30 | val normal: Random[Double] = 31 | ??? 32 | 33 | /** Create a Random value that always generates `in`. */ 34 | def always[A](in: A): Random[A] = 35 | ??? 36 | } 37 | ``` 38 | 39 | We're now ready to implement our `Random` data type. You should have all the tools you need to do this, but look at the solution below if you need a hint. 40 | 41 |
42 | We can implement `Random` using the same reification strategy we used for `Image`. 43 |
44 | 45 | When you've completed your implementation compare it to ours. 46 | 47 |
48 | Our implementation is below. It uses the same reification strategy we used for `Image`, converting `flatMap` and `map` into data structures (`FlatMap` and `Map` respectively). We add another case, `Primitive`, to store the basic "atoms" we build more complicated random processes from. 49 | 50 | The `Map` case is not strictly necessary, as we could implement it in terms of `FlatMap` and `Primitive`. 51 | 52 | ```tut:book 53 | object random { 54 | sealed trait Random[A] { 55 | def run(rng: scala.util.Random): A = 56 | this match { 57 | case Primitive(f) => f(rng) 58 | case FlatMap(r, f) => f(r.run(rng)).run(rng) 59 | case Map(r, f) => f(r.run(rng)) 60 | } 61 | 62 | def flatMap[B](f: A => Random[B]): Random[B] = 63 | FlatMap(this, f) 64 | 65 | def map[B](f: A => B): Random[B] = 66 | Map(this, f) 67 | 68 | def zip[B](that: Random[B]): Random[(A,B)] = 69 | for { 70 | a <- this 71 | b <- that 72 | } yield (a, b) 73 | } 74 | object Random { 75 | val double: Random[Double] = 76 | Primitive(rng => rng.nextDouble()) 77 | 78 | val int: Random[Int] = 79 | Primitive(rng => rng.nextInt()) 80 | 81 | /** Generate a value from a normal or Gaussian distribution. */ 82 | val normal: Random[Double] = 83 | Primitive(rng => rng.nextGaussian()) 84 | 85 | /** Create a Random value that always generates `in`. */ 86 | def always[A](in: A): Random[A] = 87 | Primitive(rng => in) 88 | } 89 | final case class Primitive[A](f: scala.util.Random => A) extends Random[A] 90 | final case class FlatMap[A,B](random: Random[A], f: A => Random[B]) extends Random[B] 91 | final case class Map[A,B](random: Random[A], f: A => B) extends Random[B] 92 | } 93 | ``` 94 |
95 | -------------------------------------------------------------------------------- /src/pages/foundation/atoms.md: -------------------------------------------------------------------------------- 1 | ## Atoms and Operations 2 | 3 | Now we have thought about the properties of our system, let's think about the most basic types, and the operations on those types. In other words, what is the basic API (application programming interface) that our system will provide? We don't need to write code right now, but it will help to be somewhat formal. 4 | 5 |
6 | There is no right solution to this. Indeed we're hoping that your design will be different to ours, as this will allow you to compare different approaches and learn by doing so. 7 | 8 | In Doodle the core type is `Image`. Basic images are geometric shapes like `Circle`, `Rectangle`, and `Triangle`. 9 | 10 | Operations on `Image` include layout operations: 11 | 12 | - `Image above Image = Image` 13 | - `Image on Image = Image` 14 | - `Image beside Image = Image` 15 | - `Image at Vec = Image` 16 | 17 | Here I'm writing how the operations work in terms of types. An `Image` on an `Image` returns an `Image`, for example. 18 | 19 | There are also operations for changing the style: 20 | 21 | - `Image fillColor Color = Image` 22 | - `Image lineColor Color = Image` 23 | - `Image lineWidth Double = Image` 24 |
25 | 26 | Design a data type to represent the "atoms" in your model. You should probably use an algebraic data type. Hint: You might want to look at the [Canvas](https://github.com/underscoreio/doodle-case-study/blob/master/shared/src/main/scala/doodle/backend/Canvas.scala), which is the low-level interface we'll implement our higher-level interface against. This will help you determine what's possible in the context of the case study, and the types involved. 27 | 28 |
29 | We can start with a very simple model like so: "An `Image` is a `Circle` or a `Rectangle`". You should be immediately be able to translate this into code. 30 | 31 | ```scala 32 | sealed trait Image 33 | final case class Circle(radius: Double) extends Image 34 | final case class Rectangle(width: Double, height: Double) extends Image 35 | ``` 36 | 37 | The `Canvas` abstraction we are rendering to is built on paths. We could model this as, say "A `PathElement` is a `MoveTo`, `LineTo`, or `CurveTo`" and "A `Path` is a sequence of `PathElements`". This is actually the representation that Doodle uses, but we provide a higher-level interface like `Image` above for convenience. The `Path` interface can also be directly converted into code. (At this point in Essential Scala we haven't seen the `Seq` type yet. It represents a sequence of elements.) 38 | 39 | ```scala 40 | sealed trait PathElement 41 | final case class MoveTo(x: Double, y: Double) extends PathElement 42 | final case class LineTo(x: Double, y: Double) extends PathElement 43 | final case class CurveTo(cp1x: Double, cp1y: Double, cp2x: Double, cp2y: Double, endX: Double, endY: Double) extends PathElement 44 | 45 | final case class Path(elements: Seq[PathElement]) 46 | 47 | final case class Image(path: Path) 48 | ``` 49 |
50 | 51 | Now create the method signatures for the operations you'll define on your data type. You don't need to fill in the bodies yet---you can just leave then as `???` so the code compiles but won't run. 52 | 53 |
54 | If you're following Doodle you will have methods similar to 55 | 56 | ```scala 57 | sealed trait Image { 58 | def on(that: Image): Image = 59 | ??? 60 | 61 | def beside(that: Image): Image = 62 | ??? 63 | 64 | def above(that: Image): Image = 65 | ??? 66 | } 67 | final case class Circle(radius: Double) extends Image 68 | final case class Rectangle(width: Double, height: Double) extends Image 69 | ``` 70 | 71 | You might also want methods to add stroke and fill, but for the purposes of this case study we can leave them out for now. 72 |
73 | 74 | You will also need a method `draw` that accepts a `doodle.backend.Canvas` and actually renders the images using the canvas. More on that in a moment. 75 | -------------------------------------------------------------------------------- /src/pages/animation/easing.md: -------------------------------------------------------------------------------- 1 | ## Easing Functions 2 | 3 | Consider animating a UI element, such as a toolbar that slides out. We could simply linearly interpolate the position of the toolbar between the start and end points, but this simple movement lacks visual appeal. UI designers like to use movement that is more natural looking. For example, our toolbar's movement might accelarate over time, and it might bounce when it reaches it's final destination. 4 | 5 | Functions that interpoloate position over time are known as *tweening functions* or *easing functions*. The most commonly used easing functions come from the work of [Robert Penner][penner]. His easing functions include include linear functions, quadratics and higher order polynomials, and more complication equations that give oscillating behaviour. (If you've studied physics you are probably thinking of damped spring models right now. That's not a bad place to start.) 6 | 7 | In this section we are going to build a library of easing functions. This is a good example of functional design, and it will help us make more interesting animations with our animations library. 8 | 9 | ### The Easing Function 10 | 11 | In our representation, an easing function will accept and return a number between 0 and 1. The input is the *time*, from 0% to 100%. The output indicates position between 0% and 100% of the final position. The output should always start at 0 and finish at 1, but can vary arbitrarily inbetween. The more mathematically inclined will recognise this as the parametric form of a path. A quadratic easing function can then be defined as *f(t) = t^2*, for *t in [0, 1]*. 12 | 13 | The obvious representation in Scala is to use a function `Double => Double`. However, not all `Double => Double` functions will be easing functions, and we'll want to add domain specific methods to our easing functions. This is makes sense to wrap our `Double => Double` functions in a type like[^unboxing] 14 | 15 | ~~~ scala 16 | final case class Easing(get: Double => Double) 17 | ~~~ 18 | 19 | We should also implement some basic easing functions. Here are some equations to use: 20 | 21 | - the linear function *f(t) = t*; 22 | - the quadratic *f(t) = t^2*; and 23 | - the "elastic" function *f(t) = 2^(-10t) * sin( (t-p/4) * 2pi / p) + 1* where *p = 0.3* 24 | 25 | Implement these functions. You might it useful to import `scala.math`. Now implement an appropriate `apply` method on `Easing`. 26 | 27 | ### Composing Easing Functions 28 | 29 | Just like Bilbo, we sometimes want to go there and back again when animating objects. An *ease in* function goes from 0 to 1 ait's input goes from 0 to 1. An *ease out* function is the reverse, going from 1 to 0 as input varies from 0 to 1. The functions we have implemented above are all ease in functions. 30 | 31 | Given an ease in function we can construct an ease out. How? We can run it backwards and take the output away from 1. So if *f* is an ease in function, *g(t) = 1 - f(1-t)* is the corresponding ease out. 32 | 33 | How should we represent this in code? We can easily add a method `easeOut` to `Easing`, transforming an ease in to an ease out. What would the result type of this method be? If it's an `Easing` we could apply `easeOut` again, which yields a broken `Easing`. Hmmm... 34 | 35 | A better solution is to rename out type `Easing` to `EaseIn` and create a new type `EaseOut`. Add a method `easeOut` to `EaseIn`, and a method `easeIn` to `EaseOut`. These methods should be inverses. 36 | 37 | Implement this. 38 | 39 | Given some basic functions we can compose new tweening functions from existing ones. For example, we might use the quadratic above for the first half of the element's movement, and *f(t) = (t-1)^2 + 1* for the second half (so the element deccelerates towards its final position.) 40 | 41 | T1. Implement a few tweening functions. This shouldn't take you very long. 42 | 43 | [penner]: http://robertpenner.com/easing 44 | 45 | [^unboxing]: The more perfomrance oriented of your might object to the additional indirection introduced by the `Easing` wrapper. We can remove it in many cases by using a *value type*. This goes beyond Essential Scala, but it is a fairly simple thing to implement: simply extend `AnyVal`. 46 | -------------------------------------------------------------------------------- /src/pages/animation/notes.md: -------------------------------------------------------------------------------- 1 | ## Hints for Implementing the Assignment 2 | 3 | In our meeting we agreed on the following interface: 4 | 5 | ```scala 6 | trait EventStream[A] { 7 | def map[B](f: A => B): EventStream[B] 8 | 9 | def foldp[B](seed: B)(f: (A, B) => B): EventStream[B] 10 | 11 | def join[B, C](that: EventStream[B])(f: (A, B) => C): EventStream[C] 12 | } 13 | ``` 14 | 15 | We discussed two general models for implementing reactive systems: push and pull based evaluation. You can implement either, but I'm going to describe a push based model. 16 | 17 | In a push model, evaluation starts at a source, when an event arrives, and propagates out to connected nodes. Pull based is the opposite---it starts from sinks and propogates back to sources, polling for changes. 18 | 19 | We'll call the direction from source to sink in the graph "forward". Nodes that are forward and connected to a given node are "listeners". 20 | 21 | For every node, a pull based model must known the listeners of that node. When a node receives a change we should perform any actions associated with that node and then push the update to its listeners. 22 | 23 | (Is the order in which we push updates---for example, depth- or breadth-first---important?) 24 | 25 | 26 | ### Map 27 | 28 | Branch your work of the `atoms-and-operations` branch. (If you are feeling adventurous, try the `feature/keydown` branch, which adds a callback for key presses.) 29 | 30 | Let's start by just implementing `map`. Unlike with, say, a `List`, we can't actually do the `map` at the point it is called, as the data is not available yet. We can use the same trick we use with `Image`: represent the computation as data. 31 | 32 | Implement `map`. 33 | 34 | (Did you find you had to implement a private interface to push updates? Can you pull this out o the public interface?) 35 | 36 | ### Animations 37 | 38 | Now implement a method that, given a `Canvas`, converts the `setAnimationFrameCallback` to an `EventStream`, with one event per call for a new frame. (Where is a good place for this method to live?) 39 | 40 | Finally implement a method (where?) that, given a `Canvas` and an `EventStream[Image]` animates the `Image` on the `Canvas`. It's sufficient to, for each frame, `clear` the `Canvas` and then `draw` the `Image`. 41 | 42 | ### Fold 43 | 44 | Now implement `foldp` (or just `fold`) using the same technique you used to implement `map`. 45 | 46 | ### Join 47 | 48 | Implementing `join` is probably the trickiest aspect of this section. The intention with `join` is to combine the most recent values from each `EventStream`. A `join` node is therefore a listener to two nodes. We will need store the most recent value from each stream we're listening to, and also represent the possibility there is no most recent value. 49 | 50 | ### Extensions 51 | 52 | We've now built a very simple reactive system. There are lots of further areas to ponder. Here are a few. 53 | 54 | #### Error Handling 55 | 56 | It's possible that nodes could fail. How should we handle this? 57 | 58 | #### Finite EventStreams 59 | 60 | Similarly to failure, a node may not be capable of producing any more values (think of representing, say, a web request as an `EventStream`). Should we represent this? 61 | 62 | #### Resource Collection 63 | 64 | How can be clean up nodes that are no longer needed, so we don't leak memory? 65 | 66 | #### Monads 67 | 68 | `flatMap` is notably absent from our `EventStream` API. What would is mean to add `flatMap`? 69 | 70 | #### Evaluation Semantics 71 | 72 | We briefly pondered the difference between depth- and breadth-first evaluation above. What about concurrency? The reactive model is a natural fit for concurrent processing. Can we extend it to support concurrency? How should we do this? While we're thinking about concurrency, are there race conditions in the existing implementation? 73 | 74 | To get you started, consider the following network. What support it evaluation to? What does it evaluate to? 75 | 76 | ~~~ scala 77 | val a = EventStream.zeros // 0 0 0 0 .... 78 | val x = a map (_+1) foldp(0)(_+_) // 1 2 3 ... 79 | val y = a map (_-1) foldp(0)(_+_) // -1 -2 -3 ... 80 | val z = (x zip y) map (case (x, y) => x + y) // 0 0 0 ... ? 81 | ~~~ 82 | 83 | Should nodes start processing events as soon as they are created, or should they wait for some signal to start? What difference does it make? 84 | -------------------------------------------------------------------------------- /src/pages/foundation/background.md: -------------------------------------------------------------------------------- 1 | ## Background 2 | 3 | Our goal is to make it easy to create interesting vector graphics and animations. Here is a reasonable example of an interesting image: 4 | 5 | ![Colour archery target](src/pages/foundation/target3.png) 6 | 7 | Here's the code to draw it using Doodle: 8 | 9 | ```scala 10 | ( 11 | ( Circle(10) fillColor Color.red ) on 12 | ( Circle(20) fillColor Color.white ) on 13 | ( Circle(30) fillColor Color.red lineWidth 2 ) above 14 | ( Rectangle(6, 20) above Rectangle(20, 6) fillColor Color.brown ) above 15 | ( Rectangle(80, 25) lineWidth 0 fillColor Color.green ) 16 | ).draw 17 | ``` 18 | 19 | Note how we build larger images from smaller ones. For example, to build the target we place three circles on top of one another. 20 | 21 | ```scala 22 | ( Circle(10) fillColor Color.red ) on 23 | ( Circle(20) fillColor Color.white ) on 24 | ( Circle(30) fillColor Color.red lineWidth 2 ) 25 | ``` 26 | 27 | Note also that layout is done at a high level. We specify where images lie in relation to one-another, and Doodle works out the absolute coordinates to use when rendering to the screen. 28 | 29 | **We strongly suggest playing around with Doodle before undertaking this part of the case study.** [The code](https://github.com/underscoreio/doodle) is online, along with many [examples](https://github.com/underscoreio/doodle/tree/develop/shared/src/main/scala/doodle/examples). You can also read [Creative Scala](http://underscore.io/training/courses/creative-scala/), a free textbook that uses Doodle to introduce Scala. 30 | 31 | The typical imperative APIs for drawing vector graphics, such as the Java 2D API or the HTML Canvas, work at a very low level of abstraction. This puts a lot of the burden on the programmer. An example of such as an API is 32 | 33 | ``` scala 34 | trait Canvas { 35 | def setStroke(stroke: Stroke): Unit 36 | def setFill(color: Color): Unit 37 | 38 | def stroke(): Unit 39 | def fill(): Unit 40 | 41 | def beginPath(): Unit 42 | def moveTo(x: Double, y: Double): Unit 43 | def lineTo(x: Double, y: Double): Unit 44 | def bezierCurveTo(cp1x: Double, cp1y: Double, cp2x: Double, cp2y: Double, endX: Double, endY: Double): Unit 45 | def endPath(): Unit 46 | } 47 | ``` 48 | 49 | To draw a shape we call `beginPath`, then issue a series of commands (`moveTo`, `lineTo`, or, `bezierCurveTo`), followed by an optional `endPath`. We can draw just the outline of the shape, in which case we next call `stroke`, or just fill it (via `fill`), or do both. The colors used for the stroke and fill and set by calls to `setStroke` and `setFill` 50 | 51 | For example, to draw a circle we could use the following method: 52 | 53 | ``` scala 54 | def circle(centerX: Double, centerY: Double, radius: Double): Unit = { 55 | // See http://spencermortensen.com/articles/bezier-circle/ for approximation 56 | // of a circle with a Bezier curve. 57 | val c = 0.551915024494 58 | val cR = c * radius 59 | beginPath() 60 | moveTo(centerX, centerY + radius) 61 | bezierCurveTo(centerX + cR, centerY + radius, 62 | centerX + radius, centerY + cR, 63 | centerX + radius, centerY) 64 | bezierCurveTo(centerX + radius, centerY - cR, 65 | centerX + cR, centerY - radius, 66 | centerX, centerY - radius) 67 | bezierCurveTo(centerX - cR, centerY - radius, 68 | centerX - radius, centerY - cR, 69 | centerX - radius, centerY) 70 | bezierCurveTo(centerX - radius, centerY + cR, 71 | centerX - cR, centerY + radius, 72 | centerX, centerY + radius) 73 | endPath() 74 | } 75 | ``` 76 | 77 | In our experience there is a lot wrong with this sort of API. We have seen that it is very inconvenient compared to Doodle. Another problem is there is a lot of state involved. There is a single global stroke color, for example. This makes it difficult to abstract parts of an image into methods, as they can overwrite each other's stroke color. Similarly it's not defined what happens if we nest calls to `beginPath`, or if we call `stroke` or `fill` before calling `beginPath`, and so on. All of this state makes it hard to create reusable components from which we can build up bigger pictures. 78 | 79 | The use of a global coordinate system makes layout difficult. Imagine we have code to draw two pictures, and we now want to put these pictures side by side. First we had better have had the foresight to make the starting point of each picture a parameter to the method that draws them, or we won't be able to shift them around. Then we have to calculate our layout manually---work out how wide each picture is, and move them by an appropriate amount so they are balanced across the screen. In Doodle we can simply say `image1 beside image2`. 80 | 81 | We can see the imperative API is a lot more inconvenient to use. We could of course build our own abstractions, like `circle` above, to provide the facilities that Doodle offers. Indeed that is exactly what we are going to do! 82 | -------------------------------------------------------------------------------- /src/pages/random/random.md: -------------------------------------------------------------------------------- 1 | ## A Random API 2 | We'll represent our API with a type `Random`. Our basic and very incomplete model is 3 | 4 | ```scala 5 | sealed trait Random { 6 | def run(rng: scala.util.Random): ??? 7 | } 8 | ``` 9 | 10 | where the `run` method is what we call to actually create a random value. How should we modify `Random` to give a sensible result type to `run`? 11 | 12 |
13 | We need to add a type variable to `Random` to represent the type of value that the `Random` instance will produce when it is run. 14 | 15 | ```tut:book:silent 16 | sealed trait Random[A] { 17 | def run(rng: scala.util.Random): A = 18 | ??? 19 | } 20 | ``` 21 |
22 | 23 | 24 | ### Independent and Conditional Distributions 25 | 26 | What other methods do we want on `Random`? Probability distributions are well studied and we know we can build them using two tools: 27 | 28 | - independent distributions; and 29 | - conditional distributions. 30 | 31 | What do these mean? Let's focus on conditional distributions, because we can express independent distributions using them. 32 | 33 | A conditional distribution is a distribution that is defined in terms of some input value. Concretely, imagine we are implementing a process to render a random walk, as depicted in [@fig:random:brownian-motion]. In a random walk we have a notion of a particle that is located at some point. At each time step we take the current point and add some random noise to generate the next point. 34 | 35 | ```scala 36 | def walk(current: Point): Random[Point] = { 37 | // This code won't work but is illustrative of what we want to achieve. 38 | current + noise 39 | } 40 | ``` 41 | 42 | The `walk` method gives us as way to update the current point. Now imagine running this process over many steps. At each step we'll start with a `Random[Point]`, which we'd like to compose with `walk` above, giving a new `Random[Point]`. We can write down a type equation giving us 43 | 44 | ```scala 45 | Random[Point] ??? walk = Random[Point] 46 | ``` 47 | 48 | We're using `walk` as a `Point => Random[Point]` function, so we can write 49 | 50 | ```scala 51 | Random[Point] ??? (Point => Random[Point]) = Random[Point] 52 | ``` 53 | 54 | We've seen a method we can replace `???` with to make this type equation hold. What method is it? 55 | 56 |
57 | If we replace `???` with `flatMap` the type equation holds. 58 | 59 | ```scala 60 | Random[Point] flatMap (Point => Random[Point]) = Random[Point] 61 | ``` 62 |
63 | 64 | So it seems we can implement conditional distributions using `flatMap`. If we have `flatMap` what other method should we have? 65 | 66 |
67 | At a minimum, we should have `map`, to complete the monad API. 68 |
69 | 70 | Let's return to independent distributions now. Independent distributions are those that have no dependency between one another. A simple example is generating a point, where we generate the x- and y-coordinates independently. If `Random.double` generates a random `Double`, we might generate a random point with code like 71 | 72 | ```scala 73 | val randomPoint: Random[Point] = 74 | (Random.double zip Random.double).map { (pt: (Double, Double)) => 75 | val (x, y) = pt 76 | Point(x, y) 77 | } 78 | ``` 79 | 80 | This assumes we have a method `zip` on `Random` with type 81 | 82 | ```scala 83 | def zip[B](that: Random[B]): Random[(A,B)] = 84 | ??? 85 | ``` 86 | 87 | Implement `zip` using `flatMap` and `map`. 88 | 89 | 90 |
91 | Here's a sample solution. 92 | 93 | ```tut:book 94 | sealed trait Random[A] { 95 | def run(rng: scala.util.Random): A = 96 | ??? 97 | 98 | def flatMap[B](f: A => Random[B]): Random[B] = 99 | ??? 100 | 101 | def map[B](f: A => B): Random[B] = 102 | ??? 103 | 104 | def zip[B](that: Random[B]): Random[(A,B)] = 105 | this flatMap { a => 106 | that map { b => 107 | (a, b) 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | You might recognise that the `flatMap` / `map` pattern can be implemented with a for comprehension as follows. 114 | 115 | ```tut:book 116 | sealed trait Random[A] { 117 | def run(rng: scala.util.Random): A = 118 | ??? 119 | 120 | def flatMap[B](f: A => Random[B]): Random[B] = 121 | ??? 122 | 123 | def map[B](f: A => B): Random[B] = 124 | ??? 125 | 126 | def zip[B](that: Random[B]): Random[(A,B)] = 127 | for { 128 | a <- this 129 | b <- that 130 | } yield (a, b) 131 | } 132 | ``` 133 |
134 | 135 | 136 | ### Other Methods 137 | 138 | What other methods should we have, particularly on the companion object of `Random`? 139 | 140 |
141 | At a minimum we need some constructors to create `Random` instances. We had an example above of `Random.double`. If we look at the methods on `scala.util.Random`, that might inspire some other constructors. For example, we could define 142 | 143 | ```scala 144 | object Random { 145 | val double: Random[Double] = 146 | ??? 147 | 148 | val int: Random[Int] = 149 | ??? 150 | 151 | /** Generate a value from a normal or Gaussian distribution. */ 152 | val normal: Random[Double] = 153 | ??? 154 | 155 | /** Create a Random value that always generates `in`. */ 156 | def always[A](in: A): Random[A] = 157 | ??? 158 | } 159 | ``` 160 |
161 | -------------------------------------------------------------------------------- /src/pages/animation/background.md: -------------------------------------------------------------------------------- 1 | ## Background 2 | 3 | Our goal is to produce smooth animations and allow our animations to react to key presses and other events. 4 | Let's start by looking at the first part of our goal: producing smooth animations. 5 | 6 | To make smooth animations we must account for the characteristics of the screen. 7 | Screens typically redraw sixty times a second, so we should create new frames at the same rate. 8 | We also need to produce our frames at the point in time when the screen is ready to redraw, or we might observe [screen tearing](https://en.wikipedia.org/wiki/Screen_tearing). 9 | 10 | The typical imperative solution is to setup a callback that is called every time a new frame is needed. The `Canvas` interface provides this facility, with a method `setAnimationFrameCallback`. 11 | 12 | ```scala 13 | def setAnimationFrameCallback(callback: Double => Unit): Unit 14 | ``` 15 | 16 | We pass a function to `setAnimationFrameCallback`, and the `Canvas` calls this function every time the screen is ready for a new frame. We then call other methods on `Canvas` to actually create that frame. 17 | 18 | This interface has the all problems of the imperative approach to drawing that we abandoned in the last chapter: it doesn't compose, is difficult to work with, and is difficult to reason about. What would a better, functional, approach be? 19 | 20 | When we look at a what an animation is, we find it quite amenable to a functional approach. Imagine we are animating a ball moving about the screen. The current position of the ball is a function of the previous postion and the current velocity. 21 | 22 | ![Current position is equal to the previous position plus the current velocity.](src/pages/animation/current-position.png) 23 | 24 | The current velocity is itself a function of the user input and the previous velocity. 25 | 26 | Let's see how this looks in code. 27 | 28 | We start with a type to represent user input. 29 | 30 | ```tut:silent:book 31 | // User input is a Key 32 | sealed trait Key 33 | final case object Up extends Key 34 | final case object Down extends Key 35 | final case object Left extends Key 36 | final case object Right extends Key 37 | ``` 38 | 39 | Now we can calculate velocity as a function of the velocity at the previous timestep and user input. 40 | 41 | ```scala 42 | // Velocity is represented as a two dimensional vector of type `Vec` 43 | def currentVelocity(previousVelocity: Vec, input: Key): Vec = 44 | input match { 45 | case Up => previousVelocity + Vec(0, 1) 46 | case Down => previousVelocity + Vec(0, -1) 47 | case Left => previousVelocity + Vec(-1, 0) 48 | case Right => previousVelocity + Vec(1, 0) 49 | } 50 | ``` 51 | 52 | Location is a function of the location at the previous time step and the velocity. 53 | 54 | ```scala 55 | // Location is represented as a two dimensional vector, by abuse of notation 56 | def currentLocation(previousLocation: Vec, velocity: Vec): Vec = 57 | previousLocation + velocity 58 | ``` 59 | 60 | Given the current location we can draw a ball at that location. (You might not have implemented the `at` method in your version of Doodle. It places an `Image` at the given coordinates in the local coordinate system of the enclosing `Image`, or in the global coordinate system if there is no enclosing `Image`.) 61 | 62 | ```scala 63 | // A simple image of a ball 64 | val ball = Circle(10) fillColor Color.red 65 | 66 | def currentBall(currentLocation: Vec): Image = 67 | ball at currentLocation 68 | ``` 69 | 70 | This is a good example of functional code: we've broken the problem down into small independent functions that we then compose to build the complete solution. We're still missing some parts though: how is user input obtained for example? 71 | 72 | If we ignore interactivity for now, we can actually run the code above using `Lists` to provide the input. We `scanLeft` `currentVelocity` and `currentLocation`, and `map` `currentBall`. 73 | 74 | You might not have seen the `scan` methods before. They are equivalent to fold but they collect the intermediate results in a list. Taking summing the elements of a `List` using a fold like so: 75 | 76 | ```tut:book 77 | List(1, 2, 3, 4).foldLeft(0){ _ + _ } 78 | ``` 79 | 80 | If we replace `foldLeft` with `scanLeft` we get a list of the partial sums. 81 | 82 | ```tut:book 83 | List(1, 2, 3, 4).scanLeft(0){ _ + _ } 84 | ``` 85 | 86 | We can apply this to our `Image` example to get a list of intermediate image frames. 87 | 88 | ```scala 89 | val input = List(Up, Up, Down, Down, Left, Right, Left, Right) 90 | 91 | val images: List[Image] = 92 | input.scanLeft(Vec(0, 0)){ currentVelocity }. 93 | scanLeft(Vec(0, 0)){ currentLocation }. 94 | map(currentBall) 95 | ``` 96 | 97 | Our resulting list of images is something that we could display to make an animation. 98 | 99 | This system is fine for rendering animations from prerecorded input, but how what about responding in real-time to user input? The values of a `List` are all known in advance, while user input only becomes available when keys are pressed. What we want is some kind of sequence of data where the elements are generated by external input. Imagine, for example, something like a list of keypresses where the next element springs into existence when the user presses a key. We've seen above that the basic interface of `map` and `scanLeft` allows us to express at least some animations. 100 | 101 | We can think of a list as representing data in space. Different list indices correspond to different locations in the computer's memory. What we want is an abstraction that represents data *in time*. Indexing in this event stream corresponds to accessing events at different times. (We won't actually implement indexing as it would allow time travel, but it provides a useful conceptual model.) 102 | 103 | The next leap is to realise that it's the interface allowing transformation (`map`, `scanLeft`, and so on) that is important, not the list-like nature. We don't want to actually store all the past inputs in memory like we would in a list. 104 | We can make an analogy to an assembly line. 105 | The raw materials flowing onto the assembly line are the user input. 106 | Our transformations are stages of the assemblly line, turning input into output. 107 | Our assembly line can branch or merge as needed. 108 | The final destination of our assembly line is the screen and our final output should be `Images`. 109 | -------------------------------------------------------------------------------- /src/pages/typeclasses/typeclasses.md: -------------------------------------------------------------------------------- 1 | ## Type Classes 2 | 3 | We're now ready to tackle the main pattern for which we use implicits: type classes. 4 | 5 | When we designed our `EventStream` interface we drew inspiration from the existing API of `List`. It can be useful to be able to abstract over `List` and `EventStream`. If we defined such an API, we could write event processing algorithms in terms of this API, making them oblivious to the concrete implementation they run on. Then we could run our algorithms in real-time data using the `EventStream` API and over batch (offline) data using `Lists` without making an code changes. 6 | 7 | This is a perfect application for type classes. We have two types (`EventStream` and `List`) that share a common interface but don't share a common useful supertype. In fact their are many other types that have an API much like `EventStreams` (in the standard library, `Option` and `Future` come to mind, while in Essential Scala we have implemented some of these methods for types like `Sum`). A few type classes would allow us to unify a whole load of otherwise different types, and allow us to talk in a more abstract way about operations over them. 8 | 9 | So, what should our type classes be? We have briefly discussed functors---things that have a `map` method. What about `join` and `scanLeft`? Things that can be joined are called applicative functors, or just applicatives for short. Our scan operation has the same signature as `scanLeft` on a `List`. There is no standard type class so we'll create our own called `Scannable`. Finally we'll through `Monads` into the mix, even though `EventStream` doesn't have `flatMap` method, because they are so useful in other contexts. 10 | 11 | We have an informal idea of the type classes. Now let's get specific. For a type `F[A]` 12 | 13 | - A `Functor` has a method `map[B](fa: F[A])(f: A => B): F[B]` 14 | - An `Applicative` is a `Functor` and has 15 | - `zip(fa: F[A], fb: F[B]): F[(A, B)]` 16 | - `point[A](a: A): F[A]` 17 | - A `Monad` is a `Functor` and has 18 | - `flatMap[B](fa: F[A])(f: A => F[B]): F[B]` 19 | - `point[A](a: A): F[A]` 20 | Note you can implement `map` in terms of `flatMap` and `point`. 21 | - A `Scannable` has a method `scanLeft[B](seed: B)(f: (B,A) => B): F[B]` 22 | 23 | Implement these type classes, putting your code in a package `doodle.typeclasses`. Create type class instances (where you can) for `EventStream` and `List`. Put the `EventStream` instances in its companion object, and the `List` instances in `doodle.typeclasses`. 24 | 25 | You will run into a problem doing this. Read on for the solution but *make sure you attempt the exercise before you do*. 26 | 27 | You perhaps tried defining a type class like 28 | 29 | ```scala 30 | trait Function[F] { 31 | def map[A,B](fa: F[A])(f: A => B): F[B] 32 | } 33 | ``` 34 | 35 | and received an error like `error: F does not take type parameters`. To solve this problem we need to learn about kinds and higher-kinded types. 36 | 37 | Kinds are like types for types. The describe the number of "holes" or parameters in a type. We distinguish between regular types like `Int` and `String `that have no holes, and *type constructors* like `List` and `EventStream` that have holes that we can fill to produce types like `List[Int]` and `EventStream[Image]`. Type constructors are analogous to functions of a single parameter, operating on the type level rather than the value level. 38 | 39 | When we write a generic type parameter like the `F` in `trait Functor[F]` we must also tell Scala its kind. As you've probably guessed, no extra annotation means a regular type. To indicate a type constructor taking a single parameter we would write `F[_]`. `F` is the name of the type constructor, and `[_]` indicates it has a single parameter or hole. For example `trait Functor[F[_]]` 40 | 41 | The specifying a kind on a type variable is like giving a type declaration on a regular method parameter. Just like a parameter we don't repeat the kind when we use the type variable. For example, if we write 42 | 43 | ~~~ scala 44 | trait Functor[F[_]] 45 | ~~~ 46 | 47 | this declares a type variable called `F` with kind `[_]` (so a type constructor with a single type parameter). When we use `F` we don't write the `[_]`. Here's an example: 48 | 49 | ~~~ scala 50 | trait Functor[F[_]] { 51 | def map[A,B](fa: F[A])(f: A => B): F[B] 52 | } 53 | ~~~ 54 | 55 | We must enable *higher kinds* to use this feature of Scala, by importing `scala.language.higherKinds`. 56 | 57 | Here's the complete example for `Functor`. 58 | 59 | ~~~ scala 60 | import scala.language.higherKinds 61 | 62 | trait Functor[F[_]] { 63 | def map[A,B](fa: F[A])(f: A => B): F[B] 64 | } 65 | ~~~ 66 | 67 | Using your new knowledge of higher kinded types, implement the rest of the type classes and the type class instances. 68 | 69 |
70 | Once you have the hang of higher-kinded types you should find this fairly mechanical. 71 | 72 | First the type classes themselves. 73 | 74 | ```scala 75 | import scala.language.higherKinds 76 | 77 | trait Functor[F[_]] { 78 | def map[A, B](fa: F[A])(f: A => B): F[B] 79 | } 80 | 81 | trait Monad[F[_]] extends Functor[F] { 82 | def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] 83 | def point[A](a: A): F[A] 84 | } 85 | 86 | trait Applicative[F[_]] extends Functor[F] { 87 | def zip[A, B](fa: F[A])(fb: F[B]): F[(A, B)] 88 | def point[A](a: A): F[A] 89 | } 90 | 91 | trait Scannable[F[_]] { 92 | def scanLeft[A,B](fa: F[A])(b: B)(f: (B,A) => B): F[B] 93 | } 94 | ``` 95 | 96 | Now the instances 97 | 98 | ```scala 99 | object ListInstances { 100 | implicit object list extends Functor[List] with Monad[List] with Applicative[List] with Scannable[List] { 101 | def map[A, B](fa: List[A])(f: A => B): List[B] = 102 | fa.map(f) 103 | def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = 104 | fa.flatMap(f) 105 | def point[A](a: A): List[A] = 106 | List(a) 107 | def zip[A, B](fa: List[A])(fb: List[B]): List[(A, B)] = 108 | fa.zip(fb) 109 | def scanLeft[A,B](fa: List[A])(b: B)(f: (B,A) => B): List[B] = 110 | fa.scanLeft(b)(f) 111 | } 112 | } 113 | 114 | object EventStream { 115 | implicit object eventStream extends Functor[EventStream] with Monad[EventStream] with Applicative[EventStream]with Scannable[EventStream] { 116 | def map[A, B](fa: EventStream[A])(f: A => B): EventStream[B] = 117 | fa.map(f) 118 | def point[A](a: A): EventStream[A] = 119 | EventStream.now(a) 120 | def zip[A, B](fa: EventStream[A])(fb: EventStream[B]): EventStream[(A, B)] = 121 | fa.zip(fb) 122 | def scanLeft[A,B](fa: EventStream[A])(b: B)(f: (B,A) => B): EventStream[B] = 123 | fa.scanLeft(b)(f) 124 | } 125 | } 126 | ``` 127 |
128 | 129 | For extra bonus points implement type class instances for normal types (i.e. for any type `A`). This is known as the identity monad / functor / applicative. Hint: types are not type constructors---they have the wrong kind! However you can get the compiler to consider types as type constructors by declaring a *type synonym* like `type Id[A] = A`. 130 | 131 |
132 | ```scala 133 | object IdInstances { 134 | type Id[A] = A 135 | 136 | implicit object list extends Functor[Id] with Monad[Id] with Applicative[Id] with Scannable[Id] { 137 | def map[A, B](fa: Id[A])(f: A => B): Id[B] = 138 | f(fa) 139 | def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] = 140 | f(fa) 141 | def point[A](a: A): Id[A] = 142 | a 143 | def zip[A, B](fa: Id[A])(fb: Id[B]): Id[(A, B)] = 144 | (fa, fb) 145 | def scanLeft[A,B](fa: Id[A])(b: B)(f: (B,A) => B): Id[B] = 146 | f(b,fa) 147 | } 148 | } 149 | ``` 150 |
151 | 152 | Why is the identity monad / functor /applicative useful? 153 | 154 | 155 |
156 | It allows us to treat normal values as if they were monads etc. and hence abstract over code that uses "real" monads / functors / applicatives and code that doesn't. This often occurs when code is used in some contexts where it runs concurrently (e.g. in `Future`) and in other contexts where it doesn't. 157 |
158 | 159 | Go hog wild, and use your new found powers to write methods producing animations either as a `List` or an `EventStream`. This allows us to easily view individual frames (by producing a `List`) or to view the entire animations (by using an `EventStream`). 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/pages/random/examples.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | ```tut:invisible 4 | object random { 5 | sealed trait Random[A] { 6 | def run(rng: scala.util.Random): A = 7 | this match { 8 | case Primitive(f) => f(rng) 9 | case FlatMap(r, f) => f(r.run(rng)).run(rng) 10 | case Map(r, f) => f(r.run(rng)) 11 | } 12 | 13 | def flatMap[B](f: A => Random[B]): Random[B] = 14 | FlatMap(this, f) 15 | 16 | def map[B](f: A => B): Random[B] = 17 | Map(this, f) 18 | 19 | def zip[B](that: Random[B]): Random[(A,B)] = 20 | for { 21 | a <- this 22 | b <- that 23 | } yield (a, b) 24 | } 25 | object Random { 26 | val double: Random[Double] = 27 | Primitive(rng => rng.nextDouble()) 28 | 29 | val int: Random[Int] = 30 | Primitive(rng => rng.nextInt()) 31 | 32 | /** Generate a value from a normal or Gaussian distribution. */ 33 | val normal: Random[Double] = 34 | Primitive(rng => rng.nextGaussian()) 35 | 36 | /** Create a Random value that always generates `in`. */ 37 | def always[A](in: A): Random[A] = 38 | Primitive(rng => in) 39 | } 40 | final case class Primitive[A](f: scala.util.Random => A) extends Random[A] 41 | final case class FlatMap[A,B](random: Random[A], f: A => Random[B]) extends Random[B] 42 | final case class Map[A,B](random: Random[A], f: A => B) extends Random[B] 43 | } 44 | import random._ 45 | final case class Vec(x: Double, y: Double) 46 | sealed trait Image { 47 | def above(that: Image): Image = ??? 48 | def beside(that: Image): Image = ??? 49 | } 50 | final case class Circle(r: Double) extends Image 51 | final case class Triangle(w: Double, h: Double) extends Image 52 | ``` 53 | 54 | We'll now explore some of the fun things we can do with our new library, building up to the picture in [@fig:random:sierpinski-confection]. 55 | 56 | ![A sierpinski triangle rendered in glorious confectionary colours and random choice of triangles or circles for the leaf images.](src/pages/random/sierpinski-confection.pdf+svg){#fig:random:sierpinski-confection} 57 | 58 | ### Random Choice 59 | 60 | Add to the `Random` API the ability to make a random choice between two (or more, if you're feeling inspired) alternatives. There are many ways you could implement this, so part of the challenge is exploring different alternatives. 61 | 62 |
63 | 64 | The simplest way we can think of is to add a constructor for a `Random[Boolean]`. 65 | 66 | ```tut:book:silent 67 | val boolean: Random[Boolean] = 68 | Primitive(rng => rng.nextBoolean()) 69 | ``` 70 | 71 | Slightly easier to use is a method that chooses between two alternatives like an `if` expression. We can implement it in terms of `boolean` above. This uses a Scala feature, called call-by-name parameters, that we haven't seen so far. 72 | 73 | ```tut:book:silent 74 | def conditional[A](pred: => Boolean)(t: => A)(f: => A): Random[A] = 75 | boolean map { b => 76 | if(b) t else f 77 | } 78 | ``` 79 | 80 | Finally we could implmement a method that will choose one of any number of elements it is passed with equal probability for each. Here we use another new Scala feature, varargs. 81 | 82 | ```tut:book:silent 83 | def oneOf[A](elts: A*): Random[A] = { 84 | val length = elts.length 85 | Primitive { rng => 86 | val index = rng.nextInt(length) 87 | elts(index) 88 | } 89 | } 90 | ``` 91 | 92 | The `oneOf` method provides the most general interface. 93 |
94 | 95 | ### Circle or Square 96 | 97 | Now using the API you build above, create a method 98 | 99 | ```tut:book:silent 100 | def shape(size: Double): Random[Image] = 101 | ??? 102 | ``` 103 | 104 | This method should create either a triangle or a circle based on a random choice. The triangle should have width `size` and the circle should have *diameter* `size`. 105 | 106 |
107 | Here's one solution using the `boolean` interface. 108 | 109 | ```tut:book:silent 110 | def shape(size: Double): Random[Image] = { 111 | for { 112 | b <- boolean 113 | } yield if(b) Triangle(size, size) else Circle(size / 2) 114 | } 115 | ``` 116 | 117 | Using `oneOf` we can write much simpler code. 118 | 119 | ```tut:book:silent 120 | def shape(size: Double): Random[Image] = 121 | oneOf(Triangle(size, size), Circle(size / 2)) 122 | ``` 123 |
124 | 125 | 126 | ### Sierpinski Triangle 127 | 128 | We're now ready to draw a Sierpinski triangle, though not yet with fun colors. The Sierpinski triangle is an example of a structural recursion over the natural numbers. We're familiar with structural recursion, but how does it work over the natural numbers (and what are the natural numbers)? 129 | 130 | The natural numbers are the integers from 0 upwards. In other words 0, 1, 2, 3, ... We can define the natural numbers recursively as, a natural number `N` is 131 | 132 | - `0`; OR 133 | - `1 + M`, where `M` is a natural number. 134 | 135 | This means the skeleton for structural recursion on the natural numbers is 136 | 137 | ```scala 138 | def doSomething(n: Int) = 139 | this match { 140 | case 0 => ??? // Base case here 141 | case n => ??? doSomething(n - 1) // Recursive case here 142 | } 143 | ``` 144 | 145 | For example, to draw concentric circles we can write 146 | 147 | ```scala 148 | def singleCircle(n: Int): Image = 149 | Circle(50 + 5 * n) lineColor (Color.red fadeOut (n / 20).normalized) 150 | 151 | def concentricCircles(n: Int): Image = 152 | n match { 153 | case 0 => singleCircle(n) 154 | case n => singleCircle(n) on concentricCircles(n - 1) 155 | } 156 | ``` 157 | 158 | Implement the Sierpinski triangle using structural recursion over the natural numbers. Hint: the pattern for creating the Sierpinski triangle is `a on (b beside c)`. 159 | 160 |
161 | Here's our implementation, which creates a Sier*pink*si triangle. The `size` parameter is optional but allows a bit more creative freedom. 162 | 163 | ```scala 164 | def triangle(size: Double): Image = { 165 | println(s"Creating a triangle") 166 | Triangle(size, size) lineColor Color.magenta 167 | } 168 | 169 | def sierpinski(n: Int, size: Double): Image = { 170 | println(s"Creating a Sierpinski with n = $n") 171 | n match { 172 | case 0 => 173 | triangle(size) 174 | case n => 175 | val smaller = sierpinski(n - 1, size/2) 176 | smaller above (smaller beside smaller) 177 | } 178 | } 179 | ``` 180 |
181 | 182 | Now we'll turn it up to eleven and create a Sierpinski triangle where the leaves are randomly circles or triangles. We can use `shape` that we defined earlier. 183 | 184 | Implement 185 | 186 | ```tut:book:silent 187 | def randomSierpinski(n: Int): Random[Image] = 188 | ??? 189 | ``` 190 | 191 | Note that this method returns a `Random[Image]`, *NOT* an `Image`. 192 | 193 |
194 | The trick here is working how to combine the `Random[Image]` in the recursive case. We find this easier to write using a for comprehension, rather than nested `flatMap` and `map` calls. 195 | 196 | ```tut:book:silent 197 | def randomSierpinski(n: Int, size: Double): Random[Image] = { 198 | n match { 199 | case 0 => 200 | shape(size) 201 | case n => 202 | val smaller = randomSierpinski(n - 1, size/2) 203 | for { 204 | a <- smaller 205 | b <- smaller 206 | c <- smaller 207 | } yield a above (b beside c) 208 | } 209 | } 210 | ``` 211 |
212 | 213 | ### Random Colors 214 | 215 | Now we're going to add the final step, randomising the color. We're not going to give directions here, leaving you to explore, but here are a few tips. 216 | 217 | - It's much easier to work with colors in the `HSLA` (hue, saturation, lightness, and alpha) representation that the `RGBA` one. 218 | - The `hue` parameter to the `HSLA` constructor is an angle that indicates how far around the color wheel we should turn. You can convert a number between 0.0 and 1.0 to an `Angle` using the `turns` syntax. 1.0 means a full turn around the circle, and 0.0 is no turn. 219 | - The `saturation` parameter determines how intense the color is. Values between about 0.5 and 1.0 are good. Use the `normalized` syntax to create a `Normalized` from a `Double`. 220 | - The `lightness` parameter determines how far between black and white the color. You probably want moderate values here. 221 | - The final parameter, `alpha`, determines transparency. You can play with this if you want or just set it to 1.0. 222 | 223 | Here's an example of constructing a color in the `HSLA` representation. 224 | 225 | ```scala 226 | import doodle.syntax._ 227 | 228 | val hue = 0.5.turns // halfway around the color wheel, a cyan 229 | val saturation = 0.7.normalized // slightly desaturated, a bit pastel 230 | val lightness = 0.5.normalized // not too light, not too dark 231 | val alpha = 1.0.normalized 232 | val color = HSLA(hue, saturation, lightness, alpha) 233 | ``` 234 | 235 | Start by creating a method that will construct a `Random[Color]`. 236 | 237 |
238 | There's no right answer here, but you might be interested to see how we made the reddish confectionary colors in [@fig:random:sierpinski-confection]. We transform `Random.double` to generate values within a smaller range. 239 | 240 | Note that `reddish` is a `val`, not a `def`. A `Random[A]` represents a computation that when `run` will generate values of type `A`, so we don't need to use `def` to delay evaluation. 241 | 242 | ```scala 243 | val reddish: Random[Color] = { 244 | val hue = Random.double map { d => (d - 0.5) * 0.2 } 245 | val saturation = Random.double map { s => s * 0.3 + 0.4 } 246 | val lightness = Random.double map { l => l * 0.3 + 0.3 } 247 | 248 | for { 249 | h <- hue 250 | s <- saturation 251 | l <- lightness 252 | } yield HSLA(h.turns, s.normalized, l.normalized, 1.normalized) 253 | } 254 | ``` 255 |
256 | 257 | Now use your random color generator to a glorious Sierpinski image. 258 | 259 |
260 | Here's the complete code that generates the image we saw at the start. 261 | 262 | ```scala 263 | object RandomSierpinski { 264 | val reddish: Random[Color] = { 265 | val hue = Random.double map { d => (d - 0.5) * 0.2 } 266 | val saturation = Random.double map { s => s * 0.3 + 0.4 } 267 | val lightness = Random.double map { l => l * 0.3 + 0.3 } 268 | 269 | for { 270 | h <- hue 271 | s <- saturation 272 | l <- lightness 273 | } yield HSLA(h.turns, s.normalized, l.normalized, 1.normalized) 274 | } 275 | 276 | def triangle(size: Double): Image = { 277 | Triangle(size, size) 278 | } 279 | def circle(size: Double): Image = { 280 | Circle(size / 2) 281 | } 282 | 283 | def shape(size: Double): Random[Image] = { 284 | for { 285 | b <- Random.boolean 286 | s = if(b) triangle(size) else circle(size) 287 | h <- reddish 288 | } yield s fillColor h 289 | } 290 | 291 | def sierpinski(n: Int, size: Double): Random[Image] = { 292 | n match { 293 | case 0 => 294 | shape(size) 295 | case n => 296 | val smaller = sierpinski(n - 1, size/2) 297 | for { 298 | a <- smaller 299 | b <- smaller 300 | c <- smaller 301 | } yield a above (b beside c) 302 | } 303 | } 304 | 305 | val image = sierpinski(5, 512).run(scala.util.Random) 306 | } 307 | ``` 308 |
309 | 310 | [vec]: https://github.com/underscoreio/doodle-case-study/blob/master/shared/src/main/scala/doodle/core/Vec.scala 311 | -------------------------------------------------------------------------------- /src/pages/foundation/implementation.md: -------------------------------------------------------------------------------- 1 | ## Implementation 2 | 3 | We're now ready to implement the complete system. We have provided a framework of code to build on, which you can find on [Github](https://github.com/underscoreio/doodle-case-study). The code you'll interact with can found within `src/main/scala/doodle`. Within this directory you'll find the following sub-directories: 4 | 5 | - `core`, which is where most of your code should go, and where you'll find many useful utilities; 6 | - `backend`, containing the `Canvas` interface; 7 | - `example`, containing a few simple examples; 8 | - `jvm`, containing an implementation of `Canvas` for the JVM's Java2D library; and 9 | - `syntax`, which has a few utilities that you can ignore for now. 10 | 11 | 12 | ### Implementation Techniques 13 | 14 | There is something that you might find a bit unexpected in the implementation of your library: nothing should be drawn until you call the `draw` method. This is necessary as we need to know the entire image before we can layout its components. Concretely, if we're rendering one image beside another, we need to know their heights so we can vertically center them. If we draw images as soon as they were created we won't know that they should be laid out in this way. The upshot is when we call, say, `image1 beside image2`, we need to represent this as a data structure somehow. 15 | 16 | The idea of separating the description of the computation (the image data structure) from the process that carries it out (drawing) is a classic functional programming technique, and one we will see multiple times. Being functional programming, we have a fancy word for this: *reification*. Reification means to make concrete something that was abstract. In our case we're turning what seems like an action (e.g. `beside`) into a concrete data structure. 17 | 18 | When we come to do layout we need to know the height and width of each component image. We can easily calculate this with bounding boxes---they are easy to implement and sufficient if we only allow horizontal and vertical composition. 19 | 20 | 21 | ### Your Mission 22 | 23 | Your final mission is to finish off the library: 24 | 25 | - implement the methods for combining images; and 26 | - implement `draw` 27 | 28 | Along the way you will probably have to implement a bounding box abstraction. 29 | 30 | If you're not sure where to start, follow along with the rest of this section. If you think you can do it yourself, get stuck in! 31 | 32 | When you have finished, you can compare your implementation to ours by switching to the [feature/atoms-and-operations branch](https://github.com/underscoreio/doodle-case-study/tree/feature/atoms-and-operations) in your fork of the case study repository. 33 | 34 | #### Drawing Something 35 | 36 | Let's start with this representation: 37 | 38 | ```scala 39 | import doodle.backend.Canvas 40 | 41 | sealed trait Image { 42 | def on(that: Image): Image = 43 | ??? 44 | 45 | def beside(that: Image): Image = 46 | ??? 47 | 48 | def above(that: Image): Image = 49 | ??? 50 | 51 | def draw(canvas: Canvas): Unit = 52 | ??? 53 | } 54 | final case class Circle(radius: Double) extends Image 55 | final case class Rectangle(width: Double, height: Double) extends Image 56 | ``` 57 | 58 | Our first mission is to get some visible progress, so we'll implement `draw`. We're completely ignoring layout at this point, so you can just draw images at the origin (or anywhere else that takes your fancy). 59 | 60 | What pattern will we use in the implementation? 61 | 62 |
63 | `Image` is an algebraic data type, so we'll use structural recursion. 64 |
65 | 66 | Implement `draw`. 67 | 68 |
69 | Save this file as `Image.scala` in `shared/src/main/scala/doodle/core` 70 | 71 | ```scala 72 | package doodle 73 | package core 74 | 75 | import doodle.backend.Canvas 76 | 77 | sealed trait Image { 78 | def on(that: Image): Image = 79 | ??? 80 | 81 | def beside(that: Image): Image = 82 | ??? 83 | 84 | def above(that: Image): Image = 85 | ??? 86 | 87 | def draw(canvas: Canvas): Unit = 88 | this match { 89 | case Circle(r) => canvas.circle(0.0, 0.0, r) 90 | case Rectangle(w,h) => canvas.rectangle(-w/2, h/2, w/2, -h/2) 91 | } 92 | } 93 | final case class Circle(radius: Double) extends Image 94 | final case class Rectangle(width: Double, height: Double) extends Image 95 | ``` 96 | 97 | With this in-place you should be able to render some simple images. From the sbt console, try 98 | 99 | ```scala 100 | val canvas = Java2DCanvas.canvas 101 | Circle(10).draw(canvas) 102 | ``` 103 |
104 | 105 | Now we can draw stuff on the screen, let's implement the methods to combine images: `above`, `beside`, and `on`. Each method can be implemented in one line, but there is a crucial leap you need to make to implement them. Have a think about it, and read the solution when you've worked it out. 106 | 107 |
108 | We need to represent the layout operations as data, which means we need to extend the `Image` algebraic data type with cases for layout. Then the method bodies just constructs the correct instance that represents the operation. 109 | 110 | When we add these new cases to our algebraic data type we also need to add them to `draw` as well (as per the structural recursion pattern, but the compiler will complain in any case if we forget them.) Right now we're not actually doing any layout so we just recurse down the data structure and draw the leaves. 111 | 112 | ```scala 113 | package doodle 114 | package core 115 | 116 | import doodle.backend.Canvas 117 | 118 | sealed trait Image { 119 | def on(that: Image): Image = 120 | On(this, that) 121 | 122 | def beside(that: Image): Image = 123 | Beside(this, that) 124 | 125 | def above(that: Image): Image = 126 | Above(this, that) 127 | 128 | def draw(canvas: Canvas): Unit = 129 | this match { 130 | case Circle(r) => canvas.circle(0.0, 0.0, r) 131 | case Rectangle(w,h) => canvas.rectangle(-w/2, h/2, w/2, -h/2) 132 | case Above(a, b) => a.draw(canvas); b.draw(canvas) 133 | case On(o, u) => o.draw(canvas); u.draw(canvas) 134 | case Beside(l, r) => l.draw(canvas); r.draw(canvas) 135 | } 136 | } 137 | final case class Circle(radius: Double) extends Image 138 | final case class Rectangle(width: Double, height: Double) extends Image 139 | final case class Above(above: Image, below: Image) extends Image 140 | final case class On(on: Image, under: Image) extends Image 141 | final case class Beside(left: Image, right: Image) extends Image 142 | ``` 143 |
144 | 145 | Now we are ready to tackle the actual layout algorithm. To work out where every `Image` should be placed we need to know how much space it takes up. We can implement bounding boxes to give us this information. A bounding box is simply a rectangle that encloses an image. Bounding boxes are not precise, but they are sufficient for our choice of primitive images and layout methods. 146 | 147 | When combining bounding boxes we will need to know the coordinate system we use to represent their coordinates. We can't use the global canvas coordinate system---the reason we're implementing this system is to work out the location of images in the global system---so we need to use a coordinate system that is local to each image. A simple choice is to say the origin is the center of the bounding box. 148 | 149 | We can represent a bounding box as a class 150 | 151 | ```scala 152 | final case class BoundingBox(left: Double, top: Double, right: Double, bottom: Double) 153 | ``` 154 | 155 | What methods should be have on `BoundingBox`? 156 | 157 |
158 | We will want methods to combine bounding boxes that mirror the methods to combine `Images`. So, `above`, `beside`, and `on`. We might also find it useful to store the width and height. 159 |
160 | 161 | Implement these methods on `BoundingBox`. 162 | 163 |
164 | ```scala 165 | package doodle 166 | package core 167 | 168 | final case class BoundingBox(left: Double, top: Double, right: Double, bottom: Double) { 169 | val height: Double = top - bottom 170 | 171 | val width: Double = right - left 172 | 173 | def above(that: BoundingBox): BoundingBox = 174 | BoundingBox( 175 | this.left min that.left, 176 | (this.height + that.height) / 2, 177 | this.right max that.right, 178 | -(this.height + that.height) / 2 179 | ) 180 | 181 | def beside(that: BoundingBox): BoundingBox = 182 | BoundingBox( 183 | -(this.width + that.width) / 2, 184 | this.top max that.top, 185 | (this.width + that.width) / 2, 186 | this.bottom min that.bottom 187 | ) 188 | 189 | def on(that: BoundingBox): BoundingBox = 190 | BoundingBox( 191 | this.left min that.left, 192 | this.top max that.top, 193 | this.right max that.right, 194 | this.bottom min that.bottom 195 | ) 196 | } 197 | ``` 198 |
199 | 200 | Now implement a `boundingBox` method (or instance variable, as you see fit) on `Image` that returns the bounding box for the image. 201 | 202 |
203 | More structural recursion! Note we can implement `boundingBox` as an instance variable as it is fixed for all time, and therefore we don't need to recalculate it. 204 | 205 | ```scala 206 | package doodle 207 | package core 208 | 209 | import doodle.backend.Canvas 210 | 211 | sealed trait Image { 212 | val boundingBox: BoundingBox = 213 | this match { 214 | case Circle(r) => 215 | BoundingBox(-r, r, r, -r) 216 | case Rectangle(w, h) => 217 | BoundingBox(-w/2, h/2, w/2, -h/2) 218 | case Above(a, b) => 219 | a.boundingBox above b.boundingBox 220 | case On(o, u) => 221 | o.boundingBox on u.boundingBox 222 | case Beside(l, r) => 223 | l.boundingBox beside r.boundingBox 224 | } 225 | 226 | def on(that: Image): Image = 227 | On(this, that) 228 | 229 | def beside(that: Image): Image = 230 | Beside(this, that) 231 | 232 | def above(that: Image): Image = 233 | Above(this, that) 234 | 235 | def draw(canvas: Canvas): Unit = 236 | this match { 237 | case Circle(r) => canvas.circle(0.0, 0.0, r) 238 | case Rectangle(w,h) => canvas.rectangle(-w/2, h/2, w/2, -h/2) 239 | case Above(a, b) => a.draw(canvas); b.draw(canvas) 240 | case On(o, u) => o.draw(canvas); u.draw(canvas) 241 | case Beside(l, r) => l.draw(canvas); r.draw(canvas) 242 | } 243 | } 244 | final case class Circle(radius: Double) extends Image 245 | final case class Rectangle(width: Double, height: Double) extends Image 246 | final case class Above(above: Image, below: Image) extends Image 247 | final case class On(on: Image, under: Image) extends Image 248 | final case class Beside(left: Image, right: Image) extends Image 249 | ``` 250 |
251 | 252 | Now we have enough information to do layout. Our `Image` is a tree. The top level boudning box tells us how big the entire image is. We can decide the origin of this bounding box is the origin of the global canvas coordinate system. Then we can walk down the tree (yet more structural recursion) translating the local coordinate system into the global system. When we reach a leaf node (so, a primite image), we can actually draw it. We already have the skeleton for this in `draw`---we just need to pass along the mapping from the local coordinate system to the global one. We can use a method like 253 | 254 | ```scala 255 | def draw(canvas: Canvas, originX: Double, originY: Double): Unit = 256 | ??? 257 | ``` 258 | 259 | which we call from the standard `draw` with the origin coordinates initially set to zero. 260 | 261 | Now implement this variant of `draw`. 262 | 263 |
264 | Below is the complete code. 265 | 266 | ```scala 267 | package doodle 268 | package core 269 | 270 | import doodle.backend.Canvas 271 | 272 | sealed trait Image { 273 | val boundingBox: BoundingBox = 274 | this match { 275 | case Circle(r) => 276 | BoundingBox(-r, r, r, -r) 277 | case Rectangle(w, h) => 278 | BoundingBox(-w/2, h/2, w/2, -h/2) 279 | case Above(a, b) => 280 | a.boundingBox above b.boundingBox 281 | case On(o, u) => 282 | o.boundingBox on u.boundingBox 283 | case Beside(l, r) => 284 | l.boundingBox beside r.boundingBox 285 | } 286 | 287 | def on(that: Image): Image = 288 | On(this, that) 289 | 290 | def beside(that: Image): Image = 291 | Beside(this, that) 292 | 293 | def above(that: Image): Image = 294 | Above(this, that) 295 | 296 | def draw(canvas: Canvas): Unit = 297 | draw(canvas, 0.0, 0.0) 298 | 299 | def draw(canvas: Canvas, originX: Double, originY: Double): Unit = 300 | this match { 301 | case Circle(r) => 302 | canvas.circle(0.0, 0.0, r) 303 | case Rectangle(w,h) => 304 | canvas.rectangle(-w/2, h/2, w/2, -h/2) 305 | case Above(a, b) => 306 | val box = this.boundingBox 307 | val aBox = a.boundingBox 308 | val bBox = b.boundingBox 309 | 310 | val aboveOriginY = originY + box.top - (aBox.height / 2) 311 | val belowOriginY = originY + box.bottom + (bBox.height / 2) 312 | 313 | a.draw(canvas, originX, aboveOriginY) 314 | b.draw(canvas, originX, belowOriginY) 315 | case On(o, u) => 316 | o.draw(canvas, originX, originY) 317 | u.draw(canvas, originX, originY) 318 | case Beside(l, r) => 319 | val box = this.boundingBox 320 | val lBox = l.boundingBox 321 | val rBox = r.boundingBox 322 | 323 | val leftOriginX = originX + box.left + (lBox.width / 2) 324 | val rightOriginX = originX + box.right - (rBox.width / 2) 325 | l.draw(canvas, leftOriginX, originY) 326 | r.draw(canvas, rightOriginX, originY) 327 | } 328 | } 329 | final case class Circle(radius: Double) extends Image 330 | final case class Rectangle(width: Double, height: Double) extends Image 331 | final case class Above(above: Image, below: Image) extends Image 332 | final case class On(on: Image, under: Image) extends Image 333 | final case class Beside(left: Image, right: Image) extends Image 334 | ``` 335 |
336 | -------------------------------------------------------------------------------- /src/pages/animation/implementation.md: -------------------------------------------------------------------------------- 1 | ## Implementation 2 | 3 | We're now going to implement `Stream`. 4 | We're going to tackle the implementation in a number of steps: 5 | 6 | 1. We will start by ignoring concurrency, and assuming all our data arrives from an `Iterator` (a built-in Scala type). With this assumption, and by reducing our interface a little bit, we can quickly build a working system. 7 | 8 | 2. We will add `scanLeft`, which requires we add state and leads to a slighly more complicated implementation. 9 | 10 | 3. We will add concurrency, which makes our implementation substantially more complicated but makes it useful for our end-goal: animating shapes. 11 | 12 | A bit of terminology will be useful. 13 | We will say that data flows from *source* to *sink*. 14 | Moving from source to sink is moving *downstream*, and the reverse is going *upstream*. 15 | 16 | 17 | ### Basics 18 | 19 | Let's start by implementing the following API: 20 | 21 | ```tut:silent:book 22 | object streamWrapper { 23 | sealed trait Stream[A] { 24 | def map[B](f: A => B): Stream[B] 25 | 26 | def zip[B](that: Stream[B]): Stream[(A,B)] 27 | 28 | def runFold[B](zero: B)(f: (B, A) => B): B 29 | } 30 | object Stream { 31 | def fromIterator[A](source: Iterator[A]): Stream[A] = 32 | ??? 33 | } 34 | }; import streamWrapper._ 35 | ``` 36 | 37 | Notice that we've dropped `scanLeft`, and we've renamed `join` to `zip`. 38 | We'll see why later. 39 | 40 | The API has the following components: 41 | 42 | - we create a `Stream` using `fromIterator`; 43 | - we transform a `Stream` using `map` and `zip`; and 44 | - we run a `Stream` using `runFold`. 45 | 46 | You can implement this using exactly the same technique we used with `Image`: 47 | 48 | - reify the majority of the API; and 49 | - implement an "interpreter" in `runFold`. 50 | 51 | We encourage you to try this on your own before reading our solution. 52 | Our solution is broken into stages so you can refer to the different parts if you get stuck. 53 | 54 | First we reify the API. 55 | Remember that reification means "turn into data". 56 | This basically means creating an algebraic data type. 57 | 58 |
59 | ```tut:silent:book 60 | object streamWrapper { 61 | sealed trait Stream[A] { 62 | import Stream._ 63 | 64 | def zip[B](that: Stream[B]): Stream[(A,B)] = 65 | Zip(this, that) 66 | 67 | def map[B](f: A => B): Stream[B] = 68 | Map(this, f) 69 | 70 | def runFold[B](zero: B)(f: (B, A) => B): B = 71 | ??? 72 | } 73 | object Stream { 74 | def fromIterator[A](source: Iterator[A]): Stream[A] = 75 | FromIterator(source) 76 | 77 | def always[A](element: A): Stream[A] = 78 | FromIterator(Iterator.continually(element)) 79 | 80 | def apply[A](elements: A*): Stream[A] = 81 | FromIterator(Iterator(elements: _*)) 82 | 83 | // Stream algebraic data type 84 | final case class Zip[A,B](left: Stream[A], right: Stream[B]) extends Stream[(A,B)] 85 | final case class Map[A,B](source: Stream[A], f: A => B) extends Stream[B] 86 | final case class FromIterator[A](source: Iterator[A]) extends Stream[A] 87 | } 88 | }; import streamWrapper._ 89 | ``` 90 |
91 | 92 | Now we can implement the interpreter in `runFold`. 93 | We want to process elements one at a time, like we will in the full system, so the straightforward structural recursion approach won't work. (Try it and see---you'll end up processing all elements at once.) 94 | We will first implement a method `next` that gets the next element from the `Stream`. 95 | This is a structural recursion so is straightforward to implement. 96 | 97 |
98 | ```tut:silent:book 99 | object streamWrapper { 100 | sealed trait Stream[A] { 101 | import Stream._ 102 | // etc ... 103 | 104 | def runFold[B](zero: B)(f: (B, A) => B): B = { 105 | def next[A](stream: Stream[A]): A = 106 | stream match { 107 | case FromIterator(source) => source.next() 108 | case Map(source, f) => f(next(source)) 109 | case Zip(left, right) => (next(left), next(right)) 110 | } 111 | 112 | ??? 113 | } 114 | } 115 | object Stream { 116 | // etc ... 117 | 118 | // Stream algebraic data type 119 | final case class Zip[A,B](left: Stream[A], right: Stream[B]) extends Stream[(A,B)] 120 | final case class Map[A,B](source: Stream[A], f: A => B) extends Stream[B] 121 | final case class FromIterator[A](source: Iterator[A]) extends Stream[A] 122 | } 123 | }; import streamWrapper._ 124 | ``` 125 |
126 | 127 | Now we implement `runFold` using `next`. 128 | 129 |
130 | ```tut:silent:book 131 | object streamWrapper { 132 | sealed trait Stream[A] { 133 | import Stream._ 134 | // etc ... 135 | 136 | def runFold[B](zero: B)(f: (B, A) => B): B = { 137 | def next[A](stream: Stream[A]): A = 138 | stream match { 139 | case FromIterator(source) => source.next() 140 | case Map(source, f) => f(next(source)) 141 | case Zip(left, right) => (next(left), next(right)) 142 | } 143 | 144 | // Never terminates 145 | def loop(result: B): B = { 146 | loop(f(result, next(this))) 147 | } 148 | 149 | loop(zero) 150 | } 151 | } 152 | object Stream { 153 | // etc ... 154 | 155 | // Stream algebraic data type 156 | final case class Zip[A,B](left: Stream[A], right: Stream[B]) extends Stream[(A,B)] 157 | final case class Map[A,B](source: Stream[A], f: A => B) extends Stream[B] 158 | final case class FromIterator[A](source: Iterator[A]) extends Stream[A] 159 | } 160 | }; import streamWrapper._ 161 | ``` 162 |
163 | 164 | ```tut:invisible 165 | object streamWrapper { 166 | sealed trait Stream[A] { 167 | import Stream._ 168 | 169 | def zip[B](that: Stream[B]): Stream[(A,B)] = 170 | Zip(this, that) 171 | 172 | def map[B](f: A => B): Stream[B] = 173 | Map(this, f) 174 | 175 | def runFold[B](zero: B)(f: (B, A) => B): B = { 176 | def next[A](stream: Stream[A]): A = 177 | stream match { 178 | case FromIterator(source) => source.next() 179 | case Map(source, f) => f(next(source)) 180 | case Zip(left, right) => (next(left), next(right)) 181 | } 182 | 183 | // Never terminates 184 | def loop(result: B): B = { 185 | loop(f(result, next(this))) 186 | } 187 | 188 | loop(zero) 189 | } 190 | } 191 | object Stream { 192 | def fromIterator[A](source: Iterator[A]): Stream[A] = 193 | FromIterator(source) 194 | 195 | def always[A](element: A): Stream[A] = 196 | FromIterator(Iterator.continually(element)) 197 | 198 | def apply[A](elements: A*): Stream[A] = 199 | FromIterator(Iterator(elements: _*)) 200 | 201 | // Stream algebraic data type 202 | final case class Zip[A,B](left: Stream[A], right: Stream[B]) extends Stream[(A,B)] 203 | final case class Map[A,B](source: Stream[A], f: A => B) extends Stream[B] 204 | final case class FromIterator[A](source: Iterator[A]) extends Stream[A] 205 | } 206 | }; import streamWrapper._ 207 | ``` 208 | 209 | This code compiles but has one flaw: we never check if our `Iterator` has more data (using the `hasNext` method.) 210 | Try running, for example, 211 | 212 | ```tut:fail:book 213 | Stream(1, 2, 3).runFold(0)(_ + _) 214 | ``` 215 | 216 | Why has our method created broken code? 217 | It's because `Iterator` is a stateful abstraction and not an algebraic data type, so the type system and our usual techniques don't work for us when using `Iterator`. 218 | We have to rely on testing and our memory to get it right. 219 | 220 | Let's modify `next` to return an `Option` indicating if more data is available, and add the correct check to the `FromIterator` case in `next`. 221 | 222 |
223 | The full working code is 224 | 225 | ```tut:silent:book 226 | object streamWrapper { 227 | sealed trait Stream[A] { 228 | import Stream._ 229 | 230 | def zip[B](that: Stream[B]): Stream[(A,B)] = 231 | Zip(this, that) 232 | 233 | def map[B](f: A => B): Stream[B] = 234 | Map(this, f) 235 | 236 | def runFold[B](zero: B)(f: (B, A) => B): B = { 237 | // Use `Option` to indicate if the stream has terminated. 238 | // `None` indicates no more values are available. 239 | def next[A](stream: Stream[A]): Option[A] = 240 | stream match { 241 | case FromIterator(source) => 242 | if(source.hasNext) Some(source.next()) else None 243 | case Map(source, f) => 244 | next(source).map(f) 245 | case Zip(left, right) => 246 | for { 247 | l <- next(left) 248 | r <- next(right) 249 | } yield (l, r) 250 | } 251 | 252 | def loop(result: B): B = 253 | next(this) match { 254 | case None => result 255 | case Some(a) => 256 | loop(f(result, a)) 257 | } 258 | 259 | loop(zero) 260 | } 261 | } 262 | object Stream { 263 | def fromIterator[A](source: Iterator[A]): Stream[A] = 264 | FromIterator(source) 265 | 266 | def always[A](element: A): Stream[A] = 267 | FromIterator(Iterator.continually(element)) 268 | 269 | def apply[A](elements: A*): Stream[A] = 270 | FromIterator(Iterator(elements: _*)) 271 | 272 | // Stream algebraic data type 273 | final case class Zip[A,B](left: Stream[A], right: Stream[B]) extends Stream[(A,B)] 274 | final case class Map[A,B](source: Stream[A], f: A => B) extends Stream[B] 275 | final case class FromIterator[A](source: Iterator[A]) extends Stream[A] 276 | } 277 | }; import streamWrapper._ 278 | ``` 279 |
280 | 281 | 282 | ### Adding State 283 | 284 | Now we're going to add `scanLeft`. 285 | Remember the signature for `scanLeft` is 286 | 287 | ```scala 288 | def scanLeft[B](seed: B)(f: (B,A) => B): Stream[B] 289 | ``` 290 | 291 | Implementing `scanLeft` requires we keep state between successive calls in our interpreter. 292 | If we reify `scanLeft` and add state we'll break substitution (try it and see!) 293 | One way to implement a better system is to transform `Stream` into another representation, and that representation can contain state. 294 | Because this internal representation is entirely hidden within `runFold` it can never be observed from outside the system and hence it's ok to use. 295 | 296 | Here's how we did it. 297 | 298 |
299 | `Stream` is defined mostly as before but now we convert it to `Observable` within `runFold`. 300 | 301 | ```tut:silent:book 302 | object streamWrapper { 303 | sealed trait Observable[A]{ 304 | def runFold[B](zero: B)(f: (B, A) => B): B = { 305 | def next[A](observable: Observable[A]): Option[A] = 306 | stream match { 307 | case FromIterator(source) => 308 | if(source.hasNext) Some(source.next()) else None 309 | case Map(source, f) => 310 | next(source).map(f) 311 | case Zip(left, right) => 312 | for { 313 | l <- next(left) 314 | r <- next(right) 315 | } yield (l, r) 316 | case s @ ScanLeft(source, z, f) => 317 | s.zero = f(z, next(source)) 318 | s.zero 319 | } 320 | 321 | def loop(result: B): B = 322 | next(this) match { 323 | case None => result 324 | case Some(a) => 325 | loop(f(result, a)) 326 | } 327 | 328 | loop(zero) 329 | } 330 | } 331 | object Observable { 332 | def fromStream(source: Stream[A]): Observable[A] = { 333 | source match { 334 | case Stream.Map(source, f) => Map(fromStream(source), f) 335 | case Stream.ScanLeft(source, zero, f) => ScanLeft(fromStream(source), zero, f) 336 | case Stream.Zip(left, right) => Zip(fromStream(left), fromStream(right)) 337 | case Stream.FromIterator(handler) => Source(source) 338 | } 339 | 340 | // Observable algebraic data type 341 | final case class ScanLeft[A,B](source: Observable[A], var zero: B, f: (B,A) => B) 342 | final case class Zip[A,B](left: Observable[A], right: Observable[B]) extends Observable[(A,B)] 343 | final case class Map[A,B](source: Observable[A], f: A => B) extends Observable[B] 344 | final case class FromIterator[A](source: Iterator[A]) extends Observable[A] 345 | } 346 | 347 | sealed trait Stream[A] { 348 | import Stream._ 349 | 350 | def zip[B](that: Stream[B]): Stream[(A,B)] = 351 | Zip(this, that) 352 | 353 | def map[B](f: A => B): Stream[B] = 354 | Map(this, f) 355 | 356 | def scanLeft[B](zero: B)(f: (B, A) => B): Stream[B] 357 | 358 | def runFold[B](zero: B)(f: (B,A) => B): B = { 359 | Observable.fromStream(this).runFold(zero)(f) 360 | } 361 | } 362 | object Stream { 363 | def fromIterator[A](source: Iterator[A]): Stream[A] = 364 | FromIterator(source) 365 | 366 | def always[A](element: A): Stream[A] = 367 | FromIterator(Iterator.continually(element)) 368 | 369 | def apply[A](elements: A*): Stream[A] = 370 | FromIterator(Iterator(elements: _*)) 371 | 372 | // Stream algebraic data type 373 | final case class Zip[A,B](left: Stream[A], right: Stream[B]) extends Stream[(A,B)] 374 | final case class Map[A,B](source: Stream[A], f: A => B) extends Stream[B] 375 | final case class FromIterator[A](source: Iterator[A]) extends Stream[A] 376 | } 377 | }; import streamWrapper._ 378 | ``` 379 |
380 | 381 | This solution has a lot of repetition but it's easy code to read and write, as it's still using all our familiar patterns. 382 | 383 | 384 | ### Adding Concurrency 385 | 386 | We're now going to add concurrency to our system, meaning our inputs will arrive over time. 387 | This entails two changes: 388 | 389 | - our inputs will come from other threads so we need to worry about concurrent access; and 390 | - we'll implement new methods that deal with joining concurrent streams. 391 | 392 | #### Input 393 | 394 | Our input will arrive from callbacks. 395 | We need to add a new way to create `Streams`, passing in a callback handler with which the interpreter will register a callback. 396 | Concretely this means adding to the `Stream` companion object the method 397 | 398 | ```tut:silent:book 399 | def fromCallbackHandler[A](handler: (A => Unit) => Unit): Stream[A] = 400 | ??? 401 | ``` 402 | 403 | It's easy enough to reify this method but what should we do when the callback is called? 404 | We should assume that the callback will be called from a different thread, which means we'll need to store the data somewhere till we're ready to use it. 405 | There is also the possibility of a race condition: we could try to read and write the data at the same time. 406 | To guard against this we need to use a data structure that is safe for concurrent access. 407 | A `java.util.concurrent.ArrayBlockingQueue` is a simple choice. 408 | 409 | Our implementation is as follows: 410 | 411 | - When an `Observable` `FromCallbackHandler` is constructured it registers a callback with the provided handler. This callback stores any value it receives into an `ArrayBlockingQueue`. 412 | 413 | - When `next` processes a `FromCallbackHandler` it `takes` value from the `ArrayBlockingQueue`. 414 | 415 | In a real system we'd want a more flexible implementation, to allow the user to specify the exact queuing semantics (e.g. how big should our queue be, and what is our behaviour on the putting side when the queue is full?) 416 | 417 | For our purposes the following implementation does the job. 418 | 419 |
420 | ```tut:silent:book 421 | object streamWrapper { 422 | sealed trait Observable[A]{ 423 | def runFold[B](zero: B)(f: (B, A) => B): B = { 424 | def next[A](observable: Observable[A]): Option[A] = 425 | stream match { 426 | case FromIterator(source) => 427 | if(source.hasNext) Some(source.next()) else None 428 | case FromCallbackHandler(h, q) => 429 | Some(q.take()) 430 | case Map(source, f) => 431 | next(source).map(f) 432 | case Zip(left, right) => 433 | for { 434 | l <- next(left) 435 | r <- next(right) 436 | } yield (l, r) 437 | case s @ ScanLeft(source, z, f) => 438 | s.zero = f(z, next(source)) 439 | s.zero 440 | } 441 | 442 | def loop(result: B): B = 443 | next(this) match { 444 | case None => result 445 | case Some(a) => 446 | loop(f(result, a)) 447 | } 448 | 449 | loop(zero) 450 | } 451 | } 452 | object Observable { 453 | import java.util.concurrent.{BlockingQueue, ArrayBlockingQueue} 454 | 455 | def fromStream(source: Stream[A]): Observable[A] = { 456 | source match { 457 | case Stream.Map(source, f) => Map(fromStream(source), f) 458 | case Stream.ScanLeft(source, zero, f) => ScanLeft(fromStream(source), zero, f) 459 | case Stream.Zip(left, right) => Zip(fromStream(left), fromStream(right)) 460 | case Stream.FromIterator(source) => FromIterator(source) 461 | case Stream.FromCallbackHandler(handler) => FromCallbackHandler(handler) 462 | } 463 | 464 | // Observable algebraic data type 465 | final case class ScanLeft[A,B](source: Observable[A], var zero: B, f: (B,A) => B) 466 | final case class Zip[A,B](left: Observable[A], right: Observable[B]) extends Observable[(A,B)] 467 | final case class Map[A,B](source: Observable[A], f: A => B) extends Observable[B] 468 | final case class FromIterator[A](source: Iterator[A]) extends Observable[A] 469 | final case class FromCallbackHandler[A]( 470 | handler: (A => Unit) => Unit, 471 | queue: BlockingQueue = new ArrayBlockingQueue(1)) extends Observable[A] { 472 | handler { a => 473 | queue.put(a) 474 | } 475 | } 476 | } 477 | 478 | sealed trait Stream[A] { 479 | import Stream._ 480 | 481 | def zip[B](that: Stream[B]): Stream[(A,B)] = 482 | Zip(this, that) 483 | 484 | def map[B](f: A => B): Stream[B] = 485 | Map(this, f) 486 | 487 | def scanLeft[B](zero: B)(f: (B, A) => B): Stream[B] 488 | 489 | def runFold[B](zero: B)(f: (B,A) => B): B = { 490 | Observable.fromStream(this).runFold(zero)(f) 491 | } 492 | } 493 | object Stream { 494 | def fromIterator[A](source: Iterator[A]): Stream[A] = 495 | FromIterator(source) 496 | 497 | def fromCallbackHandler[A](handler: (A => Unit) => Unit): Stream[A] = 498 | FromCallbackHandler(handler) 499 | 500 | def always[A](element: A): Stream[A] = 501 | FromIterator(Iterator.continually(element)) 502 | 503 | def apply[A](elements: A*): Stream[A] = 504 | FromIterator(Iterator(elements: _*)) 505 | 506 | // Stream algebraic data type 507 | final case class Zip[A,B](left: Stream[A], right: Stream[B]) extends Stream[(A,B)] 508 | final case class Map[A,B](source: Stream[A], f: A => B) extends Stream[B] 509 | final case class FromIterator[A](source: Iterator[A]) extends Stream[A] 510 | final case class FromCallbackHandler[A](handler: (A => Unit) => Unit) extends Stream[A] 511 | } 512 | }; import streamWrapper._ 513 | ``` 514 |
515 | 516 | 517 | #### Concurrent Joins 518 | 519 | To be continued ... 520 | -------------------------------------------------------------------------------- /src/pages/random/sierpinski-confection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | --------------------------------------------------------------------------------