├── FINALREMARKS.md ├── README.md ├── basics ├── build.sbt ├── project │ ├── build.properties │ └── plugins.sbt ├── sbt └── src │ └── main │ └── scala │ ├── answers │ ├── Part01Answers.scala │ ├── Part03Answers.scala │ ├── Part04Answers.scala │ ├── Part07Answers.scala │ ├── Part09Answers.scala │ ├── Part10Answers.scala │ ├── Part13Answers.scala │ └── Part14Answers.scala │ └── flatten │ ├── Part01.scala │ ├── Part02.scala │ ├── Part03.scala │ ├── Part04.scala │ ├── Part05.scala │ ├── Part06.scala │ ├── Part07.scala │ ├── Part08.scala │ ├── Part09.scala │ ├── Part10.scala │ ├── Part11.scala │ ├── Part12.scala │ ├── Part13.scala │ └── Part14.scala └── play-specific ├── .gitignore ├── app ├── controllers │ ├── Part15.scala │ ├── Part16.scala │ └── Part17.scala ├── models │ └── User.scala ├── services │ └── UserService.scala └── views │ ├── index.scala.html │ └── main.scala.html ├── build.sbt ├── conf ├── application.conf └── routes └── project ├── build.properties └── plugins.sbt /FINALREMARKS.md: -------------------------------------------------------------------------------- 1 | Final Remarks 2 | === 3 | 4 | #### Execution contexts 5 | 6 | I cheated with `ExecutionContext`s, by importing them globally. This is not a good practice. Don't do it. 7 | 8 | An `ExecutionContext` is needed for `map` and `flatMap` on `scala.concurrent.Future`, so also for `for` comprehensions with `Future`, and for the `Monad` instance for `Future`. 9 | 10 | The `Monad` instance for `Future` (`scalaz.contrib.std.futureInstance`) is a `def` and it takes an `ExecutionContext` as implicit parameter. So if you import `futureInstance` and the compiler still complains that it can't find the `Functor` or `Monad` instance for `Future`, make sure that you have an implicit `ExecutionContext` in scope. 11 | 12 | #### Stacking monad transformers 13 | A Monad transformer is a monad, so you can stack 'em: put a monad transformer in a monad transformer, and you have three behaviours! 14 | 15 | But doing this is a code smell: you're probably waiting too long with interpreting your effects, and thereby leaking too much details to outer layers. 16 | 17 | #### Failing futures 18 | 19 | Don't use failing `Future`s. 20 | 21 | `Future` is useful as a container that does async computations. But `scala.concurrent.Future` also works as a `Try`, which is basically an `Either` specialized so that the `Left` side is always `Throwable`. It's annoying to let Future deal with both. For futures that may fail in a way that you want to handle, convert them to `Future[Throwable \/ A]` when possible, and make the `Future` always successful. 22 | 23 | #### Errors are context dependent 24 | 25 | Errors depend on contexts. A `findById` method should probably return an `Option` and not a `\/`, because by itself there is no sensible error. 26 | 27 | Its caller probably dóes have that context, and can transform the option to a suitable error. For example, if the caller is a Play Action method, a suitable error would be a `NotFound` result. 28 | 29 | #### What about Validation, you promised validation! 30 | 31 | We've used `Validation` from Scala 6 a lot. But we used it the way that we used `\/` from Scalaz 7 in this tutorial. 32 | 33 | In Scalaz 7, `Validation` is no longer a monad, and not usable in `for`-comprehensions. It's intended use is different, not for sequential execution but for aggregating failures. We should replace most uses of Scalaz 6 `Validation` by Scalaz 7 `\/`. 34 | 35 | #### Not everything is a monad 36 | 37 | There are laws. Certain properties must hold for the operations in order for them to form a monad. If these properties don't hold, all hell breaks loose: things we decided we could do because we're dealing with a monad are no longer true. 38 | 39 | The laws are: 40 | 41 | // Left identity: 42 | Monad.point(a).flatMap(f) === f(a) 43 | 44 | // Right identity (explained for Option): 45 | m.flatMap(Monad.point(_)) === m 46 | 47 | // Associativity 48 | m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g)) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flatten your code 2 | === 3 | 4 | This project is a hands-on tutorial with exercises and answers, that teaches how to flatten stacked containers like `Future[Option[A]]` so that they can be used in `for`-comprehensions. 5 | 6 | There's an accompanying presentation: https://speakerdeck.com/eamelink/flatten-your-code 7 | 8 | Prerequisites 9 | --- 10 | * Some Scala experience 11 | * Frustration with deeply nested `map` and `flatMap` structures. 12 | 13 | Usage 14 | --- 15 | Most of the code is not runnable: we only rely on the typechecker. That means it's critical to look at the code in an IDE (or [ENSIME](http://ensime.org)) that can show you the type of values. 16 | 17 | 18 | Structure 19 | --- 20 | There are two `sbt` projects: `basics` and `play-specific`. 21 | 22 | The first 14 parts are in `basics`. There is a package `flatten` with the tutorial and exercises, and a package `answers` with answers to the exercises. 23 | 24 | The final 3 parts are in the `controllers` package of `play-specific`. 25 | 26 | ToC 27 | --- 28 | 29 | * Parts 1-5 introduce `for`-comprehensions. 30 | * Parts 6 and 7 deal with nested containers. 31 | * Parts 8-12 generalize the concept from part 7. These parts are *optional*, you can safely skip to part 13. 32 | * Parts 13 and 14 continue with nested containers. 33 | * Parts 15 to 17 show the concepts in a Play context. 34 | 35 | Scalaz documentation 36 | --- 37 | You can find Scalaz documentation on the http://docs.typelevel.org/ 38 | -------------------------------------------------------------------------------- /basics/build.sbt: -------------------------------------------------------------------------------- 1 | name := "flatten-basics" 2 | 3 | scalaVersion := "2.11.8" 4 | 5 | libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.2" 6 | 7 | scalacOptions ++= Seq("-feature") 8 | -------------------------------------------------------------------------------- /basics/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.11 2 | -------------------------------------------------------------------------------- /basics/project/plugins.sbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamelink/flatten/0ab98ee4b9a65773e23f449dc26cd7f68d051bbb/basics/project/plugins.sbt -------------------------------------------------------------------------------- /basics/sbt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # A more capable sbt runner, coincidentally also called sbt. 4 | # Author: Paul Phillips 5 | 6 | # todo - make this dynamic 7 | declare -r sbt_release_version="0.13.1" 8 | declare -r sbt_unreleased_version="0.13.2-SNAPSHOT" # -sbt-dev doesn't work at present 9 | declare -r buildProps="project/build.properties" 10 | 11 | declare sbt_jar sbt_dir sbt_create sbt_launch_dir 12 | declare scala_version java_home sbt_explicit_version 13 | declare verbose debug quiet noshare batch trace_level log_level 14 | declare sbt_saved_stty 15 | 16 | echoerr () { [[ -z "$quiet" ]] && echo "$@" >&2; } 17 | vlog () { [[ -n "$verbose$debug" ]] && echoerr "$@"; } 18 | dlog () { [[ -n "$debug" ]] && echoerr "$@"; } 19 | 20 | # we'd like these set before we get around to properly processing arguments 21 | for arg in "$@"; do 22 | case "$arg" in 23 | -q|-quiet) quiet=true ;; 24 | -d|-debug) debug=true ;; 25 | -v|-verbose) verbose=true ;; 26 | *) ;; 27 | esac 28 | done 29 | 30 | # spaces are possible, e.g. sbt.version = 0.13.0 31 | build_props_sbt () { 32 | [[ -r "$buildProps" ]] && \ 33 | grep '^sbt\.version' "$buildProps" | tr '=' ' ' | awk '{ print $2; }' 34 | } 35 | 36 | update_build_props_sbt () { 37 | local ver="$1" 38 | local old="$(build_props_sbt)" 39 | 40 | [[ -r "$buildProps" ]] && [[ "$ver" != "$old" ]] && { 41 | perl -pi -e "s/^sbt\.version\b.*\$/sbt.version=${ver}/" "$buildProps" 42 | grep -q '^sbt.version[ =]' "$buildProps" || printf "\nsbt.version=%s\n" "$ver" >> "$buildProps" 43 | 44 | echoerr "!!!" 45 | echoerr "!!! Updated file $buildProps setting sbt.version to: $ver" 46 | echoerr "!!! Previous value was: $old" 47 | echoerr "!!!" 48 | } 49 | } 50 | 51 | sbt_version () { 52 | if [[ -n "$sbt_explicit_version" ]]; then 53 | echo "$sbt_explicit_version" 54 | else 55 | local v="$(build_props_sbt)" 56 | if [[ -n "$v" ]]; then 57 | echo "$v" 58 | else 59 | echo "$sbt_release_version" 60 | fi 61 | fi 62 | } 63 | 64 | # restore stty settings (echo in particular) 65 | onSbtRunnerExit() { 66 | [[ -n "$sbt_saved_stty" ]] || return 67 | dlog "" 68 | dlog "restoring stty: $sbt_saved_stty" 69 | stty "$sbt_saved_stty" 70 | unset sbt_saved_stty 71 | } 72 | 73 | # save stty and trap exit, to ensure echo is reenabled if we are interrupted. 74 | trap onSbtRunnerExit EXIT 75 | sbt_saved_stty="$(stty -g 2>/dev/null)" 76 | dlog "Saved stty: $sbt_saved_stty" 77 | 78 | # this seems to cover the bases on OSX, and someone will 79 | # have to tell me about the others. 80 | get_script_path () { 81 | local path="$1" 82 | [[ -L "$path" ]] || { echo "$path" ; return; } 83 | 84 | local target="$(readlink "$path")" 85 | if [[ "${target:0:1}" == "/" ]]; then 86 | echo "$target" 87 | else 88 | echo "${path%/*}/$target" 89 | fi 90 | } 91 | 92 | die() { 93 | echo "Aborting: $@" 94 | exit 1 95 | } 96 | 97 | make_url () { 98 | version="$1" 99 | 100 | case "$version" in 101 | 0.7.*) echo "http://simple-build-tool.googlecode.com/files/sbt-launch-0.7.7.jar" ;; 102 | 0.10.* ) echo "$sbt_launch_repo/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; 103 | 0.11.[12]) echo "$sbt_launch_repo/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; 104 | *) echo "$sbt_launch_repo/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; 105 | esac 106 | } 107 | 108 | init_default_option_file () { 109 | local overriding_var="${!1}" 110 | local default_file="$2" 111 | if [[ ! -r "$default_file" && "$overriding_var" =~ ^@(.*)$ ]]; then 112 | local envvar_file="${BASH_REMATCH[1]}" 113 | if [[ -r "$envvar_file" ]]; then 114 | default_file="$envvar_file" 115 | fi 116 | fi 117 | echo "$default_file" 118 | } 119 | 120 | declare -r cms_opts="-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC" 121 | declare -r jit_opts="-XX:ReservedCodeCacheSize=256m -XX:+TieredCompilation" 122 | declare -r default_jvm_opts="-Dfile.encoding=UTF8 -XX:MaxPermSize=384m -Xms512m -Xmx1536m -Xss2m $jit_opts $cms_opts" 123 | declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" 124 | declare -r latest_28="2.8.2" 125 | declare -r latest_29="2.9.3" 126 | declare -r latest_210="2.10.3" 127 | declare -r latest_211="2.11.0-M5" 128 | 129 | declare -r script_path="$(get_script_path "$BASH_SOURCE")" 130 | declare -r script_name="${script_path##*/}" 131 | 132 | # some non-read-onlies set with defaults 133 | declare java_cmd="java" 134 | declare sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" 135 | declare jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" 136 | declare sbt_launch_repo="http://typesafe.artifactoryonline.com/typesafe/ivy-releases" 137 | 138 | # pull -J and -D options to give to java. 139 | declare -a residual_args 140 | declare -a java_args 141 | declare -a scalac_args 142 | declare -a sbt_commands 143 | 144 | # args to jvm/sbt via files or environment variables 145 | declare -a extra_jvm_opts extra_sbt_opts 146 | 147 | # if set, use JAVA_HOME over java found in path 148 | [[ -e "$JAVA_HOME/bin/java" ]] && java_cmd="$JAVA_HOME/bin/java" 149 | 150 | # directory to store sbt launchers 151 | declare sbt_launch_dir="$HOME/.sbt/launchers" 152 | [[ -d "$sbt_launch_dir" ]] || mkdir -p "$sbt_launch_dir" 153 | [[ -w "$sbt_launch_dir" ]] || sbt_launch_dir="$(mktemp -d -t sbt_extras_launchers.XXXXXX)" 154 | 155 | build_props_scala () { 156 | if [[ -r "$buildProps" ]]; then 157 | versionLine="$(grep '^build.scala.versions' "$buildProps")" 158 | versionString="${versionLine##build.scala.versions=}" 159 | echo "${versionString%% .*}" 160 | fi 161 | } 162 | 163 | execRunner () { 164 | # print the arguments one to a line, quoting any containing spaces 165 | [[ "$verbose" || "$debug" ]] && echo "# Executing command line:" && { 166 | for arg; do 167 | if [[ -n "$arg" ]]; then 168 | if printf "%s\n" "$arg" | grep -q ' '; then 169 | printf "\"%s\"\n" "$arg" 170 | else 171 | printf "%s\n" "$arg" 172 | fi 173 | fi 174 | done 175 | echo "" 176 | } 177 | 178 | if [[ -n "$batch" ]]; then 179 | exec /dev/null; then 202 | curl --fail --silent "$url" --output "$jar" 203 | elif which wget >/dev/null; then 204 | wget --quiet -O "$jar" "$url" 205 | fi 206 | } && [[ -r "$jar" ]] 207 | } 208 | 209 | acquire_sbt_jar () { 210 | for_sbt_version="$(sbt_version)" 211 | sbt_url="$(jar_url "$for_sbt_version")" 212 | sbt_jar="$(jar_file "$for_sbt_version")" 213 | 214 | [[ -r "$sbt_jar" ]] || download_url "$sbt_url" "$sbt_jar" 215 | } 216 | 217 | usage () { 218 | cat < display stack traces with a max of frames (default: -1, traces suppressed) 226 | -no-colors disable ANSI color codes 227 | -sbt-create start sbt even if current directory contains no sbt project 228 | -sbt-dir path to global settings/plugins directory (default: ~/.sbt/) 229 | -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) 230 | -ivy path to local Ivy repository (default: ~/.ivy2) 231 | -no-share use all local caches; no sharing 232 | -offline put sbt in offline mode 233 | -jvm-debug Turn on JVM debugging, open at the given port. 234 | -batch Disable interactive mode 235 | -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted 236 | 237 | # sbt version (default: from $buildProps if present, else latest release) 238 | !!! The only way to accomplish this pre-0.12.0 if there is a build.properties file which 239 | !!! contains an sbt.version property is to update the file on disk. That's what this does. 240 | -sbt-version use the specified version of sbt (default: $sbt_release_version) 241 | -sbt-jar use the specified jar as the sbt launcher 242 | -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir) 243 | -sbt-launch-repo repo url for downloading sbt launcher jar (default: $sbt_launch_repo) 244 | 245 | # scala version (default: as chosen by sbt) 246 | -28 use $latest_28 247 | -29 use $latest_29 248 | -210 use $latest_210 249 | -211 use $latest_211 250 | -scala-home use the scala build at the specified directory 251 | -scala-version use the specified version of scala 252 | -binary-version use the specified scala version when searching for dependencies 253 | 254 | # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) 255 | -java-home alternate JAVA_HOME 256 | 257 | # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution 258 | # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found 259 | $default_jvm_opts 260 | JVM_OPTS environment variable holding either the jvm args directly, or 261 | the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts') 262 | Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument. 263 | -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present) 264 | -Dkey=val pass -Dkey=val directly to the jvm 265 | -J-X pass option -X directly to the jvm (-J is stripped) 266 | 267 | # passing options to sbt, OR to this runner 268 | SBT_OPTS environment variable holding either the sbt args directly, or 269 | the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts') 270 | Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument. 271 | -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present) 272 | -S-X add -X to sbt's scalacOptions (-S is stripped) 273 | EOM 274 | } 275 | 276 | addJava () { 277 | dlog "[addJava] arg = '$1'" 278 | java_args=( "${java_args[@]}" "$1" ) 279 | } 280 | addSbt () { 281 | dlog "[addSbt] arg = '$1'" 282 | sbt_commands=( "${sbt_commands[@]}" "$1" ) 283 | } 284 | addScalac () { 285 | dlog "[addScalac] arg = '$1'" 286 | scalac_args=( "${scalac_args[@]}" "$1" ) 287 | } 288 | addResidual () { 289 | dlog "[residual] arg = '$1'" 290 | residual_args=( "${residual_args[@]}" "$1" ) 291 | } 292 | addResolver () { 293 | addSbt "set resolvers += $1" 294 | } 295 | addDebugger () { 296 | addJava "-Xdebug" 297 | addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" 298 | } 299 | setScalaVersion () { 300 | [[ "$1" == *"-SNAPSHOT" ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' 301 | addSbt "++ $1" 302 | } 303 | 304 | process_args () 305 | { 306 | require_arg () { 307 | local type="$1" 308 | local opt="$2" 309 | local arg="$3" 310 | 311 | if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then 312 | die "$opt requires <$type> argument" 313 | fi 314 | } 315 | while [[ $# -gt 0 ]]; do 316 | case "$1" in 317 | -h|-help) usage; exit 1 ;; 318 | -v|-verbose) verbose=true && log_level=Info && shift ;; 319 | -d|-debug) debug=true && log_level=Debug && shift ;; 320 | -q|-quiet) quiet=true && log_level=Error && shift ;; 321 | 322 | -trace) require_arg integer "$1" "$2" && trace_level="$2" && shift 2 ;; 323 | -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; 324 | -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; 325 | -no-share) noshare=true && shift ;; 326 | -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; 327 | -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; 328 | -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; 329 | -offline) addSbt "set offline := true" && shift ;; 330 | -jvm-debug) require_arg port "$1" "$2" && addDebugger "$2" && shift 2 ;; 331 | -batch) batch=true && shift ;; 332 | -prompt) require_arg "expr" "$1" "$2" && addSbt "set shellPrompt in ThisBuild := (s => { val e = Project.extract(s) ; $2 })" && shift 2 ;; 333 | 334 | -sbt-create) sbt_create=true && shift ;; 335 | -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;; 336 | -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;; 337 | -sbt-dev) sbt_explicit_version="$sbt_unreleased_version" && shift ;; 338 | -sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;; 339 | -sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;; 340 | -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;; 341 | -binary-version) require_arg version "$1" "$2" && addSbt "set scalaBinaryVersion in ThisBuild := \"$2\"" && shift 2 ;; 342 | -scala-home) require_arg path "$1" "$2" && addSbt "set every scalaHome := Some(file(\"$2\"))" && shift 2 ;; 343 | -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;; 344 | -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;; 345 | -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;; 346 | 347 | -D*) addJava "$1" && shift ;; 348 | -J*) addJava "${1:2}" && shift ;; 349 | -S*) addScalac "${1:2}" && shift ;; 350 | -28) setScalaVersion "$latest_28" && shift ;; 351 | -29) setScalaVersion "$latest_29" && shift ;; 352 | -210) setScalaVersion "$latest_210" && shift ;; 353 | -211) setScalaVersion "$latest_211" && shift ;; 354 | 355 | *) addResidual "$1" && shift ;; 356 | esac 357 | done 358 | } 359 | 360 | # process the direct command line arguments 361 | process_args "$@" 362 | 363 | # skip #-styled comments and blank lines 364 | readConfigFile() { 365 | while read line; do 366 | [[ $line =~ ^# ]] || [[ -z $line ]] || echo "$line" 367 | done < "$1" 368 | } 369 | 370 | # if there are file/environment sbt_opts, process again so we 371 | # can supply args to this runner 372 | if [[ -r "$sbt_opts_file" ]]; then 373 | vlog "Using sbt options defined in file $sbt_opts_file" 374 | while read opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") 375 | elif [[ -n "$SBT_OPTS" && ! ("$SBT_OPTS" =~ ^@.*) ]]; then 376 | vlog "Using sbt options defined in variable \$SBT_OPTS" 377 | extra_sbt_opts=( $SBT_OPTS ) 378 | else 379 | vlog "No extra sbt options have been defined" 380 | fi 381 | 382 | [[ -n "$extra_sbt_opts" ]] && process_args "${extra_sbt_opts[@]}" 383 | 384 | # reset "$@" to the residual args 385 | set -- "${residual_args[@]}" 386 | argumentCount=$# 387 | 388 | # only exists in 0.12+ 389 | setTraceLevel() { 390 | case "$(sbt_version)" in 391 | "0.7."* | "0.10."* | "0.11."* ) 392 | echoerr "Cannot set trace level in sbt version $(sbt_version)" 393 | ;; 394 | *) 395 | addSbt "set every traceLevel := $trace_level" 396 | ;; 397 | esac 398 | } 399 | 400 | # set scalacOptions if we were given any -S opts 401 | [[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[@]}\"" 402 | 403 | # Update build.properties on disk to set explicit version - sbt gives us no choice 404 | [[ -n "$sbt_explicit_version" ]] && update_build_props_sbt "$sbt_explicit_version" 405 | vlog "Detected sbt version $(sbt_version)" 406 | 407 | [[ -n "$scala_version" ]] && echoerr "Overriding scala version to $scala_version" 408 | 409 | # no args - alert them there's stuff in here 410 | (( argumentCount > 0 )) || { 411 | vlog "Starting $script_name: invoke with -help for other options" 412 | residual_args=( shell ) 413 | } 414 | 415 | # verify this is an sbt dir or -create was given 416 | [[ -r ./build.sbt || -d ./project || -n "$sbt_create" ]] || { 417 | cat < instead of `toRightDisjunction` 18 | for { 19 | username <- getUserName(data) 20 | user <- getUser(username) \/> "User not found" 21 | email = getEmail(user) 22 | validatedEmail <- validateEmail(email) \/> "Invalid e-mail address" 23 | success <- sendEmail(email) 24 | } yield success 25 | 26 | // So we can now use three kinds of values in our for-comprehension: 27 | // - Values inside a \/ with `<-`, 28 | // - Plain values with `=` 29 | // - Option values with `<-` and `\/>`. 30 | 31 | // Bonus exercise answer, downgrading \/ to Option: 32 | for { 33 | username <- getUserName(data).toOption 34 | user <- getUser(username) 35 | email = getEmail(user) 36 | validatedEmail <- validateEmail(email) 37 | success <- sendEmail(email).toOption 38 | } yield success 39 | } -------------------------------------------------------------------------------- /basics/src/main/scala/answers/Part07Answers.scala: -------------------------------------------------------------------------------- 1 | package answers 2 | 3 | import flatten.Part07 4 | import scala.concurrent.Future 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | 7 | trait Part07Answers extends Part07 { 8 | 9 | case class FinishedFutureOption[A](contents: Future[Option[A]]) { 10 | def flatMap[B](fn: A => FutureOption[B]) = FutureOption { 11 | contents.flatMap { 12 | case Some(value) => fn(value).contents 13 | case None => Future.successful(None) 14 | } 15 | } 16 | 17 | def map[B](fn: A => B) = FutureOption { 18 | contents.map { option => 19 | option.map(fn) 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /basics/src/main/scala/answers/Part09Answers.scala: -------------------------------------------------------------------------------- 1 | package answers 2 | 3 | import flatten.Part09 4 | 5 | trait Part09Answers extends Part09 { 6 | 7 | trait Serializable[A] { 8 | def serialize(value: A): Array[Byte] 9 | } 10 | 11 | def toBytes[A: Serializable](value: A) = implicitly[Serializable[A]].serialize(value) 12 | 13 | implicit val StringSerializable = new Serializable[String] { 14 | override def serialize(value: String) = value.toString.getBytes 15 | } 16 | 17 | implicit val IntSerializable = new Serializable[Int] { 18 | override def serialize(value: Int) = value.toString.getBytes 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /basics/src/main/scala/answers/Part10Answers.scala: -------------------------------------------------------------------------------- 1 | package answers 2 | 3 | import flatten.Part10 4 | 5 | trait Part10Answers extends Part10 { 6 | 7 | val optionMonad = new Monad[Option] { 8 | override def map[A, B](container: Option[A])(function: A => B) = container.map(function) 9 | override def flatMap[A, B](container: Option[A])(function: A => Option[B]) = container.flatMap(function) 10 | override def create[B](value: B) = Some(value) 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /basics/src/main/scala/answers/Part13Answers.scala: -------------------------------------------------------------------------------- 1 | package answers 2 | 3 | import flatten.Part13 4 | import scalaz.OptionT 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scalaz.std.scalaFuture.futureInstance 8 | 9 | trait Part13Answers extends Part13 { 10 | 11 | for { 12 | username <- OptionT(Future.successful(getUserName(data))) 13 | user <- OptionT(getUser(username)) 14 | email = getEmail(user) 15 | validatedEmail <- OptionT(Future.successful(validateEmail(email))) 16 | success <- OptionT(sendEmail(email)) 17 | } yield success 18 | 19 | } 20 | -------------------------------------------------------------------------------- /basics/src/main/scala/answers/Part14Answers.scala: -------------------------------------------------------------------------------- 1 | package answers 2 | 3 | import flatten.Part14 4 | import scala.concurrent.Future 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scalaz.std.scalaFuture.futureInstance 7 | import scalaz.Scalaz._ 8 | import scalaz._ 9 | 10 | trait Part14Answers extends Part14 { 11 | 12 | for { 13 | username <- getUserName(data) |> Future.successful |> OptionT.apply 14 | user <- getUser(username) |> OptionT.apply 15 | email = getEmail(user) 16 | validatedEmail <- validateEmail(email) |> Future.successful |> OptionT.apply 17 | success <- sendEmail(email) |> OptionT.apply 18 | } yield success 19 | 20 | } 21 | -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part01.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | trait Part01 { 4 | 5 | // These are the service methods from which we're going to build a program. 6 | // We'll reuse these five methods in all the parts, although they will evolve a bit. 7 | def getUserName(data: Map[String, String]): Option[String] = ??? 8 | def getUser(name: String): Option[User] = ??? 9 | def getEmail(user: User): String = ??? 10 | def validateEmail(email: String): Option[String] = ??? 11 | def sendEmail(email: String): Option[Boolean] = ??? 12 | 13 | val data = Map[String, String]() 14 | 15 | // The program that we're making is also very similar in all the parts. 16 | // We're getting a username from a Map with data, then we lookup the corresponding user. 17 | // We get the email address from the user, validate it, and send an email to it. 18 | 19 | // Not great: nested maps and flatMaps! 20 | val result1 = getUserName(data).map { username => 21 | getUser(username).map { user => 22 | val email = getEmail(user) 23 | validateEmail(email).map { validatedEmail => 24 | sendEmail(validatedEmail) 25 | } 26 | } 27 | } 28 | 29 | // Exercise, rewrite the above as a for-comprehension 30 | } 31 | 32 | trait User -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part02.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | trait Part02 { 4 | 5 | // In Part 1, our service methods returned `Option`, and we composed a program 6 | // that returned an `Option` as well. 7 | 8 | // But `Option` is seldomly sufficient, no error reporting! 9 | 10 | // For example, it's impossible to distinquish between a user not being found or 11 | // the email address failing to validate. 12 | 13 | // So, instead of `Option`, we can use `Either`, which supports a failure value. 14 | def getUserName(data: Map[String, String]): Either[String, String] = ??? 15 | def getUser(name: String): Either[String, User] = ??? 16 | def getEmail(user: User): String = ??? 17 | def validateEmail(email: String): Either[String, String] = ??? 18 | def sendEmail(email: String): Either[String, Boolean] = ??? 19 | 20 | val data = Map[String, String]() 21 | 22 | // Scala Either doesn't have `map` or `flatMap` methods. 23 | // But you can create a 'right projection', which does have them! 24 | val result1 = getUserName(data).right.flatMap { username => 25 | getUser(username).right.flatMap { user => 26 | val email = getEmail(user) 27 | validateEmail(email).right.flatMap { validatedEmail => 28 | sendEmail(validatedEmail) 29 | } 30 | } 31 | } 32 | 33 | // So using right projections, you can use `Either` in a for comprehension: 34 | for { 35 | username <- getUserName(data).right 36 | user <- getUser(username).right 37 | } yield user 38 | 39 | 40 | // However, there is a problem! `RightProjection`s `map` and `flatMap` methods don't 41 | // return a new `RightProjection`, but an `Either`. And often a for-comprehension will 42 | // desugar to chained `maps` or `flatMaps`, and that breaks: 43 | 44 | /* For example this: 45 | 46 | for { 47 | username <- getUserName(data).right 48 | userNameUpper = username.toUpperCase 49 | } yield userNameUpper 50 | 51 | */ 52 | 53 | // Will desugar to something similar to getUserName(data).right.map{...}.map{...} 54 | // But after the first `map`, you get an Either, which doesn't have a `map` method, so the second map breaks. 55 | 56 | // So our program that we could change to using for-comprehensions while it consisted of `Option`s, can't be 57 | // changed to for comprehensions when it uses `Either`. 58 | 59 | /* This doesn't work: 60 | 61 | for { 62 | username <- getUserName(data).right 63 | user <- getUser(username).right 64 | email = getEmail(user) 65 | validatedEmail <- validateEmail(email).right 66 | success <- sendEmail(email).right 67 | } yield success 68 | */ 69 | 70 | // Exercise: Uncomment the failing code to see the compilation errors. 71 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part03.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scalaz.\/ 4 | 5 | trait Part03 { 6 | 7 | /* 8 | 9 | So Scala's `Either` is pretty useless for what we want. This is caused by 10 | `Either` being _unbiased_. It equally values the Left and Right sides, so it 11 | believes that `map` doesn't make since without specifying which side you want 12 | to map. That's why you need a left or right projection before you can map 13 | an Either. 14 | 15 | Luckily, Scalaz also has an either-like type! Classname: \/. 16 | Looks like the mathematical symbol for disjunction, which is the same thing. 17 | 18 | And \/ is right-biased, so it has `map` and `flatMap` methods, and they work 19 | on the right side. 20 | 21 | So the type Either[String, Int] 22 | would, using Scalaz, be \/[String, Int] 23 | 24 | But you can use infix notation for the type and write this as: String \/ Int 25 | 26 | The data constructors are \/- and -\/ instead of Right and Left. 27 | */ 28 | 29 | // Here are our service methods again, but now changed to return a Scalaz \/. 30 | def getUserName(data: Map[String, String]): String \/ String = ??? 31 | def getUser(name: String): String \/ User = ??? 32 | def getEmail(user: User): String = ??? 33 | def validateEmail(email: String): String \/ String = ??? 34 | def sendEmail(email: String): String \/ Boolean = ??? 35 | 36 | val data = Map[String, String]() 37 | 38 | // Exercise, write the same program as in Part01 and Part02, with a for-comprehension 39 | 40 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part04.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scalaz.{ \/, \/- } 4 | import scalaz.syntax.std.option._ 5 | 6 | trait Part04 { 7 | trait User 8 | 9 | // In practice, not all of your functions will always return the same type. 10 | // Here, `getUser` and `validateEmail` now return `Option` instead of `\/` 11 | def getUserName(data: Map[String, String]): \/[String, String] = ??? 12 | def getUser(name: String): Option[User] = ??? 13 | def getEmail(user: User): String = ??? 14 | def validateEmail(email: String): Option[String] = ??? 15 | def sendEmail(email: String): \/[String, Boolean] = ??? 16 | 17 | val data = Map[String, String]() 18 | 19 | // In a `for`-comprehension, we need all the containers to be the same. 20 | // So we must think about a suitable container that can express everything that 21 | // we need to express. 22 | 23 | // In this case, that would probably be \/. We can upgrade `Option` to `\/` by 24 | // specifying a left side, for when the Option is a None. 25 | 26 | // This is done with the method `toRightDisjunction`: 27 | 28 | Some(5).toRightDisjunction("Left side!") // == \/-(5) 29 | None.toRightDisjunction("Left side!") // == -\/("Left side!") 30 | 31 | // There's a symbolic method for this as well: \/> 32 | Some(5) \/> "Left side!" // == \/-(5) 33 | 34 | // Exercise, write our usual program with a for-comprehension, using 'toRightDisjunction' or '\/>' 35 | 36 | 37 | // If you're entirely not interested in error messages, you can also decide to 38 | // 'downgrade' the \/ values to Option. There's a 'toOption' method on \/ for that. 39 | 40 | // Bonus exercise: write the program again, but now downgrading \/ to Option. 41 | 42 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part05.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scalaz.\/ 4 | import scalaz.syntax.std.option._ 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | 8 | trait Part05 { 9 | 10 | // There's nothing special about the types you can use in a for-comprehension. The Scala compiler 11 | // will happily desugar for you, and as long as the type you work with has the proper `map` and `flatMap` 12 | // methods where required, it will be fine. 13 | 14 | // For for-comprehensions with no `yield`, the last step gets desugared to `foreach`. 15 | 16 | /* And if you use guards, like this: 17 | 18 | for { 19 | foo <- bar if quux 20 | } 21 | */ 22 | // Then a `withFilter` method is required. 23 | 24 | 25 | // Future has a `flatMap` and `map` method as well, so can also be used in a `for` comprehension 26 | val fa = Future(3) 27 | val fb = Future(5) 28 | 29 | for { 30 | a <- fa 31 | b <- fb 32 | } yield a + b 33 | 34 | 35 | // Debugging! 36 | 37 | // Lines in a `for`-comprehension *must* be a generator (<-) or a value definition(=). What if we just want to println an intermediate value? 38 | for { 39 | a <- Option(3) 40 | b <- Option(5) 41 | _ = println("A = " + a) 42 | c <- Option(11) 43 | } yield a + b + c 44 | 45 | 46 | // If your yield gets unwieldy, you can also just assign it to a value 47 | for { 48 | a <- Option(3) 49 | b <- Option(5) 50 | c <- Option(11) 51 | result = a + b + c 52 | } yield result 53 | 54 | 55 | 56 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part06.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scalaz.\/ 4 | import scalaz.syntax.std.option._ 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | 8 | trait Part06 { 9 | trait User 10 | 11 | // Let's suppose that our API is partially asynchronous 12 | def getUserName(data: Map[String, String]): Option[String] = ??? 13 | def getUser(name: String): Future[Option[User]] = ??? 14 | def getEmail(user: User): String = ??? 15 | def validateEmail(email: String): Option[String] = ??? 16 | def sendEmail(email: String): Future[Option[Boolean]] = ??? 17 | 18 | val data = Map[String, String]() 19 | 20 | // This causes a problem, the for comprehension desugars to `flatMap`, but we only `flatMap` 21 | // the outer value. We can't get to the inner value! 22 | 23 | // Simplified, given these methods: 24 | val fa: Future[Option[Int]] = ??? 25 | val fb: Future[Option[Int]] = ??? 26 | 27 | /* The problem is that `a` and `b` are Option[Int], and not Int! 28 | for { 29 | a <- fa 30 | b <- fb 31 | } yield a - b 32 | */ 33 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part07.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scalaz.\/ 4 | import scalaz.syntax.std.option._ 5 | import scala.concurrent.Future 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | 8 | trait Part07 { 9 | 10 | // So we're dealing with Future[Option[A]] values. And we want to use them in for-comprehensions. 11 | 12 | // We can't make the for-comprehension desugar differently. 13 | 14 | // So what we need is a `flatMap` operation, that treats these two containers as a single container, 15 | // and that maps the inner value. 16 | 17 | // Let's make that! We'll create a new datastructure that can get the A value out of a Future[Option[A]], 18 | // apply a function to it, and put it back. 19 | 20 | // Exercise: Finish FutureOption 21 | case class FutureOption[A](contents: Future[Option[A]]) { 22 | def flatMap[B](fn: A => FutureOption[B]): FutureOption[B] = FutureOption { 23 | contents.flatMap { 24 | ??? 25 | } 26 | } 27 | 28 | def map[B](fn: A => B): FutureOption[B] = ??? 29 | } 30 | 31 | // We can use it like this: 32 | val multiBoxedA = Future(Option(5)) 33 | val multiBoxedB = Future(Option(3)) 34 | 35 | // Here, `a` and `b` are Int! 36 | val result = for { 37 | a <- FutureOption(multiBoxedA) 38 | b <- FutureOption(multiBoxedB) 39 | } yield a + b 40 | 41 | // Then at the end, get the regular structure out of our FutureOption class 42 | val finalFuture: Future[Option[Int]] = result.contents 43 | 44 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part08.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | /* 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * Parts 8 - 12 are optional (but recommended!). If you're not interested in generalizing the FutureOption 13 | * from part 7, you can skip these parts and continue with Part 13! 14 | * 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | */ 24 | trait Part08 { 25 | // Now we go off the path a bit, and take a scenic route along type classes. 26 | 27 | // We'll look at various way to do polymorphism. Our example is that we want to serialize 28 | // objects to bytes. 29 | 30 | 31 | // Subtype polymorphism: all types that must be serialized extend a common trait. 32 | object SubtypePolymorphism { 33 | 34 | trait Serializable { 35 | def bytes: Array[Byte] 36 | } 37 | 38 | def toBytes(value: Serializable) = value.bytes 39 | } 40 | // Often impractical, because all classes must extend Serializable. What if we want to serialize `String` or `Int`??? 41 | 42 | 43 | 44 | // An alternative is ad-hoc polymorphism 45 | // Ad-hoc polymorphism is also known as function overloading: 46 | object AdHocPolymorphism { 47 | def toBytes(value: String): Array[Byte] = value.getBytes 48 | def toBytes(value: Int): Array[Byte] = value.toString.getBytes 49 | } 50 | // Also impractical, because now our serialization library must know about all possible types we want to serialize. What about the custom types in our app? 51 | 52 | 53 | 54 | // Solution: glue objects: an object that knows how to serialize a single type. 55 | // We can create these for all types we want to serialize, without needing to change those types. 56 | // Also, the library that accepts objects-that-can-be-serialized doesn't need to know about all these types in advance 57 | object GlueObjects { 58 | trait Serializable[A] { 59 | def serialize(value: A): Array[Byte] 60 | } 61 | 62 | def toBytes[A](value: A, serializer: Serializable[A]): Array[Byte] = serializer.serialize(value) 63 | 64 | val StringSerializable = new Serializable[String] { 65 | override def serialize(value: String) = value.getBytes 66 | } 67 | 68 | val IntSerializable = new Serializable[Int] { 69 | override def serialize(value: Int) = value.toString.getBytes 70 | } 71 | 72 | // Using this: 73 | val myString = "Bananaphone!" 74 | val myStringBytes = toBytes(myString, StringSerializable) 75 | } 76 | // This works fine, but it's often a bit awkward to use with these glue objects. 77 | 78 | 79 | // In scala, this can be made nicer by making the glue object implicit: 80 | object GlueObjects2 { 81 | trait Serializable[A] { 82 | def serialize(value: A): Array[Byte] 83 | } 84 | 85 | def toBytes[A](value: A)(implicit serializer: Serializable[A]) = serializer.serialize(value) 86 | 87 | // Or using a `Context Bound`, which is syntactic sugar for the one above 88 | def toBytes2[A : Serializable](value: A) = implicitly[Serializable[A]].serialize(value) 89 | 90 | implicit val StringSerializable = new Serializable[String] { 91 | override def serialize(value: String) = value.getBytes 92 | } 93 | 94 | implicit val IntSerializable = new Serializable[Int] { 95 | override def serialize(value: Int) = value.toString.getBytes 96 | } 97 | 98 | // Using this: 99 | val myString = "Bananaphone!" 100 | val myStringBytes = toBytes(myString) 101 | 102 | } 103 | // This eliminates syntactic overhead. 104 | 105 | // This pattern is called 'type classes'. Our trait `Serializable[A]` is called the type class. 106 | // The implemented glue objects `StringSerializable` and `IntSerializable` are called 'type class instances'. 107 | 108 | } 109 | -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part09.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | trait Part09 { 4 | 5 | // Most well-known typeclasses in Play are the ones for JSON: Reads, Writes and Format 6 | // Play provides some `instances` of them: Format[String], Reads[DateTime], etc. 7 | // You can define `instances` of these type classes for your own classes. 8 | 9 | // Exercise: without looking at the previous part, create a type class `Serializable`, a function `toBytes` that impicitly uses this 10 | // typeclass, and instances for `Int` and `String`. 11 | 12 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part10.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | 6 | trait Part10 { 7 | 8 | // Here's `FutureOption` again: 9 | case class FutureOption[A](contents: Future[Option[A]]) { 10 | def flatMap[B](fn: A => FutureOption[B]) = FutureOption { 11 | contents.flatMap { 12 | case Some(value) => fn(value).contents 13 | case None => Future.successful(None) 14 | } 15 | } 16 | 17 | def map[B](fn: A => B) = FutureOption { 18 | contents.map { option => 19 | option.map(fn) 20 | } 21 | } 22 | } 23 | 24 | 25 | // Note that from the outer container, `Future`, we only use the following: 26 | // - The `map` method 27 | // - The `flatMap` method 28 | // - Creating a new one: `Future.successful` 29 | 30 | // As it turns out, these functionalities are part of a type class called `Monad`: 31 | 32 | trait Monad[F[_]] { 33 | def map[A, B](container: F[A])(function: A => B): F[B] 34 | def flatMap[A, B](container: F[A])(function: A => F[B]): F[B] 35 | def create[B](value: B): F[B] 36 | } 37 | 38 | // Instance for Future: 39 | val futureMonad = new Monad[Future] { 40 | override def map[A, B](container: Future[A])(function: A => B) = container.map(function) 41 | override def flatMap[A, B](container: Future[A])(function: A => Future[B]) = container.flatMap(function) 42 | override def create[B](value: B) = Future.successful(value) 43 | } 44 | 45 | // Exercise: create a `Monad` instance for `Option` 46 | 47 | 48 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part11.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | 6 | trait Part11 { 7 | 8 | // Here's `FutureOption` again: 9 | case class FutureOption[A](contents: Future[Option[A]]) { 10 | def flatMap[B](fn: A => FutureOption[B]) = FutureOption { 11 | contents.flatMap { 12 | case Some(value) => fn(value).contents 13 | case None => Future.successful(None) 14 | } 15 | } 16 | 17 | def map[B](fn: A => B) = FutureOption { 18 | contents.map { option => 19 | option.map(fn) 20 | } 21 | } 22 | } 23 | 24 | // We've seen that we only use the properties of a `Monad` from the `Future` here, so we can generalize `FutureOption`: 25 | 26 | trait Monad[F[_]] { 27 | def map[A, B](container: F[A])(function: A => B): F[B] 28 | def flatMap[A, B](container: F[A])(function: A => F[B]): F[B] 29 | def create[B](value: B): F[B] 30 | } 31 | 32 | case class AnyMonadOption[F[_], A](contents: F[Option[A]])(implicit monadInstanceForF: Monad[F]) { 33 | def flatMap[B](fn: A => AnyMonadOption[F, B]) = AnyMonadOption[F, B] { 34 | monadInstanceForF.flatMap(contents){ 35 | case Some(value) => fn(value).contents 36 | case None => monadInstanceForF.create(None) 37 | } 38 | } 39 | 40 | def map[B](fn: A => B) = AnyMonadOption[F, B] { 41 | monadInstanceForF.map(contents){ option => 42 | option.map(fn) 43 | } 44 | } 45 | } 46 | 47 | // So now you can not only use it for Future[Option[Int], but for any other outer monad as well, like Option[Option[Int]] or String \/ Option[Int] 48 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part12.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | trait Part12 { 4 | 5 | // In the previous parts we've built our own `FutureOption` container, and then generalized that 6 | // to an `AnyMonadOption` container, that combines Option with any other monad. 7 | 8 | // Scalaz defines a similar thing, and calls it 'Option Transformer'. The class is named `OptionT`. 9 | 10 | // Similarly, there is also an `EitherT`, an Either Transformer, which combines a \/ (so not a scala.Either) 11 | // with any other monad. 12 | 13 | // Keep in mind that an OptionT works when the Option is the *inner* container, so Future[Option[A]], and not Option[Future[A]]. 14 | // Similarly, an EitherT works with Future[String \/ A] or Option[Throwable \/ A] but not String \/ Option[A] 15 | 16 | 17 | // The Monad typeclass that we defined looks like: 18 | trait Monad[F[_]] { 19 | def map[A, B](container: F[A])(function: A => B): F[B] 20 | def flatMap[A, B](container: F[A])(function: A => F[B]): F[B] 21 | def create[B](value: B): F[B] 22 | } 23 | 24 | 25 | // But it turns out that we can defined `map` in terms of `flatMap` and `create`. 26 | trait Monad2[F[_]] { 27 | def flatMap[A, B](container: F[A])(function: A => F[B]): F[B] 28 | def create[B](value: B): F[B] 29 | 30 | def map[A, B](container: F[A])(function: A => B): F[B] = flatMap(container)(function andThen create) 31 | } 32 | 33 | // This means that if we define an instance, we only need to define `flatMap` and `create`. 34 | 35 | // flatMap is called 'bind' in Scalaz. It also often has the symbol >>= 36 | // create is called 'point' in Scalaz. 37 | 38 | 39 | // We called the implicit Monad instance parameter `monadInstanceForF`. Scalaz typically call these instances the same as the type they are for: 40 | // Our version: (implicit monadInstanceForF: Monad[F]) 41 | // Typical Scalaz: (implicit F: Monad[F]) 42 | 43 | 44 | // Scalaz calls the 'contents' parameter 'run', so you can do: myTransformer.run to get the original structure out. 45 | } -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part13.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scalaz.OptionT 4 | import scala.concurrent.Future 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scalaz.std.scalaFuture.futureInstance 7 | 8 | /* 9 | * If you skipped parts 8 - 12, welcome back! 10 | */ 11 | trait Part13 { 12 | 13 | // In parts 8 to 12, we've derived a _Monad Transformer_, a class that transforms two containers 14 | // into a single container, that can be conveniently used in a `for-comprehension`. 15 | 16 | // Scalaz has a bunch of monad transformers as well. With two nested containers, like Future[Option[A]], 17 | // you need the monad transformer that corresponds with the inner container, in this case the Option Transformer, 18 | // which is called OptionT in Scalaz. 19 | 20 | // A small example: 21 | val fa: Future[Option[Int]] = ??? 22 | val fb: Future[Option[Int]] = ??? 23 | 24 | // Here, a and b are Int, extracted from both the Future and the Option! 25 | val finalOptionT = for { 26 | a <- OptionT(fa) 27 | b <- OptionT(fb) 28 | } yield a + b 29 | 30 | // And to get back to the normal structure: 31 | val finalFutureOption: Future[Option[Int]] = finalOptionT.run 32 | 33 | 34 | 35 | // Back to our good old service methods, where some are now asynchronous: returning a Future. 36 | def getUserName(data: Map[String, String]): Option[String] = ??? 37 | def getUser(name: String): Future[Option[User]] = ??? 38 | def getEmail(user: User): String = ??? 39 | def validateEmail(email: String): Option[String] = ??? 40 | def sendEmail(email: String): Future[Option[Boolean]] = ??? 41 | val data: Map[String, String] = ??? 42 | 43 | // Again, we want to use them in a for comprehension. 44 | 45 | // Now we need to do two things: 46 | // 1) Upgrade the `Option[A]` values to `Option[Future[A]]`, so they're all the same container type 47 | // 2) Put all `Option[Future[A]]` into an Option Transformer, OptionT. 48 | 49 | // Exercise: Make a for-comprehension 50 | } 51 | -------------------------------------------------------------------------------- /basics/src/main/scala/flatten/Part14.scala: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import scalaz.Scalaz._ 4 | 5 | trait Part14 extends Part13 { 6 | 7 | // In the previous part, we had constructs like 8 | 9 | // OptionT(Future.successful(theThing)). 10 | 11 | // The `OptionT` and `Future.successful` parts are not so interesting, they're just to make the container right. 12 | 13 | // Scalaz has a function application operator, that reverses function and parameter. 14 | 15 | // This: 16 | val y1 = double(5) 17 | // Is equivalent to this: 18 | val y2 = 5 |> double 19 | 20 | // Exercise, rewrite the for-comprehension from part 13, but use `|>` for applying Future.successful and EitherT.apply 21 | 22 | 23 | def double(i: Int) = i * 2 24 | } -------------------------------------------------------------------------------- /play-specific/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | -------------------------------------------------------------------------------- /play-specific/app/controllers/Part15.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | import scalaz._ 6 | import scala.concurrent.Future 7 | import services.UserService 8 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 9 | 10 | /** 11 | * Original version, with `map` and `flatMap`. 12 | * 13 | * This is even using direct pattern matching on `Future` contents, to avoid double maps there. 14 | * 15 | * Note especially how far the `getOrElse` with the error is from the problem, in the outer maps. 16 | */ 17 | object Part15 extends Controller { 18 | 19 | def index = Action.async { request => 20 | val data = request.queryString.mapValues(_.head) 21 | 22 | UserService.getUserName(data).map { username => 23 | UserService.getUser(username).flatMap { 24 | case None => Future.successful(NotFound("User not found")) 25 | case Some(user) => { 26 | val email = UserService.getEmail(user) 27 | UserService.validateEmail(email).bimap( 28 | validatedEmail => { 29 | UserService.sendEmail(validatedEmail) map { 30 | case true => Ok("Mail successfully sent!") 31 | case false => InternalServerError("Failed to send email :(") 32 | } 33 | }, 34 | errorMsg => Future.successful(InternalServerError(errorMsg))).fold(identity, identity) // This is annoying. We can do this with 'merge' in Scalaz 7.1 35 | } 36 | } 37 | } getOrElse Future.successful(BadRequest("Username missing from data!")) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /play-specific/app/controllers/Part16.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | import scalaz._ 6 | import scala.concurrent.Future 7 | import services.UserService 8 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 9 | import scalaz._ 10 | import scalaz.Scalaz._ 11 | import scalaz.contrib.std.futureInstance 12 | 13 | object Part16 extends Controller { 14 | 15 | def index = Action.async { request => 16 | val data = request.queryString.mapValues(_.head) 17 | 18 | val result = for { 19 | username <- UserService.getUserName(data) \/> BadRequest("Username missing from request") |> Future.successful |> EitherT.apply 20 | user <- UserService.getUser(username).map { _ \/> NotFound("User not found") } |> EitherT.apply 21 | email = UserService.getEmail(user) 22 | validatedEmail <- UserService.validateEmail(email).leftMap(InternalServerError(_)) |> Future.successful |> EitherT.apply 23 | success <- UserService.sendEmail(validatedEmail).map { \/-(_) } |> EitherT.apply 24 | } yield { 25 | if(success) Ok("Mail successfully sent!") 26 | else InternalServerError("Failed to send email :(") 27 | } 28 | 29 | result.run.map { _.fold(identity, identity) } 30 | 31 | } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /play-specific/app/controllers/Part17.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | import scalaz._ 6 | import scala.concurrent.Future 7 | import services.UserService 8 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 9 | import scalaz._ 10 | import scalaz.Scalaz._ 11 | import scalaz.contrib.std.futureInstance 12 | 13 | object Part17 extends Controller { 14 | 15 | def index = Action.async { request => 16 | val data = request.queryString.mapValues(_.head) 17 | 18 | val serviceResult = for { 19 | username <- UserService.getUserName(data) |> HttpResult.fromOption(BadRequest("Username missing from request")) 20 | user <- UserService.getUser(username) |> HttpResult.fromFOption(NotFound("User not found")) 21 | email = UserService.getEmail(user) 22 | validatedEmail <- UserService.validateEmail(email) |> HttpResult.fromEither(InternalServerError(_)) 23 | success <- UserService.sendEmail(validatedEmail) |> HttpResult.fromFuture 24 | } yield { 25 | if(success) Ok("Mail successfully sent!") 26 | else InternalServerError("Failed to send email :(") 27 | } 28 | 29 | constructResult(serviceResult) 30 | 31 | } 32 | 33 | 34 | 35 | // Type alias for our result type 36 | type HttpResult[A] = EitherT[Future, SimpleResult, A] 37 | 38 | // Constructors for our result type 39 | object HttpResult { 40 | def point[A](a: A): HttpResult[A] = EitherT(Future.successful(\/-(a))) 41 | def fromFuture[A](fa: Future[A]): HttpResult[A] = EitherT(fa.map(\/-(_))) 42 | def fromEither[A](va: SimpleResult \/ A): HttpResult[A] = EitherT(Future.successful(va)) 43 | def fromEither[A, B](failure: B => SimpleResult)(va: B \/ A): HttpResult[A] = EitherT(Future.successful(va.leftMap(failure))) 44 | def fromOption[A](failure: SimpleResult)(oa: Option[A]): HttpResult[A] = EitherT(Future.successful(oa \/> failure)) 45 | def fromFOption[A](failure: SimpleResult)(foa: Future[Option[A]]): HttpResult[A] = EitherT(foa.map(_ \/> failure)) 46 | def fromFEither[A, B](failure: B => SimpleResult)(fva: Future[B \/ A]): HttpResult[A] = EitherT(fva.map(_.leftMap(failure))) 47 | } 48 | 49 | // Converter from our result type to a Play result 50 | def constructResult(result: HttpResult[SimpleResult]) = result.run.map { _.fold(identity, identity) } 51 | 52 | 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /play-specific/app/models/User.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | case class User(username: String, email: String) -------------------------------------------------------------------------------- /play-specific/app/services/UserService.scala: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import models._ 4 | import scala.concurrent.Future 5 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 6 | import scalaz.{ \/, \/-, -\/ } 7 | 8 | object UserService { 9 | 10 | private val users = Seq( 11 | User("eamelink", "erik@lunatech.com"), 12 | User("pedro", "peter@lunatech.com"), 13 | User("sietse", "targeter"), 14 | User("paco", "paco@example.com")) 15 | 16 | /** 17 | * Extract the username from a Map with data 18 | */ 19 | def getUserName(data: Map[String, String]): Option[String] = 20 | data.get("username") 21 | 22 | /** 23 | * Find the user with a given username. 24 | */ 25 | def getUser(username: String): Future[Option[User]] = 26 | Future(users.find(_.username == username)) 27 | 28 | /** 29 | * Get the email address of a user 30 | */ 31 | def getEmail(user: User): String = user.email 32 | 33 | /** 34 | * Validate an email address. 35 | * Returns a -\/ with an error message if `email` is not a valid email address, 36 | * or a copy of the email address if it is. 37 | */ 38 | def validateEmail(email: String): String \/ String = 39 | if (email contains "@") 40 | \/-(email) 41 | else 42 | -\/("Not a valid email address: " + email) 43 | 44 | /** 45 | * Send an email. 46 | * Returns true if successful, false otherwise 47 | */ 48 | def sendEmail(email: String): Future[Boolean] = 49 | Future.successful( 50 | if (email endsWith "example.com") false 51 | else true) 52 | 53 | } -------------------------------------------------------------------------------- /play-specific/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play") { 4 | 5 | @play20.welcome(message) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /play-specific/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /play-specific/build.sbt: -------------------------------------------------------------------------------- 1 | name := "flatten-play" 2 | 3 | version := "1.0-SNAPSHOT" 4 | 5 | libraryDependencies ++= Seq( 6 | "org.scalaz" %% "scalaz-core" % "7.0.6", 7 | // Typeclass instances for Future. Not necessary for Scalaz 7.1. 8 | "org.typelevel" %% "scalaz-contrib-210" % "0.1.5") 9 | 10 | play.Project.playScalaSettings 11 | -------------------------------------------------------------------------------- /play-specific/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | application.secret="y7=nm5PNuE61FsH5BcS5@iK>kps4@GM6NUu;c2KUxGA0]6oLaG4q