├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Gruntfile.coffee ├── README.md ├── book_deploy.sh ├── book_deploy_rsa.enc ├── build.sbt ├── docker-compose.yml ├── go.sh ├── index.md ├── package.json ├── project ├── build.properties └── plugins.sbt ├── sbt.sh ├── src ├── covers │ ├── epub-cover.png │ ├── html-cover.pdf │ ├── html-cover.png │ └── pdf-cover.pdf ├── css │ └── book.less ├── meta │ ├── epub.yaml │ ├── html.yaml │ ├── metadata.yaml │ └── pdf.yaml ├── pages │ ├── conclusion │ │ └── index.md │ ├── foreword.md │ ├── generic │ │ ├── coproducts.md │ │ ├── debugging.md │ │ ├── exercises.md.disabled │ │ ├── index.md │ │ ├── lazy.md │ │ ├── products.md │ │ ├── summary.md │ │ └── type-classes.md │ ├── intro │ │ ├── contributors.md │ │ ├── generic.md │ │ ├── index.md │ │ ├── source-code.md │ │ └── thisbook.md │ ├── labelled-generic │ │ ├── coproducts.md │ │ ├── index.md │ │ ├── literal-types.md │ │ ├── products.md │ │ └── summary.md │ ├── links.md │ ├── nat │ │ ├── index.md │ │ ├── length.md │ │ ├── ops.md │ │ ├── random.md │ │ └── summary.md │ ├── notes │ │ ├── libraries.md │ │ ├── outline.md │ │ └── todos.md │ ├── ops │ │ ├── index.md │ │ ├── init-last.md │ │ ├── migration.md │ │ ├── penultimate.md │ │ ├── record.md │ │ └── summary.md │ ├── parts │ │ ├── part1.md │ │ ├── part2.md │ │ └── part3.md │ ├── poly │ │ ├── index.md │ │ ├── map-flatmap-fold.md │ │ ├── monomorphic-map.pdf │ │ ├── monomorphic-map.svg │ │ ├── poly.md │ │ ├── polymorphic-map.pdf │ │ ├── polymorphic-map.svg │ │ ├── product-mapper.md │ │ ├── summary.md │ │ └── type-charts.sketch │ ├── representations │ │ ├── adts.md │ │ ├── coproduct.png │ │ ├── coproducts.md │ │ ├── hlist.png │ │ ├── index.md │ │ ├── products.md │ │ ├── representations.sketch │ │ └── summary.md │ ├── solutions.md │ └── type-level-programming │ │ ├── dependent-functions.md │ │ ├── dependent-types.md │ │ ├── index.md │ │ └── summary.md └── template │ ├── cover-notes.html │ ├── cover-notes.tex │ ├── images │ ├── brand.pdf │ ├── brand.svg │ ├── hero-arrow-overlay-white.svg │ ├── hero-left-overlay-white.pdf │ ├── hero-left-overlay-white.svg │ ├── hero-right-overlay-white.pdf │ ├── hero-right-overlay-white.svg │ ├── spaaace.pdf │ └── spaaace.png │ ├── template.epub.html │ ├── template.html │ └── template.tex └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | _site/* 2 | _theme_packages/* 3 | 4 | Thumbs.db 5 | .DS_Store 6 | 7 | !.gitkeep 8 | 9 | .rbenv-version 10 | .rvmrc 11 | /bower_components/ 12 | /dist/ 13 | /node_modules/ 14 | 15 | *.sublime-workspace 16 | 17 | target/ 18 | project/target 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | language: scala 5 | scala: 6 | - 2.12.2 7 | cache: 8 | directories: 9 | - "$HOME/.ivy2/cache" 10 | - "$HOME/.sbt/boot" 11 | - "$HOME/.cache/.coursier" 12 | script: 13 | - sudo mkdir .sbt 14 | - sudo mkdir dist 15 | - docker-compose run book npm install 16 | - docker-compose run book ./sbt.sh pdf html epub ; export SBT_RESULT=$? 17 | - test "$SBT_RESULT" == "0" 18 | after_success: 19 | - openssl aes-256-cbc -K $encrypted_232d7e8fc499_key -iv $encrypted_232d7e8fc499_iv 20 | -in book_deploy_rsa.enc -out book_deploy_rsa -d 21 | - "./book_deploy.sh" 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "jekyll" 3 | gem "kramdown" 4 | gem "typogruby" 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | blankslate (2.1.2.4) 5 | celluloid (0.16.0) 6 | timers (~> 4.0.0) 7 | classifier-reborn (2.0.1) 8 | fast-stemmer (~> 1.0) 9 | coffee-script (2.3.0) 10 | coffee-script-source 11 | execjs 12 | coffee-script-source (1.8.0) 13 | colorator (0.1) 14 | execjs (2.2.2) 15 | fast-stemmer (1.0.2) 16 | ffi (1.9.6) 17 | hitimes (1.2.2) 18 | jekyll (2.4.0) 19 | classifier-reborn (~> 2.0) 20 | colorator (~> 0.1) 21 | jekyll-coffeescript (~> 1.0) 22 | jekyll-gist (~> 1.0) 23 | jekyll-paginate (~> 1.0) 24 | jekyll-sass-converter (~> 1.0) 25 | jekyll-watch (~> 1.1) 26 | kramdown (~> 1.3) 27 | liquid (~> 2.6.1) 28 | mercenary (~> 0.3.3) 29 | pygments.rb (~> 0.6.0) 30 | redcarpet (~> 3.1) 31 | safe_yaml (~> 1.0) 32 | toml (~> 0.1.0) 33 | jekyll-coffeescript (1.0.1) 34 | coffee-script (~> 2.2) 35 | jekyll-gist (1.1.0) 36 | jekyll-paginate (1.1.0) 37 | jekyll-sass-converter (1.2.1) 38 | sass (~> 3.2) 39 | jekyll-watch (1.1.1) 40 | listen (~> 2.7) 41 | kramdown (1.4.2) 42 | liquid (2.6.1) 43 | listen (2.7.11) 44 | celluloid (>= 0.15.2) 45 | rb-fsevent (>= 0.9.3) 46 | rb-inotify (>= 0.9) 47 | mercenary (0.3.4) 48 | parslet (1.5.0) 49 | blankslate (~> 2.0) 50 | posix-spawn (0.3.9) 51 | pygments.rb (0.6.0) 52 | posix-spawn (~> 0.3.6) 53 | yajl-ruby (~> 1.1.0) 54 | rb-fsevent (0.9.4) 55 | rb-inotify (0.9.5) 56 | ffi (>= 0.5.0) 57 | redcarpet (3.2.0) 58 | rubypants (0.2.0) 59 | safe_yaml (1.0.4) 60 | sass (3.4.6) 61 | timers (4.0.1) 62 | hitimes 63 | toml (0.1.2) 64 | parslet (~> 1.5.0) 65 | typogruby (1.0.17) 66 | rubypants 67 | yajl-ruby (1.1.0) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | jekyll 74 | kramdown 75 | typogruby 76 | -------------------------------------------------------------------------------- /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 | # lib : "src/build" 11 | page : "target/pages" 12 | template : "src/template" 13 | } 14 | }) 15 | return 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Type Astronaut's Guide to Shapeless 2 | 3 | Copyright 2016 Dave Gurnell. 4 | Text and diagrams licensed [CC-BY-SA 3.0][text-license]. 5 | Code samples licensed [Apache 2.0][code-license] 6 | 7 | ## Reading the Book 8 | 9 | You have three options for grabbing the book: 10 | 11 | - read/download the book at the [Underscore][underscore] web site; 12 | - order a print copy from [Underscore][underscore]; 13 | - a french translation is [also available][fr]. 14 | 15 | ## Related Material 16 | 17 | Accompanying code samples can be found here:
18 | https://github.com/underscoreio/shapeless-guide-code 19 | 20 | Check the `solutions` branch for complete versions of each example. 21 | 22 | ## Building the eBook 23 | 24 | Install Docker and use `go.sh` to boot an instance 25 | with most of the right dependencies: 26 | 27 | ~~~ 28 | bash$ ./go.sh 29 | ~~~ 30 | 31 | Then run `npm install` to install the remaining dependencies: 32 | 33 | ~~~ 34 | npm install 35 | ~~~ 36 | 37 | And finally use `sbt` to build the book: 38 | 39 | ~~~ 40 | sbt pdf 41 | ~~~ 42 | 43 | ## Building a printable book 44 | 45 | To build a black and white, 46 | print-ready version of the book, 47 | edit `src/meta/pdf.yaml` and set 48 | `blackandwhiteprintable` to `true`. 49 | Then run `sbt pdf` as above. 50 | 51 | ## Contributing 52 | 53 | Please raise an issue or submit a PR. 54 | If you submit a PR, make sure to add yourself to 55 | `src/pages/intro/constributors.md`! 56 | 57 | ## Acknowledgements 58 | 59 | Thanks to Miles Sabin, Richard Dallaway, Noel Welsh, Travis Brown, 60 | and our [fellow space-farers on Github][contributors] 61 | for their invaluable help and feedback. 62 | 63 | [text-license]: https://creativecommons.org/licenses/by-sa/3.0/ 64 | [code-license]: http://www.apache.org/licenses/LICENSE-2.0 65 | [shapeless]: https://github.com/milessabin/shapeless 66 | [pdf]: https://github.com/underscoreio/shapeless-guide/blob/develop/dist/shapeless-guide.pdf 67 | [slides]: https://github.com/davegurnell/shapeless-guide-slides 68 | [code]: https://github.com/underscoreio/shapeless-guide-code 69 | [contributors]: https://github.com/underscoreio/shapeless-guide/graphs/contributors 70 | [underscore]: https://underscore.io/books/shapeless-guide 71 | [lulu]: http://www.lulu.com/shop/dave-gurnell/the-type-astronauts-guide-to-shapeless/paperback/product-22992219.html 72 | [fr]: https://github.com/crakjie/shapeless-guide 73 | -------------------------------------------------------------------------------- /book_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Deploy the PDF, HTML, EPUB files of an Underscore book (source) 4 | # into another Git repository (target) 5 | # 6 | set -e 7 | 8 | # Configuration 9 | # 1. The key for writing into the other repository. 10 | # This is committed to the repo, encrypted by Travis, 11 | # with filename extension '.enc'. 12 | export KEY_FILENAME="book_deploy_rsa" 13 | # 2. The branch we deploy from. 14 | # Only deploy for non-PR commits to this branch. 15 | export DEPLOY_BRANCH="develop" 16 | # 3. Folder inside target of where to place the artifacts: 17 | export TARGET_DIR=books/shapeless-guide/ 18 | # 4. Commit message prefix (for the "books" repository) 19 | export COMMIT_PREFIX="deploy shapeless guide via travis" 20 | # End of configuration 21 | 22 | if [[ "${TRAVIS_PULL_REQUEST}" == "false" && "${TRAVIS_BRANCH}" == "${DEPLOY_BRANCH}" ]]; then 23 | echo "Starting deploy to Github Pages" 24 | mkdir -p "~/.ssh" 25 | echo -e "Host github.com\n\tStrictHostKeyChecking no\nIdentityFile ~/.ssh/${KEY_FILENAME}\n" >> ~/.ssh/config 26 | cp ${KEY_FILENAME} ~/.ssh/${KEY_FILENAME} 27 | chmod 600 ~/.ssh/${KEY_FILENAME} 28 | 29 | git config --global user.email "hello@underscore.io" 30 | git config --global user.name "Travis Build" 31 | 32 | export SRC_DIR=`pwd` # e.g., /home/travis/build/underscoreio/insert-book-name-here 33 | 34 | export TEMP_DIR=/tmp/dist 35 | mkdir -p $TEMP_DIR 36 | cd $TEMP_DIR 37 | git clone git@github.com:underscoreio/books.git 38 | 39 | mkdir -p $TARGET_DIR 40 | cd $TARGET_DIR 41 | 42 | cp $SRC_DIR/dist/*.pdf . 43 | cp $SRC_DIR/dist/*.html . 44 | cp $SRC_DIR/dist/*.epub . 45 | 46 | git add . 47 | git commit -m "$COMMIT_PREFIX $TRAVIS_JOB_NUMBER $TRAVIS_COMMIT [ci skip]" 48 | git push git@github.com:underscoreio/books.git master:master 49 | 50 | rm -rf $TEMP_DIR 51 | fi 52 | -------------------------------------------------------------------------------- /book_deploy_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/book_deploy_rsa.enc -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = project.in(file(".")) 2 | .settings(tutSettings) 3 | 4 | tutSourceDirectory := sourceDirectory.value / "pages" 5 | 6 | tutTargetDirectory := target.value / "pages" 7 | 8 | scalaOrganization in ThisBuild := "org.typelevel" 9 | scalaVersion in ThisBuild := "2.12.1" 10 | 11 | scalacOptions ++= Seq( 12 | "-deprecation", 13 | "-encoding", "UTF-8", 14 | "-unchecked", 15 | "-feature", 16 | "-Xlint", 17 | //"-Xfatal-warnings", 18 | "-Ywarn-dead-code", 19 | "-Yliteral-types" 20 | ) 21 | 22 | resolvers ++= Seq(Resolver.sonatypeRepo("snapshots")) 23 | 24 | libraryDependencies ++= Seq( 25 | "com.chuusai" %% "shapeless" % "2.3.2", 26 | "org.scalacheck" %% "scalacheck" % "1.13.5", 27 | "org.typelevel" %% "cats" % "0.9.0" 28 | ) 29 | 30 | lazy val pdf = taskKey[Unit]("Build the PDF version of the book") 31 | lazy val html = taskKey[Unit]("Build the HTML version of the book") 32 | lazy val epub = taskKey[Unit]("Build the ePub version of the book") 33 | lazy val all = taskKey[Unit]("Build all versions of the book") 34 | 35 | pdf := { tutQuick.value ; "grunt pdf".! } 36 | html := { tutQuick.value ; "grunt html".! } 37 | epub := { tutQuick.value ; "grunt epub".! } 38 | 39 | all := { 40 | tutQuick.value 41 | "grunt pdf".! 42 | "grunt html".! 43 | "grunt epub".! 44 | } 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | book: 4 | image: underscoreio/book:latest 5 | volumes: 6 | - .:/source 7 | -------------------------------------------------------------------------------- /go.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker-compose run book bash 3 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/index.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shapeless-guide", 3 | "description": "", 4 | "version": "0.0.1", 5 | "dependencies": { 6 | "grunt": "0.4.5", 7 | "grunt-browserify": "3.3.0", 8 | "grunt-contrib-clean": "0.6.0", 9 | "grunt-contrib-connect": "0.9.0", 10 | "grunt-contrib-copy": "0.7.0", 11 | "grunt-contrib-less": "1.0.0", 12 | "grunt-contrib-watch": "0.6.1", 13 | "grunt-css-url-embed": "1.5.0", 14 | "js-yaml": "3.2.6", 15 | "pandoc-filter": "^0.1.4" 16 | }, 17 | "devDependencies": { 18 | "bootstrap": "3.3.1", 19 | "coffeeify": "1.0.0", 20 | "jquery": "2.1.1", 21 | "uglifyify": "2.6.0", 22 | "underscore": "1.7.0", 23 | "underscore-ebook-template": "git+https://github.com/underscoreio/underscore-ebook-template.git#0.6.2" 24 | }, 25 | "scripts": { 26 | "test": "echo \"Error: no test specificed\" && exit 1" 27 | }, 28 | "author": "Dave Gurnell" 29 | } 30 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.16 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.7") 2 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0") 3 | -------------------------------------------------------------------------------- /sbt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export JAVA_OPTS="-Xmx3g -XX:+TieredCompilation -XX:ReservedCodeCacheSize=256m -XX:+UseNUMA -XX:+UseParallelGC -XX:+CMSClassUnloadingEnabled" 4 | 5 | sbt "$@" 6 | -------------------------------------------------------------------------------- /src/covers/epub-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/covers/epub-cover.png -------------------------------------------------------------------------------- /src/covers/html-cover.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/covers/html-cover.pdf -------------------------------------------------------------------------------- /src/covers/html-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/covers/html-cover.png -------------------------------------------------------------------------------- /src/covers/pdf-cover.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/covers/pdf-cover.pdf -------------------------------------------------------------------------------- /src/css/book.less: -------------------------------------------------------------------------------- 1 | @book-color: #042742; 2 | 3 | .hero { 4 | background: @book-color url(src/covers/html-cover.png) center top no-repeat; 5 | 6 | .container { 7 | height: 600px; 8 | } 9 | 10 | .hero-text { 11 | top: auto; 12 | height: auto; 13 | bottom: 0; 14 | padding-bottom: 60px; 15 | 16 | h1.title { 17 | margin-bottom: 0; 18 | font-size: 36px; 19 | } 20 | 21 | h3.author { 22 | margin-bottom: 10px; 23 | font-size: 30px; 24 | } 25 | 26 | p.date { 27 | margin-bottom: 30px; 28 | } 29 | 30 | img.brand-logo { 31 | height: 80px; 32 | } 33 | } 34 | } 35 | 36 | footer { 37 | background: @book-color; 38 | } -------------------------------------------------------------------------------- /src/meta/epub.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | solutions: 3 | headingText: "Solutions to Exercises" 4 | ... -------------------------------------------------------------------------------- /src/meta/html.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | solutions: 3 | headingText: "Solutions to Exercises" 4 | ... -------------------------------------------------------------------------------- /src/meta/metadata.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Used by Pandoc to print covers: 3 | title: "The Type Astronaut's Guide to Shapeless" 4 | author: "Dave Gurnell" 5 | date: "April 2017" 6 | # Used by Grunt to name output files: 7 | filenameStem: "shapeless-guide" 8 | # Used by Grunt to create a ZIP of source code: 9 | exercisesRepo: null 10 | tocDepth: 3 11 | copyright: "2016-17" 12 | # Enable pandoc-crossref (must be installed on user's computer) 13 | # There's no distinction here between section and chapter prefixes, 14 | # we disable all prefixes for consistency and write them by hand: 15 | usePandocCrossref: true 16 | figPrefixTemplate: "$$i$$" 17 | eqnPrefixTemplate: "$$i$$" 18 | tblPrefixTemplate: "$$i$$" 19 | lstPrefixTemplate: "$$i$$" 20 | secPrefixTemplate: "$$i$$" 21 | # Used by Grunt to assemble pandoc command line: 22 | pages: 23 | - foreword.md 24 | 25 | - intro/index.md 26 | - intro/generic.md 27 | - intro/thisbook.md 28 | - intro/source-code.md 29 | - intro/contributors.md 30 | 31 | - parts/part1.md 32 | 33 | - representations/index.md 34 | - representations/adts.md 35 | - representations/products.md 36 | - representations/coproducts.md 37 | - representations/summary.md 38 | 39 | - generic/index.md 40 | - generic/type-classes.md 41 | - generic/products.md 42 | - generic/coproducts.md 43 | - generic/lazy.md 44 | - generic/debugging.md 45 | - generic/summary.md 46 | 47 | - type-level-programming/index.md 48 | - type-level-programming/dependent-types.md 49 | - type-level-programming/dependent-functions.md 50 | - type-level-programming/summary.md 51 | 52 | - labelled-generic/index.md 53 | - labelled-generic/literal-types.md 54 | - labelled-generic/products.md 55 | - labelled-generic/coproducts.md 56 | - labelled-generic/summary.md 57 | 58 | - parts/part2.md 59 | 60 | - ops/index.md 61 | - ops/init-last.md 62 | - ops/penultimate.md 63 | - ops/migration.md 64 | - ops/record.md 65 | - ops/summary.md 66 | 67 | - poly/index.md 68 | - poly/poly.md 69 | - poly/map-flatmap-fold.md 70 | - poly/product-mapper.md 71 | - poly/summary.md 72 | 73 | - nat/index.md 74 | - nat/length.md 75 | - nat/random.md 76 | - nat/ops.md 77 | - nat/summary.md 78 | 79 | - conclusion/index.md 80 | 81 | # - notes/todos.md 82 | - links.md 83 | ... 84 | -------------------------------------------------------------------------------- /src/meta/pdf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | papersize: "a5paper" 3 | fontsize: "11pt" 4 | geometry: "margin=.75in" 5 | solutions: 6 | headingText: "Solutions to Exercises" 7 | # blackandwhiteprintable: true 8 | # "links-as-notes": true 9 | ... 10 | -------------------------------------------------------------------------------- /src/pages/conclusion/index.md: -------------------------------------------------------------------------------- 1 | # Prepare for launch! #{-} 2 | 3 | With Part II's look at `shapeless.ops` 4 | we have arrived at the end of this guide. 5 | We hope you found it useful for understanding 6 | this fascinating and powerful library, 7 | and wish you all the best 8 | on your future journeys as a type astronaut. 9 | 10 | As functional programmers 11 | we value abstraction above all else. 12 | Concepts like functors and monads 13 | arise from years of programming research: 14 | writing code, spotting patterns, 15 | and making abstractions to remove redundancy. 16 | Shapeless raises the bar for abstraction in Scala. 17 | Tools like `Generic` and `LabelledGeneric` 18 | provide an interface for abstracting over data types 19 | that were previously frustratingly unique and distinct. 20 | 21 | There have traditionally been two barriers to entry 22 | for aspiring new shapeless users. 23 | The first is the wealth of theoretical knowledge 24 | and implementation detail 25 | required to understand the patterns we need. 26 | Hopefully this guide has helped in this regard. 27 | 28 | The second barrier is the fear and uncertainty 29 | surrounding a library that is seen 30 | as "academic" or "advanced". 31 | We can overcome this by sharing knowledge---use cases, 32 | pros and cons, implementation strategies, and so on---to 33 | widen the understanding of this valuable tool. 34 | So please share this book with a friend... 35 | and let's scrap some boilerplate together! 36 | 37 | \clearpage 38 | \pagestyle{empty} 39 | -------------------------------------------------------------------------------- /src/pages/foreword.md: -------------------------------------------------------------------------------- 1 | # Foreword {-} 2 | 3 | Back at the beginning of 2011, when I first started doing the 4 | experiments in generic programming that would eventually turn into 5 | shapeless, I had no idea that five years later it would have evolved 6 | into such a widely used library. I am profoundly grateful to the 7 | people who have trusted me and added shapeless as a dependency to 8 | their own projects: the vote of confidence that this represents is a 9 | huge motivator for any open source project. I am also hugely grateful 10 | to the many people who have contributed to shapeless over the years: 11 | eighty one at the time of writing. Without their help shapeless would 12 | be a far less interesting and useful library. 13 | 14 | These positives notwithstanding, shapeless has suffered from one of 15 | the common failings of open source projects: a lack of comprehensive, 16 | accurate and accessible documentation. The responsibility for this 17 | lies squarely at my door: despite acknowledging the lack I have never 18 | been able to find the time to do anything about it. To some extent 19 | shapeless has been saved from this by Travis Brown's heroic Stack 20 | Overflow performance and also by the many people who have given talks 21 | about and run workshops on shapeless (in particular I'd like to 22 | highlight Sam Halliday's "Shapeless for Mortals" workshop). 23 | 24 | But Dave Gurnell has changed all that: we now have this wonderful book 25 | length treatment of shapeless's most important application: type class 26 | derivation via generic programming. In doing this he has pulled 27 | together fragments of folklore and documentation, has picked my brain, 28 | and turned the impenetrable tangle into something which is clear, 29 | concise and very practical. With any luck he will be able to make good 30 | on my regular claims that at its core shapeless is a very simple 31 | library embodying a set of very simple concepts. 32 | 33 | Thanks Dave, you've done us all a great service. 34 | 35 | Miles Sabin\ 36 | Creator of shapeless 37 | -------------------------------------------------------------------------------- /src/pages/generic/coproducts.md: -------------------------------------------------------------------------------- 1 | ## Deriving instances for coproducts {#sec:generic:coproducts} 2 | 3 | ```tut:book:invisible 4 | // ---------------------------------------------- 5 | // Forward definitions 6 | 7 | trait CsvEncoder[A] { 8 | def encode(value: A): List[String] 9 | } 10 | 11 | object CsvEncoder { 12 | def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] = 13 | enc 14 | } 15 | 16 | def writeCsv[A](values: List[A])(implicit encoder: CsvEncoder[A]): String = 17 | values.map(encoder.encode).map(_.mkString(",")).mkString("\n") 18 | 19 | def createEncoder[A](func: A => List[String]): CsvEncoder[A] = 20 | new CsvEncoder[A] { 21 | def encode(value: A): List[String] = 22 | func(value) 23 | } 24 | 25 | implicit val stringEncoder: CsvEncoder[String] = 26 | createEncoder(str => List(str)) 27 | 28 | implicit val intEncoder: CsvEncoder[Int] = 29 | createEncoder(num => List(num.toString)) 30 | 31 | implicit val booleanEncoder: CsvEncoder[Boolean] = 32 | createEncoder(bool => List(if(bool) "cone" else "glass")) 33 | 34 | import shapeless.{HList, HNil, ::} 35 | 36 | implicit val hnilEncoder: CsvEncoder[HNil] = 37 | createEncoder(hnil => Nil) 38 | 39 | implicit def hlistEncoder[H, T <: HList]( 40 | implicit 41 | hEncoder: CsvEncoder[H], 42 | tEncoder: CsvEncoder[T] 43 | ): CsvEncoder[H :: T] = createEncoder { 44 | case h :: t => 45 | hEncoder.encode(h) ++ tEncoder.encode(t) 46 | } 47 | 48 | import shapeless.Generic 49 | 50 | implicit def genericEncoder[A, R]( 51 | implicit 52 | gen: Generic.Aux[A, R], 53 | rEncoder: CsvEncoder[R] 54 | ): CsvEncoder[A] = 55 | createEncoder(value => rEncoder.encode(gen.to(value))) 56 | 57 | // ---------------------------------------------- 58 | ``` 59 | 60 | In the last section we created a set of rules 61 | to automatically derive a `CsvEncoder` for any product type. 62 | In this section we will apply the same patterns to coproducts. 63 | Let's return to our shape ADT as an example: 64 | 65 | ```tut:book:silent 66 | sealed trait Shape 67 | final case class Rectangle(width: Double, height: Double) extends Shape 68 | final case class Circle(radius: Double) extends Shape 69 | ``` 70 | 71 | The generic representation for `Shape` 72 | is `Rectangle :+: Circle :+: CNil`. 73 | In Section [@sec:generic:product-generic] 74 | we defined product encoders for `Rectangle` and `Circle`. 75 | Now, to write generic `CsvEncoders` for `:+:` and `CNil`, 76 | we can use the same principles we used for `HLists`: 77 | 78 | ```tut:book:silent 79 | import shapeless.{Coproduct, :+:, CNil, Inl, Inr} 80 | 81 | implicit val cnilEncoder: CsvEncoder[CNil] = 82 | createEncoder(cnil => throw new Exception("Inconceivable!")) 83 | 84 | implicit def coproductEncoder[H, T <: Coproduct]( 85 | implicit 86 | hEncoder: CsvEncoder[H], 87 | tEncoder: CsvEncoder[T] 88 | ): CsvEncoder[H :+: T] = createEncoder { 89 | case Inl(h) => hEncoder.encode(h) 90 | case Inr(t) => tEncoder.encode(t) 91 | } 92 | ``` 93 | 94 | There are two key points of note: 95 | 96 | 1. Because `Coproducts` are *disjunctions* of types, 97 | the encoder for `:+:` has to *choose* 98 | whether to encode a left or right value. 99 | We pattern match on the two subtypes of `:+:`, 100 | which are `Inl` for left and `Inr` for right. 101 | 102 | 2. Alarmingly, the encoder for `CNil` throws an exception! 103 | Don't panic, though. 104 | Remember that we can't 105 | create values of type `CNil`, 106 | so the `throw` expression is dead code. 107 | It's ok to fail abruptly here because 108 | we will never reach this point. 109 | 110 | If we place these definitions 111 | alongside our product encoders from Section [@sec:generic:products], 112 | we should be able to serialize a list of shapes. 113 | Let's give it a try: 114 | 115 | ```tut:book:silent 116 | val shapes: List[Shape] = List( 117 | Rectangle(3.0, 4.0), 118 | Circle(1.0) 119 | ) 120 | ``` 121 | 122 | ```tut:book:fail 123 | writeCsv(shapes) 124 | ``` 125 | 126 | Oh no, it failed! 127 | The error message is unhelpful as we discussed earlier. 128 | The reason for the failure is 129 | we don't have a `CsvEncoder` instance for `Double`: 130 | 131 | ```tut:book:silent 132 | implicit val doubleEncoder: CsvEncoder[Double] = 133 | createEncoder(d => List(d.toString)) 134 | ``` 135 | 136 | With this definition in place, everything works as expected: 137 | 138 | ```tut:book 139 | writeCsv(shapes) 140 | ``` 141 | 142 |
143 | *SI-7046 and you* 144 | 145 | There is a Scala compiler bug called [SI-7046][link-si7046] 146 | that can cause coproduct generic resolution to fail. 147 | The bug causes certain parts of the macro API, 148 | on which shapeless depends, to be sensitive 149 | to the order of the definitions in our source code. 150 | Problems can often be worked around 151 | by reordering code and renaming files, 152 | but such workarounds tend to be volatile and unreliable. 153 | 154 | If you are using Lightbend Scala 2.11.8 or earlier 155 | and coproduct resolution fails for you, 156 | consider upgrading to Lightbend Scala 2.11.9 157 | or Typelevel Scala 2.11.8. 158 | SI-7046 is fixed in each of these releases. 159 |
160 | 161 | ### Aligning CSV output 162 | 163 | Our CSV encoder isn't very practical in its current form. 164 | It allows fields from `Rectangle` and `Circle` to 165 | occupy the same columns in the output. 166 | To fix this problem we need to modify 167 | the definition of `CsvEncoder` 168 | to incorporate the width of the data type 169 | and space the output accordingly. 170 | The examples repo linked 171 | in Section [@sec:intro:source-code] 172 | contains a complete implementation of `CsvEncoder` 173 | that addresses this problem. 174 | -------------------------------------------------------------------------------- /src/pages/generic/debugging.md: -------------------------------------------------------------------------------- 1 | ## Debugging implicit resolution {#sec:generic:debugging} 2 | 3 | ```tut:book:invisible 4 | import shapeless._ 5 | 6 | trait CsvEncoder[A] { 7 | val width: Int 8 | def encode(value: A): List[String] 9 | } 10 | 11 | object CsvEncoder { 12 | def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] = 13 | enc 14 | } 15 | 16 | def createEncoder[A](w: Int)(func: A => List[String]): CsvEncoder[A] = 17 | new CsvEncoder[A] { 18 | val width = w 19 | def encode(value: A): List[String] = 20 | func(value) 21 | } 22 | 23 | implicit val stringEncoder: CsvEncoder[String] = 24 | createEncoder(1)(str => List(str)) 25 | 26 | implicit val intEncoder: CsvEncoder[Int] = 27 | createEncoder(1)(num => List(num.toString)) 28 | 29 | implicit val doubleEncoder: CsvEncoder[Double] = 30 | createEncoder(1)(num => List(num.toString)) 31 | 32 | implicit val booleanEncoder: CsvEncoder[Boolean] = 33 | createEncoder(1)(bool => List(if(bool) "yes" else "no")) 34 | 35 | implicit def optionEncoder[A](implicit encoder: CsvEncoder[A]): CsvEncoder[Option[A]] = 36 | createEncoder(encoder.width)(opt => opt.map(encoder.encode).getOrElse(List.fill(encoder.width)(""))) 37 | 38 | implicit val hnilEncoder: CsvEncoder[HNil] = 39 | createEncoder(0)(hnil => Nil) 40 | 41 | implicit def hlistEncoder[H, T <: HList]( 42 | implicit 43 | hEncoder: Lazy[CsvEncoder[H]], 44 | tEncoder: CsvEncoder[T] 45 | ): CsvEncoder[H :: T] = 46 | createEncoder(hEncoder.value.width + tEncoder.width) { 47 | case h :: t => 48 | hEncoder.value.encode(h) ++ tEncoder.encode(t) 49 | } 50 | 51 | implicit val cnilEncoder: CsvEncoder[CNil] = 52 | createEncoder(0)(cnil => ???) 53 | 54 | implicit def coproductEncoder[H, T <: Coproduct]( 55 | implicit 56 | hEncoder: Lazy[CsvEncoder[H]], 57 | tEncoder: CsvEncoder[T] 58 | ): CsvEncoder[H :+: T] = 59 | createEncoder(hEncoder.value.width + tEncoder.width) { 60 | case Inl(h) => hEncoder.value.encode(h) ++ List.fill(tEncoder.width)("") 61 | case Inr(t) => List.fill(hEncoder.value.width)("") ++ tEncoder.encode(t) 62 | } 63 | 64 | implicit def genericEncoder[A, R]( 65 | implicit 66 | gen: Generic.Aux[A, R], 67 | enc: Lazy[CsvEncoder[R]] 68 | ): CsvEncoder[A] = 69 | createEncoder(enc.value.width)(a => enc.value.encode(gen.to(a))) 70 | ``` 71 | 72 | Failures in implicit resolution 73 | can be confusing and frustrating. 74 | Here are a couple of techniques to use 75 | when implicits go bad. 76 | 77 | ### Debugging using *implicitly* 78 | 79 | What can we do when the compiler 80 | simply fails to find an implicit value? 81 | The failure could be caused by 82 | the resolution of any one of the implicits in use. 83 | For example: 84 | 85 | ```tut:book:silent 86 | case class Foo(bar: Int, baz: Float) 87 | ``` 88 | 89 | ```tut:book:fail 90 | CsvEncoder[Foo] 91 | ``` 92 | 93 | The reason for the failure is that 94 | we haven't defined a `CsvEncoder` for `Float`. 95 | However, this may not be obvious in application code. 96 | We can work through the expected expansion sequence 97 | to find the source of the error, 98 | inserting calls to `CsvEncoder.apply` or `implicitly` 99 | above the error to see if they compile. 100 | We start with the generic representation of `Foo`: 101 | 102 | ```tut:book:fail 103 | CsvEncoder[Int :: Float :: HNil] 104 | ``` 105 | 106 | This fails so we know we have to search deeper in the expansion. 107 | The next step is to try the components of the `HList`: 108 | 109 | ```tut:book:silent 110 | CsvEncoder[Int] 111 | ``` 112 | 113 | ```tut:book:fail 114 | CsvEncoder[Float] 115 | ``` 116 | 117 | `Int` passes but `Float` fails. 118 | `CsvEncoder[Float]` is a leaf in our tree of expansions, 119 | so we know to start by implementing this missing instance. 120 | If adding the instance doesn't fix the problem 121 | we repeat the process to find the next point of failure. 122 | 123 | ### Debugging using *reify* 124 | 125 | The `reify` method from `scala.reflect` 126 | takes a Scala expression as a parameter and returns 127 | an AST object representing the expression tree, 128 | complete with type annotations: 129 | 130 | ```tut:book:silent 131 | import scala.reflect.runtime.universe._ 132 | ``` 133 | 134 | ```tut:book 135 | println(reify(CsvEncoder[Int])) 136 | ``` 137 | 138 | The types inferred during implicit resolution 139 | can give us hints about problems. 140 | After implicit resolution, 141 | any remaining existential types such as `A` or `T` 142 | provide a sign that something has gone wrong. 143 | Similarly, "top" and "bottom" types such as `Any` and `Nothing` 144 | are evidence of failure. 145 | -------------------------------------------------------------------------------- /src/pages/generic/exercises.md.disabled: -------------------------------------------------------------------------------- 1 | 2 | ### Exercise: Aligning columns in CSV output 3 | 4 | It would perhaps be better if we separated 5 | the data for rectangles and circles 6 | into two separate sets of columns. 7 | We can do this by adding a `width` field to `CsvEncoder`: 8 | 9 | ```disabledtut:book:silent 10 | trait CsvEncoder[A] { 11 | def width: Int 12 | def encode(value: A): List[String] 13 | } 14 | ``` 15 | 16 | If we follow through with all of our definitions, 17 | we can produce instances 18 | that place each field in the ADT in a different column. 19 | We will leave this as an exercise to the reader. 20 | 21 |
22 | We start by modifying the definition of `createEncoder` 23 | to accept a `width` parameter: 24 | 25 | ```disabledtut:book:silent 26 | def createEncoder[A](cols: Int)(func: A => List[String]): CsvEncoder[A] = 27 | new CsvEncoder[A] { 28 | val width = cols 29 | def encode(value: A): List[String] = 30 | func(value) 31 | } 32 | ``` 33 | 34 | Then we modify our base encoders to each record a width of `1`: 35 | 36 | ```disabledtut:book:silent 37 | implicit val stringEncoder: CsvEncoder[String] = 38 | createEncoder(1)(str => List(str)) 39 | 40 | implicit val intEncoder: CsvEncoder[Int] = 41 | createEncoder(1)(num => List(num.toString)) 42 | 43 | implicit val booleanEncoder: CsvEncoder[Boolean] = 44 | createEncoder(1)(bool => List(if(bool) "cone" else "glass")) 45 | 46 | implicit val doubleEncoder: CsvEncoder[Double] = 47 | createEncoder(1)(d => List(d.toString)) 48 | ``` 49 | 50 | Our encoders for `HNil` and `CNil` have width `0` and our 51 | encoders for `::` and `:+:` have a width determined by 52 | the encoders for their heads and tails: 53 | 54 | ```disabledtut:book:silent 55 | import shapeless.{HList, HNil, ::} 56 | 57 | implicit val hnilEncoder: CsvEncoder[HNil] = 58 | createEncoder(0)(hnil => Nil) 59 | 60 | implicit def hlistEncoder[H, T <: HList]( 61 | implicit 62 | hEncoder: CsvEncoder[H], 63 | tEncoder: CsvEncoder[T] 64 | ): CsvEncoder[H :: T] = 65 | createEncoder(hEncoder.width + tEncoder.width) { 66 | case h :: t => 67 | hEncoder.encode(h) ++ tEncoder.encode(t) 68 | } 69 | ``` 70 | 71 | Our `:+:` encoder pads its output with a number of columns 72 | equal to the width of the encoder it isn't using for serialization: 73 | 74 | ```disabledtut:book:silent 75 | import shapeless.{Coproduct, CNil, :+:, Inl, Inr} 76 | 77 | implicit val cnilEncoder: CsvEncoder[CNil] = 78 | createEncoder(0) { cnil => 79 | throw new Exception("The impossible has happened!") 80 | } 81 | 82 | implicit def coproductEncoder[H, T <: Coproduct]( 83 | implicit 84 | hEncoder: CsvEncoder[H], 85 | tEncoder: CsvEncoder[T] 86 | ): CsvEncoder[H :+: T] = 87 | createEncoder(hEncoder.width + tEncoder.width) { 88 | case Inl(h) => hEncoder.encode(h) ++ List.fill(tEncoder.width)("") 89 | case Inr(t) => List.fill(hEncoder.width)("") ++ tEncoder.encode(t) 90 | } 91 | ``` 92 | 93 | Finally, our ADT encoder mirrors the width of 94 | the encoder for its generic representation: 95 | 96 | ```disabledtut:book:silent 97 | import shapeless.Generic 98 | 99 | implicit def genericEncoder[A, R]( 100 | implicit 101 | gen: Generic.Aux[A, R], 102 | lEncoder: CsvEncoder[R] 103 | ): CsvEncoder[A] = 104 | createEncoder(lEncoder.width) { value => 105 | lEncoder.encode(gen.to(value)) 106 | } 107 | ``` 108 | 109 | With all these definitions in place, 110 | our `writeCsv` method gains the ability to align its output correctly: 111 | 112 | ```disabledtut:book:invisible 113 | def writeCsv[A](values: List[A])(implicit encoder: CsvEncoder[A]): String = 114 | values.map(encoder.encode).map(_.mkString(",")).mkString("\n") 115 | ``` 116 | 117 | ```disabledtut:book 118 | writeCsv(shapes) 119 | ``` 120 |
121 | -------------------------------------------------------------------------------- /src/pages/generic/index.md: -------------------------------------------------------------------------------- 1 | # Automatically deriving type class instances {#sec:generic} 2 | 3 | In the last chapter we saw how the `Generic` type class 4 | allowed us to convert any instance of an ADT to 5 | a generic encoding made of `HLists` and `Coproducts`. 6 | In this chapter we will look at our first serious use case: 7 | automatic derivation of type class instances. 8 | -------------------------------------------------------------------------------- /src/pages/generic/lazy.md: -------------------------------------------------------------------------------- 1 | ## Deriving instances for recursive types 2 | 3 | ```tut:book:invisible 4 | // ---------------------------------------------- 5 | // Forward definitions 6 | 7 | trait CsvEncoder[A] { 8 | def encode(value: A): List[String] 9 | } 10 | 11 | object CsvEncoder { 12 | def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] = 13 | enc 14 | } 15 | 16 | def writeCsv[A](values: List[A])(implicit encoder: CsvEncoder[A]): String = 17 | values.map(encoder.encode).map(_.mkString(",")).mkString("\n") 18 | 19 | def createEncoder[A](func: A => List[String]): CsvEncoder[A] = 20 | new CsvEncoder[A] { 21 | def encode(value: A): List[String] = 22 | func(value) 23 | } 24 | 25 | implicit val stringEncoder: CsvEncoder[String] = 26 | createEncoder(str => List(str)) 27 | 28 | implicit val intEncoder: CsvEncoder[Int] = 29 | createEncoder(num => List(num.toString)) 30 | 31 | implicit val booleanEncoder: CsvEncoder[Boolean] = 32 | createEncoder(bool => List(if(bool) "cone" else "glass")) 33 | 34 | import shapeless.{HList, HNil, ::} 35 | 36 | implicit val hnilEncoder: CsvEncoder[HNil] = 37 | createEncoder(hnil => Nil) 38 | 39 | implicit def hlistEncoder[H, T <: HList]( 40 | implicit 41 | hEncoder: CsvEncoder[H], 42 | tEncoder: CsvEncoder[T] 43 | ): CsvEncoder[H :: T] = createEncoder { 44 | case h :: t => 45 | hEncoder.encode(h) ++ tEncoder.encode(t) 46 | } 47 | 48 | import shapeless.Generic 49 | 50 | implicit def genericEncoder[A, R]( 51 | implicit 52 | gen: Generic.Aux[A, R], 53 | rEncoder: CsvEncoder[R] 54 | ): CsvEncoder[A] = 55 | createEncoder(value => rEncoder.encode(gen.to(value))) 56 | 57 | // ---------------------------------------------- 58 | ``` 59 | 60 | Let's try something more ambitious---a binary tree: 61 | 62 | ```tut:book:silent 63 | sealed trait Tree[A] 64 | case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A] 65 | case class Leaf[A](value: A) extends Tree[A] 66 | ``` 67 | 68 | Theoretically we should already have 69 | all of the definitions in place 70 | to summon a CSV writer for this definition. 71 | However, calls to `writeCsv` fail to compile: 72 | 73 | ```tut:book:fail 74 | CsvEncoder[Tree[Int]] 75 | ```` 76 | 77 | The problem is that our type is recursive. 78 | The compiler senses an infinite loop 79 | applying our implicits and gives up. 80 | 81 | ### Implicit divergence 82 | 83 | Implicit resolution is a search process. 84 | The compiler uses heuristics to determine 85 | whether it is "converging" on a solution. 86 | If the heuristics don't yield favorable results 87 | for a particular branch of search, 88 | the compiler assumes the branch is not converging 89 | and moves onto another. 90 | 91 | One heuristic is specifically designed 92 | to avoid infinite loops. 93 | If the compiler sees the same target type twice 94 | in a particular branch of search, 95 | it gives up and moves on. 96 | We can see this happening if 97 | we look at the expansion for `CsvEncoder[Tree[Int]]` 98 | The implicit resolution process 99 | goes through the following types: 100 | 101 | ```scala 102 | CsvEncoder[Tree[Int]] // 1 103 | CsvEncoder[Branch[Int] :+: Leaf[Int] :+: CNil] // 2 104 | CsvEncoder[Branch[Int]] // 3 105 | CsvEncoder[Tree[Int] :: Tree[Int] :: HNil] // 4 106 | CsvEncoder[Tree[Int]] // 5 uh oh 107 | ``` 108 | 109 | We see `Tree[A]` twice in lines 1 and 5, 110 | so the compiler moves onto another branch of search. 111 | The eventual consequence is that 112 | it fails to find a suitable implicit. 113 | 114 | In fact, the situation is worse than this. 115 | If the compiler sees the same type constructor twice 116 | and the complexity of the type parameters is *increasing*, 117 | it assumes that branch of search is "diverging". 118 | This is a problem for shapeless 119 | because types like `::[H, T]` and `:+:[H, T]` 120 | can appear several times as the compiler expands 121 | different generic representations. 122 | This causes the compiler to give up prematurely 123 | even though it would eventually find a solution 124 | if it persisted with the same expansion. 125 | Consider the following types: 126 | 127 | ```tut:book:silent 128 | case class Bar(baz: Int, qux: String) 129 | case class Foo(bar: Bar) 130 | ``` 131 | 132 | The expansion for `Foo` looks like this: 133 | 134 | ```scala 135 | CsvEncoder[Foo] // 1 136 | CsvEncoder[Bar :: HNil] // 2 137 | CsvEncoder[Bar] // 3 138 | CsvEncoder[Int :: String :: HNil] // 4 uh oh 139 | ``` 140 | 141 | The compiler attempts to resolve a `CsvEncoder[::[H, T]]` 142 | twice in this branch of search, on lines 2 and 4. 143 | The type parameter for `T` is more complex on line 4 than on line 2, 144 | so the compiler assumes (incorrectly in this case) 145 | that the branch of search is diverging. 146 | It moves onto another branch and, again, 147 | the result is failure to generate a suitable instance. 148 | 149 | ### *Lazy* 150 | 151 | Implicit divergence would be a show-stopper 152 | for libraries like shapeless. 153 | Fortunately, shapeless provides 154 | a type called `Lazy` as a workaround. 155 | `Lazy` does two things: 156 | 157 | 1. it suppresses implicit divergence at compile time 158 | by guarding against the aforementioned 159 | over-defensive convergence heuristics; 160 | 161 | 2. it defers evaluation of the implicit parameter at runtime, 162 | permitting the derivation of self-referential implicits. 163 | 164 | We use `Lazy` by wrapping it around specific implicit parameters. 165 | As a rule of thumb, it is always a good idea to wrap 166 | the "head" parameter of any `HList` or `Coproduct` rule 167 | and the `Repr` parameter of any `Generic` rule in `Lazy`: 168 | 169 | ```tut:book:invisible:reset 170 | // Forward definitions ------------------------- 171 | import shapeless._ 172 | 173 | trait CsvEncoder[A] { 174 | def encode(value: A): List[String] 175 | } 176 | 177 | object CsvEncoder { 178 | def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] = 179 | enc 180 | } 181 | 182 | def createEncoder[A](func: A => List[String]): CsvEncoder[A] = 183 | new CsvEncoder[A] { 184 | def encode(value: A): List[String] = 185 | func(value) 186 | } 187 | 188 | implicit val intEncoder: CsvEncoder[Int] = 189 | createEncoder(num => List(num.toString)) 190 | 191 | implicit val hnilEncoder: CsvEncoder[HNil] = 192 | createEncoder(hnil => Nil) 193 | 194 | implicit val cnilEncoder: CsvEncoder[CNil] = 195 | createEncoder(cnil => throw new Exception("Inconceivable!")) 196 | 197 | // ---------------------------------------------- 198 | ``` 199 | 200 | ```tut:book:silent 201 | implicit def hlistEncoder[H, T <: HList]( 202 | implicit 203 | hEncoder: Lazy[CsvEncoder[H]], // wrap in Lazy 204 | tEncoder: CsvEncoder[T] 205 | ): CsvEncoder[H :: T] = createEncoder { 206 | case h :: t => 207 | hEncoder.value.encode(h) ++ tEncoder.encode(t) 208 | } 209 | ``` 210 | 211 | ```tut:book:silent 212 | implicit def coproductEncoder[H, T <: Coproduct]( 213 | implicit 214 | hEncoder: Lazy[CsvEncoder[H]], // wrap in Lazy 215 | tEncoder: CsvEncoder[T] 216 | ): CsvEncoder[H :+: T] = createEncoder { 217 | case Inl(h) => hEncoder.value.encode(h) 218 | case Inr(t) => tEncoder.encode(t) 219 | } 220 | ``` 221 | 222 | ```tut:book:silent 223 | implicit def genericEncoder[A, R]( 224 | implicit 225 | gen: Generic.Aux[A, R], 226 | rEncoder: Lazy[CsvEncoder[R]] // wrap in Lazy 227 | ): CsvEncoder[A] = createEncoder { value => 228 | rEncoder.value.encode(gen.to(value)) 229 | } 230 | ``` 231 | 232 | ```tut:book:invisible 233 | sealed trait Tree[A] 234 | final case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A] 235 | final case class Leaf[A](value: A) extends Tree[A] 236 | ``` 237 | 238 | This prevents the compiler giving up prematurely, 239 | and enables the solution to work 240 | on complex/recursive types like `Tree`: 241 | 242 | ```tut:book 243 | CsvEncoder[Tree[Int]] 244 | ``` 245 | -------------------------------------------------------------------------------- /src/pages/generic/products.md: -------------------------------------------------------------------------------- 1 | ## Deriving instances for products {#sec:generic:products} 2 | 3 | In this section we're going to use shapeless 4 | to derive type class instances for product types 5 | (i.e. case classes). 6 | We'll use two intuitions: 7 | 8 | 1. If we have type class instances 9 | for the head and tail of an `HList`, 10 | we can derive an instance for the whole `HList`. 11 | 12 | 2. If we have a case class `A`, a `Generic[A]`, 13 | and a type class instance for the generic's `Repr`, 14 | we can combine them to create an instance for `A`. 15 | 16 | Take `CsvEncoder` and `IceCream` as examples: 17 | 18 | - `IceCream` has a generic `Repr` of type 19 | `String :: Int :: Boolean :: HNil`. 20 | 21 | - The `Repr` is made up of 22 | a `String`, an `Int`, a `Boolean`, and an `HNil`. 23 | If we have `CsvEncoders` for these types, 24 | we can create an encoder for the whole thing. 25 | 26 | - If we can derive a `CsvEncoder` for the `Repr`, 27 | we can create one for `IceCream`. 28 | 29 | ```tut:book:invisible 30 | // ---------------------------------------------- 31 | // Forward definitions 32 | 33 | trait CsvEncoder[A] { 34 | def encode(value: A): List[String] 35 | } 36 | 37 | object CsvEncoder { 38 | def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] = 39 | enc 40 | } 41 | 42 | def writeCsv[A](values: List[A])(implicit encoder: CsvEncoder[A]): String = 43 | values.map(encoder.encode).map(_.mkString(",")).mkString("\n") 44 | 45 | case class IceCream(name: String, numCherries: Int, inCone: Boolean) 46 | 47 | val iceCreams: List[IceCream] = List( 48 | IceCream("Sundae", 1, false), 49 | IceCream("Cornetto", 0, true), 50 | IceCream("Banana Split", 0, false) 51 | ) 52 | 53 | case class Employee(name: String, number: Int, manager: Boolean) 54 | 55 | val employees: List[Employee] = List( 56 | Employee("Bill", 1, true), 57 | Employee("Peter", 2, false), 58 | Employee("Milton", 3, false) 59 | ) 60 | // ---------------------------------------------- 61 | ``` 62 | 63 | ### Instances for *HLists* 64 | 65 | Let's start by defining an instance constructor 66 | and `CsvEncoders` for `String`, `Int`, and `Boolean`: 67 | 68 | ```tut:book:silent 69 | def createEncoder[A](func: A => List[String]): CsvEncoder[A] = 70 | new CsvEncoder[A] { 71 | def encode(value: A): List[String] = func(value) 72 | } 73 | 74 | implicit val stringEncoder: CsvEncoder[String] = 75 | createEncoder(str => List(str)) 76 | 77 | implicit val intEncoder: CsvEncoder[Int] = 78 | createEncoder(num => List(num.toString)) 79 | 80 | implicit val booleanEncoder: CsvEncoder[Boolean] = 81 | createEncoder(bool => List(if(bool) "yes" else "no")) 82 | ``` 83 | 84 | We can combine these building blocks 85 | to create an encoder for our `HList`. 86 | We'll use two rules: 87 | one for `HNil` and one for `::` as shown below: 88 | 89 | ```tut:book:silent 90 | import shapeless.{HList, ::, HNil} 91 | 92 | implicit val hnilEncoder: CsvEncoder[HNil] = 93 | createEncoder(hnil => Nil) 94 | 95 | implicit def hlistEncoder[H, T <: HList]( 96 | implicit 97 | hEncoder: CsvEncoder[H], 98 | tEncoder: CsvEncoder[T] 99 | ): CsvEncoder[H :: T] = 100 | createEncoder { 101 | case h :: t => 102 | hEncoder.encode(h) ++ tEncoder.encode(t) 103 | } 104 | ``` 105 | 106 | Taken together, these five instances 107 | allow us to summon `CsvEncoders` for any `HList` 108 | involving `Strings`, `Ints`, and `Booleans`: 109 | 110 | ```tut:book:silent 111 | val reprEncoder: CsvEncoder[String :: Int :: Boolean :: HNil] = 112 | implicitly 113 | ``` 114 | 115 | ```tut:book 116 | reprEncoder.encode("abc" :: 123 :: true :: HNil) 117 | ``` 118 | 119 | ### Instances for concrete products {#sec:generic:product-generic} 120 | 121 | We can combine our derivation rules for `HLists` 122 | with an instance of `Generic` 123 | to produce a `CsvEncoder` for `IceCream`: 124 | 125 | ```tut:book:silent 126 | import shapeless.Generic 127 | 128 | implicit val iceCreamEncoder: CsvEncoder[IceCream] = { 129 | val gen = Generic[IceCream] 130 | val enc = CsvEncoder[gen.Repr] 131 | createEncoder(iceCream => enc.encode(gen.to(iceCream))) 132 | } 133 | ``` 134 | 135 | and use it as follows: 136 | 137 | ```tut:book 138 | writeCsv(iceCreams) 139 | ``` 140 | 141 | This solution is specific to `IceCream`. 142 | Ideally we'd like to have a single rule 143 | that handles all case classes 144 | that have a `Generic` and a matching `CsvEncoder`. 145 | Let's work through the derivation step by step. 146 | Here's a first cut: 147 | 148 | ```scala 149 | implicit def genericEncoder[A]( 150 | implicit 151 | gen: Generic[A], 152 | enc: CsvEncoder[???] 153 | ): CsvEncoder[A] = createEncoder(a => enc.encode(gen.to(a))) 154 | ``` 155 | 156 | The first problem we have is 157 | selecting a type to put in place of the `???`. 158 | We want to write the `Repr` type associated with `gen`, 159 | but we can't do this: 160 | 161 | ```tut:book:fail 162 | implicit def genericEncoder[A]( 163 | implicit 164 | gen: Generic[A], 165 | enc: CsvEncoder[gen.Repr] 166 | ): CsvEncoder[A] = 167 | createEncoder(a => enc.encode(gen.to(a))) 168 | ``` 169 | 170 | The problem here is a scoping issue: 171 | we can't refer to a type member of one parameter 172 | from another parameter in the same block. 173 | The trick to solving this is 174 | to introduce a new type parameter to our method 175 | and refer to it in each of the associated value parameters: 176 | 177 | ```tut:book:silent 178 | implicit def genericEncoder[A, R]( 179 | implicit 180 | gen: Generic[A] { type Repr = R }, 181 | enc: CsvEncoder[R] 182 | ): CsvEncoder[A] = 183 | createEncoder(a => enc.encode(gen.to(a))) 184 | ``` 185 | 186 | We'll cover this coding style in more detail in the next chapter. 187 | Suffice to say, this definition now compiles and works as expected 188 | and we can use it with any case class as expected. 189 | Intuitively, this definition says: 190 | 191 | > *Given a type `A` and an `HList` type `R`, 192 | > an implicit `Generic` to map `A` to `R`, 193 | > and a `CsvEncoder` for `R`, 194 | > create a `CsvEncoder` for `A`.* 195 | 196 | We now have a complete system that handles any case class. 197 | The compiler expands a call like: 198 | 199 | ```tut:book:silent 200 | writeCsv(iceCreams) 201 | 202 | ``` 203 | 204 | to use our family of derivation rules: 205 | 206 | ```tut:book:silent 207 | writeCsv(iceCreams)( 208 | genericEncoder( 209 | Generic[IceCream], 210 | hlistEncoder(stringEncoder, 211 | hlistEncoder(intEncoder, 212 | hlistEncoder(booleanEncoder, hnilEncoder))))) 213 | ``` 214 | 215 | and can infer the correct expansions 216 | for many different product types. 217 | I'm sure you'll agree, 218 | it's nice not to have to write this code by hand! 219 | 220 |
221 | *Aux type aliases* 222 | 223 | Type refinements like `Generic[A] { type Repr = L }` 224 | are verbose and difficult to read, 225 | so shapeless provides a type alias `Generic.Aux` 226 | to rephrase the type member as a type parameter: 227 | 228 | ```scala 229 | package shapeless 230 | 231 | object Generic { 232 | type Aux[A, R] = Generic[A] { type Repr = R } 233 | } 234 | ``` 235 | 236 | Using this alias we get a much more readable definition: 237 | 238 | ```tut:book:silent 239 | implicit def genericEncoder[A, R]( 240 | implicit 241 | gen: Generic.Aux[A, R], 242 | env: CsvEncoder[R] 243 | ): CsvEncoder[A] = 244 | createEncoder(a => env.encode(gen.to(a))) 245 | ``` 246 | 247 | Note that the `Aux` type isn't changing any semantics---it's 248 | just making things easier to read. 249 | This "`Aux` pattern" is used frequently 250 | in the shapeless codebase. 251 |
252 | 253 | ### So what are the downsides? 254 | 255 | If all of the above seems pretty magical, 256 | allow us to provide one significant dose of reality. 257 | If things go wrong, the compiler isn't great at telling us why. 258 | 259 | There are two main reasons the code above might fail to compile. 260 | The first is when the compiler can't find 261 | an instance of `Generic`. 262 | For example, here we try to call `writeCsv` 263 | with a non-case class: 264 | 265 | ```tut:book:silent 266 | class Foo(bar: String, baz: Int) 267 | ``` 268 | 269 | ```tut:book:fail 270 | writeCsv(List(new Foo("abc", 123))) 271 | ``` 272 | 273 | In this case the error message is relatively easy to understand. 274 | If shapeless can't calculate a `Generic` 275 | it means that the type in question isn't an ADT---somewhere 276 | in the algebra there is a type that isn't a case class 277 | or a sealed abstract type. 278 | 279 | The other potential source of failure 280 | is when the compiler can't calculate 281 | a `CsvEncoder` for our `HList`. 282 | This normally happens because we don't have 283 | an encoder for one of the fields in our ADT. 284 | For example, we haven't yet defined 285 | a `CsvEncoder` for `java.util.Date`, 286 | so the following code fails: 287 | 288 | ```tut:book:silent 289 | import java.util.Date 290 | 291 | case class Booking(room: String, date: Date) 292 | ``` 293 | 294 | ```tut:book:fail 295 | writeCsv(List(Booking("Lecture hall", new Date()))) 296 | ``` 297 | 298 | The message we get here isn't very helpful. 299 | All the compiler knows is 300 | it tried a lot of combinations of implicits 301 | and couldn't make them work. 302 | It has no idea which combination came closest to the desired result, 303 | so it can't tell us where the source(s) of failure lie. 304 | 305 | There's not much good news here. 306 | We have to find the source of the error ourselves 307 | by a process of elimination. 308 | We'll discuss debugging techniques 309 | in Section [@sec:generic:debugging]. 310 | For now, the main redeeming feature 311 | is that implicit resolution always fails at compile time. 312 | There's little chance that we will end up 313 | with code that fails during execution. 314 | -------------------------------------------------------------------------------- /src/pages/generic/summary.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | In this chapter we discussed how to use 4 | `Generic`, `HLists`, and `Coproducts` 5 | to automatically derive type class instances. 6 | We also covered the `Lazy` type 7 | as a means of handling complex/recursive types. 8 | Taking all of this into account, 9 | we can write a common skeleton 10 | for deriving type class instances as follows. 11 | 12 | First, define the type class: 13 | 14 | ```tut:book:silent 15 | trait MyTC[A] 16 | ``` 17 | 18 | Define primitive instances: 19 | 20 | ```tut:book:silent 21 | implicit def intInstance: MyTC[Int] = ??? 22 | implicit def stringInstance: MyTC[String] = ??? 23 | implicit def booleanInstance: MyTC[Boolean] = ??? 24 | ``` 25 | 26 | Define instances for `HList`: 27 | 28 | ```tut:book:silent 29 | import shapeless._ 30 | 31 | implicit def hnilInstance: MyTC[HNil] = ??? 32 | 33 | implicit def hlistInstance[H, T <: HList]( 34 | implicit 35 | hInstance: Lazy[MyTC[H]], // wrap in Lazy 36 | tInstance: MyTC[T] 37 | ): MyTC[H :: T] = ??? 38 | ``` 39 | 40 | If required, define instances for `Coproduct`: 41 | 42 | ```tut:book:silent 43 | implicit def cnilInstance: MyTC[CNil] = ??? 44 | 45 | implicit def coproductInstance[H, T <: Coproduct]( 46 | implicit 47 | hInstance: Lazy[MyTC[H]], // wrap in Lazy 48 | tInstance: MyTC[T] 49 | ): MyTC[H :+: T] = ??? 50 | ``` 51 | 52 | Finally, define an instance for `Generic`: 53 | 54 | ```tut:book:silent 55 | implicit def genericInstance[A, R]( 56 | implicit 57 | generic: Generic.Aux[A, R], 58 | rInstance: Lazy[MyTC[R]] // wrap in Lazy 59 | ): MyTC[A] = ??? 60 | ``` 61 | 62 | In the next chapter we'll cover some useful theory 63 | and programming patterns 64 | to help write code in this style. 65 | In Chapter [@sec:labelled-generic] 66 | we will revisit type class derivation 67 | using a variant of `Generic` that 68 | allows us to inspect field and type names 69 | in our ADTs. 70 | -------------------------------------------------------------------------------- /src/pages/generic/type-classes.md: -------------------------------------------------------------------------------- 1 | ## Recap: type classes {#sec:generic:type-classes} 2 | 3 | Before we get into the depths of instance derivation, 4 | let's quickly recap on the important aspects of type classes. 5 | 6 | Type classes are a programming pattern borrowed from Haskell 7 | (the word "class" has nothing to do with 8 | classes in object oriented programming). 9 | We encode them in Scala using traits and implicits. 10 | A *type class* is a parameterised trait 11 | representing some sort of general functionality 12 | that we would like to apply to a wide range of types: 13 | 14 | ```tut:book:silent 15 | // Turn a value of type A into a row of cells in a CSV file: 16 | trait CsvEncoder[A] { 17 | def encode(value: A): List[String] 18 | } 19 | ``` 20 | 21 | We implement our type class with *instances* 22 | for each type we care about. 23 | If we want the instances to automatically be in scope 24 | we can place them in the type class' companion object. 25 | Otherwise we can place them in a separate library object 26 | for the user to import manually: 27 | 28 | ```tut:book:silent 29 | // Custom data type: 30 | case class Employee(name: String, number: Int, manager: Boolean) 31 | 32 | // CsvEncoder instance for the custom data type: 33 | implicit val employeeEncoder: CsvEncoder[Employee] = 34 | new CsvEncoder[Employee] { 35 | def encode(e: Employee): List[String] = 36 | List( 37 | e.name, 38 | e.number.toString, 39 | if(e.manager) "yes" else "no" 40 | ) 41 | } 42 | ``` 43 | 44 | We mark each instance with the keyword `implicit`, 45 | and define one or more entry point methods 46 | that accept an implicit parameter of the corresponding type: 47 | 48 | ```tut:book:silent 49 | def writeCsv[A](values: List[A])(implicit enc: CsvEncoder[A]): String = 50 | values.map(value => enc.encode(value).mkString(",")).mkString("\n") 51 | ``` 52 | 53 | We'll test `writeCsv` with some test data: 54 | 55 | ```tut:book:silent 56 | val employees: List[Employee] = List( 57 | Employee("Bill", 1, true), 58 | Employee("Peter", 2, false), 59 | Employee("Milton", 3, false) 60 | ) 61 | ``` 62 | 63 | When we call `writeCsv`, 64 | the compiler calculates the value of the type parameter 65 | and searches for an implicit `CsvEncoder` 66 | of the corresponding type: 67 | 68 | ```tut:book 69 | writeCsv(employees) 70 | ``` 71 | 72 | We can use `writeCsv` with any data type we like, 73 | provided we have a corresponding implicit `CsvEncoder` in scope: 74 | 75 | ```tut:book:silent 76 | case class IceCream(name: String, numCherries: Int, inCone: Boolean) 77 | 78 | implicit val iceCreamEncoder: CsvEncoder[IceCream] = 79 | new CsvEncoder[IceCream] { 80 | def encode(i: IceCream): List[String] = 81 | List( 82 | i.name, 83 | i.numCherries.toString, 84 | if(i.inCone) "yes" else "no" 85 | ) 86 | } 87 | 88 | val iceCreams: List[IceCream] = List( 89 | IceCream("Sundae", 1, false), 90 | IceCream("Cornetto", 0, true), 91 | IceCream("Banana Split", 0, false) 92 | ) 93 | ``` 94 | 95 | ```tut:book 96 | writeCsv(iceCreams) 97 | ``` 98 | 99 | ### Resolving instances 100 | 101 | Type classes are very flexible 102 | but they require us to define instances 103 | for every type we care about. 104 | Fortunately, the Scala compiler has a few tricks up its sleeve 105 | to resolve instances for us given sets of user-defined rules. 106 | For example, we can write a rule 107 | that creates a `CsvEncoder` for `(A, B)` 108 | given `CsvEncoders` for `A` and `B`: 109 | 110 | ```tut:book:silent 111 | implicit def pairEncoder[A, B]( 112 | implicit 113 | aEncoder: CsvEncoder[A], 114 | bEncoder: CsvEncoder[B] 115 | ): CsvEncoder[(A, B)] = 116 | new CsvEncoder[(A, B)] { 117 | def encode(pair: (A, B)): List[String] = { 118 | val (a, b) = pair 119 | aEncoder.encode(a) ++ bEncoder.encode(b) 120 | } 121 | } 122 | ``` 123 | 124 | When all the parameters to an `implicit def` 125 | are themselves marked as `implicit`, 126 | the compiler can use it as a resolution rule 127 | to create instances from other instances. 128 | For example, if we call `writeCsv` 129 | and pass in a `List[(Employee, IceCream)]`, 130 | the compiler is able to combine 131 | `pairEncoder`, `employeeEncoder`, and `iceCreamEncoder` 132 | to produce the required `CsvEncoder[(Employee, IceCream)]`: 133 | 134 | ```tut:book 135 | writeCsv(employees zip iceCreams) 136 | ``` 137 | 138 | Given a set of rules 139 | encoded as `implicit vals` and `implicit defs`, 140 | the compiler is capable of *searching* for 141 | combinations to give it the required instances. 142 | This behaviour, known as "implicit resolution", 143 | is what makes the type class pattern so powerful in Scala. 144 | 145 | Even with this power, 146 | the compiler can't pull apart 147 | our case classes and sealed traits. 148 | We are required to define instances for ADTs by hand. 149 | Shapeless' generic representations change all of this, 150 | allowing us to derive instances for any ADT for free. 151 | 152 | ### Idiomatic type class definitions {#sec:generic:idiomatic-style} 153 | 154 | The commonly accepted idiomatic style for type class definitions 155 | includes a companion object containing some standard methods: 156 | 157 | ```tut:book:silent 158 | object CsvEncoder { 159 | // "Summoner" method 160 | def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] = 161 | enc 162 | 163 | // "Constructor" method 164 | def instance[A](func: A => List[String]): CsvEncoder[A] = 165 | new CsvEncoder[A] { 166 | def encode(value: A): List[String] = 167 | func(value) 168 | } 169 | 170 | // Globally visible type class instances 171 | } 172 | ``` 173 | 174 | The `apply` method, known as a "summoner" or "materializer", 175 | allows us to summon a type class instance given a target type: 176 | 177 | ```tut:book 178 | CsvEncoder[IceCream] 179 | ``` 180 | 181 | In simple cases the summoner does the same job as 182 | the `implicitly` method defined in `scala.Predef`: 183 | 184 | ```tut:book 185 | implicitly[CsvEncoder[IceCream]] 186 | ``` 187 | 188 | However, 189 | as we will see in Section [@sec:type-level-programming:depfun], 190 | when working with shapeless we encounter situations 191 | where `implicitly` doesn't infer types correctly. 192 | We can always define the summoner method to do the right thing, 193 | so it's worth writing one for every type class we create. 194 | We can also use a special method from shapeless called "`the`" 195 | (more on this later): 196 | 197 | ```tut:book:silent 198 | import shapeless._ 199 | ``` 200 | 201 | ```tut:book 202 | the[CsvEncoder[IceCream]] 203 | ``` 204 | 205 | The `instance` method, sometimes named `pure`, 206 | provides a terse syntax for creating new type class instances, 207 | reducing the boilerplate of anonymous class syntax: 208 | 209 | ```tut:book:silent 210 | implicit val booleanEncoder: CsvEncoder[Boolean] = 211 | new CsvEncoder[Boolean] { 212 | def encode(b: Boolean): List[String] = 213 | if(b) List("yes") else List("no") 214 | } 215 | ``` 216 | 217 | down to something much shorter: 218 | 219 | ```tut:book:invisible 220 | import CsvEncoder.instance 221 | ``` 222 | 223 | ```tut:book:silent 224 | implicit val booleanEncoder: CsvEncoder[Boolean] = 225 | instance(b => if(b) List("yes") else List("no")) 226 | ``` 227 | 228 | Unfortunately, 229 | several limitations of typesetting code in a book 230 | prevent us writing long singletons 231 | containing lots of methods and instances. 232 | We therefore tend to describe definitions 233 | outside of their context in the companion object. 234 | Bear this in mind as you read 235 | and check the accompanying repo 236 | linked in Section [@sec:intro:source-code] 237 | for complete worked examples. 238 | -------------------------------------------------------------------------------- /src/pages/intro/contributors.md: -------------------------------------------------------------------------------- 1 | ## Acknowledgements 2 | 3 | Thanks to Miles Sabin, Richard Dallaway, Noel Welsh, and Travis Brown, 4 | for their invaluable contributions to this guide. 5 | 6 | Special thanks to Sam Halliday for this excellent workshop 7 | [Shapeless for Mortals][link-fommil-scalax-2015], 8 | which provided the initial inspiration and skeleton, 9 | and to Rob Norris and his fellow contributors 10 | for the awesome [Tut][link-tut], 11 | which keeps our examples compiling correctly. 12 | 13 | Finally, thanks to everyone who has contributed on Github. 14 | Here is an alphabetical list of contributors: 15 | 16 | Aaron S. Hawley, 17 | Aleksandr Vinokurov, 18 | Aristotelis Dossas, 19 | Chris Birchall, 20 | Dani Rey, 21 | Dennis Hunziker, 22 | ErunamoJAZZ, 23 | Evgeny Veretennikov, 24 | Jasper Moeys, 25 | Kevin Wright, 26 | ltbs, 27 | Matt Kohl, 28 | Maximilien Riehl, 29 | Mike Limansky, 30 | Philippus Baalman, 31 | Piotr Gołębiewski, 32 | Richard Dallaway, 33 | ronanM, 34 | s3ni0r, 35 | Tony Lottsm 36 | and Yoshimura Yuu 37 | 38 | A separately maintained French translation 39 | of this book is also available [on Github][link-fr]. 40 | Thanks to etienne and fellow contributors 41 | for producing and maintaining it! 42 | 43 | If you spot an error or potential improvement, 44 | please raise an issue or submit a PR 45 | on the [Github page][link-book-repo]. 46 | -------------------------------------------------------------------------------- /src/pages/intro/generic.md: -------------------------------------------------------------------------------- 1 | ## What is generic programming? 2 | 3 | Types are helpful because they are specific: 4 | they show us how different pieces of code fit together, 5 | help us prevent bugs, 6 | and guide us toward solutions when we code. 7 | 8 | Sometimes, however, types are *too* specific. 9 | There are situations where we want 10 | to exploit similarities between types to avoid repetition. 11 | For example, consider the following definitions: 12 | 13 | ```tut:book:silent 14 | case class Employee(name: String, number: Int, manager: Boolean) 15 | 16 | case class IceCream(name: String, numCherries: Int, inCone: Boolean) 17 | ``` 18 | 19 | These two case classes represent different kinds of data 20 | but they have clear similarities: 21 | they both contain three fields of the same types. 22 | Suppose we want to implement a generic operation 23 | such as serializing to a CSV file. 24 | Despite the similarity between the two types, 25 | we have to write two separate serialization methods: 26 | 27 | ```tut:book:silent 28 | def employeeCsv(e: Employee): List[String] = 29 | List(e.name, e.number.toString, e.manager.toString) 30 | 31 | def iceCreamCsv(c: IceCream): List[String] = 32 | List(c.name, c.numCherries.toString, c.inCone.toString) 33 | ``` 34 | 35 | Generic programming is about overcoming differences like these. 36 | Shapeless makes it convenient to convert specific types 37 | into generic ones that we can manipulate with common code. 38 | 39 | For example, we can use the code below to 40 | convert employees and ice creams to values of the same type. 41 | Don't worry if you don't follow this example yet---we'll 42 | get to grips with the various concepts later on: 43 | 44 | ```tut:book:silent 45 | import shapeless._ 46 | ``` 47 | 48 | ```tut:book 49 | val genericEmployee = Generic[Employee].to(Employee("Dave", 123, false)) 50 | val genericIceCream = Generic[IceCream].to(IceCream("Sundae", 1, false)) 51 | ``` 52 | 53 | Both values are now of the same type. 54 | They are both heterogeneous lists (`HLists` for short) 55 | containing a `String`, an `Int`, and a `Boolean`. 56 | We'll look at `HLists` and the important role they play soon. 57 | For now the point is that we can serialize each value 58 | with the same function: 59 | 60 | ```tut:book:silent 61 | def genericCsv(gen: String :: Int :: Boolean :: HNil): List[String] = 62 | List(gen(0), gen(1).toString, gen(2).toString) 63 | ``` 64 | 65 | ```tut:book 66 | genericCsv(genericEmployee) 67 | genericCsv(genericIceCream) 68 | ``` 69 | 70 | This example is basic 71 | but it hints at the essence of generic programming. 72 | We reformulate problems so we can solve them using generic building blocks, 73 | and write small kernels of code that work with a wide variety of types. 74 | Generic programming with shapeless 75 | allows us to eliminate huge amounts of boilerplate, 76 | making Scala applications easier to read, write, and maintain. 77 | 78 | Does that sound compelling? Thought so. Let's jump in! 79 | -------------------------------------------------------------------------------- /src/pages/intro/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This book is a guide to using [shapeless][link-shapeless], 4 | a library for *generic programming* in Scala. 5 | Shapeless is a large library, 6 | so rather than cover everything it has to offer 7 | we will concentrate on a few compelling use cases 8 | and use them to build a picture 9 | of the tools and patterns available. 10 | 11 | Before we start, let's talk about what generic programming is 12 | and why shapeless is so exciting to Scala developers. 13 | -------------------------------------------------------------------------------- /src/pages/intro/source-code.md: -------------------------------------------------------------------------------- 1 | ## Source code and examples {#sec:intro:source-code} 2 | 3 | This book is open source. 4 | You can find the [Markdown source on Github][link-book-repo]. 5 | The book receives constant updates from the community 6 | so be sure to check the Github repo 7 | for the most up-to-date version. 8 | 9 | We also maintain a copy of the book 10 | on the [Underscore web site][link-underscore-book]. 11 | If you grab a copy of the book from there 12 | we will notify you whenever we release an update. 13 | 14 | There are complete implementations of 15 | the major examples in an [accompanying repo][link-code-repo]. 16 | See the README for installation details. 17 | We assume shapeless 2.3.2 and either 18 | Typelevel Scala 2.11.8+ or 19 | Lightbend Scala 2.11.9+ / 2.12.1+. 20 | 21 | Most of the examples in this book 22 | are compiled and executed using 23 | version 2.12.1 of the Typelevel Scala compiler. 24 | Among other niceties 25 | this version of Scala introduces *infix type printing*, 26 | which cleans up the console output on the REPL: 27 | 28 | ```tut:book:invisible 29 | import shapeless._ 30 | ``` 31 | 32 | ```tut:book 33 | val repr = "Hello" :: 123 :: true :: HNil 34 | ``` 35 | 36 | If you are using an older version of Scala 37 | you might end up with prefix type printing like this: 38 | 39 | ```scala 40 | val repr = "Hello" :: 123 :: true :: HNil 41 | // repr: shapeless.::[String,shapeless.::[Int,shapeless.::[Boolean,shapeless.HNil]]] = "Hello" :: 123 :: true :: HNil 42 | ``` 43 | 44 | Don't panic! 45 | Aside from the printed form of the result 46 | (infix versus prefix syntax), 47 | these types are the same. 48 | If you find the prefix types difficult to read, 49 | we recommend upgrading to a newer version of Scala. 50 | Simply add the following to your `build.sbt`, 51 | substituting in contemporary version numbers as appropriate: 52 | 53 | ```scala 54 | scalaOrganization := "org.typelevel" 55 | scalaVersion := "2.12.1" 56 | ``` 57 | 58 | The `scalaOrganization` setting 59 | is only supported in SBT 0.13.13 or later. 60 | You can specify an SBT version 61 | by writing the following in `project/build.properties` 62 | (create the file if it isn't there in your project): 63 | 64 | ``` 65 | sbt.version=0.13.13 66 | ``` 67 | -------------------------------------------------------------------------------- /src/pages/intro/thisbook.md: -------------------------------------------------------------------------------- 1 | ## About this book {#sec:intro:about-this-book} 2 | 3 | This book is divided into two parts. 4 | 5 | In Part I we introduce *type class derivation*, 6 | which allows us to create type class instances 7 | for any algebraic data type 8 | using only a handful of generic rules. 9 | Part I consists of four chapters: 10 | 11 | - In Chapter [@sec:representations] 12 | we introduce *generic representations*. 13 | We also introduce shapeless' `Generic` type class, 14 | which can produce a generic encoding 15 | for any case class or sealed trait. 16 | 17 | - In Chapter [@sec:generic] we use `Generic` 18 | to derive instances of a custom type class. 19 | We create an example type class 20 | to encode Scala data as 21 | Comma Separated Values (CSV), 22 | but the techniques we cover 23 | can be extended to many situations. 24 | We also introduce shapeless' `Lazy` type, 25 | which lets us handle recursive data like lists and trees. 26 | 27 | - In Chapter [@sec:type-level-programming] 28 | we introduce the theory and programming patterns we need 29 | to generalise the techniques from earlier chapters. 30 | Specifically we look at dependent types, 31 | dependently typed functions, 32 | and type level programming. 33 | This allows us to access 34 | more advanced applications of shapeless. 35 | 36 | - In Chapter [@sec:labelled-generic] we introduce `LabelledGeneric`, 37 | a variant of `Generic` that exposes field and type names 38 | as part of its generic representations. 39 | We also introduce additional theory: 40 | literal types, singleton types, phantom types, and type tagging. 41 | We demonstrate `LabelledGeneric` by creating 42 | a JSON encoder that preserves field and type names in its output. 43 | 44 | In Part II we introduce the "ops type classes" 45 | provided in the `shapeless.ops` package. 46 | Ops type classes form an extensive library of tools 47 | for manipulating generic representations. 48 | Rather than discuss every op in detail, 49 | we provide a theoretical primer in three chapters: 50 | 51 | - In Chapter [@sec:ops] we discuss 52 | the general layout of the ops type classes 53 | and provide an example 54 | that strings several simple ops together 55 | to form a powerful "case class migration" tool. 56 | 57 | - In Chapter [@sec:poly] we introduce 58 | *polymorphic functions*, 59 | also known as `Polys`, 60 | and show how they are used in 61 | ops type classes for mapping, 62 | flat mapping, and folding 63 | over generic representations. 64 | 65 | - Finally, in Chapter [@sec:nat] we introduce 66 | the `Nat` type that shapeless uses 67 | to represent natural numbers at the type level. 68 | We introduce several related ops type classes, 69 | and use `Nat` to develop 70 | our own version of Scalacheck's `Arbitrary`. 71 | -------------------------------------------------------------------------------- /src/pages/labelled-generic/coproducts.md: -------------------------------------------------------------------------------- 1 | ## Deriving coproduct instances with *LabelledGeneric* 2 | 3 | ```tut:book:invisible:reset 4 | // ---------------------------------------------- 5 | // Forward definitions: 6 | 7 | sealed trait JsonValue 8 | final case class JsonObject(fields: List[(String, JsonValue)]) extends JsonValue 9 | final case class JsonArray(items: List[JsonValue]) extends JsonValue 10 | final case class JsonString(value: String) extends JsonValue 11 | final case class JsonNumber(value: Double) extends JsonValue 12 | final case class JsonBoolean(value: Boolean) extends JsonValue 13 | case object JsonNull extends JsonValue 14 | 15 | trait JsonEncoder[A] { 16 | def encode(value: A): JsonValue 17 | } 18 | 19 | object JsonEncoder { 20 | def apply[A](implicit enc: JsonEncoder[A]): JsonEncoder[A] = 21 | enc 22 | } 23 | 24 | def createEncoder[A](func: A => JsonValue): JsonEncoder[A] = 25 | new JsonEncoder[A] { 26 | def encode(value: A): JsonValue = 27 | func(value) 28 | } 29 | 30 | implicit val stringEncoder: JsonEncoder[String] = 31 | createEncoder(str => JsonString(str)) 32 | 33 | implicit val doubleEncoder: JsonEncoder[Double] = 34 | createEncoder(num => JsonNumber(num)) 35 | 36 | implicit val intEncoder: JsonEncoder[Int] = 37 | createEncoder(num => JsonNumber(num)) 38 | 39 | implicit val booleanEncoder: JsonEncoder[Boolean] = 40 | createEncoder(bool => JsonBoolean(bool)) 41 | 42 | implicit def listEncoder[A](implicit encoder: JsonEncoder[A]): JsonEncoder[List[A]] = 43 | createEncoder(list => JsonArray(list.map(encoder.encode))) 44 | 45 | implicit def optionEncoder[A](implicit encoder: JsonEncoder[A]): JsonEncoder[Option[A]] = 46 | createEncoder(opt => opt.map(encoder.encode).getOrElse(JsonNull)) 47 | 48 | trait JsonObjectEncoder[A] extends JsonEncoder[A] { 49 | def encode(value: A): JsonObject 50 | } 51 | 52 | def createObjectEncoder[A](func: A => JsonObject): JsonObjectEncoder[A] = 53 | new JsonObjectEncoder[A] { 54 | def encode(value: A): JsonObject = 55 | func(value) 56 | } 57 | 58 | import shapeless.{HList, ::, HNil, Witness, Lazy} 59 | import shapeless.labelled.FieldType 60 | 61 | implicit val hnilObjectEncoder: JsonObjectEncoder[HNil] = 62 | createObjectEncoder(hnil => JsonObject(Nil)) 63 | 64 | implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList]( 65 | implicit 66 | witness: Witness.Aux[K], 67 | hEncoder: Lazy[JsonEncoder[H]], 68 | tEncoder: JsonObjectEncoder[T] 69 | ): JsonObjectEncoder[FieldType[K, H] :: T] = { 70 | val fieldName: String = witness.value.name 71 | createObjectEncoder { hlist => 72 | val head = hEncoder.value.encode(hlist.head) 73 | val tail = tEncoder.encode(hlist.tail) 74 | JsonObject((fieldName, head) :: tail.fields) 75 | } 76 | } 77 | 78 | import shapeless.LabelledGeneric 79 | 80 | implicit def genericObjectEncoder[A, H]( 81 | implicit 82 | generic: LabelledGeneric.Aux[A, H], 83 | hEncoder: Lazy[JsonObjectEncoder[H]] 84 | ): JsonObjectEncoder[A] = 85 | createObjectEncoder(value => hEncoder.value.encode(generic.to(value))) 86 | 87 | sealed trait Shape 88 | final case class Rectangle(width: Double, height: Double) extends Shape 89 | final case class Circle(radius: Double) extends Shape 90 | 91 | // ---------------------------------------------- 92 | ``` 93 | 94 | Applying `LabelledGeneric` with `Coproducts` 95 | involves a mixture of the concepts we've covered already. 96 | Let's start by examining 97 | a `Coproduct` type derived by `LabelledGeneric`. 98 | We'll re-visit our `Shape` ADT from Chapter [@sec:generic]: 99 | 100 | ```tut:book:silent 101 | import shapeless.LabelledGeneric 102 | 103 | sealed trait Shape 104 | final case class Rectangle(width: Double, height: Double) extends Shape 105 | final case class Circle(radius: Double) extends Shape 106 | ``` 107 | 108 | ```tut:book 109 | LabelledGeneric[Shape].to(Circle(1.0)) 110 | ``` 111 | 112 | Here is that `Coproduct` type in a more readable format: 113 | 114 | ```scala 115 | // Rectangle with KeyTag[Symbol with Tagged["Rectangle"], Rectangle] :+: 116 | // Circle with KeyTag[Symbol with Tagged["Circle"], Circle] :+: 117 | // CNil 118 | ``` 119 | 120 | As you can see, the result is a `Coproduct` of the subtypes of `Shape`, 121 | each tagged with the type name. 122 | We can use this information to write `JsonEncoders` for `:+:` and `CNil`: 123 | 124 | ```tut:book:silent 125 | import shapeless.{Coproduct, :+:, CNil, Inl, Inr, Witness, Lazy} 126 | import shapeless.labelled.FieldType 127 | 128 | implicit val cnilObjectEncoder: JsonObjectEncoder[CNil] = 129 | createObjectEncoder(cnil => throw new Exception("Inconceivable!")) 130 | 131 | implicit def coproductObjectEncoder[K <: Symbol, H, T <: Coproduct]( 132 | implicit 133 | witness: Witness.Aux[K], 134 | hEncoder: Lazy[JsonEncoder[H]], 135 | tEncoder: JsonObjectEncoder[T] 136 | ): JsonObjectEncoder[FieldType[K, H] :+: T] = { 137 | val typeName = witness.value.name 138 | createObjectEncoder { 139 | case Inl(h) => 140 | JsonObject(List(typeName -> hEncoder.value.encode(h))) 141 | 142 | case Inr(t) => 143 | tEncoder.encode(t) 144 | } 145 | } 146 | ``` 147 | 148 | `coproductEncoder` follows the same pattern as `hlistEncoder`. 149 | We have three type parameters: 150 | `K` for the type name, 151 | `H` for the value at the head of the `HList`, 152 | and `T` for the value at the tail. 153 | We use `FieldType` and `:+:` in the result type 154 | to declare the relationships between the three, 155 | and we use a `Witness` to access the runtime value of the type name. 156 | The result is an object containing a single key/value pair, 157 | the key being the type name and the value the result: 158 | 159 | ```tut:book:silent 160 | val shape: Shape = Circle(1.0) 161 | ``` 162 | 163 | ```tut:book 164 | JsonEncoder[Shape].encode(shape) 165 | ``` 166 | 167 | Other encodings are possible with a little more work. 168 | We can add a `"type"` field to the output, for example, 169 | or even allow the user to configure the format. 170 | Sam Halliday's [spray-json-shapeless][link-spray-json-shapeless] 171 | is an excellent example of a codebase 172 | that is approachable while providing a great deal of flexibility. 173 | -------------------------------------------------------------------------------- /src/pages/labelled-generic/index.md: -------------------------------------------------------------------------------- 1 | # Accessing names during implicit derivation {#sec:labelled-generic} 2 | 3 | Often, the type class instances we define 4 | need access to more than just types. 5 | In this chapter we will look at 6 | a variant of `Generic` called `LabelledGeneric` 7 | that gives us access to field names and type names. 8 | 9 | To begin with we have some theory to cover. 10 | `LabelledGeneric` uses some clever techniques 11 | to expose name information at the type level. 12 | To understand these techniques 13 | we must discuss *literal types*, 14 | *singleton types*, 15 | *phantom types*, 16 | and *type tagging*. 17 | -------------------------------------------------------------------------------- /src/pages/labelled-generic/literal-types.md: -------------------------------------------------------------------------------- 1 | ## Literal types 2 | 3 | A Scala value may have multiple types. 4 | For example, the string `"hello"` 5 | has at least three types: 6 | `String`, `AnyRef`, 7 | and `Any`[^multiple-inheritance]: 8 | 9 | ```tut:book 10 | "hello" : String 11 | "hello" : AnyRef 12 | "hello" : Any 13 | ``` 14 | 15 | [^multiple-inheritance]: 16 | `String` also has a bunch of other types 17 | like `Serializable` and `Comparable` 18 | but let's ignore those for now. 19 | 20 | Interestingly, `"hello"` also has another type: 21 | a "singleton type" 22 | that belongs exclusively to that one value. 23 | This is similar to the singleton type we get 24 | when we define a companion object: 25 | 26 | ```tut:book:silent 27 | object Foo 28 | ``` 29 | 30 | ```tut:book 31 | Foo 32 | ``` 33 | 34 | The type `Foo.type` is the type of `Foo`, 35 | and `Foo` is the only value with that type. 36 | 37 | Singleton types applied to literal values are called *literal types*. 38 | These have existed in Scala for a long time, 39 | but we don't normally interact with them 40 | because the default behaviour of the compiler is 41 | to "widen" literals to their nearest non-singleton type. 42 | For example, these two expressions are essentially equivalent: 43 | 44 | ```tut:book 45 | "hello" 46 | 47 | ("hello" : String) 48 | ``` 49 | 50 | Shapeless provides a few tools for working with literal types. 51 | First, there is a `narrow` macro that converts a 52 | literal expression to a singleton-typed literal expression: 53 | 54 | ```tut:book:silent 55 | import shapeless.syntax.singleton._ 56 | ``` 57 | 58 | ```scala 59 | var x = 42.narrow 60 | // x: Int(42) = 42 61 | ``` 62 | 63 | Note the type of `x` here: `Int(42)` is a literal type. 64 | It is a subtype of `Int` that only contains the value `42`. 65 | If we attempt to assign a different number to `x`, 66 | we get a compile error: 67 | 68 | ```scala 69 | x = 43 70 | // :16: error: type mismatch: 71 | // found : Int(43) 72 | // required: Int(42) 73 | // x = 43 74 | // ^ 75 | ``` 76 | 77 | However, `x` is still an `Int` according to normal subtyping rules. 78 | If we operate on `x` we get a regular type of result: 79 | 80 | ```tut:book:invisible 81 | var x: 42 = 42 82 | ``` 83 | 84 | ```tut:book 85 | x + 1 86 | ``` 87 | 88 | We can use `narrow` on any literal in Scala: 89 | 90 | ```scala 91 | 1.narrow 92 | // res7: Int(1) = 1 93 | 94 | true.narrow 95 | // res8: Boolean(true) = true 96 | 97 | "hello".narrow 98 | // res9: String("hello") = hello 99 | 100 | // and so on... 101 | ``` 102 | 103 | ```tut:book:invisible 104 | 1 : 1 105 | true : true 106 | "hello" : "hello" 107 | ``` 108 | 109 | However, we can't use it on compound expressions: 110 | 111 | ```scala 112 | math.sqrt(4).narrow 113 | // :17: error: Expression scala.math.`package`.sqrt(4.0) does not evaluate to a constant or a stable reference value 114 | // math.sqrt(4.0).narrow 115 | // ^ 116 | // :17: error: value narrow is not a member of Double 117 | // math.sqrt(4.0).narrow 118 | // ^ 119 | ``` 120 | 121 |
122 | *Literal types in Scala* 123 | 124 | Until recently, Scala had no syntax for writing literal types. 125 | The types were there in the compiler 126 | but we couldn't express them directly in code. 127 | However, as of Typelevel Scala 2.11.8 we have 128 | direct syntax support for literal types which can be enabled via the `-Yliteral-types` compiler option. 129 | 130 | Lightbend Scala 2.13.0 will also gain literal types by default (with no compiler option needed or available) and allows you to write declarations like the following: 131 | 132 | ```tut:book 133 | val theAnswer: 42 = 42 134 | ``` 135 | 136 | The type `42` is the same as the type `Int(42)` 137 | we saw in printed output earlier. 138 | You'll still see `Int(42)` in output for legacy reasons, 139 | but the canonical syntax going forward is `42`. 140 |
141 | 142 | ## Type tagging and phantom types {#sec:labelled-generic:type-tagging} 143 | 144 | Shapeless uses literal types 145 | to model the names of fields in case classes. 146 | It does this by "tagging" the types of the fields 147 | with the literal types of their names. 148 | Before we see how shapeless does this, 149 | we'll do it ourselves to show that there's no magic 150 | (well... minimal magic, at any rate). 151 | Suppose we have a number: 152 | 153 | ```tut:book:silent 154 | val number = 42 155 | ``` 156 | 157 | This number is an `Int` in two worlds: 158 | at runtime, where it has an actual value 159 | and methods that we can call, 160 | and at compile-time, 161 | where the compiler uses the type 162 | to calculate which pieces of code work together 163 | and to search for implicits. 164 | 165 | We can modify the type of `number` at compile time 166 | without modifying its run-time behaviour 167 | by "tagging" it with a "phantom type". 168 | Phantom types are types with no run-time semantics, 169 | like this: 170 | 171 | ```tut:book:silent 172 | trait Cherries 173 | ``` 174 | 175 | We can tag `number` using `asInstanceOf`. 176 | We end up with a value that is both 177 | an `Int` and a `Cherries` at compile-time, 178 | and an `Int` at run-time: 179 | 180 | ```tut:book 181 | val numCherries = number.asInstanceOf[Int with Cherries] 182 | ``` 183 | 184 | Shapeless uses this trick to tag 185 | fields and subtypes in an ADT 186 | with the singleton types of their names. 187 | If you find using `asInstanceOf` uncomfortable then don't worry: 188 | shapeless provides two tagging syntaxes 189 | to avoid such unsavoriness. 190 | 191 | The first syntax, `->>`, 192 | tags the expression on the right of the arrow 193 | with the singleton type of the literal expression on the left: 194 | 195 | ```tut:book:silent 196 | import shapeless.labelled.{KeyTag, FieldType} 197 | import shapeless.syntax.singleton._ 198 | 199 | val someNumber = 123 200 | ``` 201 | 202 | ```tut:book 203 | val numCherries = "numCherries" ->> someNumber 204 | ``` 205 | 206 | Here we are tagging `someNumber` with 207 | the following phantom type: 208 | 209 | ```scala 210 | KeyTag["numCherries", Int] 211 | ``` 212 | 213 | The tag encodes both the name and type of the field, 214 | the combination of which is useful 215 | when searching for entries in a `Repr` using implicit resolution. 216 | 217 | The second syntax takes the tag as a type 218 | rather than a literal value. 219 | This is useful when we know what tag to use 220 | but don't have the ability 221 | to write specific literals in our code: 222 | 223 | ```tut:book:silent 224 | import shapeless.labelled.field 225 | ``` 226 | 227 | ```tut:book 228 | field[Cherries](123) 229 | ``` 230 | 231 | `FieldType` is a type alias that simplifies 232 | extracting the tag and base types from a tagged type: 233 | 234 | ```scala 235 | type FieldType[K, V] = V with KeyTag[K, V] 236 | ``` 237 | 238 | As we'll see in a moment, 239 | shapeless uses this mechanism to tag 240 | fields and subtypes with 241 | their names in our source code. 242 | 243 | Tags exist purely at compile time 244 | and have no runtime representation. 245 | How do we convert them to values we can use at runtime? 246 | Shapeless provides a type class called `Witness` for this purpose[^witness]. 247 | If we combine `Witness` and `FieldType`, 248 | we get something very compelling---the 249 | ability to extract the field name 250 | from a tagged field: 251 | 252 | [^witness]: The term "witness" is borrowed from 253 | [mathematical proofs][link-witness]. 254 | 255 | ```tut:book:silent 256 | import shapeless.Witness 257 | ``` 258 | 259 | ```tut:book 260 | val numCherries = "numCherries" ->> 123 261 | ``` 262 | 263 | ```tut:book:silent 264 | // Get the tag from a tagged value: 265 | def getFieldName[K, V](value: FieldType[K, V]) 266 | (implicit witness: Witness.Aux[K]): K = 267 | witness.value 268 | ``` 269 | 270 | ```tut:book 271 | getFieldName(numCherries) 272 | ``` 273 | 274 | ```tut:book:silent 275 | // Get the untagged type of a tagged value: 276 | def getFieldValue[K, V](value: FieldType[K, V]): V = 277 | value 278 | ``` 279 | 280 | ```tut:book 281 | getFieldValue(numCherries) 282 | ``` 283 | 284 | If we build an `HList` of tagged elements, 285 | we get a data structure that has some of the properties of a `Map`. 286 | We can reference fields by tag, 287 | manipulate and replace them, 288 | and maintain all of the type and naming information along the way. 289 | Shapeless calls these structures "records". 290 | 291 | ### Records and *LabelledGeneric* 292 | 293 | Records are `HLists` of tagged elements: 294 | 295 | ```tut:book:silent 296 | import shapeless.{HList, ::, HNil} 297 | ``` 298 | 299 | ```tut:book 300 | val garfield = ("cat" ->> "Garfield") :: ("orange" ->> true) :: HNil 301 | ``` 302 | 303 | For clarity, the type of `garfield` is as follows: 304 | 305 | ```scala 306 | // FieldType["cat", String] :: 307 | // FieldType["orange", Boolean] :: 308 | // HNil 309 | ``` 310 | 311 | We don't need to go into depth regarding records here; 312 | suffice to say that records are the generic representation 313 | used by `LabelledGeneric`. 314 | `LabelledGeneric` tags each item in a product or coproduct 315 | with the corresponding field or type name from the concrete ADT 316 | (although the names are represented as `Symbols`, not `Strings`). 317 | Shapeless provides a suite of `Map`-like operations on records, 318 | some of which we'll cover in Section [@sec:ops:record]. 319 | For now, though, let's derive some type classes using `LabelledGeneric`. 320 | -------------------------------------------------------------------------------- /src/pages/labelled-generic/products.md: -------------------------------------------------------------------------------- 1 | ## Deriving product instances with *LabelledGeneric* 2 | 3 | We'll use a running example of JSON encoding 4 | to illustrate `LabelledGeneric`. 5 | We'll define a `JsonEncoder` type class 6 | that converts values to a JSON AST. 7 | This is the approach taken by 8 | Argonaut, Circe, Play JSON, Spray JSON, 9 | and many other Scala JSON libraries. 10 | 11 | First we'll define our JSON data type: 12 | 13 | ```tut:book:silent 14 | sealed trait JsonValue 15 | case class JsonObject(fields: List[(String, JsonValue)]) extends JsonValue 16 | case class JsonArray(items: List[JsonValue]) extends JsonValue 17 | case class JsonString(value: String) extends JsonValue 18 | case class JsonNumber(value: Double) extends JsonValue 19 | case class JsonBoolean(value: Boolean) extends JsonValue 20 | case object JsonNull extends JsonValue 21 | ``` 22 | 23 | then the type class for encoding values as JSON: 24 | 25 | ```tut:book:silent 26 | trait JsonEncoder[A] { 27 | def encode(value: A): JsonValue 28 | } 29 | 30 | object JsonEncoder { 31 | def apply[A](implicit enc: JsonEncoder[A]): JsonEncoder[A] = enc 32 | } 33 | ``` 34 | 35 | then a few primitive instances: 36 | 37 | ```tut:book:silent 38 | def createEncoder[A](func: A => JsonValue): JsonEncoder[A] = 39 | new JsonEncoder[A] { 40 | def encode(value: A): JsonValue = func(value) 41 | } 42 | 43 | implicit val stringEncoder: JsonEncoder[String] = 44 | createEncoder(str => JsonString(str)) 45 | 46 | implicit val doubleEncoder: JsonEncoder[Double] = 47 | createEncoder(num => JsonNumber(num)) 48 | 49 | implicit val intEncoder: JsonEncoder[Int] = 50 | createEncoder(num => JsonNumber(num)) 51 | 52 | implicit val booleanEncoder: JsonEncoder[Boolean] = 53 | createEncoder(bool => JsonBoolean(bool)) 54 | ``` 55 | 56 | and a few instance combinators: 57 | 58 | ```tut:book:silent 59 | implicit def listEncoder[A] 60 | (implicit enc: JsonEncoder[A]): JsonEncoder[List[A]] = 61 | createEncoder(list => JsonArray(list.map(enc.encode))) 62 | 63 | implicit def optionEncoder[A] 64 | (implicit enc: JsonEncoder[A]): JsonEncoder[Option[A]] = 65 | createEncoder(opt => opt.map(enc.encode).getOrElse(JsonNull)) 66 | ``` 67 | 68 | Ideally, when we encode ADTs as JSON, 69 | we would like to use the correct field names in the output JSON: 70 | 71 | ```tut:book:silent 72 | case class IceCream(name: String, numCherries: Int, inCone: Boolean) 73 | 74 | val iceCream = IceCream("Sundae", 1, false) 75 | 76 | // Ideally we'd like to produce something like this: 77 | val iceCreamJson: JsonValue = 78 | JsonObject(List( 79 | "name" -> JsonString("Sundae"), 80 | "numCherries" -> JsonNumber(1), 81 | "inCone" -> JsonBoolean(false) 82 | )) 83 | ``` 84 | 85 | This is where `LabelledGeneric` comes in. 86 | Let's summon an instance for `IceCream` 87 | and see what kind of representation it produces: 88 | 89 | ```tut:book:silent 90 | import shapeless.LabelledGeneric 91 | ``` 92 | 93 | ```tut:book 94 | val gen = LabelledGeneric[IceCream].to(iceCream) 95 | ``` 96 | 97 | For clarity, the full type of the `HList` is: 98 | 99 | ```scala 100 | // String with KeyTag[Symbol with Tagged["name"], String] :: 101 | // Int with KeyTag[Symbol with Tagged["numCherries"], Int] :: 102 | // Boolean with KeyTag[Symbol with Tagged["inCone"], Boolean] :: 103 | // HNil 104 | ``` 105 | 106 | The type here is slightly more complex than we have seen. 107 | Instead of representing the field names with literal string types, 108 | shapeless is representing them with symbols tagged with literal string types. 109 | The details of the implementation aren't particularly important: 110 | we can still use `Witness` and `FieldType` to extract the tags, 111 | but they come out as `Symbols` instead of `Strings`[^future-tags]. 112 | 113 | [^future-tags]: Future versions of shapeless may switch 114 | to using `Strings` as tags. 115 | 116 | ### Instances for *HLists* 117 | 118 | Let's define `JsonEncoder` instances for `HNil` and `::`. 119 | Our encoders are going to generate and manipulate `JsonObjects`, 120 | so we'll introduce a new type of encoder to make that easier: 121 | 122 | ```tut:book:silent 123 | trait JsonObjectEncoder[A] extends JsonEncoder[A] { 124 | def encode(value: A): JsonObject 125 | } 126 | 127 | def createObjectEncoder[A](fn: A => JsonObject): JsonObjectEncoder[A] = 128 | new JsonObjectEncoder[A] { 129 | def encode(value: A): JsonObject = 130 | fn(value) 131 | } 132 | ``` 133 | 134 | The definition for `HNil` is then straightforward: 135 | 136 | ```tut:book:silent 137 | import shapeless.{HList, ::, HNil, Lazy} 138 | 139 | implicit val hnilEncoder: JsonObjectEncoder[HNil] = 140 | createObjectEncoder(hnil => JsonObject(Nil)) 141 | ``` 142 | 143 | The definition of `hlistEncoder` involves a few moving parts 144 | so we'll go through it piece by piece. 145 | We'll start with the definition we might expect 146 | if we were using regular `Generic`: 147 | 148 | ```tut:book:silent 149 | implicit def hlistObjectEncoder[H, T <: HList]( 150 | implicit 151 | hEncoder: Lazy[JsonEncoder[H]], 152 | tEncoder: JsonObjectEncoder[T] 153 | ): JsonEncoder[H :: T] = ??? 154 | ``` 155 | 156 | `LabelledGeneric` will give us an `HList` of tagged types, 157 | so let's start by introducing a new type variable for the key type: 158 | 159 | ```tut:book:silent 160 | import shapeless.Witness 161 | import shapeless.labelled.FieldType 162 | 163 | implicit def hlistObjectEncoder[K, H, T <: HList]( 164 | implicit 165 | hEncoder: Lazy[JsonEncoder[H]], 166 | tEncoder: JsonObjectEncoder[T] 167 | ): JsonObjectEncoder[FieldType[K, H] :: T] = ??? 168 | ``` 169 | 170 | In the body of our method 171 | we're going to need the value associated with `K`. 172 | We'll add an implicit `Witness` to do this for us: 173 | 174 | ```tut:book:silent 175 | implicit def hlistObjectEncoder[K, H, T <: HList]( 176 | implicit 177 | witness: Witness.Aux[K], 178 | hEncoder: Lazy[JsonEncoder[H]], 179 | tEncoder: JsonObjectEncoder[T] 180 | ): JsonObjectEncoder[FieldType[K, H] :: T] = { 181 | val fieldName = witness.value 182 | ??? 183 | } 184 | ``` 185 | 186 | We can access the value of `K` using `witness.value`, 187 | but the compiler has no way of knowing 188 | what type of tag we're going to get. 189 | `LabelledGeneric` uses `Symbols` for tags, 190 | so we'll put a type bound on `K` 191 | and use `symbol.name` to convert it to a `String`: 192 | 193 | ```tut:book:silent 194 | implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList]( 195 | implicit 196 | witness: Witness.Aux[K], 197 | hEncoder: Lazy[JsonEncoder[H]], 198 | tEncoder: JsonObjectEncoder[T] 199 | ): JsonObjectEncoder[FieldType[K, H] :: T] = { 200 | val fieldName: String = witness.value.name 201 | ??? 202 | } 203 | ``` 204 | 205 | The rest of the definition uses 206 | the principles we covered in Chapter [@sec:generic]: 207 | 208 | ```tut:book:silent 209 | implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList]( 210 | implicit 211 | witness: Witness.Aux[K], 212 | hEncoder: Lazy[JsonEncoder[H]], 213 | tEncoder: JsonObjectEncoder[T] 214 | ): JsonObjectEncoder[FieldType[K, H] :: T] = { 215 | val fieldName: String = witness.value.name 216 | createObjectEncoder { hlist => 217 | val head = hEncoder.value.encode(hlist.head) 218 | val tail = tEncoder.encode(hlist.tail) 219 | JsonObject((fieldName, head) :: tail.fields) 220 | } 221 | } 222 | 223 | ``` 224 | 225 | ### Instances for concrete products 226 | 227 | Finally let's turn to our generic instance. 228 | This is identical to the definitions we've seen before, 229 | except that we're using `LabelledGeneric` instead of `Generic`: 230 | 231 | ```tut:book:silent 232 | import shapeless.LabelledGeneric 233 | 234 | implicit def genericObjectEncoder[A, H]( 235 | implicit 236 | generic: LabelledGeneric.Aux[A, H], 237 | hEncoder: Lazy[JsonObjectEncoder[H]] 238 | ): JsonEncoder[A] = 239 | createObjectEncoder { value => 240 | hEncoder.value.encode(generic.to(value)) 241 | } 242 | ``` 243 | 244 | And that's all we need! 245 | With these definitions in place 246 | we can serialize instances of any case class 247 | and retain the field names in the resulting JSON: 248 | 249 | ```tut:book 250 | JsonEncoder[IceCream].encode(iceCream) 251 | ``` 252 | -------------------------------------------------------------------------------- /src/pages/labelled-generic/summary.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | In this chapter we discussed `LabelledGeneric`, 4 | a variant of `Generic` that exposes type and field names 5 | in its generic representations. 6 | 7 | The names exposed by `LabelledGeneric` 8 | are encoded as type-level tags 9 | so we can target them during implicit resolution. 10 | We started the chapter discussing *literal types* 11 | and the way shapeless uses them in its tags. 12 | We also discussed the `Witness` type class, 13 | which is used to reify literal types as values. 14 | 15 | Finally, we combined `LabelledGeneric`, 16 | literal types, and `Witness` to build a `JsonEcoder` library 17 | that includes sensible names in its output. 18 | 19 | The key take home point from this chapter 20 | is that none of this code uses runtime reflection. 21 | Everything is implemented with types, implicits, 22 | and a small set of macros that are internal to shapeless. 23 | The code we're generating is consequently 24 | very fast and reliable at runtime. 25 | -------------------------------------------------------------------------------- /src/pages/links.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [link-book-repo]: https://github.com/underscoreio/shapeless-guide 4 | [link-code-repo]: https://github.com/underscoreio/shapeless-guide-code 5 | [link-underscore-book]: https://underscore.io/books/shapeless-guide 6 | 7 | 8 | 9 | [code-ops-hlist]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala 10 | [code-ops-coproduct]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/coproducts.scala 11 | [code-ops-record]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/records.scala 12 | [code-ops-hlist-align]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala#L1973-L1997 13 | [code-ops-hlist-init]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala#L814-L840 14 | [code-ops-hlist-intersection]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala#L1297-L1352 15 | [code-ops-hlist-last]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala#L786-L812 16 | [code-ops-hlist-length]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala#L271-L292 17 | [code-ops-coproduct-length]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/coproduct.scala#L516-L541 18 | [code-syntax-hlist-init-last]: https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/syntax/hlists.scala#L103-L112 19 | 20 | 21 | 22 | [link-argonaut]: https://argonaut.io 23 | [link-circe]: https://github.com/travisbrown/circe 24 | [link-contributors]: https://github.com/underscoreio/shapeless-guide/graphs/contributors 25 | [link-doobie]: https://github.com/tpolecat/doobie 26 | [link-ensime]: https://ensime.org 27 | [link-finch]: https://github.com/finagle/finch 28 | [link-frameless]: https://github.com/adelbertc/frameless 29 | [link-parboiled2]: https://github.com/sirthias/parboiled2 30 | [link-play-json]: https://www.playframework.com/documentation/2.5.x/ScalaJson 31 | [link-scodec]: https://github.com/scodec/scodec 32 | [link-scalacheck]: https://scalacheck.org 33 | [link-scalacheck-shapeless]: https://github.com/alexarchambault/scalacheck-shapeless 34 | [link-shapeless]: https://github.com/milessabin/shapeless 35 | [link-spray-json]: https://github.com/spray/spray-json 36 | [link-spray-json-shapeless]: https://github.com/milessabin/spray-json-shapeless 37 | [link-tut]: https://github.com/tpolecat/tut 38 | [link-witness]: https://en.wikipedia.org/wiki/Witness_(mathematics) 39 | 40 | 41 | 42 | [link-folone-scalax-2014]: https://skillsmatter.com/skillscasts/5946-42-rise-of-the-dependent-types 43 | [link-fommil-scalax-2015]: http://fommil.com/scalax15/ 44 | [link-kevinwright-type-classes]: http://stackoverflow.com/questions/5408861/what-are-type-classes-in-scala-useful-for 45 | [link-propensive-nescala-2016]: https://www.youtube.com/watch?v=R8GksuRw3VI 46 | [link-tpolecat-adts]: http://tpolecat.github.io/presentations/algebraic_types.html 47 | [link-travisbrown-generic-type-class-derivation]: https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation 48 | [link-travisbrown-incomplete-generic-type-class-derivation]: https://meta.plasm.us/posts/2015/06/21/deriving-incomplete-type-class-instances 49 | [link-travisbrown-type-level-checksum]: https://meta.plasm.us/posts/2013/06/09/learning-shapeless 50 | [link-dallaway-twenty-two]: http://underscore.io/blog/posts/2016/10/11/twenty-two.html 51 | 52 | [link-typelevel-scala-singleton-type-literals]: https://github.com/typelevel/scala#typelevel-scala-2118 53 | [link-lightbend-scala-singleton-type-literals]: https://github.com/scala/scala/pull/5310 54 | [link-si7046]: https://issues.scala-lang.org/browse/SI-7046 55 | 56 | 57 | 58 | [link-underscore]: https://underscore.io 59 | [link-fr]: https://github.com/crakjie/shapeless-guide -------------------------------------------------------------------------------- /src/pages/nat/index.md: -------------------------------------------------------------------------------- 1 | # Counting with types {#sec:nat} 2 | 3 | From time to time we need to count things at the type level. 4 | For example, we may need to know the length of an `HList` 5 | or the number of terms we have expanded so far in a computation. 6 | We can represent numbers as values easily enough, 7 | but if we want to influence implicit resolution 8 | we need to represent them at the type level. 9 | This chapter covers the theory behind counting with types, 10 | and provides some compelling use cases for type class derivation. 11 | 12 | ## Representing numbers as types 13 | 14 | Shapeless uses "Church encoding" 15 | to represent natural numbers at the type level. 16 | It provides a type `Nat` with two subtypes: 17 | `_0` representing zero, 18 | and `Succ[N]` representing `N+1`: 19 | 20 | ```tut:book:silent 21 | import shapeless.{Nat, Succ} 22 | 23 | type Zero = Nat._0 24 | type One = Succ[Zero] 25 | type Two = Succ[One] 26 | // etc... 27 | ``` 28 | 29 | Shapeless provides aliases for the first 22 `Nats` as `Nat._N`: 30 | 31 | ```tut:book:silent 32 | Nat._1 33 | Nat._2 34 | Nat._3 35 | // etc... 36 | ``` 37 | 38 | `Nat` has no runtime semantics. 39 | We have to use the `ToInt` type class 40 | to convert a `Nat` to a runtime `Int`: 41 | 42 | ```tut:book:silent 43 | import shapeless.ops.nat.ToInt 44 | 45 | val toInt = ToInt[Two] 46 | ``` 47 | 48 | ```tut:book 49 | toInt.apply() 50 | ``` 51 | 52 | The `Nat.toInt` method provides 53 | a convenient shorthand for calling `toInt.apply()`. 54 | It accepts the instance of `ToInt` as an implicit parameter: 55 | 56 | ```tut:book 57 | Nat.toInt[Nat._3] 58 | ``` 59 | -------------------------------------------------------------------------------- /src/pages/nat/length.md: -------------------------------------------------------------------------------- 1 | ## Length of generic representations 2 | 3 | One use case for `Nat` is 4 | determining the lengths of `HLists` and `Coproducts`. 5 | Shapeless provides the 6 | `shapeless.ops.hlist.Length` and 7 | `shapeless.ops.coproduct.Length` type classes for this: 8 | 9 | ```tut:book:silent 10 | import shapeless._ 11 | import shapeless.ops.{hlist, coproduct, nat} 12 | ``` 13 | 14 | ```tut:book 15 | val hlistLength = hlist.Length[String :: Int :: Boolean :: HNil] 16 | val coproductLength = coproduct.Length[Double :+: Char :+: CNil] 17 | ``` 18 | 19 | Instances of `Length` have a type member `Out` 20 | that represents the length as a `Nat`: 21 | 22 | ```tut:book 23 | Nat.toInt[hlistLength.Out] 24 | Nat.toInt[coproductLength.Out] 25 | ``` 26 | 27 | Let's use this in a concrete example. 28 | We'll create a `SizeOf` type class that 29 | counts the number of fields in a case class 30 | and exposes it as a simple `Int`: 31 | 32 | ```tut:book:silent 33 | trait SizeOf[A] { 34 | def value: Int 35 | } 36 | 37 | def sizeOf[A](implicit size: SizeOf[A]): Int = size.value 38 | ``` 39 | 40 | To create an instance of `SizeOf` we need three things: 41 | 42 | 1. a `Generic` to calculate the corresponding `HList` type; 43 | 2. a `Length` to calculate the length of the `HList` as a `Nat`; 44 | 3. a `ToInt` to convert the `Nat` to an `Int`. 45 | 46 | Here's a working implementation 47 | written in the style described in Chapter [@sec:type-level-programming]: 48 | 49 | ```tut:book:silent 50 | implicit def genericSizeOf[A, L <: HList, N <: Nat]( 51 | implicit 52 | generic: Generic.Aux[A, L], 53 | size: hlist.Length.Aux[L, N], 54 | sizeToInt: nat.ToInt[N] 55 | ): SizeOf[A] = 56 | new SizeOf[A] { 57 | val value = sizeToInt.apply() 58 | } 59 | ``` 60 | 61 | We can test our code as follows: 62 | 63 | ```tut:book:silent 64 | case class IceCream(name: String, numCherries: Int, inCone: Boolean) 65 | ``` 66 | 67 | ```tut:book 68 | sizeOf[IceCream] 69 | ``` 70 | -------------------------------------------------------------------------------- /src/pages/nat/ops.md: -------------------------------------------------------------------------------- 1 | ## Other operations involving *Nat* 2 | 3 | Shapeless provides a suite of other operations based on `Nat`. 4 | The `apply` methods on `HList` and `Coproduct` 5 | can accept `Nats` as value or type parameters: 6 | 7 | ```tut:book:silent 8 | import shapeless._ 9 | 10 | val hlist = 123 :: "foo" :: true :: 'x' :: HNil 11 | ``` 12 | 13 | ```tut:book 14 | hlist.apply[Nat._1] 15 | hlist.apply(Nat._3) 16 | ``` 17 | 18 | There are also operations such as 19 | `take`, `drop`, `slice`, and `updatedAt`: 20 | 21 | ```tut:book 22 | hlist.take(Nat._3).drop(Nat._1) 23 | hlist.updatedAt(Nat._1, "bar").updatedAt(Nat._2, "baz") 24 | ``` 25 | 26 | These operations and their associated type classes 27 | are useful for manipulating 28 | individual elements within a product or coproduct. 29 | -------------------------------------------------------------------------------- /src/pages/nat/random.md: -------------------------------------------------------------------------------- 1 | ## Case study: random value generator 2 | 3 | Property-based testing libraries like [ScalaCheck][link-scalacheck] 4 | use type classes to generate random data for unit tests. 5 | For example, ScalaCheck provides the `Arbitrary` type class 6 | that we can use as follows: 7 | 8 | ```tut:book:silent 9 | import org.scalacheck._ 10 | ``` 11 | 12 | ```tut:book 13 | for(i <- 1 to 3) println(Arbitrary.arbitrary[Int].sample) 14 | for(i <- 1 to 3) println(Arbitrary.arbitrary[(Boolean, Byte)].sample) 15 | ``` 16 | 17 | ScalaCheck provides built-in instances of `Arbitrary` 18 | for a wide range of standard Scala types. 19 | However, creating instances of `Arbitrary` for user ADTs 20 | is still a time-consuming manual process. 21 | This makes shapeless integration via libraries like 22 | [scalacheck-shapeless][link-scalacheck-shapeless] very attractive. 23 | 24 | In this section we will create a simple `Random` type class 25 | to generate random values of user-defined ADTs. 26 | We will show how `Length` and `Nat` form 27 | a crucial part of the implementation. 28 | As usual we start with 29 | the definition of the type class itself: 30 | 31 | ```tut:book:invisible 32 | // Ensure we always get the same output: 33 | scala.util.Random.setSeed(0) 34 | ``` 35 | 36 | ```tut:book:silent 37 | trait Random[A] { 38 | def get: A 39 | } 40 | 41 | def random[A](implicit r: Random[A]): A = r.get 42 | ``` 43 | 44 | ### Simple random values 45 | 46 | Let's start with some basic instances of `Random`: 47 | 48 | ```tut:book:silent 49 | // Instance constructor: 50 | def createRandom[A](func: () => A): Random[A] = 51 | new Random[A] { 52 | def get = func() 53 | } 54 | 55 | // Random numbers from 0 to 9: 56 | implicit val intRandom: Random[Int] = 57 | createRandom(() => scala.util.Random.nextInt(10)) 58 | 59 | // Random characters from A to Z: 60 | implicit val charRandom: Random[Char] = 61 | createRandom(() => ('A'.toInt + scala.util.Random.nextInt(26)).toChar) 62 | 63 | // Random booleans: 64 | implicit val booleanRandom: Random[Boolean] = 65 | createRandom(() => scala.util.Random.nextBoolean) 66 | ``` 67 | 68 | We can use these simple generators 69 | via the `random` method as follows: 70 | 71 | ```tut:book 72 | for(i <- 1 to 3) println(random[Int]) 73 | for(i <- 1 to 3) println(random[Char]) 74 | ``` 75 | 76 | ### Random products 77 | 78 | We can create random values for products 79 | using the `Generic` and `HList` techniques 80 | from Chapter [@sec:generic]: 81 | 82 | ```tut:book:silent 83 | import shapeless._ 84 | 85 | implicit def genericRandom[A, R]( 86 | implicit 87 | gen: Generic.Aux[A, R], 88 | random: Lazy[Random[R]] 89 | ): Random[A] = 90 | createRandom(() => gen.from(random.value.get)) 91 | 92 | implicit val hnilRandom: Random[HNil] = 93 | createRandom(() => HNil) 94 | 95 | implicit def hlistRandom[H, T <: HList]( 96 | implicit 97 | hRandom: Lazy[Random[H]], 98 | tRandom: Random[T] 99 | ): Random[H :: T] = 100 | createRandom(() => hRandom.value.get :: tRandom.get) 101 | ``` 102 | 103 | This gets us as far as summoning random instances for case classes: 104 | 105 | ```tut:book:silent 106 | case class Cell(col: Char, row: Int) 107 | ``` 108 | 109 | ```tut:book 110 | for(i <- 1 to 5) println(random[Cell]) 111 | ``` 112 | 113 | ### Random coproducts 114 | 115 | This is where we start hitting problems. 116 | Generating a random instance of a coproduct 117 | involves choosing a random subtype. 118 | Let's start with a naïve implementation: 119 | 120 | ```tut:book:silent 121 | implicit val cnilRandom: Random[CNil] = 122 | createRandom(() => throw new Exception("Inconceivable!")) 123 | 124 | implicit def coproductRandom[H, T <: Coproduct]( 125 | implicit 126 | hRandom: Lazy[Random[H]], 127 | tRandom: Random[T] 128 | ): Random[H :+: T] = 129 | createRandom { () => 130 | val chooseH = scala.util.Random.nextDouble < 0.5 131 | if(chooseH) Inl(hRandom.value.get) else Inr(tRandom.get) 132 | } 133 | ``` 134 | 135 | There problems with this implementation 136 | lie in the 50/50 choice in calculating `chooseH`. 137 | This creates an uneven probability distribution. 138 | For example, consider the following type: 139 | 140 | ```tut:book:silent 141 | sealed trait Light 142 | case object Red extends Light 143 | case object Amber extends Light 144 | case object Green extends Light 145 | ``` 146 | 147 | The `Repr` for `Light` is `Red :+: Amber :+: Green :+: CNil`. 148 | An instance of `Random` for this type 149 | will choose `Red` 50% of the time 150 | and `Amber :+: Green :+: CNil` 50% of the time. 151 | A correct distribution would be 152 | 33% `Red` and 67% `Amber :+: Green :+: CNil`. 153 | 154 | And that's not all. 155 | If we look at the overall probability distribution 156 | we see something even more alarming: 157 | 158 | - `Red` is chosen 1/2 of the time 159 | - `Amber` is chosen 1/4 of the time 160 | - `Green` is chosen 1/8 of the time 161 | - *`CNil` is chosen 1/16 of the time* 162 | 163 | Our coproduct instances will throw exceptions 6.75% of the time! 164 | 165 | ```scala 166 | for(i <- 1 to 100) random[Light] 167 | // java.lang.Exception: Inconceivable! 168 | // ... 169 | ``` 170 | 171 | To fix this problem we have to alter 172 | the probability of choosing `H` over `T`. 173 | The correct behaviour should be to choose 174 | `H` `1/n` of the time, 175 | where `n` is the length of the coproduct. 176 | This ensures an even probability distribution 177 | across the subtypes of the coproduct. 178 | It also ensures we choose the head 179 | of a single-subtype `Coproduct` 100% of the time, 180 | which means we never call `cnilProduct.get`. 181 | Here's an updated implementation: 182 | 183 | ```tut:book:silent 184 | import shapeless.ops.coproduct 185 | import shapeless.ops.nat.ToInt 186 | 187 | implicit def coproductRandom[H, T <: Coproduct, L <: Nat]( 188 | implicit 189 | hRandom: Lazy[Random[H]], 190 | tRandom: Random[T], 191 | tLength: coproduct.Length.Aux[T, L], 192 | tLengthAsInt: ToInt[L] 193 | ): Random[H :+: T] = { 194 | createRandom { () => 195 | val length = 1 + tLengthAsInt() 196 | val chooseH = scala.util.Random.nextDouble < (1.0 / length) 197 | if(chooseH) Inl(hRandom.value.get) else Inr(tRandom.get) 198 | } 199 | } 200 | 201 | ``` 202 | 203 | With these modifications 204 | we can generate random values of any product or coproduct: 205 | 206 | ```tut:book 207 | for(i <- 1 to 5) println(random[Light]) 208 | ``` 209 | 210 | Generating test data for ScalaCheck 211 | normally requires a great deal of boilerplate. 212 | Random value generation is a compelling use case for shapeless 213 | of which `Nat` forms an essential component. 214 | -------------------------------------------------------------------------------- /src/pages/nat/summary.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | In this chapter we discussed 4 | how shapeless represents natural numbers 5 | and how we can use them in type classes. 6 | We saw some predefined ops type classes 7 | that let us do things like calculate lengths 8 | and access elements by index, 9 | and created our own type classes 10 | that use `Nat` in other ways. 11 | 12 | Between `Nat`, `Poly`, and the variety of 13 | types we have seen in the last few chapters, 14 | we have seen just a small fraction of 15 | the toolbox provided in `shapeless.ops`. 16 | There are many other ops type classes 17 | that provide a comprehensive foundation 18 | on which to build our own code. 19 | However, the theory laid out here 20 | is enough to understand the majority of ops 21 | needed to derive our own type classes. 22 | The source code in the `shapeless.ops` 23 | packages should now be approachable enough 24 | to pick up other useful ops. 25 | -------------------------------------------------------------------------------- /src/pages/notes/outline.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | This stuff won't be here when the book is done. 4 | 5 | ## Outline 6 | 7 | TODO: Introduce `illTyped` wherever makes sense. 8 | 9 | - DONE Introduction 10 | 11 | - What is generic programming? 12 | - Abstracting over types 13 | - Abstracting over arity 14 | - Patterns in our code 15 | - Algebraic data types 16 | - Type classes 17 | - The rest of this book 18 | 19 | - DONE Generic representations 20 | 21 | - Products and coproducts 22 | - `HList` 23 | - `Coproduct` 24 | - `Generic` 25 | - Converting to/from generic representations 26 | - Converting between different specific representations 27 | 28 | - Deriving type class instances 29 | 30 | - Example using `Generic` and products 31 | - `Lazy` (probably) 32 | - `Strict` (maybe) 33 | - Example using `Generic`, products, and coproducts 34 | 35 | - Making use of field and type names 36 | 37 | - `Witness` and singleton types 38 | - `Widen` and `Narrow`, `SingletonOps` 39 | - `narrow` from `shapeless.syntax.singleton` 40 | - `LabelledGeneric` 41 | - `FieldType`, `KeyTag`, `field` 42 | - `->>` from `shapeless.syntax.singleton` 43 | 44 | - Other useful tools 45 | - `Typeable` 46 | - `Annotation` and `Annotations` 47 | - `Default`, `Default.AsRecord`, and `Default.AsOptions` 48 | 49 | - Counting with types 50 | - `Nat` 51 | - `ToInt` 52 | 53 | - Working with tuples 54 | - `Tupler` 55 | - `shapeless.Tuple.fill` (depends on `Nat`) 56 | 57 | - Working with functions 58 | 59 | - `FnFromProduct` and `FnToProduct` 60 | - `shapeless.syntax.std.function.{fromProduct, toProduct}` 61 | - `ProductArgs` and `FromProductArgs` ?? 62 | 63 | - Performance concerns 64 | - `Cached` 65 | - `Lazy` and `Strict` 66 | - `cachedImplicit` 67 | 68 | - Working with HLists 69 | - go through the most common ops from the use cases 70 | 71 | - Working with tuples 72 | - this would be a short section 73 | - relate a lot of this back to HLists 74 | 75 | - Working with coproducts 76 | - go through the most common ops from the use cases 77 | 78 | - Working with records (include this? not used much) 79 | - What is a record? 80 | - these in no particular order 81 | - `Keys` 82 | - `RemoveAll` 83 | - `Remover` 84 | - `Selector` 85 | - `Values` 86 | 87 | - Polymorphic functions (include this? not used much) 88 | - `Poly` and `Case` 89 | 90 | - Dealing with higher kinds (include this? not used much) 91 | - `Generic1` 92 | - `IsHCons1` 93 | - `IsCCons1` 94 | -------------------------------------------------------------------------------- /src/pages/notes/todos.md: -------------------------------------------------------------------------------- 1 | # TODOs #{-} 2 | 3 | Still to do: 4 | 5 | - Mention type class instance selection in Chapter 4 6 | - Low and high priority 7 | - `LowPriorityImplicit` 8 | - Function interop 9 | - Performance 10 | - `cachedImplicit` 11 | - Maybe `Cached` 12 | - Maybe 13 | - Check cross references 14 | - **DONE** Generic applied to tuples 15 | - **DONE** Mention SI-7046 in Chapter 3 16 | - **DONE** Complete the "debugging" section in Chapter 4 17 | - **DONE** Built-in `HList` and `Coproduct` operations 18 | - **DONE** Migrating case class as a basic example 19 | - **DONE** Polymorphic functions 20 | - **DONE** Mapping over `HLists` as an example 21 | - **DONE** Counting with `Nat` 22 | - **DONE** Generating `Arbitrary` instances as an example 23 | - **DONE** Callout box on quirkiness of type inference with poly: 24 | - **DONE** `val len1: Int = lengthPoly("foo")` fails, but... 25 | - **DONE** `val len2 = lengthPoly("foo")` compiles, but... 26 | - **DONE** `val len3: Int = lengthPoly[String]("foo")` fails 27 | - **DONE** Built-in record operations 28 | - **DONE** Final summary 29 | - **SHIP IT!** 30 | -------------------------------------------------------------------------------- /src/pages/ops/index.md: -------------------------------------------------------------------------------- 1 | # Working with *HLists* and *Coproducts* {#sec:ops} 2 | 3 | In Part I we discussed methods for 4 | deriving type class instances 5 | for algebraic data types. 6 | We can use type class derivation 7 | to augment almost any type class, 8 | although in more complex cases 9 | we may have to write a lot of supporting code 10 | for manipulating `HLists` and `Coproducts`. 11 | 12 | In Part II we'll look at the `shapeless.ops` package, 13 | which provides a set of helpful tools 14 | that we can use as building blocks. 15 | Each op comes in two parts: 16 | a *type class* that we can use during implicit resolution, 17 | and *extension methods* that we can call on `HList` and `Coproduct`. 18 | 19 | There are three general sets of ops, 20 | available from three packages: 21 | 22 | - `shapeless.ops.hlist` defines type classes for `HLists`. 23 | These can be used directly via extension methods on `HList`, 24 | defined in `shapeless.syntax.hlist`. 25 | 26 | - `shapeless.ops.coproduct` defines type classes for `Coproducts`. 27 | These can be used directly via extension methods on `Coproduct`, 28 | defined in `shapeless.syntax.coproduct`. 29 | 30 | - `shapeless.ops.record` defines type classes for shapeless records 31 | (`HLists` containing 32 | tagged elements---Section [@sec:labelled-generic:type-tagging]). 33 | These can be used via extension methods on `HList`, 34 | imported from `shapeless.record`, 35 | and defined in `shapeless.syntax.record`. 36 | 37 | We don't have room in this book 38 | to cover all of the available ops. 39 | Fortunately, in most cases the code 40 | is understandable and well documented. 41 | Rather than provide an exhaustive guide, 42 | we will touch on 43 | the major theoretical and structural points 44 | and show you how to extract further information 45 | from the shapeless codebase. 46 | -------------------------------------------------------------------------------- /src/pages/ops/init-last.md: -------------------------------------------------------------------------------- 1 | ## Simple ops examples 2 | 3 | `HList` has `init` and `last` 4 | extension methods based on two type classes: 5 | `shapeless.ops.hlist.Init` and 6 | `shapeless.ops.hlist.Last`. 7 | While `init` drops the last element of an `HList`, 8 | `last` drops all except the last one. 9 | `Coproduct` has similar methods and type classes. 10 | These serve as perfect examples of the ops pattern. 11 | Here are simplified definitions of the extension methods: 12 | 13 | ```scala 14 | package shapeless 15 | package syntax 16 | 17 | implicit class HListOps[L <: HList](l : L) { 18 | def last(implicit last: Last[L]): last.Out = last.apply(l) 19 | def init(implicit init: Init[L]): init.Out = init.apply(l) 20 | } 21 | ``` 22 | 23 | The return type of each method is determined 24 | by a dependent type on the implicit parameter. 25 | The instances for each type class provide the actual mapping. 26 | Here's the skeleton definition of `Last` as an example: 27 | 28 | ```scala 29 | trait Last[L <: HList] { 30 | type Out 31 | def apply(in: L): Out 32 | } 33 | 34 | object Last { 35 | type Aux[L <: HList, O] = Last[L] { type Out = O } 36 | implicit def pair[H]: Aux[H :: HNil, H] = ??? 37 | implicit def list[H, T <: HList] 38 | (implicit last: Last[T]): Aux[H :: T, last.Out] = ??? 39 | } 40 | ``` 41 | 42 | We can make a couple of interesting observations 43 | about this implementation. 44 | First, we can typically implement ops type classes 45 | with a small number of instances (just two in this case). 46 | We can therefore package *all* of the required instances 47 | in the companion object of the type class, 48 | allowing us to call the corresponding extension methods 49 | without any imports from `shapeless.ops`: 50 | 51 | ```tut:book:silent 52 | import shapeless._ 53 | ``` 54 | 55 | ```tut:book 56 | ("Hello" :: 123 :: true :: HNil).last 57 | ("Hello" :: 123 :: true :: HNil).init 58 | ``` 59 | 60 | Second, the type class is only defined for `HLists` 61 | with at least one element. 62 | This gives us a degree of static checking. 63 | If we try to call `last` on an empty `HList`, 64 | we get a compile error: 65 | 66 | ```tut:book:fail 67 | HNil.last 68 | ``` 69 | -------------------------------------------------------------------------------- /src/pages/ops/migration.md: -------------------------------------------------------------------------------- 1 | ## Case study: case class migrations {#sec:ops:migration} 2 | 3 | The power of ops type classes fully crystallizes 4 | when we chain them together 5 | as building blocks for our own code. 6 | We'll finish this chapter with a compelling example: 7 | a type class for performing "migrations" 8 | (aka "evolutions") on case classes[^database-migrations]. 9 | For example, if version 1 of our app contains the following case class: 10 | 11 | [^database-migrations]: The term is stolen from 12 | "database migrations"---SQL scripts that 13 | automate upgrades to a database schema. 14 | 15 | ```tut:book:silent 16 | case class IceCreamV1(name: String, numCherries: Int, inCone: Boolean) 17 | ``` 18 | 19 | our migration library should enable certain 20 | mechanical "upgrades" for free: 21 | 22 | ```tut:book:silent 23 | // Remove fields: 24 | case class IceCreamV2a(name: String, inCone: Boolean) 25 | 26 | // Reorder fields: 27 | case class IceCreamV2b(name: String, inCone: Boolean, numCherries: Int) 28 | 29 | // Insert fields (provided we can determine a default value): 30 | case class IceCreamV2c( 31 | name: String, inCone: Boolean, numCherries: Int, numWaffles: Int) 32 | ``` 33 | 34 | Ideally we'd like to be able to write code like this: 35 | 36 | ```scala 37 | IceCreamV1("Sundae", 1, false).migrateTo[IceCreamV2a] 38 | ``` 39 | 40 | The type class should take care of the migration 41 | without additional boilerplate. 42 | 43 | ### The type class 44 | 45 | The `Migration` type class represents 46 | a transformation from a source to a destination type. 47 | Both of these are going to be "input" types in our derivation, 48 | so we model both as type parameters. 49 | We don't need an `Aux` type alias 50 | because there are no type members to expose: 51 | 52 | ```tut:book:silent 53 | trait Migration[A, B] { 54 | def apply(a: A): B 55 | } 56 | ``` 57 | 58 | We'll also introduce an extension method 59 | to make examples easier to read: 60 | 61 | ```tut:book:silent 62 | implicit class MigrationOps[A](a: A) { 63 | def migrateTo[B](implicit migration: Migration[A, B]): B = 64 | migration.apply(a) 65 | } 66 | ``` 67 | 68 | ### Step 1. Removing fields 69 | 70 | Let's build up the solution piece by piece, 71 | starting with field removal. 72 | We can do this in several steps: 73 | 74 | 1. convert `A` to its generic representation; 75 | 2. filter the `HList` from step 1---only retain 76 | fields that are also in `B`; 77 | 3. convert the output of step 2 to `B`. 78 | 79 | We can implement steps 1 and 3 with `Generic` or `LabelledGeneric`, 80 | and step 2 with an op called `Intersection`. 81 | `LabelledGeneric` seems a sensible choice 82 | because we need to identify fields by name: 83 | 84 | ```tut:book:silent 85 | import shapeless._ 86 | import shapeless.ops.hlist 87 | 88 | implicit def genericMigration[A, B, ARepr <: HList, BRepr <: HList]( 89 | implicit 90 | aGen : LabelledGeneric.Aux[A, ARepr], 91 | bGen : LabelledGeneric.Aux[B, BRepr], 92 | inter : hlist.Intersection.Aux[ARepr, BRepr, BRepr] 93 | ): Migration[A, B] = new Migration[A, B] { 94 | def apply(a: A): B = 95 | bGen.from(inter.apply(aGen.to(a))) 96 | } 97 | ``` 98 | 99 | Take a moment to locate [`Intersection`][code-ops-hlist-intersection] 100 | in the shapeless codebase. 101 | Its `Aux` type alias takes three parameters: 102 | two input `HLists` and one output for the intersection type. 103 | In the example above we are specifying 104 | `ARepr` and `BRepr` as the input types 105 | and `BRepr` as the output type. 106 | This means implicit resolution will only succeed 107 | if `B` has an exact subset of the fields of `A`, 108 | specified with the exact same names in the same order: 109 | 110 | ```tut:book 111 | IceCreamV1("Sundae", 1, true).migrateTo[IceCreamV2a] 112 | ``` 113 | 114 | We get a compile error if 115 | we try to use `Migration` with non-conforming types: 116 | 117 | ```tut:book:fail 118 | IceCreamV1("Sundae", 1, true).migrateTo[IceCreamV2b] 119 | ``` 120 | 121 | ### Step 2. Reordering fields 122 | 123 | We need to lean on another ops type class 124 | to add support for reordering. 125 | The [`Align`][code-ops-hlist-align] op 126 | lets us reorder the fields in one `HList` 127 | to match the order they appear in another `HList`. 128 | We can redefine our instance using `Align` as follows: 129 | 130 | ```tut:book:silent 131 | implicit def genericMigration[ 132 | A, B, 133 | ARepr <: HList, BRepr <: HList, 134 | Unaligned <: HList 135 | ]( 136 | implicit 137 | aGen : LabelledGeneric.Aux[A, ARepr], 138 | bGen : LabelledGeneric.Aux[B, BRepr], 139 | inter : hlist.Intersection.Aux[ARepr, BRepr, Unaligned], 140 | align : hlist.Align[Unaligned, BRepr] 141 | ): Migration[A, B] = new Migration[A, B] { 142 | def apply(a: A): B = 143 | bGen.from(align.apply(inter.apply(aGen.to(a)))) 144 | } 145 | ``` 146 | 147 | We introduce a new type parameter called `Unaligned` 148 | to represent the intersection of `ARepr` and `BRepr` 149 | before alignment, 150 | and use `Align` to convert `Unaligned` to `BRepr`. 151 | With this modified definition of `Migration` 152 | we can both remove and reorder fields: 153 | 154 | ```tut:book 155 | IceCreamV1("Sundae", 1, true).migrateTo[IceCreamV2a] 156 | IceCreamV1("Sundae", 1, true).migrateTo[IceCreamV2b] 157 | 158 | ``` 159 | 160 | However, if we try to add fields we still get a failure: 161 | 162 | ```tut:book:fail 163 | IceCreamV1("Sundae", 1, true).migrateTo[IceCreamV2c] 164 | ``` 165 | 166 | ### Step 3. Adding new fields 167 | 168 | We need a mechanism for calculating default values 169 | to support the addition of new fields. 170 | Shapeless doesn't provide a type class for this, 171 | but Cats does in the form of a `Monoid`. 172 | Here's a simplified definition: 173 | 174 | ```scala 175 | package cats 176 | 177 | trait Monoid[A] { 178 | def empty: A 179 | def combine(x: A, y: A): A 180 | } 181 | ``` 182 | 183 | `Monoid` defines two operations: 184 | `empty` for creating a "zero" value 185 | and `combine` for "adding" two values. 186 | We only need `empty` in our code, 187 | but it will be trivial to define `combine` as well. 188 | 189 | Cats provides instances of `Monoid` 190 | for all the primitive types we care about 191 | (`Int`, `Double`, `Boolean`, and `String`). 192 | We can define instances for `HNil` and `::` 193 | using the techniques from Chapter [@sec:labelled-generic]: 194 | 195 | ```tut:book:silent 196 | import cats.Monoid 197 | import cats.instances.all._ 198 | import shapeless.labelled.{field, FieldType} 199 | 200 | def createMonoid[A](zero: A)(add: (A, A) => A): Monoid[A] = 201 | new Monoid[A] { 202 | def empty = zero 203 | def combine(x: A, y: A): A = add(x, y) 204 | } 205 | 206 | implicit val hnilMonoid: Monoid[HNil] = 207 | createMonoid[HNil](HNil)((x, y) => HNil) 208 | 209 | implicit def emptyHList[K <: Symbol, H, T <: HList]( 210 | implicit 211 | hMonoid: Lazy[Monoid[H]], 212 | tMonoid: Monoid[T] 213 | ): Monoid[FieldType[K, H] :: T] = 214 | createMonoid(field[K](hMonoid.value.empty) :: tMonoid.empty) { 215 | (x, y) => 216 | field[K](hMonoid.value.combine(x.head, y.head)) :: 217 | tMonoid.combine(x.tail, y.tail) 218 | } 219 | ``` 220 | 221 | We need to combine `Monoid`[^monoid-pun] with a couple of other ops 222 | to complete our final implementation of `Migration`. 223 | Here's the full list of steps: 224 | 225 | 1. use `LabelledGeneric` to convert `A` to its generic representation; 226 | 2. use `Intersection` to calculate an `HList` of fields common to `A` and `B`; 227 | 3. calculate the types of fields that appear in `B` but not in `A`; 228 | 4. use `Monoid` to calculate a default value of the type from step 3; 229 | 5. append the common fields from step 2 to the new field from step 4; 230 | 6. use `Align` to reorder the fields from step 5 in the same order as `B`; 231 | 7. use `LabelledGeneric` to convert the output of step 6 to `B`. 232 | 233 | [^monoid-pun]: Pun intended. 234 | 235 | We've already seen how to implement steps 1, 2, 4, 6, and 7. 236 | We can implement step 3 using an op called `Diff` 237 | that is very similar to `Intersection`, 238 | and step 5 using another op called `Prepend`. 239 | Here's the complete solution: 240 | 241 | ```tut:book:silent 242 | implicit def genericMigration[ 243 | A, B, ARepr <: HList, BRepr <: HList, 244 | Common <: HList, Added <: HList, Unaligned <: HList 245 | ]( 246 | implicit 247 | aGen : LabelledGeneric.Aux[A, ARepr], 248 | bGen : LabelledGeneric.Aux[B, BRepr], 249 | inter : hlist.Intersection.Aux[ARepr, BRepr, Common], 250 | diff : hlist.Diff.Aux[BRepr, Common, Added], 251 | monoid : Monoid[Added], 252 | prepend : hlist.Prepend.Aux[Added, Common, Unaligned], 253 | align : hlist.Align[Unaligned, BRepr] 254 | ): Migration[A, B] = 255 | new Migration[A, B] { 256 | def apply(a: A): B = 257 | bGen.from(align(prepend(monoid.empty, inter(aGen.to(a))))) 258 | } 259 | ``` 260 | 261 | Note that this code doesn't use 262 | every type class at the value level. 263 | We use `Diff` to calculate the `Added` data type, 264 | but we don't actually need `diff.apply` at run time. 265 | Instead we use our `Monoid` to summon an instance of `Added`. 266 | 267 | With this final version of the type class instance in place 268 | we can use `Migration` for all the use cases we set out 269 | at the beginning of the case study: 270 | 271 | ```tut:book 272 | IceCreamV1("Sundae", 1, true).migrateTo[IceCreamV2a] 273 | IceCreamV1("Sundae", 1, true).migrateTo[IceCreamV2b] 274 | IceCreamV1("Sundae", 1, true).migrateTo[IceCreamV2c] 275 | ``` 276 | 277 | It's amazing what we can create with ops type classes. 278 | `Migration` has a single `implicit def` 279 | with a single line of value-level implementation. 280 | It allows us to automate migrations between *any* pair of case classes, 281 | in roughly the same amount of code we'd write 282 | to handle a *single* pair of types using the standard library. 283 | Such is the power of shapeless! 284 | -------------------------------------------------------------------------------- /src/pages/ops/penultimate.md: -------------------------------------------------------------------------------- 1 | ## Creating a custom op (the "lemma" pattern) {#sec:ops:penultimate} 2 | 3 | If we find a particular sequence of ops useful, 4 | we can package them up and re-provide them as another ops type class. 5 | This is an example of the "lemma" pattern, 6 | a term we introduced in Section [@sec:type-level-programming:summary]. 7 | 8 | Let's work through the creation of our own op as an exercise. 9 | We'll combine the power of `Last` and `Init` 10 | to create a `Penultimate` type class 11 | that retrieves the second-to-last element in an `HList`. 12 | Here's the type class definition, 13 | complete with `Aux` type alias and `apply` method: 14 | 15 | ```tut:book:silent 16 | import shapeless._ 17 | 18 | trait Penultimate[L] { 19 | type Out 20 | def apply(l: L): Out 21 | } 22 | 23 | object Penultimate { 24 | type Aux[L, O] = Penultimate[L] { type Out = O } 25 | 26 | def apply[L](implicit p: Penultimate[L]): Aux[L, p.Out] = p 27 | } 28 | ``` 29 | 30 | Again, notice that the `apply` method 31 | has a return type of `Aux[L, O]` instead of `Penultimate[L]`. 32 | This ensures type members are visible on summoned instances 33 | as discussed in the callout 34 | in Section [@sec:type-level-programming:depfun]. 35 | 36 | We only need to define one instance of `Penultimate`, 37 | combining `Init` and `Last` using the techniques 38 | covered in Section [@sec:type-level-programming:chaining]: 39 | 40 | ```tut:book:silent 41 | import shapeless.ops.hlist 42 | 43 | implicit def hlistPenultimate[L <: HList, M <: HList, O]( 44 | implicit 45 | init: hlist.Init.Aux[L, M], 46 | last: hlist.Last.Aux[M, O] 47 | ): Penultimate.Aux[L, O] = 48 | new Penultimate[L] { 49 | type Out = O 50 | def apply(l: L): O = 51 | last.apply(init.apply(l)) 52 | } 53 | ``` 54 | 55 | We can use `Penultimate` as follows: 56 | 57 | ```tut:book:silent 58 | type BigList = String :: Int :: Boolean :: Double :: HNil 59 | 60 | val bigList: BigList = "foo" :: 123 :: true :: 456.0 :: HNil 61 | ``` 62 | 63 | ```tut:book 64 | Penultimate[BigList].apply(bigList) 65 | ``` 66 | 67 | Summoning an instance of `Penultimate` 68 | requires the compiler to summon instances for `Last` and `Init`, 69 | so we inherit the same level of type checking on short `HLists`: 70 | 71 | ```tut:book:silent 72 | type TinyList = String :: HNil 73 | 74 | val tinyList = "bar" :: HNil 75 | ``` 76 | 77 | ```tut:book:fail 78 | Penultimate[TinyList].apply(tinyList) 79 | ``` 80 | 81 | We can make things more convenient for end users 82 | by defining an extension method on `HList`: 83 | 84 | ```tut:book:silent 85 | implicit class PenultimateOps[A](a: A) { 86 | def penultimate(implicit inst: Penultimate[A]): inst.Out = 87 | inst.apply(a) 88 | } 89 | ``` 90 | 91 | ```tut:book 92 | bigList.penultimate 93 | ``` 94 | 95 | We can also provide `Penultimate` for all product types 96 | by providing an instance based on `Generic`: 97 | 98 | ```tut:book:silent 99 | implicit def genericPenultimate[A, R, O]( 100 | implicit 101 | generic: Generic.Aux[A, R], 102 | penultimate: Penultimate.Aux[R, O] 103 | ): Penultimate.Aux[A, O] = 104 | new Penultimate[A] { 105 | type Out = O 106 | def apply(a: A): O = 107 | penultimate.apply(generic.to(a)) 108 | } 109 | 110 | case class IceCream(name: String, numCherries: Int, inCone: Boolean) 111 | ``` 112 | 113 | ```tut:book 114 | IceCream("Sundae", 1, false).penultimate 115 | ``` 116 | 117 | The important point here is that, 118 | by defining `Penultimate` as another type class, 119 | we have created a reusable tool that we can apply elsewhere. 120 | Shapeless provides many ops for many purposes, 121 | but it's easy to add our own to the toolbox. 122 | -------------------------------------------------------------------------------- /src/pages/ops/record.md: -------------------------------------------------------------------------------- 1 | ## Record ops {#sec:ops:record} 2 | 3 | We've spent some time in this chapter 4 | looking at type classes from the 5 | `shapeless.ops.hlist` and `shapeless.ops.coproduct` packages. 6 | We mustn't leave without mentioning a third important package: 7 | `shapeless.ops.record`. 8 | 9 | Shapeless' "record ops" provide `Map`-like 10 | operations on `HLists` of tagged elements. 11 | Here are a handful of examples involving ice creams: 12 | 13 | ```tut:book:silent 14 | import shapeless._ 15 | 16 | case class IceCream(name: String, numCherries: Int, inCone: Boolean) 17 | ``` 18 | 19 | ```tut:book 20 | val sundae = LabelledGeneric[IceCream]. 21 | to(IceCream("Sundae", 1, false)) 22 | ``` 23 | 24 | Unlike the `HList` and `Coproduct` ops we have seen already, 25 | record ops syntax requires an explicit import from `shapeless.record`: 26 | 27 | ```tut:book:silent 28 | import shapeless.record._ 29 | ``` 30 | 31 | ### Selecting fields 32 | 33 | The `get` extension method 34 | and its corresponding `Selector` type class 35 | allow us to fetch a field by tag: 36 | 37 | ```tut:book 38 | sundae.get('name) 39 | ``` 40 | 41 | ```tut:book 42 | sundae.get('numCherries) 43 | ``` 44 | 45 | Attempting to access an undefined field 46 | causes a compile error as we might expect: 47 | 48 | ```tut:book:fail 49 | sundae.get('nomCherries) 50 | ``` 51 | 52 | ### Updating and removing fields 53 | 54 | The `updated` method and `Updater` type class allow us to modify fields by key. 55 | The `remove` method and `Remover` type class allow us to delete fields by key: 56 | 57 | ```tut:book 58 | sundae.updated('numCherries, 3) 59 | ``` 60 | 61 | ```tut:book 62 | sundae.remove('inCone) 63 | ``` 64 | 65 | The `updateWith` method and `Modifier` type class allow us 66 | to modify a field with an update function: 67 | 68 | ```tut:book 69 | sundae.updateWith('name)("MASSIVE " + _) 70 | ``` 71 | 72 | ### Converting to a regular *Map* 73 | 74 | The `toMap` method and `ToMap` type class 75 | allow us to convert a record to a `Map`: 76 | 77 | ```tut:book 78 | sundae.toMap 79 | ``` 80 | 81 | ### Other operations 82 | 83 | There are other record ops that we don't have room to cover here. 84 | We can rename fields, merge records, map over their values, and much more. 85 | See the source code of `shapeless.ops.record` and `shapeless.syntax.record` 86 | for more information. 87 | -------------------------------------------------------------------------------- /src/pages/ops/summary.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | In this chapter we explored a few of the 4 | type classes that are provided in the `shapeless.ops` package. 5 | We looked at `Last` and `Init` 6 | as two simple examples of the ops pattern, 7 | and built our own `Penultimate` and `Migration` type classes 8 | by chaining together existing building blocks. 9 | 10 | Many of the ops type classes share a similar pattern 11 | to the ops we've seen here. 12 | The easiest way to learn them is to 13 | look at the source code 14 | in `shapeless.ops` and `shapeless.syntax`. 15 | 16 | In the next chapters we will look at two suites 17 | of ops type classes that require further theoretical discussion. 18 | Chapter [@sec:poly] discusses functional operations 19 | such as `map` and `flatMap` on `HLists`, 20 | and Chapter [@sec:nat] discusses 21 | how to implement type classes that require 22 | type level representations of numbers. 23 | This knowledge will help us gain 24 | a more complete understanding of 25 | the variety of type classes from `shapeless.ops`. 26 | -------------------------------------------------------------------------------- /src/pages/parts/part1.md: -------------------------------------------------------------------------------- 1 | \part{Type class derivation} 2 | -------------------------------------------------------------------------------- /src/pages/parts/part2.md: -------------------------------------------------------------------------------- 1 | \part{Shapeless ops} 2 | -------------------------------------------------------------------------------- /src/pages/parts/part3.md: -------------------------------------------------------------------------------- 1 | \part{Summary} 2 | -------------------------------------------------------------------------------- /src/pages/poly/index.md: -------------------------------------------------------------------------------- 1 | # Functional operations on *HLists* {#sec:poly} 2 | 3 | "Regular" Scala programs make heavy use 4 | of functional operations like `map` and `flatMap`. 5 | A question arises: can we perform similar operations on `HLists`? 6 | The answer is "yes", although 7 | we have to do things a little differently than in regular Scala. 8 | Unsurprisingly the mechanisms we use are type class based 9 | and there are a suite of ops type classes to help us out. 10 | 11 | Before we delve in to the type classes themselves, 12 | we need to discuss how shapeless represents 13 | *polymorphic functions* suitable for 14 | mapping over heterogeneous data structures. 15 | 16 | ## Motivation: mapping over an *HList* 17 | 18 | We'll motivate the discussion of polymorphic functions 19 | by looking at the `map` method. 20 | Figure [@fig:poly:monomorphic-map] shows 21 | a type chart for mapping over a regular list. 22 | We start with a `List[A]`, supply a function `A => B`, 23 | and end up with a `List[B]`. 24 | 25 | ![Mapping over a regular list ("monomorphic" map)](src/pages/poly/monomorphic-map.pdf+svg){#fig:poly:monomorphic-map} 26 | 27 | The heterogeneous element types in an `HList` 28 | cause this model to break down. 29 | Scala functions have fixed input and output types, 30 | so the result of our map will have to have 31 | the same element type in every position. 32 | 33 | Ideally we'd like a `map` operation like 34 | the one shown in Figure [@fig:poly:polymorphic-map], 35 | where the function inspects the type of each input 36 | and uses it to determine the type of each output. 37 | This gives us a closed, composable transformation 38 | that retains the heterogeneous nature of the `HList`. 39 | 40 | ![Mapping over a heterogeneous list ("polymorphic" map)](src/pages/poly/polymorphic-map.pdf+svg){#fig:poly:polymorphic-map} 41 | 42 | Unfortunately we can't use Scala functions 43 | to implement this kind of operation. 44 | We need some new infrastructure. 45 | -------------------------------------------------------------------------------- /src/pages/poly/map-flatmap-fold.md: -------------------------------------------------------------------------------- 1 | ## Mapping and flatMapping using *Poly* 2 | 3 | Shapeless provides a suite of 4 | functional operations based on `Poly`, 5 | each implemented as an ops type class. 6 | Let's look at `map` and `flatMap` as examples. 7 | Here's `map`: 8 | 9 | ```tut:book:silent 10 | import shapeless._ 11 | 12 | object sizeOf extends Poly1 { 13 | implicit val intCase: Case.Aux[Int, Int] = 14 | at(identity) 15 | 16 | implicit val stringCase: Case.Aux[String, Int] = 17 | at(_.length) 18 | 19 | implicit val booleanCase: Case.Aux[Boolean, Int] = 20 | at(bool => if(bool) 1 else 0) 21 | } 22 | ``` 23 | 24 | ```tut:book 25 | (10 :: "hello" :: true :: HNil).map(sizeOf) 26 | ``` 27 | 28 | Note that the elements in the resulting `HList` 29 | have types matching the `Cases` in `sizeOf`. 30 | We can use `map` with any `Poly` that 31 | provides `Cases` for every member of our starting `HList`. 32 | If the compiler can't find a `Case` for a particular member, 33 | we get a compile error: 34 | 35 | ```tut:book:fail 36 | (1.5 :: HNil).map(sizeOf) 37 | ``` 38 | 39 | We can also `flatMap` over an `HList`, 40 | as long as every corresponding case in our `Poly` 41 | returns another `HList`: 42 | 43 | ```tut:book:silent 44 | object valueAndSizeOf extends Poly1 { 45 | implicit val intCase: Case.Aux[Int, Int :: Int :: HNil] = 46 | at(num => num :: num :: HNil) 47 | 48 | implicit val stringCase: Case.Aux[String, String :: Int :: HNil] = 49 | at(str => str :: str.length :: HNil) 50 | 51 | implicit val booleanCase: Case.Aux[Boolean, Boolean :: Int :: HNil] = 52 | at(bool => bool :: (if(bool) 1 else 0) :: HNil) 53 | } 54 | ``` 55 | 56 | ```tut:book 57 | (10 :: "hello" :: true :: HNil).flatMap(valueAndSizeOf) 58 | ``` 59 | 60 | Again, we get a compilation error if there is a missing case 61 | or one of the cases doesn't return an `HList`: 62 | 63 | ```tut:book:fail 64 | // Using the wrong Poly with flatMap: 65 | (10 :: "hello" :: true :: HNil).flatMap(sizeOf) 66 | ``` 67 | 68 | `map` and `flatMap` are based on type classes 69 | called `Mapper` and `FlatMapper` respectively. 70 | We'll see an example that makes direct use of `Mapper` 71 | in Section [@sec:poly:product-mapper]. 72 | 73 | ## Folding using *Poly* 74 | 75 | In addition to `map` and `flatMap`, 76 | shapeless also provides 77 | `foldLeft` and `foldRight` operations based on `Poly2`: 78 | 79 | ```tut:book:silent 80 | import shapeless._ 81 | 82 | object sum extends Poly2 { 83 | implicit val intIntCase: Case.Aux[Int, Int, Int] = 84 | at((a, b) => a + b) 85 | 86 | implicit val intStringCase: Case.Aux[Int, String, Int] = 87 | at((a, b) => a + b.length) 88 | } 89 | ``` 90 | 91 | ```tut:book 92 | (10 :: "hello" :: 100 :: HNil).foldLeft(0)(sum) 93 | ``` 94 | 95 | We can also `reduceLeft`, `reduceRight`, `foldMap`, and so on. 96 | Each operation has its own associated type class. 97 | We'll leave it as an exercise to the reader 98 | to investigate the available operations. 99 | -------------------------------------------------------------------------------- /src/pages/poly/monomorphic-map.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/pages/poly/monomorphic-map.pdf -------------------------------------------------------------------------------- /src/pages/poly/monomorphic-map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | monomorphic-map 5 | Created with Sketch. 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 | List[A] 63 | 64 | 65 | map 66 | 67 | 68 | List[B] 69 | 70 | 71 | A => B 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/pages/poly/poly.md: -------------------------------------------------------------------------------- 1 | ## Polymorphic functions 2 | 3 | Shapeless provides a type called `Poly` 4 | for representing *polymorphic functions*, 5 | where the result type depends on the parameter types. 6 | Here is a simplified explanation of how it works. 7 | Note that the next section doesn't contain real shapeless code---we're 8 | eliding much of the flexibility and ease of use 9 | that comes with real shapeless `Polys` 10 | to create a simplified API for illustrative purposes. 11 | 12 | ### How *Poly* works 13 | 14 | At its core, a `Poly` is an object with a generic `apply` method. 15 | In addition to its regular parameter of type `A`, 16 | `Poly` accepts an implicit parameter of type `Case[A]`: 17 | 18 | ```tut:book:silent 19 | // This is not real shapeless code. 20 | // It's just for demonstration. 21 | 22 | trait Case[P, A] { 23 | type Result 24 | def apply(a: A): Result 25 | } 26 | 27 | trait Poly { 28 | def apply[A](arg: A)(implicit cse: Case[this.type, A]): cse.Result = 29 | cse.apply(arg) 30 | } 31 | ``` 32 | 33 | When we define an actual `Poly`, 34 | we provide instances of `Case` 35 | for each parameter type we care about. 36 | These implement the actual function body: 37 | 38 | ```tut:book:silent 39 | // This is not real shapeless code. 40 | // It's just for demonstration. 41 | 42 | object myPoly extends Poly { 43 | implicit def intCase = 44 | new Case[this.type, Int] { 45 | type Result = Double 46 | def apply(num: Int): Double = num / 2.0 47 | } 48 | 49 | implicit def stringCase = 50 | new Case[this.type, String] { 51 | type Result = Int 52 | def apply(str: String): Int = str.length 53 | } 54 | } 55 | 56 | ``` 57 | 58 | When we call `myPoly.apply`, 59 | the compiler searches for the relevant implicit `Case` 60 | and inserts it as usual: 61 | 62 | ```tut:book 63 | myPoly.apply(123) 64 | ``` 65 | 66 | There is some subtle scoping behaviour here 67 | that allows the compiler to locate instances of `Case` 68 | without any additional imports. 69 | `Case` has an extra type parameter `P` 70 | referencing the singleton type of the `Poly`. 71 | The implicit scope for `Case[P, A]` includes 72 | the companion objects for `Case`, `P`, and `A`. 73 | We've assigned `P` to be `myPoly.type` 74 | and the companion object for `myPoly.type` is `myPoly` itself. 75 | In other words, `Cases` defined in the body of the `Poly` 76 | are always in scope no matter where the call site is. 77 | 78 | ### *Poly* syntax 79 | 80 | The code above isn't real shapeless code. 81 | Fortunately, shapeless makes `Polys` much simpler to define. 82 | Here's our `myPoly` function rewritten in proper syntax: 83 | 84 | ```tut:book:silent 85 | import shapeless._ 86 | 87 | object myPoly extends Poly1 { 88 | implicit val intCase: Case.Aux[Int, Double] = 89 | at(num => num / 2.0) 90 | 91 | implicit val stringCase: Case.Aux[String, Int] = 92 | at(str => str.length) 93 | } 94 | ``` 95 | 96 | There are a few key differences with our earlier toy syntax: 97 | 98 | 1. We're extending a trait called `Poly1` instead of `Poly`. 99 | Shapeless has a `Poly` type and a set of subtypes, 100 | `Poly1` through `Poly22`, supporting different arities 101 | of polymorphic function. 102 | 103 | 2. The `Case.Aux` types doesn't seem to reference 104 | the singleton type of the `Poly`. 105 | `Case.Aux` is actually a type alias 106 | defined within the body of `Poly1`. 107 | The singleton type is there---we just don't see it. 108 | 109 | 3. We're using a helper method, `at`, to define cases. 110 | This acts as an instance constructor method 111 | as discussed in Section [@sec:generic:idiomatic-style]), 112 | which eliminates a lot of boilerplate. 113 | 114 | Syntactic differences aside, 115 | the shapeless version of `myPoly` is functionally 116 | identical to our toy version. 117 | We can call it with an `Int` or `String` parameter 118 | and get back a result of the corresponding return type: 119 | 120 | ```tut:book 121 | myPoly.apply(123) 122 | myPoly.apply("hello") 123 | ``` 124 | 125 | Shapeless also supports `Polys` with more than one parameter. 126 | Here is a binary example: 127 | 128 | ```tut:book:silent 129 | object multiply extends Poly2 { 130 | implicit val intIntCase: Case.Aux[Int, Int, Int] = 131 | at((a, b) => a * b) 132 | 133 | implicit val intStrCase: Case.Aux[Int, String, String] = 134 | at((a, b) => b * a) 135 | } 136 | ``` 137 | 138 | ```tut:book 139 | multiply(3, 4) 140 | multiply(3, "4") 141 | ``` 142 | 143 | Because `Cases` are just implicit values, 144 | we can define cases based on type classes 145 | and do all of the advanced implicit resolution 146 | covered in previous chapters. 147 | Here's a simple example that 148 | totals numbers in different contexts: 149 | 150 | ```tut:book:silent 151 | import scala.math.Numeric 152 | 153 | object total extends Poly1 { 154 | implicit def base[A](implicit num: Numeric[A]): 155 | Case.Aux[A, Double] = 156 | at(num.toDouble) 157 | 158 | implicit def option[A](implicit num: Numeric[A]): 159 | Case.Aux[Option[A], Double] = 160 | at(opt => opt.map(num.toDouble).getOrElse(0.0)) 161 | 162 | implicit def list[A](implicit num: Numeric[A]): 163 | Case.Aux[List[A], Double] = 164 | at(list => num.toDouble(list.sum)) 165 | } 166 | ``` 167 | 168 | ```tut:book 169 | total(10) 170 | total(Option(20.0)) 171 | total(List(1L, 2L, 3L)) 172 | ``` 173 | 174 |
175 | *Idiosyncrasies of type inference* 176 | 177 | `Poly` pushes Scala's type inference out of its comfort zone. 178 | We can easily confuse the compiler by 179 | asking it to do too much inference at once. 180 | For example, the following code compiles ok: 181 | 182 | ```tut:book:silent 183 | val a = myPoly.apply(123) 184 | val b: Double = a 185 | ``` 186 | 187 | However, combining the two lines causes a compilation error: 188 | 189 | ```tut:book:fail 190 | val a: Double = myPoly.apply(123) 191 | ``` 192 | 193 | If we add a type annotation, the code compiles again: 194 | 195 | ```tut:book 196 | val a: Double = myPoly.apply[Int](123) 197 | ``` 198 | 199 | This behaviour is confusing and annoying. 200 | Unfortunately there are no concrete rules to follow to avoid problems. 201 | The only general guideline is to 202 | try not to over-constrain the compiler, 203 | solve one constraint at a time, 204 | and give it a hint when it gets stuck. 205 |
206 | -------------------------------------------------------------------------------- /src/pages/poly/polymorphic-map.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/pages/poly/polymorphic-map.pdf -------------------------------------------------------------------------------- /src/pages/poly/polymorphic-map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | polymorphic-map 5 | Created with Sketch. 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 | A :: B :: C :: HNil 40 | 41 | 42 | map 43 | 44 | 45 | D :: E :: F :: HNil 46 | 47 | 48 | Poly 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/pages/poly/product-mapper.md: -------------------------------------------------------------------------------- 1 | ## Defining type classes using *Poly* {#sec:poly:product-mapper} 2 | 3 | We can use `Poly` and type classes 4 | like `Mapper` and `FlatMapper` 5 | as building blocks for our own type classes. 6 | As an example let's build a type class 7 | for mapping from one case class to another: 8 | 9 | ```tut:book:silent 10 | trait ProductMapper[A, B, P] { 11 | def apply(a: A): B 12 | } 13 | ``` 14 | 15 | We can create an instance of `ProductMapper` 16 | using `Mapper` and a pair of `Generics`: 17 | 18 | ```tut:book:silent 19 | import shapeless._ 20 | import shapeless.ops.hlist 21 | 22 | implicit def genericProductMapper[ 23 | A, B, 24 | P <: Poly, 25 | ARepr <: HList, 26 | BRepr <: HList 27 | ]( 28 | implicit 29 | aGen: Generic.Aux[A, ARepr], 30 | bGen: Generic.Aux[B, BRepr], 31 | mapper: hlist.Mapper.Aux[P, ARepr, BRepr] 32 | ): ProductMapper[A, B, P] = 33 | new ProductMapper[A, B, P] { 34 | def apply(a: A): B = 35 | bGen.from(mapper.apply(aGen.to(a))) 36 | } 37 | ``` 38 | 39 | Interestingly, although we define a type `P` for our `Poly`, 40 | we don't reference any values of type `P` anywhere in our code. 41 | The `Mapper` type class uses implicit resolution to find `Cases`, 42 | so the compiler only needs to know the singleton type of `P` 43 | to locate the relevant instances. 44 | 45 | Let's create an extension method 46 | to make `ProductMapper` easier to use. 47 | We only want the user to specify the type of `B` at the call site, 48 | so we use some indirection 49 | to allow the compiler to infer the type of the `Poly` 50 | from a value parameter: 51 | 52 | ```tut:book:silent 53 | implicit class ProductMapperOps[A](a: A) { 54 | class Builder[B] { 55 | def apply[P <: Poly](poly: P) 56 | (implicit pm: ProductMapper[A, B, P]): B = 57 | pm.apply(a) 58 | } 59 | 60 | def mapTo[B]: Builder[B] = new Builder[B] 61 | } 62 | ``` 63 | 64 | Here's an example of the method's use: 65 | 66 | ```tut:book:silent 67 | object conversions extends Poly1 { 68 | implicit val intCase: Case.Aux[Int, Boolean] = at(_ > 0) 69 | implicit val boolCase: Case.Aux[Boolean, Int] = at(if(_) 1 else 0) 70 | implicit val strCase: Case.Aux[String, String] = at(identity) 71 | } 72 | 73 | case class IceCream1(name: String, numCherries: Int, inCone: Boolean) 74 | case class IceCream2(name: String, hasCherries: Boolean, numCones: Int) 75 | ``` 76 | 77 | ```tut:book 78 | IceCream1("Sundae", 1, false).mapTo[IceCream2](conversions) 79 | ``` 80 | 81 | The `mapTo` syntax looks like a single method call, 82 | but is actually two calls: 83 | one call to `mapTo` to fix the `B` type parameter, 84 | and one call to `Builder.apply` to specify the `Poly`. 85 | Some of shapeless' built-in ops extension methods use similar tricks 86 | to provide the user with convenient syntax. 87 | -------------------------------------------------------------------------------- /src/pages/poly/summary.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | In this chapter we discussed *polymorphic functions* 4 | whose return types vary based on the types of their parameters. 5 | We saw how shapeless' `Poly` type is defined, 6 | and how it is used to implement functional operations such as 7 | `map`, `flatMap`, `foldLeft`, and `foldRight`. 8 | 9 | Each operation is implemented as an extension method on `HList`, 10 | based on a corresponding type class: 11 | `Mapper`, `FlatMapper`, `LeftFolder`, and so on. 12 | We can use these type classes, `Poly`, 13 | and the techniques from Section [@sec:type-level-programming:chaining] 14 | to create our own type classes involving 15 | sequences of sophisticated transformations. 16 | -------------------------------------------------------------------------------- /src/pages/poly/type-charts.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/pages/poly/type-charts.sketch -------------------------------------------------------------------------------- /src/pages/representations/adts.md: -------------------------------------------------------------------------------- 1 | ## Recap: algebraic data types 2 | 3 | *Algebraic data types (ADTs)*[^adts] 4 | are a functional programming concept 5 | with a fancy name but a very simple meaning. 6 | They are an idiomatic way of representing data 7 | using "ands" and "ors". For example: 8 | 9 | - a shape is a rectangle **or** a circle 10 | - a rectangle has a width **and** a height 11 | - a circle has a radius 12 | 13 | [^adts]: Not to be confused with "abstract data types", 14 | which are a different tool from computer science 15 | that has little bearing on the discussion here. 16 | 17 | In ADT terminology, 18 | "and" types such as rectangle and circle are called *products* 19 | and "or" types such as shape are called *coproducts*. 20 | In Scala we typically represent products using 21 | case classes and coproducts using sealed traits: 22 | 23 | ```tut:book:silent 24 | sealed trait Shape 25 | final case class Rectangle(width: Double, height: Double) extends Shape 26 | final case class Circle(radius: Double) extends Shape 27 | 28 | val rect: Shape = Rectangle(3.0, 4.0) 29 | val circ: Shape = Circle(1.0) 30 | ``` 31 | 32 | The beauty of ADTs is that they are completely type safe. 33 | The compiler has complete knowledge of the algebras[^algebra] we define, 34 | so it can help us write complete, 35 | correctly typed methods involving our types: 36 | 37 | [^algebra]: The word "algebra" meaning: the symbols we define, such as rectangle and circle; and the rules for manipulating those symbols, encoded as methods. 38 | 39 | ```tut:book:silent 40 | def area(shape: Shape): Double = 41 | shape match { 42 | case Rectangle(w, h) => w * h 43 | case Circle(r) => math.Pi * r * r 44 | } 45 | ``` 46 | 47 | ```tut:book 48 | area(rect) 49 | area(circ) 50 | ``` 51 | 52 | ### Alternative encodings 53 | 54 | Sealed traits and case classes are undoubtedly 55 | the most convenient encoding of ADTs in Scala. 56 | However, they aren't the *only* encoding. 57 | For example, the Scala standard library provides 58 | generic products in the form of `Tuples` 59 | and a generic coproduct in the form of `Either`. 60 | We could have chosen these to encode our `Shape`: 61 | 62 | ```tut:book:silent 63 | type Rectangle2 = (Double, Double) 64 | type Circle2 = Double 65 | type Shape2 = Either[Rectangle2, Circle2] 66 | 67 | val rect2: Shape2 = Left((3.0, 4.0)) 68 | val circ2: Shape2 = Right(1.0) 69 | ``` 70 | 71 | While this encoding is less readable than the case class encoding above, 72 | it does have some of the same desirable properties. 73 | We can still write completely type safe operations involving `Shape2`: 74 | 75 | ```tut:book:silent 76 | def area2(shape: Shape2): Double = 77 | shape match { 78 | case Left((w, h)) => w * h 79 | case Right(r) => math.Pi * r * r 80 | } 81 | ``` 82 | 83 | ```tut:book 84 | area2(rect2) 85 | area2(circ2) 86 | ``` 87 | 88 | Importantly, `Shape2` is a more *generic* encoding than `Shape`[^generic]. 89 | Any code that operates on a pair of `Doubles` 90 | will be able to operate on a `Rectangle2` and vice versa. 91 | As Scala developers we tend to prefer 92 | semantic types like `Rectangle` and `Circle` 93 | to generic ones like `Rectangle2` and `Circle2` 94 | precisely because of their specialised nature. 95 | However, in some cases generality is desirable. 96 | For example, if we're serializing data to disk, 97 | we don't care about the difference 98 | between a pair of `Doubles` and a `Rectangle2`. 99 | We just write two numbers and we're done. 100 | 101 | Shapeless gives us the best of both worlds: 102 | we can use friendly semantic types by default 103 | and switch to generic representations 104 | when we want interoperability (more on this later). 105 | However, instead of using `Tuples` and `Either`, 106 | shapeless uses its own data types to represent 107 | generic products and coproducts. 108 | We'll introduce these types in the next sections. 109 | 110 | [^generic]: We're using "generic" in an informal way here, 111 | rather than the conventional meaning of 112 | "a type with a type parameter". 113 | -------------------------------------------------------------------------------- /src/pages/representations/coproduct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/pages/representations/coproduct.png -------------------------------------------------------------------------------- /src/pages/representations/coproducts.md: -------------------------------------------------------------------------------- 1 | ## Generic coproducts 2 | 3 | Now we know how shapeless encodes product types. 4 | What about coproducts? 5 | We looked at `Either` earlier 6 | but that suffers from similar drawbacks to tuples. 7 | Again, shapeless provides its own encoding that is similar to `HList`: 8 | 9 | ```tut:book:silent 10 | import shapeless.{Coproduct, :+:, CNil, Inl, Inr} 11 | 12 | case class Red() 13 | case class Amber() 14 | case class Green() 15 | 16 | type Light = Red :+: Amber :+: Green :+: CNil 17 | ``` 18 | 19 | In general coproducts take the form 20 | `A :+: B :+: C :+: CNil` meaning "A or B or C", 21 | where `:+:` can be loosely interpreted as `Either`. 22 | The overall type of a coproduct 23 | encodes all the possible types in the disjunction, 24 | but each concrete instance 25 | contains a value for just one of the possibilities. 26 | `:+:` has two subtypes, `Inl` and `Inr`, 27 | that correspond loosely to `Left` and `Right`. 28 | We create instances of a coproduct by 29 | nesting `Inl` and `Inr` constructors: 30 | 31 | ```tut:book 32 | val red: Light = Inl(Red()) 33 | val green: Light = Inr(Inr(Inl(Green()))) 34 | ``` 35 | 36 | Every coproduct type is terminated with `CNil`, 37 | which is an empty type with no values, similar to `Nothing`. 38 | We can't instantiate `CNil` 39 | or build a `Coproduct` purely from instances of `Inr`. 40 | We always have exactly one `Inl` in a value. 41 | 42 | Again, it's worth stating that `Coproducts` aren't particularly special. 43 | The functionality above can be achieved using `Either` and `Nothing` 44 | in place of `:+:` and `CNil`. 45 | There are technical difficulties with using `Nothing`, 46 | but we could have used 47 | any other uninhabited or arbitrary singleton type in place of `CNil`. 48 | 49 | ### Switching encodings using *Generic* 50 | 51 | `Coproduct` types are difficult to parse on first glance. 52 | However, we can see how they fit 53 | into the larger picture of generic encodings. 54 | In addition to understanding case classes and case objects, 55 | shapeless' `Generic` type class also understands 56 | sealed traits and abstract classes: 57 | 58 | ```tut:book:silent 59 | import shapeless.Generic 60 | 61 | sealed trait Shape 62 | final case class Rectangle(width: Double, height: Double) extends Shape 63 | final case class Circle(radius: Double) extends Shape 64 | ``` 65 | 66 | ```tut:book 67 | val gen = Generic[Shape] 68 | ``` 69 | 70 | The `Repr` of the `Generic` for `Shape` is 71 | a `Coproduct` of the subtypes of the sealed trait: 72 | `Rectangle :+: Circle :+: CNil`. 73 | We can use the `to` and `from` methods of the generic 74 | to map back and forth between `Shape` and `gen.Repr`: 75 | 76 | ```tut:book 77 | gen.to(Rectangle(3.0, 4.0)) 78 | 79 | gen.to(Circle(1.0)) 80 | ``` 81 | -------------------------------------------------------------------------------- /src/pages/representations/hlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/pages/representations/hlist.png -------------------------------------------------------------------------------- /src/pages/representations/index.md: -------------------------------------------------------------------------------- 1 | # Algebraic data types and generic representations {#sec:representations} 2 | 3 | The main idea behind generic programming 4 | is to solve problems for a wide variety of types 5 | by writing a small amount of generic code. 6 | Shapeless provides two sets of tools to this end: 7 | 8 | 1. a set of generic data types 9 | that can be inspected, traversed, and manipulated 10 | at the type level; 11 | 12 | 2. automatic mapping between *algebraic data types (ADTs)* 13 | (encoded in Scala as case classes and sealed traits) 14 | and these generic representations. 15 | 16 | In this chapter we will start with 17 | a recap of the theory of algebraic data types 18 | and why they might be familiar to Scala developers. 19 | Then we will look at 20 | generic representations used by shapeless 21 | and discuss how they map on to concrete ADTs. 22 | Finally, we will introduce a type class called `Generic` 23 | that provides automatic mapping 24 | back and forth between ADTs and generic representations. 25 | We will finish with some simple examples 26 | using `Generic` to convert values from one type to another. 27 | -------------------------------------------------------------------------------- /src/pages/representations/products.md: -------------------------------------------------------------------------------- 1 | ## Generic product encodings 2 | 3 | In the previous section we introduced tuples 4 | as a generic representation of products. 5 | Unfortunately, Scala's built-in tuples have a couple of disadvantages 6 | that make them unsuitable for shapeless' purposes: 7 | 8 | 1. Each size of tuple has a different, unrelated type, 9 | making it difficult to write code that abstracts over sizes. 10 | 11 | 2. There is no type for zero-length tuples, 12 | which are important for representing products with zero fields. 13 | We could arguably use `Unit`, 14 | but we ideally want all generic representations 15 | to have a sensible common supertype. 16 | The least upper bound of `Unit` and `Tuple2` is `Any` 17 | so a combination of the two is impractical. 18 | 19 | For these reasons, shapeless uses a different generic encoding 20 | for product types called *heterogeneous lists* or `HLists`[^hlist-name]. 21 | 22 | [^hlist-name]: `Product` is perhaps a better name for `HList`, 23 | but the standard library unfortunately already has a type `scala.Product`. 24 | 25 | An `HList` is either the empty list `HNil`, 26 | or a pair `::[H, T]` where `H` is an arbitrary type 27 | and `T` is another `HList`. 28 | Because every `::` has its own `H` and `T`, 29 | the type of each element is encoded separately 30 | in the type of the overall list: 31 | 32 | ```tut:book:silent 33 | import shapeless.{HList, ::, HNil} 34 | 35 | val product: String :: Int :: Boolean :: HNil = 36 | "Sunday" :: 1 :: false :: HNil 37 | ``` 38 | 39 | The type and value of the `HList` above mirror one another. 40 | Both represent three members: a `String`, an `Int`, and a `Boolean`. 41 | We can retrieve the `head` and `tail` 42 | and the types of the elements are preserved: 43 | 44 | ```tut:book 45 | val first = product.head 46 | val second = product.tail.head 47 | val rest = product.tail.tail 48 | ``` 49 | 50 | The compiler knows the exact length of each `HList`, 51 | so it becomes a compilation error 52 | to take the `head` or `tail` of an empty list: 53 | 54 | ```tut:book:fail 55 | product.tail.tail.tail.head 56 | ``` 57 | 58 | We can manipulate and transform `HLists` 59 | in addition to being able to inspect and traverse them. 60 | For example, we can prepend an element with the `::` method. 61 | Again, notice how the type of the result reflects 62 | the number and types of its elements: 63 | 64 | ```tut:book:silent 65 | val newProduct = 42L :: product 66 | ``` 67 | 68 | Shapeless also provides tools for performing more complex operations 69 | such as mapping, filtering, and concatenating lists. 70 | We'll discuss these in more detail in Part II. 71 | 72 | The behaviour we get from `HLists` isn't magic. 73 | We could have achieved all of this functionality 74 | using `(A, B)` and `Unit` as alternatives to `::` and `HNil`. 75 | However, there is an advantage in 76 | keeping our representation types separate 77 | from the semantic types used in our applications. 78 | `HList` provides this separation. 79 | 80 | ### Switching representations using *Generic* 81 | 82 | Shapeless provides a type class called `Generic` 83 | that allows us to switch back and forth between 84 | a concrete ADT and its generic representation. 85 | Some behind-the-scenes macro magic 86 | allows us to summon instances of `Generic` without boilerplate: 87 | 88 | ```tut:book:silent 89 | import shapeless.Generic 90 | 91 | case class IceCream(name: String, numCherries: Int, inCone: Boolean) 92 | ``` 93 | 94 | ```tut:book 95 | val iceCreamGen = Generic[IceCream] 96 | ``` 97 | 98 | Note that the instance of `Generic` has a type member `Repr` 99 | containing the type of its generic representation. 100 | In this case `iceCreamGen.Repr` is `String :: Int :: Boolean :: HNil`. 101 | Instances of `Generic` have two methods: 102 | one for converting `to` the `Repr` type 103 | and one for converting `from` it: 104 | 105 | ```tut:book 106 | val iceCream = IceCream("Sundae", 1, false) 107 | 108 | val repr = iceCreamGen.to(iceCream) 109 | 110 | val iceCream2 = iceCreamGen.from(repr) 111 | ``` 112 | 113 | If two ADTs have the same `Repr`, 114 | we can convert back and forth between them using their `Generics`: 115 | 116 | ```tut:book:silent 117 | case class Employee(name: String, number: Int, manager: Boolean) 118 | ``` 119 | 120 | ```tut:book 121 | // Create an employee from an ice cream: 122 | val employee = Generic[Employee].from(Generic[IceCream].to(iceCream)) 123 | ``` 124 | 125 |
126 | *Other product types* 127 | 128 | It's worth noting that 129 | Scala tuples are actually case classes, 130 | so `Generic` works with them just fine: 131 | 132 | ```tut:book:silent 133 | val tupleGen = Generic[(String, Int, Boolean)] 134 | ``` 135 | 136 | ```tut:book 137 | tupleGen.to(("Hello", 123, true)) 138 | tupleGen.from("Hello" :: 123 :: true :: HNil) 139 | ``` 140 | 141 | It also works with case classes of more than 22 fields: 142 | 143 | ```tut:book:silent 144 | case class BigData( 145 | a:Int,b:Int,c:Int,d:Int,e:Int,f:Int,g:Int,h:Int,i:Int,j:Int, 146 | k:Int,l:Int,m:Int,n:Int,o:Int,p:Int,q:Int,r:Int,s:Int,t:Int, 147 | u:Int,v:Int,w:Int) 148 | ``` 149 | 150 | ```tut:book 151 | Generic[BigData].from(Generic[BigData].to(BigData( 152 | 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23))) 153 | ``` 154 | 155 | In versions 2.10 and earlier, Scala had a limit of 22 fields for case 156 | classes. This limit was nominally fixed in 2.11, but using `HLists` 157 | will help avoid the remaining [limitations of 22 fields in 158 | Scala][link-dallaway-twenty-two]. 159 | 160 |
161 | -------------------------------------------------------------------------------- /src/pages/representations/representations.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/pages/representations/representations.sketch -------------------------------------------------------------------------------- /src/pages/representations/summary.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | In this chapter we discussed the generic representations 4 | shapeless provides for algebraic data types in Scala: 5 | `HLists` for product types and `Coproducts` for coproduct types. 6 | We also introduced the `Generic` type class 7 | to map back and forth between concrete ADTs and their generic representations. 8 | We haven't yet discussed why generic encodings are so attractive. 9 | The one use case we did cover---converting between ADTs---is 10 | fun but not tremendously useful. 11 | 12 | The real power of `HLists` and `Coproducts` comes from their recursive structure. 13 | We can write code to traverse representations 14 | and calculate values from their constituent elements. 15 | In the next chapter we will look at our first real use case: 16 | automatically deriving type class instances. 17 | -------------------------------------------------------------------------------- /src/pages/solutions.md: -------------------------------------------------------------------------------- 1 | \appendix 2 | 3 |
4 |
5 | -------------------------------------------------------------------------------- /src/pages/type-level-programming/dependent-functions.md: -------------------------------------------------------------------------------- 1 | ## Dependently typed functions {#sec:type-level-programming:depfun} 2 | 3 | Shapeless uses dependent types all over the place: 4 | in `Generic`, in `Witness` (which we will see in the next chapter), 5 | and in a host of other "ops" type classes 6 | that we will survey in Part II of this guide. 7 | 8 | For example, shapeless provides a type class called `Last` 9 | that returns the last element in an `HList`. 10 | Here's a simplified version of its definition: 11 | 12 | ```scala 13 | package shapeless.ops.hlist 14 | 15 | trait Last[L <: HList] { 16 | type Out 17 | def apply(in: L): Out 18 | } 19 | ``` 20 | 21 | We can summon instances of `Last` 22 | to inspect `HLists` in our code. 23 | In the two examples below note that 24 | the `Out` types are dependent on 25 | the `HList` types we started with: 26 | 27 | ```tut:book:silent 28 | import shapeless.{HList, ::, HNil} 29 | 30 | import shapeless.ops.hlist.Last 31 | ``` 32 | 33 | ```tut:book 34 | val last1 = Last[String :: Int :: HNil] 35 | val last2 = Last[Int :: String :: HNil] 36 | ``` 37 | 38 | Once we have summoned instances of `Last`, 39 | we can use them at the value level 40 | via their `apply` methods: 41 | 42 | ```tut:book 43 | last1("foo" :: 123 :: HNil) 44 | last2(321 :: "bar" :: HNil) 45 | ``` 46 | 47 | We get two forms of protection against errors. 48 | The implicits defined for `Last` ensure 49 | we can only summon instances if 50 | the input `HList` has at least one element: 51 | 52 | ```tut:book:fail 53 | Last[HNil] 54 | ``` 55 | 56 | In addition, the type parameters 57 | on the instances of `Last` check 58 | whether we pass in the expected type of `HList`: 59 | 60 | ```tut:book:fail 61 | last1(321 :: "bar" :: HNil) 62 | ``` 63 | 64 | As a further example, let's implement 65 | our own type class, called `Second`, 66 | that returns the second element in an `HList`: 67 | 68 | ```tut:book:silent 69 | trait Second[L <: HList] { 70 | type Out 71 | def apply(value: L): Out 72 | } 73 | 74 | object Second { 75 | type Aux[L <: HList, O] = Second[L] { type Out = O } 76 | 77 | def apply[L <: HList](implicit inst: Second[L]): Aux[L, inst.Out] = 78 | inst 79 | } 80 | ``` 81 | 82 | This code uses the idiomatic layout 83 | described in Section [@sec:generic:idiomatic-style]. 84 | We define the `Aux` type in the companion object beside 85 | the standard `apply` method for summoning instances. 86 | 87 |
88 | *Summoner methods versus "implicitly" versus "the"* 89 | 90 | Note that the return type on `apply` is `Aux[L, O]`, not `Second[L]`. 91 | This is important. 92 | Using `Aux` ensures the `apply` method 93 | does not erase the type members on summoned instances. 94 | If we define the return type as `Second[L]`, 95 | the `Out` type member will be erased from the return type 96 | and the type class will not work correctly. 97 | 98 | The `implicitly` method from `scala.Predef` has this behaviour. 99 | Compare the type of an instance of `Last` summoned with `implicitly`: 100 | 101 | ```tut:book 102 | implicitly[Last[String :: Int :: HNil]] 103 | ``` 104 | 105 | to the type of an instance summoned with `Last.apply`: 106 | 107 | ```tut:book 108 | Last[String :: Int :: HNil] 109 | ``` 110 | 111 | The type summoned by `implicitly` has no `Out` type member. 112 | For this reason, we should avoid `implicitly` 113 | when working with dependently typed functions. 114 | We can either use custom summoner methods, 115 | or we can use shapeless' replacement method, `the`: 116 | 117 | ```tut:book:silent 118 | import shapeless._ 119 | ``` 120 | 121 | ```tut:book 122 | the[Last[String :: Int :: HNil]] 123 | ``` 124 |
125 | 126 | We only need a single instance, 127 | defined for `HLists` of at least two elements: 128 | 129 | ```tut:book:silent 130 | import Second._ 131 | 132 | implicit def hlistSecond[A, B, Rest <: HList]: Aux[A :: B :: Rest, B] = 133 | new Second[A :: B :: Rest] { 134 | type Out = B 135 | def apply(value: A :: B :: Rest): B = 136 | value.tail.head 137 | } 138 | ``` 139 | 140 | We can summon instances using `Second.apply`: 141 | 142 | ```tut:book:invisible 143 | import Second._ 144 | ``` 145 | 146 | ```tut:book 147 | val second1 = Second[String :: Boolean :: Int :: HNil] 148 | val second2 = Second[String :: Int :: Boolean :: HNil] 149 | ``` 150 | 151 | Summoning is subject to similar constraints as `Last`. 152 | If we try to summon an instance for an incompatible `HList`, 153 | resolution fails and we get a compile error: 154 | 155 | ```tut:book:fail 156 | Second[String :: HNil] 157 | ``` 158 | 159 | Summoned instances come with an `apply` method 160 | that operates on the relevant type of `HList` at the value level: 161 | 162 | ```tut:book 163 | second1("foo" :: true :: 123 :: HNil) 164 | second2("bar" :: 321 :: false :: HNil) 165 | ``` 166 | 167 | ```tut:book:fail 168 | second1("baz" :: HNil) 169 | ``` 170 | 171 | ## Chaining dependent functions {#sec:type-level-programming:chaining} 172 | 173 | Dependently typed functions provide 174 | a means of calculating one type from another. 175 | We can *chain* dependently typed functions 176 | to perform calculations involving multiple steps. 177 | For example, we should be able to use a `Generic` 178 | to calculate a `Repr` for a case class, 179 | and use a `Last` to calculate 180 | the type of the last element. 181 | Let's try coding this: 182 | 183 | ```tut:book:invisible 184 | import shapeless.Generic 185 | ``` 186 | 187 | ```tut:book:fail 188 | def lastField[A](input: A)( 189 | implicit 190 | gen: Generic[A], 191 | last: Last[gen.Repr] 192 | ): last.Out = last.apply(gen.to(input)) 193 | ``` 194 | 195 | Unfortunately our code doesn't compile. 196 | This is the same problem we had 197 | in Section [@sec:generic:product-generic] 198 | with our definition of `genericEncoder`. 199 | We worked around the problem by lifting 200 | the free type variable out as a type parameter: 201 | 202 | ```tut:book:silent 203 | def lastField[A, Repr <: HList](input: A)( 204 | implicit 205 | gen: Generic.Aux[A, Repr], 206 | last: Last[Repr] 207 | ): last.Out = last.apply(gen.to(input)) 208 | ``` 209 | 210 | ```tut:book:invisible 211 | case class Vec(x: Int, y: Int) 212 | case class Rect(origin: Vec, extent: Vec) 213 | ``` 214 | 215 | ```tut:book 216 | lastField(Rect(Vec(1, 2), Vec(3, 4))) 217 | ``` 218 | 219 | As a general rule, 220 | we always write code in this style. 221 | By encoding all the free variables as type parameters, 222 | we enable the compiler to 223 | unify them with appropriate types. 224 | This goes for more subtle constraints as well. 225 | For example, suppose we wanted 226 | to summon a `Generic` for 227 | a case class of exactly one field. 228 | We might be tempted to write this: 229 | 230 | ```tut:book:silent 231 | def getWrappedValue[A, H](input: A)( 232 | implicit 233 | gen: Generic.Aux[A, H :: HNil] 234 | ): H = gen.to(input).head 235 | ``` 236 | 237 | The result here is more insidious. 238 | The method definition compiles but 239 | the compiler can never 240 | find implicits at the call site: 241 | 242 | ```tut:book:silent 243 | case class Wrapper(value: Int) 244 | ``` 245 | 246 | ```tut:book:fail 247 | getWrappedValue(Wrapper(42)) 248 | ``` 249 | 250 | The error message hints at the problem. 251 | The clue is in the appearance of the type `H`. 252 | This is the name of a type parameter in the method: 253 | it shouldn't be appearing 254 | in the type the compiler is trying to unify. 255 | The problem is that the `gen` parameter is over-constrained: 256 | the compiler can't find a `Repr` 257 | *and* ensure its length at the same time. 258 | The type `Nothing` also often provides a clue, 259 | appearing when the compiler 260 | fails to unify covariant type parameters. 261 | 262 | The solution to our problem above 263 | is to separate implicit resolution into steps: 264 | 265 | 1. find a `Generic` with a suitable `Repr` for `A`; 266 | 2. provide the `Repr` that has a head type `H`. 267 | 268 | Here's a revised version of the method 269 | using `=:=` to constrain `Repr`: 270 | 271 | ```tut:book:fail 272 | def getWrappedValue[A, Repr <: HList, Head, Tail <: HList](input: A)( 273 | implicit 274 | gen: Generic.Aux[A, Repr], 275 | ev: (Head :: Tail) =:= Repr 276 | ): Head = gen.to(input).head 277 | ``` 278 | 279 | This doesn't compile 280 | because the `head` method in the method body 281 | requires an implicit parameter of type `IsHCons`. 282 | This is a much simpler error message to fix---we 283 | just need to learn a tool from shapeless' toolbox. 284 | `IsHCons` is a shapeless type class 285 | that splits an `HList` into a `Head` and `Tail`. 286 | We can use `IsHCons` instead of `=:=`: 287 | 288 | ```tut:book:silent 289 | import shapeless.ops.hlist.IsHCons 290 | 291 | def getWrappedValue[A, Repr <: HList, Head](in: A)( 292 | implicit 293 | gen: Generic.Aux[A, Repr], 294 | isHCons: IsHCons.Aux[Repr, Head, HNil] 295 | ): Head = gen.to(in).head 296 | ``` 297 | 298 | This fixes the bug. 299 | Both the method definition 300 | and the call site now compile as expected: 301 | 302 | ```tut:book 303 | getWrappedValue(Wrapper(42)) 304 | ``` 305 | 306 | The take home point here isn't 307 | that we solved the problem using `IsHCons`. 308 | Shapeless provides a lot of tools like this 309 | (see Chapters [@sec:ops] to [@sec:nat]), 310 | and we can supplement them where necessary 311 | with our own type classes. 312 | The important point is 313 | to understand the process we use 314 | to write code that compiles 315 | and is capable of finding solutions. 316 | We'll finish off this section 317 | with a step-by-step guide 318 | summarising our findings so far. 319 | -------------------------------------------------------------------------------- /src/pages/type-level-programming/dependent-types.md: -------------------------------------------------------------------------------- 1 | ## Dependent types 2 | 3 | Last chapter we spent a lot of time using `Generic`, 4 | the type class for mapping ADT types to generic representations. 5 | However, we haven't yet discussed an important bit of theory 6 | that underpins `Generic` and much of shapeless: 7 | *dependent types*. 8 | 9 | To illustrate this, let's take a closer look at `Generic`. 10 | Here's a simplified version of the definition: 11 | 12 | ```scala 13 | trait Generic[A] { 14 | type Repr 15 | def to(value: A): Repr 16 | def from(value: Repr): A 17 | } 18 | ``` 19 | 20 | Instances of `Generic` reference two other types: 21 | a type parameter `A` and a type member `Repr`. 22 | Suppose we implement a method `getRepr` as follows. 23 | What type will we get back? 24 | 25 | ```tut:book:silent 26 | import shapeless.Generic 27 | 28 | def getRepr[A](value: A)(implicit gen: Generic[A]) = 29 | gen.to(value) 30 | ``` 31 | 32 | The answer is it depends on the instance we get for `gen`. 33 | In expanding the call to `getRepr`, 34 | the compiler will search for a `Generic[A]` 35 | and the result type will be whatever `Repr` 36 | is defined in that instance: 37 | 38 | ```tut:book:silent 39 | case class Vec(x: Int, y: Int) 40 | case class Rect(origin: Vec, size: Vec) 41 | ``` 42 | 43 | ```tut:book 44 | getRepr(Vec(1, 2)) 45 | getRepr(Rect(Vec(0, 0), Vec(5, 5))) 46 | ``` 47 | 48 | What we're seeing here is called *dependent typing*: 49 | the result type of `getRepr` depends on its value parameters 50 | via their type members. 51 | Suppose we had specified `Repr` 52 | as type parameter on `Generic` 53 | instead of a type member: 54 | 55 | ```tut:book:silent 56 | trait Generic2[A, Repr] 57 | 58 | def getRepr2[A, R](value: A)(implicit generic: Generic2[A, R]): R = 59 | ??? 60 | ``` 61 | 62 | We would have had to pass the desired value of `Repr` 63 | to `getRepr` as a type parameter, 64 | effectively making `getRepr` useless. 65 | The intuitive take-away from this is 66 | that type parameters are useful as "inputs" 67 | and type members are useful as "outputs". 68 | -------------------------------------------------------------------------------- /src/pages/type-level-programming/index.md: -------------------------------------------------------------------------------- 1 | # Working with types and implicits {#sec:type-level-programming} 2 | 3 | In the last chapter we saw 4 | one of the most compelling use cases for shapeless: 5 | automatically deriving type class instances. 6 | There are plenty of even more powerful examples coming later. 7 | However, before we move on, we should take time 8 | to discuss some theory we've skipped over 9 | and establish a set of patterns for writing and debugging 10 | type- and implicit-heavy code. 11 | -------------------------------------------------------------------------------- /src/pages/type-level-programming/summary.md: -------------------------------------------------------------------------------- 1 | ## Summary {#sec:type-level-programming:summary} 2 | 3 | When coding with shapeless, 4 | we are often trying to find a target type 5 | that depends on values in our code. 6 | This relationship is called *dependent typing*. 7 | 8 | Problems involving dependent types 9 | can be conveniently expressed using implicit search, 10 | allowing the compiler to resolve 11 | intermediate and target types 12 | given a starting point at the call site. 13 | 14 | We often have to use multiple steps 15 | to calculate a result 16 | (e.g. using a `Generic` to get a `Repr`, 17 | then using another type class to get to another type). 18 | When we do this, 19 | there are a few rules we can follow 20 | to ensure our code compiles and works as expected: 21 | 22 | 1. We should extract every intermediate type 23 | out to a type parameter. 24 | Many type parameters won't be used in the result, 25 | but the compiler needs them to know which types it has to unify. 26 | 27 | 2. The compiler resolves implicits from left to right, 28 | backtracking if it can't find a working combination. 29 | We should write implicits in the order we need them, 30 | using one or more type variables 31 | to connect them to previous implicits. 32 | 33 | 3. The compiler can only solve for one constraint at a time, 34 | so we mustn't over-constrain any single implicit. 35 | 36 | 4. We should state the return type explicitly, 37 | specifying any type parameters and type members 38 | that may be needed elsewhere. 39 | Type members are often important, 40 | so we should use `Aux` types 41 | to preserve them where appropriate. 42 | If we don't state them in the return type, 43 | they won't be available to the compiler 44 | for further implicit resolution. 45 | 46 | 5. The `Aux` type alias pattern is useful 47 | for keeping code readable. 48 | We should look out for `Aux` aliases 49 | when using tools from the shapeless toolbox, 50 | and implement `Aux` aliases 51 | on our own dependently typed functions. 52 | 53 | When we find a useful chain of dependently typed operations 54 | we can capture them as a single type class. 55 | This is sometimes called the "lemma" pattern 56 | (a term borrowed from mathematical proofs). 57 | We'll see an example of this pattern 58 | in Section [@sec:ops:penultimate]. 59 | -------------------------------------------------------------------------------- /src/template/cover-notes.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/template/cover-notes.html -------------------------------------------------------------------------------- /src/template/cover-notes.tex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/template/cover-notes.tex -------------------------------------------------------------------------------- /src/template/images/brand.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/template/images/brand.pdf -------------------------------------------------------------------------------- /src/template/images/brand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | underscore 11 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/template/images/hero-arrow-overlay-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/template/images/hero-left-overlay-white.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/template/images/hero-left-overlay-white.pdf -------------------------------------------------------------------------------- /src/template/images/hero-left-overlay-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/template/images/hero-right-overlay-white.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/template/images/hero-right-overlay-white.pdf -------------------------------------------------------------------------------- /src/template/images/hero-right-overlay-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/template/images/spaaace.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/template/images/spaaace.pdf -------------------------------------------------------------------------------- /src/template/images/spaaace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/underscoreio/shapeless-guide/110d34792457e1edccb9f9361a3074a1a6640df9/src/template/images/spaaace.png -------------------------------------------------------------------------------- /src/template/template.epub.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | $pagetitle$ 9 | $if(highlighting-css)$ 10 | 13 | $endif$ 14 | $for(css)$ 15 | 16 | $endfor$ 17 | 18 | 19 | $if(titlepage)$ 20 |

21 | $title$ 22 | $if(subtitle)$ 23 | $subtitle$ 24 | $endif$ 25 |

26 | $for(author)$ 27 |

$author$

28 | $endfor$ 29 | $if(date)$ 30 |

$date$

31 | $endif$ 32 | 33 |
34 |

$title$$if(subtitle)$ $subtitle$$endif$

35 | 36 |

Copyright $copyright$ $author$.

37 | 38 |

Published by Underscore Consulting LLP, Brighton, UK.

39 | 40 |
41 | 42 | $for(include-before)$ 43 | $include-before$ 44 | 45 | $endfor$ 46 |
47 | $else$ 48 | $body$ 49 | $endif$ 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/template/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | $for(author-meta)$ 11 | 12 | $endfor$ 13 | $if(date-meta)$ 14 | 15 | $endif$ 16 | 17 | $if(title-prefix)$$title-prefix$ - $endif$$pagetitle$ 18 | 19 | 20 | $if(quotes)$ 21 | 24 | $endif$ 25 | $if(highlighting-css)$ 26 | 29 | $endif$ 30 | $for(css)$ 31 | 32 | $endfor$ 33 | 34 | $if(math)$ 35 | $math$ 36 | $endif$ 37 | 38 | $for(header-includes)$ 39 | $header-includes$ 40 | $endfor$ 41 | 42 | 43 | 44 | 45 |
46 |
47 |
48 | $if(title)$ 49 |

50 | $title$ 51 | $if(subtitle)$ 52 | $subtitle$ 53 | $endif$ 54 |

55 | $for(author)$ 56 |

$author$

57 | $endfor$ 58 | $if(date)$ 59 |

$date$

60 | $endif$ 61 | 62 |
63 | $endif$ 64 |
65 | 66 |
67 | 68 |
69 | 70 | $if(toc)$ 71 | 93 | $endif$ 94 | 95 |
96 | $body$ 97 |
98 | 99 |
100 | 101 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/template/template.tex: -------------------------------------------------------------------------------- 1 | \documentclass[$if(fontsize)$$fontsize$$else$12pt$endif$,$if(lang)$$lang$,$endif$$if(papersize)$$papersize$,$endif$$for(classoption)$$classoption$$sep$,$endfor$]{$documentclass$} 2 | 3 | % Colors ---------------------------------------- 4 | 5 | \usepackage[xcolor]{mdframed} 6 | 7 | $if(blackandwhiteprintable)$ 8 | % \definecolor{primarycolor}{gray}{0.6} 9 | % \definecolor{successcolor}{gray}{0.6} 10 | \definecolor{warningcolor}{gray}{0.6} 11 | \definecolor{dangercolor}{gray}{0.6} 12 | \definecolor{infocolor}{gray}{0.6} 13 | \definecolor{titlecolor}{gray}{0.6} 14 | \definecolor{brandcolor}{gray}{0.6} 15 | $else$ 16 | % \definecolor{primarycolor}{HTML}{428BCA} 17 | % \definecolor{successcolor}{HTML}{5CB85C} 18 | \definecolor{warningcolor}{HTML}{F0AD4E} 19 | \definecolor{dangercolor}{HTML}{D9534F} 20 | \definecolor{infocolor}{HTML}{5BC0DE} 21 | \definecolor{titlecolor}{HTML}{C02440} 22 | \definecolor{brandcolor}{HTML}{517679} 23 | $endif$ 24 | 25 | % Fonts ----------------------------------------- 26 | 27 | $if(fontfamily)$ 28 | \usepackage{$fontfamily$} 29 | $else$ 30 | \usepackage{lmodern} 31 | $endif$ 32 | 33 | $if(linestretch)$ 34 | \usepackage{setspace} 35 | \setstretch{$linestretch$} 36 | $endif$ 37 | \usepackage{amssymb,amsmath} 38 | \usepackage{ifxetex,ifluatex} 39 | \usepackage{fixltx2e} % provides \textsubscript 40 | \ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex 41 | \usepackage[T1]{fontenc} 42 | \usepackage[utf8]{inputenc} 43 | $if(euro)$ 44 | \usepackage{eurosym} 45 | $endif$ 46 | \else % if luatex or xelatex 47 | \ifxetex 48 | \usepackage{mathspec} 49 | \usepackage{xltxtra,xunicode} 50 | \else 51 | \usepackage{fontspec} 52 | \fi 53 | \defaultfontfeatures{Mapping=tex-text,Scale=MatchLowercase} 54 | \newcommand{\euro}{€} 55 | $if(mainfont)$ 56 | \setmainfont$mainfont$ 57 | $else$ 58 | \setmainfont[% 59 | Color=primary, 60 | Path=$lib-dir$/fonts/Lato/, 61 | BoldItalicFont=Lato-BlackItalic, 62 | BoldFont=Lato-Bold, 63 | ItalicFont=Lato-Italic]{Lato-Regular} 64 | $endif$ 65 | 66 | $if(sansfont)$ 67 | \setsansfont{$sansfont$} 68 | $else$ 69 | \setsansfont[% 70 | Color=primary, 71 | Path=$lib-dir$/fonts/Lato/, 72 | BoldItalicFont=Lato-BlackItalic, 73 | BoldFont=Lato-Bold, 74 | ItalicFont=Lato-Italic]{Lato-Regular} 75 | $endif$ 76 | 77 | $if(monofont)$ 78 | \setmonofont[Mapping=tex-ansi]{$monofont$} 79 | $else$ 80 | \setmonofont[Mapping=tex-ansi]{Bitstream Vera Sans Mono} 81 | $endif$ 82 | 83 | $if(mathfont)$ 84 | \setmathfont(Digits,Latin,Greek){$mathfont$} 85 | $endif$ 86 | \fi 87 | 88 | % Cover page ------------------------------------ 89 | 90 | $if(blackandwhiteprintable)$ 91 | $else$ 92 | \usepackage{pdfpages} 93 | $endif$ 94 | 95 | % ----------------------------------------------- 96 | 97 | % use upquote if available, for straight quotes in verbatim environments 98 | \IfFileExists{upquote.sty}{\usepackage{upquote}}{} 99 | % use microtype if available 100 | \IfFileExists{microtype.sty}{% 101 | \usepackage{microtype} 102 | \UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts 103 | }{} 104 | $if(geometry)$ 105 | \usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry} 106 | $endif$ 107 | $if(natbib)$ 108 | \usepackage{natbib} 109 | \bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$} 110 | $endif$ 111 | $if(biblatex)$ 112 | \usepackage{biblatex} 113 | $if(biblio-files)$ 114 | \bibliography{$biblio-files$} 115 | $endif$ 116 | $endif$ 117 | $if(lhs)$ 118 | \lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{} 119 | $endif$ 120 | 121 | % Syntax highlighting --------------------------- 122 | 123 | $if(highlighting-macros)$ 124 | $highlighting-macros$ 125 | $endif$ 126 | 127 | % ----------------------------------------------- 128 | 129 | \usepackage{fancyvrb} 130 | $if(tables)$ 131 | \usepackage{longtable,booktabs} 132 | $endif$ 133 | $if(graphics)$ 134 | \usepackage{graphicx} 135 | \makeatletter 136 | \def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi} 137 | \def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi} 138 | \makeatother 139 | $endif$ 140 | \ifxetex 141 | \usepackage[setpagesize=false, % page size defined by xetex 142 | unicode=false, % unicode breaks when used with xetex 143 | xetex]{hyperref} 144 | \else 145 | \usepackage[unicode=true]{hyperref} 146 | \fi 147 | \hypersetup{% 148 | breaklinks=true, 149 | bookmarks=true, 150 | pdfauthor={$author-meta$}, 151 | pdftitle={$title-meta$}, 152 | colorlinks=true, 153 | $if(blackandwhiteprintable)$ 154 | citecolor=black, 155 | urlcolor=black, 156 | linkcolor=black, 157 | $else$ 158 | citecolor=$if(citecolor)$$citecolor$$else$blue$endif$, 159 | urlcolor=$if(urlcolor)$$urlcolor$$else$blue$endif$, 160 | linkcolor=$if(linkcolor)$$linkcolor$$else$magenta$endif$, 161 | $endif$ 162 | pdfborder={0 0 0}} 163 | 164 | \urlstyle{same} % don't use monospace font for urls 165 | 166 | % Links as footnotes ---------------------------- 167 | 168 | $if(links-as-notes)$ 169 | % Make links footnotes instead of hotlinks: 170 | \renewcommand{\href}[2]{#2\footnote{\url{#1}}} 171 | $endif$ 172 | $if(strikeout)$ 173 | \usepackage[normalem]{ulem} 174 | % avoid problems with \sout in headers with hyperref: 175 | \pdfstringdefDisableCommands{\renewcommand{\sout}{}} 176 | $endif$ 177 | \setlength{\parindent}{0pt} 178 | \setlength{\parskip}{6pt plus 2pt minus 1pt} 179 | \setlength{\emergencystretch}{3em} % prevent overfull lines 180 | $if(numbersections)$ 181 | \setcounter{secnumdepth}{5} 182 | $else$ 183 | \setcounter{secnumdepth}{0} 184 | $endif$ 185 | $if(verbatim-in-note)$ 186 | \VerbatimFootnotes % allows verbatim text in footnotes 187 | $endif$ 188 | $if(lang)$ 189 | \ifxetex 190 | \usepackage{polyglossia} 191 | \setmainlanguage{$mainlang$} 192 | \else 193 | \usepackage[$lang$]{babel} 194 | \fi 195 | $endif$ 196 | 197 | % Document metadata ----------------------------- 198 | 199 | $if(title)$ 200 | \title{$title$$if(subtitle)$\\\vspace{0.5em}{\large $subtitle$}$endif$} 201 | $endif$ 202 | $if(author)$ 203 | \author{$for(author)$$author$$sep$ \and $endfor$} 204 | $endif$ 205 | \date{$date$} 206 | $for(header-includes)$ 207 | $header-includes$ 208 | $endfor$ 209 | 210 | % Utilities for the title page ------------------ 211 | 212 | \usepackage{tikz} 213 | 214 | \newenvironment{bottompar}{\par\vspace*{\fill}}{\clearpage} 215 | 216 | % Column support (see columns.coffee) ----------- 217 | 218 | \usepackage{multicol} 219 | 220 | % Callout support (see callout.coffee) ---------- 221 | 222 | \newmdenv[% 223 | linecolor=infocolor,linewidth=5pt, 224 | topline=false,bottomline=false,rightline=false, 225 | innertopmargin=1em,innerbottommargin=1em, 226 | splittopskip=1em,splitbottomskip=1em, 227 | backgroundcolor=infocolor!30]{InfoCallout} 228 | \newmdenv[% 229 | linecolor=warningcolor,linewidth=5pt, 230 | topline=false,bottomline=false,rightline=false, 231 | innertopmargin=1em,innerbottommargin=1em, 232 | splittopskip=1em,splitbottomskip=1em, 233 | backgroundcolor=warningcolor!30]{WarningCallout} 234 | \newmdenv[% 235 | linecolor=dangercolor!60,linewidth=5pt, 236 | topline=false,bottomline=false,rightline=false, 237 | innertopmargin=1em,innerbottommargin=1em, 238 | splittopskip=1em,splitbottomskip=1em, 239 | backgroundcolor=dangercolor!25]{DangerCallout} 240 | 241 | % \newmdenv[% 242 | % topline=false,bottomline=false,leftline=false,rightline=false, 243 | % innertopmargin=1em,innerbottommargin=1em, 244 | % splittopskip=1em,splitbottomskip=1em, 245 | % backgroundcolor=black!10]{Shaded} 246 | 247 | % Customise default code blocks ----------------- 248 | 249 | \DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\},fontsize=\small} 250 | 251 | \usepackage{listings} 252 | 253 | $if(blackandwhiteprintable)$ 254 | \definecolor{codebgcolor}{gray}{0.95} 255 | \definecolor{commentcolor}{gray}{0.4} 256 | \definecolor{stringcolor}{gray}{0.2} 257 | \newcommand{\commentstyle}{\tt\color{commentcolor}} 258 | \newcommand{\keywordstyle}{\tt\bfseries} 259 | \newcommand{\emphstyle}{\tt\itseries} 260 | \newcommand{\stringstyle}{\tt\color{stringcolor}} 261 | $else$ 262 | \definecolor{codebgcolor}{HTML}{F7F7F7} 263 | \definecolor{commentcolor}{HTML}{8E5A13} 264 | \definecolor{keywordcolor}{HTML}{234A85} 265 | \definecolor{literalcolor}{HTML}{0100CB} 266 | \definecolor{stringcolor}{HTML}{519818} 267 | \definecolor{emphcolor}{HTML}{007777} 268 | \newcommand{\commentstyle}{\tt\color{commentcolor}} 269 | \newcommand{\keywordstyle}{\tt\color{keywordcolor}} 270 | \newcommand{\emphstyle}{\tt\color{emphcolor}} 271 | \newcommand{\stringstyle}{\tt\color{stringcolor}} 272 | $endif$ 273 | 274 | \makeatletter 275 | \lst@CCPutMacro\lst@ProcessOther {"2D}{\lst@ttfamily{-{}}{-{}}} 276 | \@empty\z@\@empty 277 | \makeatother 278 | 279 | \lstset{ 280 | basicstyle=\tt\footnotesize, 281 | frame=single, 282 | breaklines=true 283 | } 284 | 285 | \lstdefinestyle{scala}{ 286 | breakatwhitespace=false, 287 | language=scala, 288 | frame=none, 289 | commentstyle=\commentstyle, 290 | extendedchars=true, 291 | keepspaces=true, 292 | keywordstyle=\keywordstyle, 293 | emphstyle=\emphstyle, 294 | rulecolor=\tt\color{black}, 295 | showspaces=false, 296 | showstringspaces=false, 297 | showtabs=false, 298 | stringstyle=\stringstyle, 299 | tabsize=2, 300 | aboveskip=0em, 301 | belowskip=0em, 302 | } 303 | 304 | \surroundwithmdframed[ 305 | hidealllines=true, 306 | backgroundcolor=codebgcolor, 307 | skipabove=12pt, 308 | innerleftmargin=6pt, 309 | innerrightmargin=6pt, 310 | innertopmargin=10pt, 311 | innerbottommargin=6pt]{lstlisting} 312 | 313 | \lstdefinelanguage{scala}{ 314 | morekeywords={abstract,case,catch,class,def,% 315 | do,else,extends,false,final,finally,% 316 | for,if,implicit,import,match,mixin,% 317 | new,null,object,override,package,% 318 | private,protected,requires,return,sealed,% 319 | super,this,throw,trait,true,try,% 320 | type,val,var,while,with,yield}, 321 | moreemph={}, 322 | otherkeywords={}, 323 | sensitive=true, 324 | morecomment=[l]{//}, 325 | morecomment=[n]{/*}{*/}, 326 | morestring=[b]", 327 | morestring=[b]""" 328 | } 329 | 330 | % Needed for recent update to Pandoc ------------ 331 | 332 | \providecommand{\tightlist}{% 333 | \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} 334 | 335 | % =============================================== 336 | % Document 337 | % =============================================== 338 | 339 | \begin{document} 340 | 341 | \frontmatter 342 | \pagestyle{empty} 343 | \pagecolor{white} 344 | 345 | % Title page ------------------------------------ 346 | 347 | $if(blackandwhiteprintable)$ 348 | $else$ 349 | \includepdf[pages={1}]{src/covers/pdf-cover.pdf} 350 | \clearpage 351 | $endif$ 352 | 353 | % This has to come after the title page. 354 | % Otherwise some weird bug causes the title image to appear really small. 355 | 356 | % Scale images if necessary, so that they will not overflow the page 357 | % margins by default, and it is still possible to overwrite the defaults 358 | % using explicit options in \includegraphics[width, height, ...]{} 359 | \setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio} 360 | 361 | % Include-before hook --------------------------- 362 | 363 | \vspace*{\fill} 364 | 365 | \begin{center} 366 | 367 | {\Large $title$$if(subtitle)$ $subtitle$$endif$} 368 | 369 | $date$ 370 | 371 | Copyright $copyright$ $author$. CC-BY-SA 3.0. 372 | 373 | Published by Underscore Consulting LLP, Brighton, UK. 374 | 375 | \end{center} 376 | 377 | \vspace{1em} 378 | 379 | $for(include-before)$ 380 | $include-before$ 381 | 382 | $endfor$ 383 | 384 | \vspace*{\fill} 385 | 386 | % Tables of contents, etc. ---------------------- 387 | 388 | \clearpage 389 | \pagestyle{empty} 390 | \hypersetup{linkcolor=black} 391 | \setcounter{tocdepth}{$if(toc-depth)$$toc-depth$$else$3$endif$} 392 | \tableofcontents 393 | 394 | % Main document body ---------------------------- 395 | 396 | \mainmatter 397 | \pagestyle{headings} 398 | 399 | $body$ 400 | 401 | % The end --------------------------------------- 402 | 403 | \end{document} 404 | --------------------------------------------------------------------------------