├── .gitignore ├── README.md ├── build.sbt ├── handson ├── handson.bat ├── project ├── build.properties └── plugins.sbt ├── sbt ├── SbtJansiLaunch.class ├── jansi-license.txt ├── jansi.jar ├── sbt-launch.jar └── sbt.boot.properties └── src ├── main └── scala │ ├── recorder │ ├── AnchoRecorder.scala │ ├── MyFunSuite.scala │ ├── MyTestPendingException.scala │ └── RecorderMacro.scala │ └── support │ ├── CustomStopper.scala │ └── HandsOnSuite.scala └── test └── scala ├── HandsOnScala.scala ├── part1-1 ├── e0_vars_vals.scala ├── e1_classes.scala ├── e2_case_classes.scala ├── e3_for_loops_and_comprehensions.scala └── e4_pattern_matching.scala ├── part1-2 ├── e4_lists.scala ├── e5_functions_and_higher_order_functions.scala ├── e6_option.scala ├── e7_maps.scala └── e8_sets.scala └── part2 ├── e0_bags.scala ├── e1_generic_bag.scala └── e2_algebraic_bag.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea* 3 | project 4 | target 5 | .classpath 6 | .project 7 | .settings 8 | lib_managed 9 | ivyrepo 10 | sbtboot 11 | sbt/repository 12 | sbt/sbt 13 | \#*# 14 | .ensime* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scala basics. 2 | ============== 3 | Authors 4 | ----------- 5 | * Ludwine Probst [@nivdul](https://twitter.com/nivdul) 6 | * Mathieu Chataigner [@mchataigner](https://twitter.com/mchataigner) 7 | * Jonathan Winandy [@ahoy_jon](https://twitter.com/ahoy_jon) 8 | * Jean Helou [@jeanhelou](https://twitter.com/jeanhelou) 9 | * 10 | 11 | Prerequisites 12 | ------------ 13 | You will need : 14 | * Access to the internet 15 | * A recent version of java (6 or above) 16 | * Your favorite text editor (it's better if it can highlight scala syntax) 17 | 18 | Launch the hands-on session 19 | ------------ 20 | 21 | In this directory, just run 22 | * ```./handson``` on linux/osx 23 | * ```handson.bat``` on windows 24 | 25 | These scripts launch SBT (the Scala Build Tool), once in the SBT session, just type : 26 | 27 | go 28 | 29 | This launches the hands-on itself. Now your duty is to fill the blanks in the files that are under `src/test/scala` (except `HandsOn.scala` which contains only utility code). 30 | 31 | The tests will run each time you modify a file, showing errors if any, and advancing through the exercises as you solve them. 32 | 33 | Have fun ! 34 | 35 | 36 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion in ThisBuild := "2.12.6" 2 | 3 | scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") 4 | 5 | traceLevel := -1 6 | 7 | logLevel := Level.Info 8 | 9 | // disable printing timing information, but still print [success] 10 | showTiming := false 11 | 12 | // disable printing a message indicating the success or failure of running a task 13 | showSuccess := false 14 | 15 | libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value 16 | 17 | libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value 18 | 19 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.0" 20 | 21 | addCommandAlias("partie1-1", "~ testOnly first_steps") 22 | 23 | addCommandAlias("partie1-2", "~ testOnly next_steps") 24 | 25 | addCommandAlias("partie2", "~ testOnly we_need_to_go_deeper") 26 | 27 | addCommandAlias("partie3", "~ testOnly cons_et_nil") 28 | 29 | addCommandAlias("partie4", "~ testOnly type_classes") 30 | 31 | addCommandAlias("partie5", "~ testOnly un_sac_avec_des_items") 32 | 33 | addCommandAlias("partie6", "~ testOnly bonus_event_sourcing") 34 | 35 | addCommandAlias("go", "~ testOnly HandsOnScala") 36 | -------------------------------------------------------------------------------- /handson: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dir=$(dirname $0) 4 | cd "$dir" 5 | 6 | sbt="$dir/sbt/sbt-launch.jar" 7 | echo $SBT_PROPS 8 | # tweak this line according to your needs 9 | java $SBT_PROPS -Dsbt.ivy.home="$dir/sbt/repository" -Dsbt.boot.directory="$dir/sbt/boot" -Dsbt.boot.properties="$dir/sbt/sbt.boot.properties" -Xmx512M -jar -Dfile.encoding=UTF8 -Xss1M -XX:+CMSClassUnloadingEnabled "$dir/$sbt" "$@" 10 | 11 | -------------------------------------------------------------------------------- /handson.bat: -------------------------------------------------------------------------------- 1 | @REM SBT launcher script 2 | @REM 3 | @REM Envioronment: 4 | @REM JAVA_HOME - location of a JDK home dir (mandatory) 5 | @REM SBT_OPTS - JVM options (optional) 6 | 7 | 8 | @setlocal 9 | 10 | @echo off 11 | 12 | chcp 65001 13 | 14 | set SBT_HOME=%~dp0 15 | set ERROR_CODE=0 16 | 17 | rem We use the value of the JAVACMD environment variable if defined 18 | set _JAVACMD=%JAVACMD% 19 | 20 | if "%_JAVACMD%"=="" ( 21 | if not "%JAVA_HOME%"=="" ( 22 | if exist "%JAVA_HOME%\bin\java.exe" set "_JAVACMD=%JAVA_HOME%\bin\java.exe" 23 | ) 24 | ) 25 | 26 | if "%_JAVACMD%"=="" set _JAVACMD=java 27 | 28 | rem We use the value of the JAVA_OPTS environment variable if defined 29 | set _JAVA_OPTS=%JAVA_OPTS% 30 | if "%_JAVA_OPTS%"=="" set _JAVA_OPTS=-Xmx512M -XX:MaxPermSize=256m -XX:ReservedCodeCacheSize=128m -Dfile.encoding=UTF8 -Dsbt.log.format=true 31 | 32 | set SBT_OPTS=-Dsbt.ivy.home=.\sbt\repository -Dsbt.boot.directory=.\sbt\boot -Dsbt.boot.properties=sbt/sbt.boot.properties -Xss1M -XX:+CMSClassUnloadingEnabled 33 | 34 | :run 35 | 36 | "%_JAVACMD%" %_JAVA_OPTS% %SBT_OPTS% -cp .\sbt\jansi.jar;.\sbt\sbt-launch.jar;.\sbt SbtJansiLaunch %* 37 | if ERRORLEVEL 1 goto error 38 | goto end 39 | 40 | :error 41 | set ERROR_CODE=1 42 | 43 | :end 44 | 45 | @endlocal 46 | 47 | exit /B %ERROR_CODE% 48 | 49 | 50 | 51 | 52 | 53 | rem set SCRIPT_DIR=%~dp0 54 | 55 | rem set SBT=sbt-launch-0.12.2.jar 56 | 57 | rem java -Dsbt.ivy.home=.\ivyrepo -Dsbt.boot.directory=.\sbtboot -Xmx512M -jar -Dfile.encoding=UTF8 -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m "%SCRIPT_DIR%\%SBT%" %* 58 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sbt/SbtJansiLaunch.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kanaka-io/scala-basics/e0199e3a557bc70211ed6145576632c1171351f1/sbt/SbtJansiLaunch.class -------------------------------------------------------------------------------- /sbt/jansi-license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /sbt/jansi.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kanaka-io/scala-basics/e0199e3a557bc70211ed6145576632c1171351f1/sbt/jansi.jar -------------------------------------------------------------------------------- /sbt/sbt-launch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kanaka-io/scala-basics/e0199e3a557bc70211ed6145576632c1171351f1/sbt/sbt-launch.jar -------------------------------------------------------------------------------- /sbt/sbt.boot.properties: -------------------------------------------------------------------------------- 1 | [scala] 2 | version: ${sbt.scala.version-auto} 3 | 4 | [app] 5 | org: ${sbt.organization-org.scala-sbt} 6 | name: sbt 7 | version: ${sbt.version-read(sbt.version)[0.13.9]} 8 | class: ${sbt.main.class-sbt.xMain} 9 | components: xsbti,extra 10 | cross-versioned: ${sbt.cross.versioned-false} 11 | resources: ${sbt.extraClasspath-} 12 | 13 | [repositories] 14 | local 15 | jcenter: https://jcenter.bintray.com/ 16 | typesafe-ivy-releases: https://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly 17 | maven-central 18 | 19 | 20 | [boot] 21 | directory: ${sbt.ivy.home}/../sbt/boot 22 | 23 | [ivy] 24 | ivy-home: ${sbt.ivy.home}/repository 25 | checksums: ${sbt.checksums-sha1,md5} 26 | override-build-repos: ${sbt.override.build.repos-false} 27 | repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories} 28 | -------------------------------------------------------------------------------- /src/main/scala/recorder/AnchoRecorder.scala: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | /** 4 | * Created with IntelliJ IDEA. 5 | * User: jon 6 | * Date: 3/26/13 7 | * Time: 11:49 PM 8 | * To change this template use File | Settings | File Templates. 9 | */ 10 | class AnchorRecorder { 11 | 12 | var records:List[AnchorValue] = Nil 13 | 14 | 15 | def record(name:String, line:Int, value:String):Unit = { 16 | records = AnchorValue(name, line, value) :: records 17 | } 18 | 19 | def reset() { 20 | records = Nil 21 | } 22 | } 23 | 24 | case class AnchorValue(name:String, line:Int, value:String) { 25 | 26 | def toMessage():String = s" anchor line $line | $name => $value" 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/recorder/MyFunSuite.scala: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | import org.scalatest.{Tag, FunSuite} 4 | import java.io.File 5 | import collection.mutable.ArrayBuffer 6 | import org.scalatest.exceptions.TestFailedException 7 | import annotation.switch 8 | 9 | trait MyFunSuite extends FunSuite { 10 | 11 | implicit val anchorRecorder = new AnchorRecorder() 12 | 13 | def testPublic(testName: String)(testFun: => Unit) { 14 | test(testName)(testFun) 15 | } 16 | } 17 | 18 | object MyFunSuite { 19 | def testBody(testName: String, suite: MyFunSuite, anchorRecorder: AnchorRecorder)(testFun: => Unit)(context: TestContext) { 20 | 21 | suite.testPublic(testName)({ 22 | 23 | val testExpressionLineStart = context.testStartLine 24 | val testExpressionLineEnd = context.testEndLine 25 | 26 | lazy val testSourceFile: Array[(String, Int)] = sourceProcessor(context.source) 27 | 28 | anchorRecorder.reset() 29 | 30 | def testCtx(errorLine: Int): String = { 31 | MyFunSuite.prettyShow(testSourceFile.drop(testExpressionLineStart - 1).take(testExpressionLineEnd - testExpressionLineStart + 2), 32 | errorLine, anchorRecorder.records).mkString("\n") 33 | } 34 | 35 | def errorCtx(errorLine: Int): String = { 36 | MyFunSuite.prettyShow(testSourceFile.drop(errorLine - 2).take(3), errorLine, anchorRecorder.records).mkString("\n") 37 | } 38 | 39 | def ctx(errorLines: List[Int]):Option[String] = { 40 | def isInTest(line:Int):Boolean = (line >= testExpressionLineStart && line <= testExpressionLineEnd) 41 | 42 | val (inTest, outTest) = errorLines.partition(isInTest) 43 | 44 | val errorCtxs = outTest.sorted.map(errorCtx) 45 | 46 | val split = if (errorCtxs.isEmpty) "" else "\n...\n" 47 | 48 | val inTest2 = if(inTest.isEmpty) List(0) else inTest 49 | 50 | Some((errorCtxs ::: (split :: testCtx(inTest2.min) :: Nil)).mkString("\n")) 51 | 52 | } 53 | 54 | def exceptionMessage(t: Throwable):String = Option(t.getMessage).getOrElse("") 55 | 56 | def exceptionToLocation(st: StackTraceElement): String = { 57 | suitePackage + java.io.File.separator + st.getFileName + ":" + st.getLineNumber 58 | } 59 | 60 | def suitePackage = suite.getClass.getPackage.toString 61 | 62 | try { 63 | testFun 64 | } catch { 65 | case e: TestFailedException => { 66 | 67 | val location = e.failedCodeFileNameAndLineNumberString.map(suitePackage + java.io.File.separator + _) 68 | 69 | throw new MyTestFailedException(exceptionMessage(e) 70 | , ctx(e.failedCodeLineNumber.toList) 71 | , e 72 | , location 73 | ) 74 | 75 | } 76 | case e: NotImplementedError => { 77 | val mes = exceptionMessage(e) 78 | mes match { 79 | case "__" => 80 | val notimpl = e.getStackTrace()(3) 81 | val location = exceptionToLocation(notimpl) 82 | 83 | throw new MyTestPendingException(mes 84 | , ctx(List(notimpl.getLineNumber)) 85 | , e 86 | , Some(location) 87 | ) 88 | case _ => 89 | val notimpl = e.getStackTrace()(2) 90 | val secondLineNumber = e.getStackTrace()(3).getLineNumber 91 | val location = exceptionToLocation(notimpl) 92 | throw new MyNotImplException(mes 93 | , ctx(List(notimpl.getLineNumber, secondLineNumber)) 94 | , e 95 | , Some(location) 96 | ) 97 | } 98 | } 99 | case e: Throwable => { 100 | val firstGoodStackTrace:Option[StackTraceElement] = e.getStackTrace.find( 101 | st => st.getClassName.contains(suite.getClass.getName) 102 | ) 103 | 104 | val location = firstGoodStackTrace.map(exceptionToLocation) 105 | val failContext = firstGoodStackTrace.map(_.getLineNumber).map(lineNum => ctx(List(lineNum))) 106 | 107 | val myctx = failContext.getOrElse("") + e.getStackTrace.take(7).mkString("\n") 108 | 109 | val mes = e.toString + location.map("\n at " + _) 110 | throw new MyException(mes 111 | , Some(myctx) 112 | , e 113 | , location 114 | ) 115 | } 116 | } 117 | }) 118 | } 119 | 120 | def mergeSourceAndAnchor(source:List[(String,Int)], anchorsMessages:List[AnchorValue]): List[(String, Int, Option[String])] = { 121 | 122 | def anchor(line:Int, anchorsMessages:List[AnchorValue]):Option[String] = { 123 | val mess = anchorsMessages.filter( _.line == line) 124 | .map( a => a.name + " => " + a.value).mkString("\n") 125 | if(mess.trim == "")None else Some(mess) 126 | } 127 | 128 | source match { 129 | case shead :: stail => 130 | (shead._1, shead._2, anchor(shead._2, anchorsMessages)) :: mergeSourceAndAnchor(stail, anchorsMessages) 131 | case _ => Nil 132 | } 133 | } 134 | 135 | def prettyShow(source:Array[(String,Int)], errorLine:Int, anchorsMessages:List[AnchorValue]): List[String] = { 136 | def intLen(i:Int) = i.toString.length 137 | 138 | val len:Int = 4 139 | 140 | def completewithspace(i:Int):String = { 141 | (" " * (len - intLen(i))) + i.toString 142 | } 143 | 144 | def spacehead(s:String):String = { 145 | val space = "(\\s*).*".r 146 | s match { 147 | case space(spaces) => spaces 148 | case _ => "" 149 | } 150 | } 151 | 152 | mergeSourceAndAnchor(source.toList, anchorsMessages.reverse).map( 153 | { 154 | case (line, number, oanchor) => 155 | val prefix: String = if(number == errorLine) " ->" else " " 156 | prefix + completewithspace(number) + " |" + line + 157 | oanchor.map(_.split("\n").map( i => "\n " + spacehead(line) + i).mkString).getOrElse("") 158 | } 159 | ) 160 | } 161 | 162 | def sourceProcessor(source:Array[String]):Array[(String,Int)] = { 163 | source.zipWithIndex.map( t => (t._1, t._2 +1)) 164 | } 165 | } 166 | 167 | class TestContext( laZysource: => String , val testStartLine:Int,val testEndLine:Int) { 168 | lazy val source:Array[String] = TestContext.sourceToArray(laZysource) 169 | } 170 | 171 | object TestContext { 172 | 173 | final val LF = '\u000A' 174 | final val FF = '\u000C' 175 | final val CR = '\u000D' 176 | final val SU = '\u001A' 177 | 178 | def sourceToArray(source:String):Array[String] = { 179 | 180 | val text = source 181 | val lineBuf = new ArrayBuffer[String]() 182 | var charBuf = new ArrayBuffer[Char]() 183 | 184 | var previousChar:Char = 'a' 185 | 186 | for (c <- text.toCharArray) { 187 | 188 | def closeLine(){ 189 | lineBuf.append(charBuf.mkString) 190 | charBuf = new ArrayBuffer[Char]() 191 | } 192 | 193 | (c: @switch) match { 194 | case CR => closeLine() 195 | case LF => if (previousChar != CR) {closeLine()} 196 | case FF|SU => closeLine() 197 | case _ => charBuf.append(c) 198 | } 199 | 200 | previousChar = c 201 | } 202 | 203 | lineBuf.toArray 204 | 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/scala/recorder/MyTestPendingException.scala: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | class CustomTestPendingException extends Exception(new NotImplementedError) 4 | 5 | class MyException( 6 | val message: String, 7 | val context: Option[String], 8 | val cause: Throwable, 9 | val fileNameAndLineNumber: Option[String]) 10 | extends Exception(message, cause) {} 11 | 12 | class MyTestPendingException( 13 | message: String, 14 | context: Option[String], 15 | cause: Throwable, 16 | fileNameAndLineNumber: Option[String]) 17 | extends MyException(message, context, cause, fileNameAndLineNumber) {} 18 | 19 | class MyNotImplException( 20 | message: String, 21 | context: Option[String], 22 | cause: Throwable, 23 | fileNameAndLineNumber: Option[String]) 24 | extends MyException(message, context, cause, fileNameAndLineNumber) {} 25 | 26 | class MyTestFailedException( 27 | message: String, 28 | context: Option[String], 29 | cause: Throwable, 30 | fileNameAndLineNumber: Option[String]) 31 | extends MyException(message, context, cause, fileNameAndLineNumber) {} 32 | 33 | -------------------------------------------------------------------------------- /src/main/scala/recorder/RecorderMacro.scala: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | 4 | import reflect.macros.Context 5 | import org.scalatest.exceptions._ 6 | import reflect.internal.Chars 7 | import collection.mutable.ArrayBuffer 8 | import annotation.switch 9 | 10 | class RecorderMacro[C <: Context](val context: C) { 11 | import context.universe._ 12 | 13 | def apply(testName: context.Expr[String]) 14 | (testFun: context.Expr[Unit]) 15 | (suite: context.Expr[MyFunSuite], anchorRecorder:context.Expr[AnchorRecorder]): context.Expr[Unit] = { 16 | 17 | val texts = getTexts(testFun.tree) 18 | 19 | reify { 20 | val testExpressionLineStart:Int = context.literal(texts._2).splice 21 | 22 | val testExpressionLineEnd:Int = context.literal(texts._3).splice 23 | 24 | MyFunSuite.testBody(testName.splice, suite.splice, anchorRecorder.splice)(testFun.splice)(new TestContext( 25 | context.literal(texts._1).splice, testExpressionLineStart, testExpressionLineEnd)) 26 | } 27 | } 28 | 29 | 30 | def getTexts(recording:Tree):(String, Int,Int) = { 31 | def lines(rec : Tree):(Int,Int) = { 32 | rec match { 33 | case Block(xs, y) => (rec.pos.line, y.pos.line) 34 | case _ => (rec.pos.line, rec.pos.line) 35 | } 36 | 37 | } 38 | val (lstart, lend) = lines(recording) 39 | 40 | val source = recording.pos.source 41 | 42 | 43 | val sourceContent:String = source.content.mkString 44 | (sourceContent, lstart, lend) 45 | 46 | } 47 | 48 | } 49 | 50 | 51 | object RecorderMacro { 52 | 53 | 54 | 55 | def apply(context: Context)(testName: context.Expr[String]) 56 | (testFun: context.Expr[Unit]) 57 | (suite: context.Expr[MyFunSuite], anchorRecorder: context.Expr[AnchorRecorder]): context.Expr[Unit] = { 58 | 59 | new RecorderMacro[context.type](context).apply(testName)(testFun)(suite, anchorRecorder) 60 | } 61 | 62 | 63 | def anchor[T: context.WeakTypeTag](context: Context)(a : context.Expr[T]):context.Expr[Unit] = { 64 | import context.universe._ 65 | 66 | val aCode = context.literal(show(a.tree)) 67 | 68 | val line = context.literal(a.tree.pos.line) 69 | 70 | val resultExp = reify {("" + a.splice)} 71 | 72 | context.Expr[Unit]( 73 | Apply(Select(Select( 74 | context.prefix.tree, newTermName("anchorRecorder")), newTermName("record")), List(aCode.tree, line.tree, resultExp.tree)) 75 | 76 | ) 77 | 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/scala/support/CustomStopper.scala: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kanaka-io/scala-basics/e0199e3a557bc70211ed6145576632c1171351f1/src/main/scala/support/CustomStopper.scala -------------------------------------------------------------------------------- /src/main/scala/support/HandsOnSuite.scala: -------------------------------------------------------------------------------- 1 | package support 2 | 3 | import org.scalatest._ 4 | import org.scalatest.matchers.Matcher 5 | import org.scalatest.events._ 6 | import org.scalatest.exceptions.TestPendingException 7 | import recorder._ 8 | 9 | import language.experimental.macros 10 | import scala.Some 11 | 12 | class ReportToTheStopper(other: Reporter) extends Reporter { 13 | var failed = false 14 | 15 | def info(a:Any) = println(a) 16 | 17 | def headerFail = "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n TEST FAILED \n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" 18 | def footerFail = "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" 19 | def headerPending = "*******************************************\n TEST PENDING \n*******************************************" 20 | def footerPending = "*******************************************" 21 | 22 | def sendInfo(header: String, suite: String, test: String, location: Option[String], message: Option[String], context: Option[String], footer: String) { 23 | header.split("\n").foreach(info(_)) 24 | 25 | info( "Suite : " + suite.replace("\n","") ) 26 | 27 | info( "Exercise : " + test.replace("\n","") ) 28 | 29 | location.collect({ case f => 30 | info( "File : " + f.replace("\n","") ) 31 | }) 32 | message.collect({ case m => 33 | info("") 34 | m.split("\n").foreach( info(_) ) 35 | }) 36 | context.collect({ case c => 37 | info("") 38 | c.split("\n").foreach( info(_) ) 39 | }) 40 | info("") 41 | footer.split("\n").foreach(info(_)) 42 | HandsOnSuite.cancelRemaining = true 43 | 44 | 45 | } 46 | 47 | def sendFail(e:MyException, suite:String, test:String) = { 48 | sendInfo(headerFail 49 | , suite 50 | , test 51 | , e.fileNameAndLineNumber 52 | , Option(e.getMessage) 53 | , e.context 54 | , footerFail 55 | ) 56 | } 57 | 58 | def sendPending(e:MyException, suite:String, test:String, mess:Option[String]) = { 59 | sendInfo(headerPending 60 | , suite 61 | , test 62 | , e.fileNameAndLineNumber 63 | , mess 64 | , e.context 65 | , footerPending 66 | ) 67 | } 68 | 69 | def apply(event: Event) { 70 | event match { 71 | case e: TestFailed => { 72 | e.throwable match { 73 | //pour les erreurs d'assertions => sans stacktrace 74 | case Some(failure: MyTestFailedException) => 75 | sendFail(failure, e.suiteName, e.testName) 76 | //pour les __ => avec context 77 | case Some(pending: MyTestPendingException) => 78 | sendPending(pending, e.suiteName, e.testName, Some("You need to replace __ by the correct value")) 79 | //pour les ??? => sans context 80 | case Some(pending: MyNotImplException) => 81 | sendPending(pending, e.suiteName, e.testName, Some("You need to replace ??? by the correct implementation")) 82 | //pour les autres erreurs => avec stacktrace 83 | case Some(failure: MyException) => 84 | sendFail(failure, e.suiteName, e.testName) 85 | //ça ne devrait pas arriver 86 | case Some(e) => 87 | e.printStackTrace 88 | println("something went wrong") 89 | //ça non plus, un TestFailed a normalement une excepetion attachée 90 | case None => 91 | sendInfo(headerFail 92 | , e.suiteName 93 | , e.testName 94 | , None 95 | , None 96 | , None 97 | , 98 | footerFail 99 | ) 100 | } 101 | } 102 | case e: TestPending => 103 | sendInfo(headerPending 104 | , e.suiteName 105 | , e.testName 106 | , None 107 | , Some("pending") 108 | , None 109 | , footerPending) 110 | case _ => other(event) 111 | } 112 | } 113 | } 114 | 115 | object HandsOnSuite { 116 | @volatile var cancelRemaining = false 117 | object partie1 extends Tag("support.partie1") 118 | object partie2 extends Tag("support.partie2") 119 | } 120 | 121 | 122 | trait HandsOnSuite extends MyFunSuite with Matchers { 123 | def __ : Matcher[Any] = { 124 | throw new NotImplementedError("__") 125 | } 126 | 127 | implicit val suite:MyFunSuite = this 128 | 129 | 130 | import HandsOnSuite._ 131 | 132 | abstract override def withFixture(test: NoArgTest): Outcome = { 133 | if (cancelRemaining) 134 | Canceled("Canceled by CancelGloballyAfterFailure because a test failed previously") 135 | else 136 | super.withFixture(test) match { 137 | case failed: Failed => 138 | cancelRemaining = true 139 | failed 140 | case outcome => outcome 141 | } 142 | } 143 | 144 | final def newInstance: Suite with OneInstancePerTest = throw new UnsupportedOperationException 145 | 146 | def anchor[A](a:A):Unit = macro RecorderMacro.anchor[A] 147 | 148 | def exercise(testName:String)(testFun: Unit)(implicit suite: MyFunSuite, anchorRecorder: AnchorRecorder):Unit = macro RecorderMacro.apply 149 | 150 | 151 | override def run(testName: Option[String], args: Args): Status = 152 | super.run(testName, args.copy( 153 | reporter = new ReportToTheStopper(args.reporter), stopper = new Stopper { 154 | override def stopRequested: Boolean = HandsOnSuite.cancelRemaining 155 | override def requestStop(): Unit = HandsOnSuite.cancelRemaining = true 156 | }) 157 | ) 158 | 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/test/scala/HandsOnScala.scala: -------------------------------------------------------------------------------- 1 | import first_steps._ 2 | import next_steps._ 3 | import we_need_to_go_deeper._ 4 | import org.scalatest._ 5 | import support.{HandsOnSuite, ReportToTheStopper} 6 | 7 | class HandsOn extends Suite { 8 | 9 | /* 10 | override def run(testName: Option[String], args: Args): Status = 11 | super.run(testName, args.copy(reporter = new ReportToTheStopper(args.reporter), stopper = new Stopper { 12 | override def stopRequested: Boolean = HandsOnSuite.cancelRemaining 13 | 14 | override def requestStop(): Unit = {} 15 | })) 16 | */ 17 | } 18 | 19 | class HandsOnScala extends HandsOn { 20 | override def nestedSuites = Vector( 21 | new first_steps, 22 | new next_steps, 23 | new we_need_to_go_deeper 24 | ) 25 | } 26 | 27 | class first_steps extends HandsOn { 28 | override def nestedSuites = Vector( 29 | new e0_vars_vals, 30 | new e1_classes, 31 | new e2_case_classes, 32 | new e3_for_loops_and_comprehensions, 33 | new e4_pattern_matching 34 | ) 35 | } 36 | 37 | class next_steps extends HandsOn { 38 | override def nestedSuites = Vector( 39 | new e4_lists, 40 | new e5_functions_and_higher_order_functions, 41 | new e6_option, 42 | new e7_maps, 43 | new e8_sets 44 | ) 45 | } 46 | 47 | class we_need_to_go_deeper extends HandsOn { 48 | override def nestedSuites = Vector( 49 | new e0_bags, 50 | new e1_generic_bag, 51 | new e2_algebraic_bag 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/test/scala/part1-1/e0_vars_vals.scala: -------------------------------------------------------------------------------- 1 | package first_steps 2 | 3 | 4 | import support.HandsOnSuite 5 | 6 | /** 7 | * The 'var' and 'val' keywords are used to declare new variables. 8 | * We can make variables not visible from the outside by adding the keyword 'private' before the definition 9 | * By default variables are public. 10 | * 11 | * - var : declares a mutable variable (which value can be modified afterwards) 12 | * 13 | * - val : declares an immutable variable (this is the most usual kind of variables) 14 | */ 15 | class e0_vars_vals extends HandsOnSuite { 16 | 17 | exercise("One can change the value affected to a var after it has been declared") { 18 | var a = 5 19 | anchor(a) 20 | a should be(__) 21 | 22 | anchor(a) 23 | 24 | a = 7 25 | 26 | anchor(a) 27 | 28 | a should be(__) 29 | } 30 | 31 | exercise("On the contrary, a val cannot be changed once defined") { 32 | val a = 5 33 | 34 | a should be(__) 35 | 36 | /* 37 | * Question : 38 | */ 39 | // What would happen if we were to uncomment the following lines ? 40 | // a = 7 41 | // a should be (7) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/scala/part1-1/e1_classes.scala: -------------------------------------------------------------------------------- 1 | package first_steps 2 | 3 | import support.HandsOnSuite 4 | 5 | /** 6 | * To define a class, we use the keyword 'class'. 7 | * Inside a class we can define fields and methods. 8 | * Once again, everything is public by default. 9 | * 10 | * There are some specificities though : 11 | * - we can define val/var parameters to a class (see the example bellow) 12 | * - getters and setters for such parameters are automatically provided by the compiler 13 | * 14 | * This makes the Scala syntax way more concise than its Java counterpart : 15 | * 16 | * In Java we would write : 17 | * 18 | * public class ClassJava { 19 | * 20 | * String name; 21 | * 22 | * public String getName() { 23 | * return this.name; 24 | * } 25 | * 26 | * public void setName(String name) { 27 | * this.name = name; 28 | * } 29 | * } 30 | * 31 | * 32 | * The exact Scala equivalent would be : 33 | * 34 | * class ClassScala(var name: String) 35 | * 36 | * The generated getters/setters do not have get/set in their names, so we would use them like so : 37 | * 38 | * println(classScalaInstance.name) 39 | * 40 | * classScalaInstance.name = "new Name" 41 | * 42 | */ 43 | 44 | class e1_classes extends HandsOnSuite { 45 | 46 | // Notice that we can omit the {} if the class' body is empty 47 | class ClassWithValParameter(val name: String) 48 | 49 | /** 50 | * When we define an immutable parameter (field) no setter is generated 51 | */ 52 | exercise("A getter is generated for a val parameter (field)") { 53 | val aClass = new ClassWithValParameter("name goes here") 54 | aClass.name should be(__) 55 | } 56 | 57 | class ClassWithVarParameter(var description: String) 58 | 59 | exercise("A getter and a setter are generated for a var parameter (field)") { 60 | val aClass = new ClassWithVarParameter("description goes here") 61 | aClass.description should be(__) 62 | 63 | aClass.description = "new description" 64 | aClass.description should be(__) 65 | } 66 | 67 | 68 | /** 69 | * We can also define private parameters (fields) 70 | */ 71 | class ClassWithPrivateVarFields(private var name: String){ 72 | override def toString: String = name 73 | def changeName(newname: String) = { 74 | name = newname 75 | } 76 | } 77 | 78 | exercise("Mutable private field of a class") { 79 | val aClass = new ClassWithPrivateVarFields("name") 80 | // NOTE: aClass.name is not accessible from the outside 81 | 82 | aClass.toString should be(__) 83 | 84 | aClass.changeName("newname") 85 | aClass.toString should be(__) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/scala/part1-1/e2_case_classes.scala: -------------------------------------------------------------------------------- 1 | package first_steps 2 | 3 | import support.HandsOnSuite 4 | 5 | /** 6 | * There is a special kind of classes, namely "case classes". 7 | * 8 | * We create a case class by simply prepending the keyword 'case' before the class definition. 9 | * By doing this, the compiler will add even more "free" code to our class : 10 | * 11 | * - the constructor is made "implicit" 12 | * => no need of the keyword 'new' to create a new instance 13 | * - a "natural" implementation of equals, hascode and toString are generated for us 14 | * - parameters are 'public val' by default (so we do not need to type it) 15 | * - a 'copy' method is generated 16 | * 17 | * Besides making our code mode concise, case classes enable pattern matching (which we'll explore later) 18 | */ 19 | class e2_case_classes extends HandsOnSuite { 20 | 21 | 22 | /** 23 | * In the following exercises, we will work with the two case classes bellow 24 | */ 25 | case class MyDog(name: String, race: String) // by default, parameters are 'public val' 26 | case class Person(firstname: String, lastname: String, age: Int = 0, phone: String = "") // we can set default values to some parameters 27 | 28 | 29 | 30 | /** 31 | * Equality 32 | * 33 | * In Scala, equality is structural, that means that the '==' operation compares the values passed to 34 | * it, and not the references, like it would do in Java 35 | */ 36 | 37 | 38 | exercise("Creating instances of case classes is easy !") { 39 | 40 | 41 | val d1 = MyDog("Scooby", "Doberman") 42 | val d2 = MyDog("Rex", "Custom") 43 | val d3 = new MyDog("Scooby", "Doberman") // we can use the keyword 'new', but it is not necessary 44 | val d4 = MyDog.apply("Rex", "Custom") // utilisation de la méthode apply 45 | 46 | (d1 == d3) should be(__) 47 | (d1 == d2) should be(__) 48 | (d2 == d3) should be(__) 49 | (d2 == d4) should be(__) 50 | } 51 | 52 | exercise("Case classes have an equals method, that 'just works'") { 53 | 54 | val p1 = new MyDog("Rex", "Chihuahua") 55 | val p2 = new MyDog("Tiger" , "Wolfhound") 56 | val p3 = new MyDog("Rex", "Chihuahua") 57 | 58 | // Scala's == is equivalent to Java's .equals 59 | (p1 == p2) should be(__) 60 | (p1 == p3) should be(__) 61 | 62 | // If you need to compare references, you can use Scala's eq, which is equivalent to Java's == 63 | (p1 eq p2) should be(__) 64 | (p1 eq p3) should be(__) 65 | } 66 | 67 | /** 68 | * The hashcode method 69 | */ 70 | exercise("Case classes come with an automatically generated hashcode method") { 71 | 72 | val p1 = new MyDog("Snap", "Pitbull") 73 | val p2 = new MyDog("Rookie", "Rotweiler") 74 | val p3 = new MyDog("Snap", "Pitbull") 75 | 76 | (p1.hashCode == p2.hashCode) should be(__) 77 | (p1.hashCode == p3.hashCode) should be(__) 78 | } 79 | 80 | 81 | /** 82 | * Accessors 83 | */ 84 | exercise("Case classes come with automatically generated accessors") { 85 | 86 | val d1 = MyDog("Scooby", "Doberman") 87 | d1.name should be(__) 88 | d1.race should be(__) 89 | 90 | // What happens if we uncomment the following line ? 91 | //d1.name = "Scooby Doo" 92 | } 93 | 94 | exercise("We can 'update' an instance using copy") { 95 | val d1 = MyDog("Scooby", "Doberman") 96 | 97 | val d2 = d1.copy(name = "Scooby Doo") // creates a copy with a different value for the field name 98 | 99 | d1.name should be(__) 100 | d1.race should be(__) 101 | 102 | d2.name should be(__) 103 | d2.race should be(__) 104 | } 105 | 106 | exercise("We can use named parameters and default values") { 107 | 108 | val p1 = Person("Sherlock", "Holmes", 23, "06-XX-XX-XX-XX") 109 | 110 | // relying on default values 111 | val p2 = Person("Doctor", "Watson") 112 | 113 | // using named parameters, we can change the order to our needs 114 | val p3 = Person(phone = "01-XX-XX-XX-XX", firstname = "Professor", lastname = "Moriarty") 115 | 116 | // copy using named paramters 117 | val p4 = p3.copy(age = 23) 118 | 119 | p1.firstname should be("Sherlock") 120 | p1.lastname should be("Holmes") 121 | p1.age should be(23) 122 | p1.phone should be(__) 123 | 124 | p2.firstname should be("Doctor") 125 | p2.lastname should be(__) 126 | p2.age should be(0) 127 | p2.phone should be(__) 128 | 129 | p3.lastname should be(__) 130 | p3.firstname should be("Professor") 131 | p3.age should be(0) 132 | p3.phone should be(__) 133 | 134 | (p3 == p4) should be(__) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/test/scala/part1-1/e3_for_loops_and_comprehensions.scala: -------------------------------------------------------------------------------- 1 | package first_steps 2 | 3 | import support.HandsOnSuite 4 | 5 | /** 6 | * Les for, comprendre le 'for' classique et le 'for comprehension'. 7 | * 8 | * Les for sont applicable sur toutes collections. 9 | */ 10 | 11 | class e3_for_loops_and_comprehensions extends HandsOnSuite { 12 | 13 | //for classique 14 | 15 | exercise("The for loop is straightforward") { 16 | 17 | val digits = Range(0, 10) //0, 1, 2, 3, 4, 5, 6, 7, 8, 9 18 | 19 | // we could also have written 20 | // val digits = 0 to 9 21 | // or 22 | // val digits = 0 until 10 23 | 24 | // Lets mutate some variable in a loop 25 | var sum = 0 26 | for (i <- digits) { 27 | sum += i 28 | } 29 | 30 | sum should equal(__) 31 | } 32 | 33 | exercise("We can add 'filters' in a for") { 34 | val digits = 0 until 10 35 | var sum = 0 36 | 37 | // Lets sum only even digits 38 | for (i <- digits if i % 2 == 0) { 39 | sum += i 40 | } 41 | 42 | sum should equal(__) 43 | } 44 | 45 | 46 | /** 47 | * When it comes to Scala, the term for-loop is a kind of misuse. 48 | * 49 | * The constructs above look like loops, but are rather named for-comprehensions in Scala. 50 | * In the examples above, we relied on side effects within the comprehensions because it resemble the loops you're 51 | * probably used to, but is not idiomatic Scala. 52 | * 53 | * But what is a for-comprehension ? 54 | * 55 | * For-comprehensions go well beyond simple iteration. They can be applied to many more data structures than just Lists. 56 | * A very important point is that it is the data structures that 'knows' how to navigate trough its elements, so the 57 | * programmer does not need to bother with that mere implementation details. 58 | * 59 | * A for-comp is composed of two parts : 60 | * 61 | * - a series of one or more 'generators' (defined using <-) that can have an optional filter (using if) 62 | * - a body where we define what we do with each individual element of the structure. The 'yield' keyword 63 | * indicates that we produce a new element for each input element, thus transforming our input structure into a new one 64 | */ 65 | exercise("For-comprehension can transform data structures") { 66 | 67 | 68 | val digits = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 69 | 70 | // Notice that a for comprehension is an expression, that can be assigned to a variable 71 | val aList = 72 | for { 73 | i <- digits if (i % 2) == 0 74 | } 75 | yield i 76 | 77 | aList should be(__) 78 | 79 | // List has a method sum that sums the elements values (only if it is a List of numeric type) 80 | aList.sum should be(__) 81 | } 82 | 83 | exercise("We can nest generators") { 84 | val xValues = 1 until 5 85 | val yValues = 1 until 3 86 | 87 | val coordinates = for { 88 | x <- xValues 89 | y <- yValues 90 | } 91 | yield (x, y) // this (x, y) creates a pair (2-tuple) with x and y 92 | coordinates(4) should be(__) 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/test/scala/part1-1/e4_pattern_matching.scala: -------------------------------------------------------------------------------- 1 | package next_steps 2 | 3 | import support.HandsOnSuite 4 | 5 | /** 6 | * Pattern matching 7 | * 8 | * Pattern matching is a very powerful technique that allows us to reason about the structure 9 | * of object (without breaking type erasure principle). 10 | * 11 | * Using pattern matching, we can extract some pieces of an object, do different things according 12 | * to its shape or properties, etc. 13 | * 14 | * It can be seen as a functional alternative to polymorphic dispatch. 15 | */ 16 | 17 | class e4_pattern_matching extends HandsOnSuite { 18 | /** 19 | * Pattern matching relies on so-called extractors. 20 | * 21 | * An extractor is a method called 'unapply' defined on the companion object of a class, 22 | * which simply "deconstructs" an instance of a class into smaller elements 23 | */ 24 | exercise("An extractor is the converse of a constructor") { 25 | class Email(val value: String) 26 | 27 | object Email { 28 | /** 29 | * Given an Email, deconstruct it to return only its value 30 | * 31 | * unapply must return an Option because (under circumstances that are not relevant here) we might 32 | * not be able to perform this deconstruction 33 | */ 34 | def unapply(email: Email): Option[String] = Option(email.value) 35 | } 36 | 37 | val mailstring = "foo@bar.com" 38 | val email = new Email(mailstring) 39 | 40 | 41 | 42 | val Email(extractedString) = email 43 | 44 | (extractedString == mailstring) should be(__) 45 | } 46 | 47 | exercise("Extractors work also with multiple fields") { 48 | class Email(val value: String, val spamRatio: Integer) 49 | object Email { 50 | /** 51 | * When the class has multiple constituents, we wrap them in a tuple (notice the parens bellow) 52 | */ 53 | def unapply(email: Email): Option[(String, Integer)] = Option((email.value, email.spamRatio)) 54 | } 55 | 56 | val email = new Email("foo@bar.com", 5) 57 | val Email(extractedString, extractedRatio) = email 58 | 59 | extractedRatio should be(__) 60 | extractedString should be(__) 61 | } 62 | 63 | /** 64 | * Case classes are meant to enable pattern matching 65 | */ 66 | exercise("The compiler generates extractors for cases classes") { 67 | case class Email(value: String) 68 | 69 | val mailstring = "foo@bar.com" 70 | val email = new Email(mailstring) 71 | val Email(extractedString) = email 72 | 73 | extractedString should be(__) 74 | } 75 | 76 | /** 77 | * Pattern matching is most often used via the 'match' construct. 78 | * This can be seen as a generalization of the switch of languages like Java or C. 79 | * 80 | * Like any other construct in Scala, a match block is a fully fledged expression, that has 81 | * a type and a value. 82 | * 83 | * It has the following form : 84 | * 85 | * expr match { 86 | * case pattern1 => expr1 87 | * case pattern2 => expr2 88 | * ... 89 | * case patternN => exprN 90 | * } 91 | * 92 | * Notice the case keyword, it's not a coincidence that it is the same that the one we use to 93 | * declare case classes ! 94 | * 95 | * Each pattern is attempted in order, the first that matches expr wins and its body (exprI) 96 | * is evaluated to give the result of the block. 97 | * If no pattern matches, a (runtime) MatchError is raised, but the compiler does its best to 98 | * detect incomplete match blocks (where the programmer did not cover all the possible cases), 99 | * in order to reduce the risk of run time MatchErrors. 100 | * 101 | */ 102 | exercise("Match block replace switch cases") { 103 | val string = "B" 104 | 105 | val actual = "B" match { 106 | case "A" => "stringA" 107 | case "B" => "stringB" 108 | case "C" => "stringC" 109 | // In the context of a pattern, _ matches everything, 110 | // it's often a way to ensure we've got every case covered 111 | case _ => "DEFAULT" 112 | } 113 | 114 | actual should be(__) 115 | 116 | val nextActual = "E" match { 117 | case "A" => "stringA" 118 | case "B" => "stringB" 119 | case "C" => "stringC" 120 | case _ => "DEFAULT" 121 | } 122 | 123 | nextActual should be(__) 124 | } 125 | 126 | 127 | /** 128 | * Inside a pattern, we can capture a field and attach it to a variable 129 | * that will be scoped to the body of the corrspounding case (after the =>) 130 | * 131 | * IMPORTANT NOTE : pattern variables MUST begin with a lower-cased character 132 | */ 133 | exercise("Match blocks can extract fields from case classes") { 134 | case class A(a: String, b: String) 135 | 136 | val someA: A = A(a = "string", b = "B") 137 | 138 | val actual = someA match { 139 | case A(aa, bb) => aa + bb // on strings, the method + stands for concatenation 140 | case _ => "DEFAULT" 141 | } 142 | 143 | actual should be(__) 144 | } 145 | 146 | exercise("Capturing all the fields is not mandatory") { 147 | case class A(a: String, b: String) 148 | val someA: A = new A(a = "string", b = "B") 149 | 150 | val actual = someA match { 151 | case A(aa, _) => aa 152 | case _ => "DEFAULT" 153 | } 154 | 155 | actual should be(__) 156 | } 157 | 158 | /** 159 | * We can also add "guards" to our patterns, using the if keyword, 160 | * this allows for further filtering based on properties of 161 | * the captured pattern variables. 162 | */ 163 | exercise("Patterns with guards") { 164 | case class B(b: String, c: Int) 165 | 166 | def bigB(b: B) = b match { 167 | case B(bb, cc) if cc > 10 => bb 168 | case _ => "Too small" 169 | } 170 | 171 | bigB(B("The answer", 42)) should be(__) 172 | 173 | bigB(B("The question", 9)) should be(__) 174 | 175 | } 176 | 177 | /** 178 | * With pattern matching, we can explore arbitrarily nested structures 179 | */ 180 | exercise("We can go deeper") { 181 | case class A(a: String) 182 | case class B(a: A, b: Int) 183 | case class C(a: A, b: B) 184 | 185 | val deeplyNested = C( 186 | a = A("Scala"), 187 | b = B( 188 | a = A("Java"), 189 | b = 8 190 | ) 191 | ) 192 | 193 | val contrived = deeplyNested match { 194 | case C( A("Erlang"), _ ) => "robust" 195 | case C( _ , B(_, version)) if version <= 7 => "obsolete" 196 | case _ => "shiny" 197 | } 198 | 199 | contrived should be(__) 200 | } 201 | 202 | /** 203 | * Finally (although it is seldom used) we can define a pattern variable 204 | * AND impose constraints on its value in one go, using the '@' keyword 205 | */ 206 | exercise("Nesting patterns") { 207 | case class A(str: String, int: Int) 208 | 209 | val someA = A("Scala", 42) 210 | 211 | val result = someA match { 212 | case aa @ A(_, i) if i > 10 => aa 213 | case _ => A("", 0) 214 | } 215 | 216 | result should be(__) 217 | } 218 | 219 | 220 | exercise("List are case classes, we can pattern match on them !") { 221 | 222 | // Seq is an abstraction that encompasses Lists, among other data structures 223 | val s = Seq("a", "b") 224 | val actual = s match { 225 | case Seq("a", "b") => "ok" 226 | case _ => "DEFAULT" 227 | } 228 | 229 | actual should be(__) 230 | 231 | val consActual = s match { 232 | case "a" :: Nil => "nok" 233 | case "a" :: "b" :: Nil => "ok" 234 | case _ => "DEFAULT" 235 | } 236 | 237 | consActual should be(__) 238 | 239 | val headtailActual = s match { 240 | case head :: tail => tail 241 | case _ => "DEFAULT" 242 | } 243 | 244 | headtailActual should be(__) 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /src/test/scala/part1-2/e4_lists.scala: -------------------------------------------------------------------------------- 1 | package next_steps 2 | 3 | 4 | import support.HandsOnSuite 5 | 6 | 7 | /** 8 | * Now on to Lists ... 9 | * 10 | * The entry point to Scala's collection API is the Iterable trait (which List obviously implements). 11 | * Every collection provides higher-order functions (we'll get into that shortly) that allows us to 12 | * conveniently manipulate them 13 | * 14 | * By default, collections are immutable but mutable versions exists for each collection types. 15 | * (You should have guessed by now that immutability is the standard for almost everything in Scala). 16 | * 17 | * 18 | * A List containing elements x1, … , xn is denoted List(x1, … , xn). 19 | * 20 | * List is modeled as a singly linked list, the trait List has two implementations : 21 | * 22 | * - Nil which is a case object and represents the empty list 23 | * - :: (usually pronounced 'cons') which represents a "link" in the linked list 24 | * 25 | * That is : 26 | * 27 | * List(x1, … , xn) == x1 :: … :: xn :: Nil 28 | * 29 | * useful link : http://www.scala-lang.org/api/current/index.html#scala.collection.immutable.List 30 | */ 31 | 32 | class e4_lists extends HandsOnSuite { 33 | 34 | exercise("Nil is the only possible empty list") { 35 | val a: List[String] = Nil // Nil est la classe qui représente une liste vide 36 | val b: List[Int] = Nil // et oui en Scala tout est OBJET ! 37 | 38 | (a == Nil) should be(__) 39 | (b == Nil) should be(__) 40 | (a == b) should be(__) 41 | (a eq b) should be(__) 42 | 43 | } 44 | 45 | exercise("Creating new lists") { 46 | val a = List(1, 2, 3) 47 | val b = List(1, 2, 3) 48 | 49 | (a eq b) should be(__) 50 | (a == b) should be(__) 51 | } 52 | 53 | /** 54 | * Some useful methods on List 55 | */ 56 | exercise("The List API") { 57 | val a = List(1, 3, 5, 7, 9) 58 | 59 | // Get element at index 2 in the list 60 | a(2) should equal(__) 61 | 62 | a.head should equal(__) 63 | 64 | a.tail should equal(__) 65 | 66 | a.length should equal(__) 67 | 68 | a.reverse should equal(__) 69 | 70 | a.toString should equal(__) 71 | 72 | // map applies a function to each successive elements in a list 73 | // and returns a new list composed of the resulting elements 74 | 75 | // The notation "x => x + 1" reads as "the function that associates x + 1 to x" 76 | // This notation defines an anonymous function, also dubbed as "lambda" 77 | // The compiler will infer the type of the parameter (x in the example above) whenever possible 78 | a.map(v => v * 3) should equal(__) 79 | 80 | // filter needs a function that takes an element of the list and returns a Boolean 81 | // it keeps only the elements for which that function returns true 82 | 83 | a.filter(v => v % 3 == 0) should equal(__) 84 | 85 | val c = List(1, 2, 5, 8, 9) 86 | val b = c.filterNot(v => v % 5 == 0) 87 | 88 | c should equal(List(1, 2, 5, 8, 9)) // the original list is not modified, as usual 89 | 90 | b should equal(__) 91 | } 92 | 93 | /** 94 | * There is a shorter syntax to create lambdas (anonymous functions) 95 | * When the parameter's type is unambiguous, both following notations are equivalent: 96 | * 97 | * x => x + 2 98 | * 99 | * _ + 2 100 | * 101 | * The _ keyword stands as a placeholder for the parameter in the lambda's body. 102 | * Be aware though that when using this notation, each occurrence of the _ placeholder 103 | * refers to a new parameter. In other words, 104 | * 105 | * _ + _ 106 | * 107 | * is equivalent to 108 | * 109 | * (x, y) => x + y 110 | * 111 | * and not to 112 | * 113 | * x => x + x 114 | */ 115 | exercise("Lambda's can be defined with a shorter syntax") { 116 | val a = List(1, 2, 3) 117 | 118 | a.map(_ * 2) should equal(__) 119 | 120 | a.filter(_ % 2 != 0) should equal(__) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/test/scala/part1-2/e5_functions_and_higher_order_functions.scala: -------------------------------------------------------------------------------- 1 | package next_steps 2 | 3 | import support.HandsOnSuite 4 | 5 | /** 6 | * Before digging deeper into the collections API, we need to look further into functions 7 | * and higher order functions 8 | * 9 | * We already know about methods, that are some kind of functions and which are defined 10 | * on a class (or an object) using the keyword 'def'. 11 | * 12 | * 13 | * Functions, on the other hand, are plain values like Ints or Booleans. We can attach 14 | * a function to a variable, pass it as a parameter to a method (or function), and return it 15 | * as a result. We can also see methods as fields of a class whose value is a function. 16 | * 17 | * Functions that manipulate other functions (by taking them as parameters and/or returning 18 | * other functions) are called higher-order functions. We've already seen examples of such 19 | * higher-order functions (or rather methods) with map and filter. 20 | * 21 | * Internally, functions are modelled as instances of a FunctionN trait. For example, functions 22 | * of no argument implement the Function0 trait, functions of one argument implement Function1, 23 | * and so on up to Function21 (that is, you cannot define functions with more than 21 arguments). 24 | * 25 | * The FunctionN traits define only one method 'apply' that take N arguments. There is a special rule 26 | * in Scala syntax that allow to call methods named 'apply' without typing the name, that is : 27 | * 28 | * someInstance.apply(x) 29 | * 30 | * is equivalent to 31 | * 32 | * someInstance(x) 33 | * 34 | * The notation "x => x + 1" we've seen before is mere syntactic sugar that eases the creation of a subclass of Function1. 35 | * 36 | * But the => keyword can also be used to denote function *types*. For example, 37 | * 38 | * Int => Int 39 | * 40 | * denotes the type of functions that take an Int as parameter and return an Int. 41 | * Such types can grow to arbitrary complexity : 42 | * 43 | * (Int, String) => Boolean is the type of functions that take an Int and a String and return a Boolean 44 | * 45 | * (String, Int => Int) => String is ??? 46 | * 47 | * (Long => Int) => (Long => Long) is ??? 48 | * 49 | * ... etc 50 | * 51 | * 52 | * 53 | */ 54 | class e5_functions_and_higher_order_functions extends HandsOnSuite { 55 | 56 | exercise("We can attach functions to variables") { 57 | /** 58 | * We could have defined lambda using this syntax : 59 | * 60 | * val lambda = { 61 | * x: Int => x + 1 62 | * } 63 | * 64 | * but the use of curly braces is only needed when the body of the 65 | * function spans multiple expressions 66 | */ 67 | val lambda = (x: Int) => x + 1 68 | def result = List(1, 2, 3) map lambda 69 | 70 | result should be(__) 71 | } 72 | 73 | /** 74 | * We can also use the keyword 'def' inside a method's body to create a local function 75 | * */ 76 | exercise("Local functions") { 77 | def result = List(1, 2, 3) map (__) 78 | 79 | result should be(List(2, 3, 4)) 80 | 81 | def shorterSyntax = List(1, 2, 3) map ( _ * 2 ) 82 | shorterSyntax should be(__) 83 | } 84 | 85 | 86 | 87 | /** 88 | * Les fonctions de plus haut niveau peuvent retourner des fonctions 89 | */ 90 | exercise("Functions returning functions") { 91 | def addWithoutSyntaxSugar(x: Int) = { 92 | new Function1[Int, Int]() { 93 | def apply(y: Int): Int = x + y 94 | } 95 | } 96 | 97 | // remember, this is equivalent to addWithoutSyntaxSugar(1).apply(2) 98 | addWithoutSyntaxSugar(1)(2) should be(__) 99 | 100 | //or more simply 101 | def add(x: Int) = (y: Int) => x + y 102 | add(2)(3) should be(__) 103 | 104 | def addFive = add(5) 105 | addFive(42) should be(__) 106 | } 107 | 108 | exercise("Functions taking functions as parameters") { 109 | def makeUpper(xs: List[String]) = xs map { 110 | _.toUpperCase 111 | } 112 | makeUpper(List("abc", "xyz", "123")) should be(List("ABC", "XYZ", "123")) 113 | 114 | def makeWhatheverYouLike(xs: List[String], transformation: String => String) = xs map transformation 115 | 116 | makeWhatheverYouLike(List("ABC", "XYZ", "123"), ???) should be(List("abc", "xyz", "123")) 117 | 118 | //using it inline 119 | List("Scala", "Erlang", "Clojure") map { 120 | _.length 121 | } should be(__) 122 | } 123 | 124 | /** 125 | * Currying 126 | * 127 | * Functions enjoy a nice property called "currying", after the name of Haskell Curry, who first formalised it 128 | * back in the 50's. 129 | * 130 | * To put it in Scala syntax, we can say that : 131 | * 132 | * (A, B) => C 133 | * 134 | * is strictly equivalent to : 135 | * 136 | * A => B => C we could also have written A => (B => C) 137 | * 138 | * In english, we would say that : 139 | * a function that takes two parameters of types A and B and returns a result of type C 140 | * is strictly equivalent to 141 | * a function of one parameter of type A that returns a function of one parameter of type B that returns a C 142 | * 143 | * In Scala, we can define functions in their curried form by specifying multiple parameters groups. 144 | * 145 | * For example : 146 | * 147 | * def addCurried(x: Int)(y: Int): Int = x + y 148 | * 149 | * Does the same thing as : 150 | * 151 | * def addSimple(x: Int, y: Int): Int = x + y 152 | * 153 | * But enables us to do things like this : 154 | * 155 | * val increment : Int => Int = addCurried(1) which can be written more concisely as ... 156 | * 157 | * val increment = addCurried(1) _ notice the _ placeholder, it tells the compiler to make a function 158 | * out of the partially applied parameters, rather than attempting a 159 | * function call (that wouldn't compile since all parameters are not 160 | * specified). 161 | * 162 | * Finally, we can transform a function of multiple parameters into its curried counterpart by calling the 'curried' 163 | * method on it. 164 | */ 165 | exercise("We can curry arbitrary functions") { 166 | def multiply(x: Int, y: Int) = x * y 167 | val multiplyCurried = (multiply _).curried 168 | 169 | multiply(4, 5) should be(__) 170 | multiplyCurried(4)(5) should be(__) 171 | } 172 | 173 | 174 | exercise("Currying allows to create specialized versions of a function") { 175 | def customFilter(f: Int => Boolean)(xs: List[Int]) = { 176 | xs filter f 177 | } 178 | def isEven(x: Int) = x % 2 == 0 179 | val xs = List(12, 11, 5, 20, 3, 13, 2) 180 | customFilter(isEven)(xs) should be(__) 181 | 182 | val onlyEvenFilter = customFilter(isEven) _ 183 | 184 | onlyEvenFilter(xs) should be(__) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/test/scala/part1-2/e6_option.scala: -------------------------------------------------------------------------------- 1 | package next_steps 2 | 3 | import support.HandsOnSuite 4 | 5 | /** 6 | * Option is a very (very) useful type. 7 | * 8 | * It can be seen as a collection that contains at most one element (but can be empty). 9 | * We use it to denote the fact that something can be present or not in a (way) safer 10 | * and (arguably) cleaner way than resorting to (the dreadful) null. 11 | * 12 | * Option[A] has two subtypes : 13 | * 14 | * - None : the empty case, defined as Option[Nothing] (remember, Nothing can satisfy every other type) 15 | * - Some[A] : the case where a content is defined 16 | * 17 | * see http://www.scala-lang.org/api/current/index.html#scala.Option 18 | */ 19 | 20 | class e6_option extends HandsOnSuite { 21 | exercise("None is equal to ...None") { 22 | None should be(__) 23 | } 24 | 25 | exercise("None is, well ... empty") { 26 | None.isEmpty should be(__) 27 | } 28 | 29 | 30 | exercise("None is 'real' object, we can call methods on it") { 31 | val optional: Option[String] = None 32 | optional.isEmpty should be(__) 33 | optional should be(__) 34 | } 35 | 36 | exercise("Some everything but None") { 37 | val optional: Option[String] = Some("Some Value") 38 | (optional == None) should be(__) 39 | optional.isEmpty should be(__) 40 | } 41 | 42 | exercise("We can call getOrElse on an option to safely extract its content or fallback to a default value") { 43 | val optional: Option[String] = Some("Some Value") 44 | val optional2: Option[String] = None 45 | optional.getOrElse("No Value") should be(__) 46 | optional2.getOrElse("No Value") should be(__) 47 | } 48 | 49 | exercise("We can use map on an Option to safely transform its content") { 50 | def toUpper(s: String) = s.toUpperCase 51 | 52 | None map toUpper should be(__) 53 | Some("scala") map toUpper should be(__) 54 | 55 | } 56 | 57 | exercise("We can use Option in for-comprehensions") { 58 | 59 | val someString = Some("scala") 60 | val someInt = Some(42) 61 | val none = None 62 | 63 | val interpolate1 = for { 64 | str <- someString 65 | int <- someInt 66 | } yield s"$str ${int * 2}" // this is string interpolation notice the s before the opening double-quote 67 | 68 | interpolate1 should be(__) 69 | 70 | val interpolate2 = for { 71 | str <- someString 72 | nn <- none 73 | int <- someInt 74 | } yield s"$str ${int * 2}" 75 | 76 | interpolate2 should be(__) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/test/scala/part1-2/e7_maps.scala: -------------------------------------------------------------------------------- 1 | package next_steps 2 | 3 | import support.HandsOnSuite 4 | 5 | 6 | /** 7 | * 8 | * Maps 9 | * 10 | * Like in most languages, Maps in Scala associate keys of a given type K to values 11 | * of a given type V and are denoted Map[K, V] 12 | * 13 | * Maps are isomophic to a list of pairs, but with enhanced operations, like searching 14 | * for a key, handling duplicate keys, and so on. 15 | * 16 | * see : http://www.scala-lang.org/api/current/index.html#scala.collection.concurrent.Map 17 | */ 18 | 19 | class e7_maps extends HandsOnSuite { 20 | 21 | exercise("Creating maps is easy") { 22 | val myMap = Map("PA" -> "Paris", "BE" -> "Besançon", "BL" -> "Belfort") 23 | myMap.size should be(__) 24 | 25 | myMap.head should be(__) 26 | 27 | // Maps retain the order of insertion, but it is irrelevant when it comes to map equality 28 | val myMapBis = Map("BE" -> "Besançon", "BL" -> "Belfort", "PA" -> "Paris") 29 | myMap.equals(myMapBis) should be(__) 30 | 31 | // Of Maps do not retain duplicate keys 32 | val myOtherMap = Map("PA" -> "Paris", "BE" -> "Besançon", "PA" -> "Palo Alto") 33 | myOtherMap.size should be(__) 34 | myOtherMap("PA") should be(__) 35 | } 36 | 37 | exercise("We can add entries to a Map using +") { 38 | val myMap = Map("PA" -> "Paris", "BE" -> "Besançon", "NA" -> "Nantes") 39 | val aNewMap = myMap + ("BL" -> "Belfort") 40 | 41 | myMap.contains("BL") should be (__) // As always, immutablity is the default 42 | aNewMap.contains("BL") should be(__) 43 | } 44 | 45 | 46 | exercise("On peut accéder/supprimer les élément d’une map") { 47 | val myMap = Map("PA" -> "Paris", "BE" -> "Besançon", "NA" -> "Nantes", "BL" -> "Belfort") 48 | 49 | // We remove an element by its key, using - 50 | val aNewMap = myMap - "NA" 51 | aNewMap.contains("NA") should be(__) 52 | 53 | // We can remove a list of keys with -- 54 | val aNewOtherMap = myMap -- List("BE", "BL") 55 | aNewOtherMap.contains("BE") should be(__) 56 | aNewOtherMap.contains("BL") should be(__) 57 | 58 | // An exception is thrown if we try to access a key that does not exists 59 | intercept[NoSuchElementException] { 60 | aNewOtherMap("BL") should be("Belfort") // this uses Map's apply method 61 | } 62 | 63 | // Using get is safer, since it returns an Option 64 | aNewOtherMap.get("BL") should be (__) 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/scala/part1-2/e8_sets.scala: -------------------------------------------------------------------------------- 1 | package next_steps 2 | 3 | import support.HandsOnSuite 4 | 5 | /** 6 | * Set, like well... Set. 7 | * 8 | * some useful links : 9 | * - http://www.scala-lang.org/api/current/index.html#scala.collection.immutable.Set 10 | * - http://docs.scala-lang.org/overviews/collections/sets.html 11 | */ 12 | 13 | class e8_sets extends HandsOnSuite { 14 | 15 | exercise("Creating Set") { 16 | val mySet = Set("South", "East", "West", "North") 17 | mySet.size should be(__) 18 | 19 | val myOtherSet = Set("South", "East", "South", "North") 20 | myOtherSet.size should be(__) 21 | } 22 | 23 | exercise("Operations on Sets") { 24 | // addition 25 | val mySet = Set("South", "East", "South") 26 | val aNewSet = mySet + "North" 27 | 28 | aNewSet.contains("North") should be(__) 29 | 30 | // removal 31 | val mySetBis = Set("South", "East", "Oueast", "North") 32 | val aNewSetBis = mySetBis - "North" 33 | 34 | // testing if an element is present 35 | aNewSetBis.contains("North") should be(__) 36 | 37 | // multiple removals 38 | val myOtherSet = Set("South", "East", "West", "North") 39 | val aNewOtherSet = myOtherSet -- List("West", "North") 40 | 41 | aNewOtherSet.contains("North") should be(__) 42 | aNewOtherSet.contains("West") should be(__) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/part2/e0_bags.scala: -------------------------------------------------------------------------------- 1 | package we_need_to_go_deeper 2 | 3 | import support.HandsOnSuite 4 | import util.Random 5 | import scala.collection 6 | 7 | /** 8 | * Lets build our own collection type ! 9 | * 10 | * Well, this will be quite a boring and useless collection, but hopefully, it 11 | * will give you some insight about how Scala's collection work internally. 12 | * 13 | * We'll define the Bag structure, which is a collection containing exactly one 14 | * element (boring, eh ?). We'll define just enough methods on it so that users 15 | * can interact with it, without the need of knowing its (dead simple) internal 16 | * structure. 17 | * 18 | * Before jumping into the implementation (i.e, replacing the ??? bellow with 19 | * actual code), we need to fill the few blanks ( __ ) in the tests at the 20 | * bottom of the file. 21 | * 22 | * Although it is a very simple structure, the methods you'll have to implement 23 | * can appear a bit strange to you. But fear not, "all" you have to do is to 24 | * apply a precept that is pervasive in Scala programming : Just Follow the Types™ 25 | * 26 | */ 27 | class e0_bags extends HandsOnSuite { 28 | 29 | class Bag(val content:Int) { 30 | 31 | /** 32 | * We already know about map, it simply applies the function to the content of 33 | * our Bag, without changing its structure. 34 | */ 35 | def map(function:Int => Int):Bag = ??? 36 | 37 | /** 38 | * flatmap is a slightly scarier beast. We pass if a function that returns a new 39 | * Bag, but instead of returning us a Bag of Bag, it somehow "flattens" (hence 40 | * the name) the result to give us a simple Bag back. 41 | * 42 | * Once again, just follow the types ... 43 | */ 44 | def flatMap(function:Int => Bag):Bag = ??? 45 | 46 | } 47 | 48 | /** 49 | * To pass this test we need the map function implemented on Bag 50 | */ 51 | exercise("I can apply a function inside a Bag") { 52 | val increment:(Int => Int) = (i:Int) => i + 1 53 | 54 | increment(0) should be(1) 55 | 56 | val myLittleBagOfZero = new Bag(0) 57 | 58 | myLittleBagOfZero.map(increment).content should be(1) 59 | } 60 | 61 | exercise("I can use for-comprehension on Bag") { 62 | 63 | val myLittleBagOfZero = new Bag(0) 64 | 65 | val more:Int = 12345 66 | 67 | val myBiggerBag = for (i <- myLittleBagOfZero) yield i + more 68 | 69 | 70 | myBiggerBag.content should be(__) 71 | 72 | } 73 | 74 | /** 75 | * For this test to pass, we need flatMap implemented on Bag. 76 | */ 77 | exercise("I can use complex for-comprehension on Bag") { 78 | 79 | 80 | 81 | val myLittleBagOfTwo = new Bag(2) 82 | val myBiggerBagOfAHundred = new Bag(100) 83 | 84 | val myBagsMerged = for (l <- myLittleBagOfTwo; b <- myBiggerBagOfAHundred) yield l * b 85 | 86 | /** 87 | * Now we can tell you. For comprehensions are mere syntactic sugar (yes, again. Some say Scala suffers 88 | * from syntax diabetes). 89 | * 90 | * But for-comprehension is really a neat feature. Without it, we would need to write the line above like : 91 | * val myBagmerged = myLittleBagOfTwo.flatMap{ l => myBiggerBagOfAHundred.map(b => l * b)) 92 | * Imagine how tedious it would be with 3 or more nested flatMap calls... 93 | */ 94 | 95 | myBagsMerged.content should be(200) 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /src/test/scala/part2/e1_generic_bag.scala: -------------------------------------------------------------------------------- 1 | package we_need_to_go_deeper 2 | 3 | import support.HandsOnSuite 4 | 5 | /** 6 | * Int is a perfectly fine type, but why should we limit our Bags to it ? 7 | * Lets see if we can make a generic Bag ! 8 | * 9 | * You know the motto : just follow the types ! 10 | */ 11 | class e1_generic_bag extends HandsOnSuite { 12 | 13 | case class Bag[A](content:A) { 14 | 15 | def map[B](function: A => B):Bag[B] = ??? 16 | 17 | def flatMap[B](function: A => Bag[B]):Bag[B] = ??? 18 | 19 | } 20 | 21 | 22 | exercise("Just like before, we can 'lift' a function into our Bag") { 23 | val littleBag = Bag(0) 24 | 25 | littleBag.map(x => x + 1).content should be(1) 26 | 27 | } 28 | 29 | exercise("Lets 'combine' Bags of different types") { 30 | 31 | val littleIntBag = Bag(0) 32 | 33 | val littleStringBag = Bag("A") 34 | 35 | val combined = for (i <- littleIntBag; s <- littleStringBag) yield { i.toString + s} 36 | 37 | 38 | combined.content should be("0A") 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/scala/part2/e2_algebraic_bag.scala: -------------------------------------------------------------------------------- 1 | package we_need_to_go_deeper 2 | 3 | import support.HandsOnSuite 4 | 5 | 6 | /** 7 | * Now what if we want to allow our Bag to be empty ? 8 | */ 9 | class e2_algebraic_bag extends HandsOnSuite { 10 | 11 | /** 12 | * We define Bag as what is called an Algebraic Data Type (ADT for short). 13 | * 14 | * It consist of a trait (remember, a trait is like an Java interface with default methods) 15 | * and some implementations, usually case classes or case objects. 16 | * 17 | * The sealed keyword means that no class can extend this trait outside this file 18 | * (this allows the compiler to know all implementations of the trait, and check 19 | * that pattern matching expressions are exhaustive) 20 | * 21 | */ 22 | sealed trait Bag { 23 | 24 | def map(function:Int => Int):Bag 25 | 26 | def flatMap(function:Int => Bag):Bag 27 | 28 | def filter(function:Int => Boolean):Bag 29 | 30 | def contentOrElse(replacement:Int):Int 31 | 32 | } 33 | 34 | /** 35 | * We can create an object that has the same name as a trait (or class), this makes 36 | * it the "companion object" of the said trait (or class) 37 | */ 38 | object Bag { 39 | def apply(content:Int):Bag = FilledBag(content) 40 | } 41 | 42 | case object EmptyBag extends Bag { 43 | 44 | override def map(function:Int => Int):Bag = ??? 45 | 46 | override def flatMap(function:Int => Bag):Bag = ??? 47 | 48 | override def filter(function:Int => Boolean):Bag = ??? 49 | 50 | override def contentOrElse(replacement:Int):Int = replacement 51 | } 52 | 53 | case class FilledBag(content:Int) extends Bag { 54 | 55 | override def map(function:Int => Int):Bag = ??? 56 | 57 | override def flatMap(function:Int => Bag):Bag = ??? 58 | 59 | override def filter(function:Int => Boolean):Bag = ??? 60 | 61 | override def contentOrElse(replacement:Int):Int = content 62 | } 63 | 64 | 65 | exercise("Bag creation") { 66 | val s0 = Bag(0) // here we call the companion object's apply method, it acts like a "smart constuctor" 67 | 68 | } 69 | 70 | exercise("I can lift a function into a bag") { 71 | 72 | val bagOfZero = Bag(0) 73 | 74 | 75 | // Thanks to the sealed trait, the compiler will warn us if our match is not exhaustive 76 | bagOfZero.map(x => x +1) match { 77 | case FilledBag(content) => content should be(1) 78 | 79 | case EmptyBag => fail("That bag shouldn't be empty") 80 | } 81 | 82 | val bagVide=EmptyBag 83 | 84 | bagVide.map(x=>x+1) match { 85 | case FilledBag(_) => fail("This bag shouldn't be full") 86 | case _ => Unit 87 | } 88 | } 89 | 90 | exercise("I can combine bags") { 91 | val bagOfTwo = Bag(2) 92 | 93 | val bagOfAHundred = Bag(100) 94 | 95 | val combinaison = for (two <- bagOfTwo; aHundred <- bagOfAHundred) yield( two * aHundred ) 96 | 97 | combinaison match { 98 | case FilledBag(content) => { 99 | content should be (200) 100 | } 101 | 102 | case _ => fail("shouldn't be empty") 103 | } 104 | val emtpyCombination = for (two <- EmptyBag; aHundred <- bagOfAHundred) yield( two * aHundred ) 105 | emtpyCombination match { 106 | case FilledBag(_) => fail("Shouldn't be full") 107 | case _ => Unit 108 | } 109 | 110 | } 111 | 112 | exercise("I can filter the content of a Bag") { 113 | 114 | val bagOfTwo = Bag(2) 115 | 116 | val bag = bagOfTwo.filter(x => x > 10) 117 | 118 | bag match { 119 | case FilledBag(_) => fail("this should be empty") 120 | case _ => Unit 121 | } 122 | 123 | bagOfTwo.filter(x => x > 1) match { 124 | case EmptyBag => fail("This should be full") 125 | case _ => Unit 126 | } 127 | 128 | } 129 | 130 | } 131 | --------------------------------------------------------------------------------