commit d92e0cb72ad5aaf32a66dec0427ae5cf3365e7dc Author: Guillaume Martres Date: Tue Feb 19 20:44:23 2019 +0100 Add reductions assignment diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..349d2e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# Dotty IDE +/.dotty-ide-artifact +/.dotty-ide.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..0d187c4 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,36 @@ +# DO NOT EDIT THIS FILE + +stages: + - build + - grade + +compile: + stage: build + image: lampepfl/moocs:dotty-2020-02-12 + except: + - tags + tags: + - cs206 + script: + - sbt packageSubmission + artifacts: + expire_in: 1 day + paths: + - submission.jar + +grade: + stage: grade + except: + - tags + tags: + - cs206 + image: + name: smarter3/moocs:parprog1-reductions-2020-02-24-2 + entrypoint: [""] + allow_failure: true + before_script: + - mkdir -p /shared/submission/ + - cp submission.jar /shared/submission/submission.jar + script: + - cd /grader + - /grader/grade | /grader/feedback-printer diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a35362b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "dotty": { + "trace": { + "remoteTracingUrl": "wss://lamppc36.epfl.ch/dotty-remote-tracer/upload/lsp.log", + "server": { "format": "JSON", "verbosity": "verbose" } + } + } +} diff --git a/assignment.sbt b/assignment.sbt new file mode 100644 index 0000000..42cdd4c --- /dev/null +++ b/assignment.sbt @@ -0,0 +1,9 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) + +courseraId := ch.epfl.lamp.CourseraId( + key = "lUUWddoGEeWPHw6r45-nxw", + itemId = "U1eU3", + premiumItemId = Some("4rXwX"), + partId = "gmSnR" +) diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..de34f52 --- /dev/null +++ b/build.sbt @@ -0,0 +1,13 @@ +course := "parprog1" +assignment := "reductions" + +scalaVersion := "0.23.0-bin-20200211-5b006fb-NIGHTLY" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") +libraryDependencies ++= Seq( + "com.storm-enroute" %% "scalameter-core" % "0.19", + "org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0", + "com.novocode" % "junit-interface" % "0.11" % Test +).map(_.withDottyCompat(scalaVersion.value)) + +testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s") +testSuite := "reductions.ReductionsSuite" diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000..81ac694 Binary files /dev/null and b/grading-tests.jar differ diff --git a/project/MOOCSettings.scala b/project/MOOCSettings.scala new file mode 100644 index 0000000..171244f --- /dev/null +++ b/project/MOOCSettings.scala @@ -0,0 +1,46 @@ +package ch.epfl.lamp + +import sbt._ +import sbt.Keys._ + +/** + * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have + * different item ids. + * + * @param key Assignment key + * @param partId Assignment partId + * @param itemId Item id of the non premium version + * @param premiumItemId Item id of the premium version (`None` if the assignment is optional) + */ +case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String]) + +/** + * Settings shared by all assignments, reused in various tasks. + */ +object MOOCSettings extends AutoPlugin { + + object autoImport { + val course = SettingKey[String]("course") + val assignment = SettingKey[String]("assignment") + val options = SettingKey[Map[String, Map[String, String]]]("options") + val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment") + val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment") + // Convenient alias + type CourseraId = ch.epfl.lamp.CourseraId + val CourseraId = ch.epfl.lamp.CourseraId + } + + import autoImport._ + + override val globalSettings: Seq[Def.Setting[_]] = Seq( + // supershell is verbose, buggy and useless. + useSuperShell := false + ) + + override val projectSettings: Seq[Def.Setting[_]] = Seq( + parallelExecution in Test := false, + // Report test result after each test instead of waiting for every test to finish + logBuffered in Test := false, + name := s"${course.value}-${assignment.value}" + ) +} diff --git a/project/StudentTasks.scala b/project/StudentTasks.scala new file mode 100644 index 0000000..7604830 --- /dev/null +++ b/project/StudentTasks.scala @@ -0,0 +1,318 @@ +package ch.epfl.lamp + +import sbt._ +import Keys._ + +// import scalaj.http._ +import java.io.{File, FileInputStream, IOException} +import org.apache.commons.codec.binary.Base64 +// import play.api.libs.json.{Json, JsObject, JsPath} +import scala.util.{Failure, Success, Try} + +/** + * Provides tasks for submitting the assignment + */ +object StudentTasks extends AutoPlugin { + + override def requires = super.requires && MOOCSettings + + object autoImport { + val packageSourcesOnly = TaskKey[File]("packageSourcesOnly", "Package the sources of the project") + val packageBinWithoutResources = TaskKey[File]("packageBinWithoutResources", "Like packageBin, but without the resources") + val packageSubmissionZip = TaskKey[File]("packageSubmissionZip") + val packageSubmission = inputKey[Unit]("package solution as an archive file") + val runGradingTests = taskKey[Unit]("run black-box tests used for final grading") + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + // submitSetting, + runGradingTestsSettings, + + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ packageSubmissionZipSettings + + lazy val runGradingTestsSettings = runGradingTests := { + val testSuiteJar = "grading-tests.jar" + if (!new File(testSuiteJar).exists) { + throw new MessageOnlyException(s"Could not find tests JarFile: $testSuiteJar") + } + + val classPath = s"${(Test / dependencyClasspath).value.map(_.data).mkString(File.pathSeparator)}${File.pathSeparator}$testSuiteJar" + val junitProcess = + Fork.java.fork( + ForkOptions(), + "-cp" :: classPath :: + "org.junit.runner.JUnitCore" :: + (Test / testSuite).value :: + Nil + ) + + // Wait for tests to complete. + junitProcess.exitValue() + } + + + /** ********************************************************** + * SUBMITTING A SOLUTION TO COURSERA + */ + + val packageSubmissionZipSettings = Seq( + packageSubmissionZip := { + val submission = crossTarget.value / "submission.zip" + val sources = (packageSourcesOnly in Compile).value + val binaries = (packageBinWithoutResources in Compile).value + IO.zip(Seq(sources -> "sources.zip", binaries -> "binaries.jar"), submission) + submission + }, + artifactClassifier in packageSourcesOnly := Some("sources"), + artifact in (Compile, packageBinWithoutResources) ~= (art => art.withName(art.name + "-without-resources")) + ) ++ + inConfig(Compile)( + Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++ + Defaults.packageTaskSettings(packageBinWithoutResources, Def.task { + val relativePaths = + (unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_)) + (mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) } + }) + ) + + val maxSubmitFileSize = { + val mb = 1024 * 1024 + 10 * mb + } + + /** Check that the jar exists, isn't empty, isn't crazy big, and can be read + * If so, encode jar as base64 so we can send it to Coursera + */ + def prepareJar(jar: File, s: TaskStreams): String = { + val errPrefix = "Error submitting assignment jar: " + val fileLength = jar.length() + if (!jar.exists()) { + s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength == 0L) { + s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength > maxSubmitFileSize) { + s.log.error(errPrefix + "jar archive is too big. Allowed size: " + + maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" + + jar.getAbsolutePath) + failSubmit() + } else { + val bytes = new Array[Byte](fileLength.toInt) + val sizeRead = try { + val is = new FileInputStream(jar) + val read = is.read(bytes) + is.close() + read + } catch { + case ex: IOException => + s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString) + failSubmit() + } + if (sizeRead != bytes.length) { + s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead) + failSubmit() + } else encodeBase64(bytes) + } + } + + /** Task to package solution to a given file path */ + lazy val packageSubmissionSetting = packageSubmission := { + val args: Seq[String] = Def.spaceDelimited("[path]").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val base64Jar = prepareJar(jar, s) + + val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath) + scala.tools.nsc.io.File(path).writeAll(base64Jar) + } + +/* + /** Task to submit a solution to coursera */ + val submit = inputKey[Unit]("submit solution to Coursera") + lazy val submitSetting = submit := { + // Fail if scalafix linting does not pass. + scalafixLinting.value + + val args: Seq[String] = Def.spaceDelimited("").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val assignmentDetails = + courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined")) + val assignmentKey = assignmentDetails.key + val courseName = + course.value match { + case "capstone" => "scala-capstone" + case "bigdata" => "scala-spark-big-data" + case other => other + } + + val partId = assignmentDetails.partId + val itemId = assignmentDetails.itemId + val premiumItemId = assignmentDetails.premiumItemId + + val (email, secret) = args match { + case email :: secret :: Nil => + (email, secret) + case _ => + val inputErr = + s"""|Invalid input to `submit`. The required syntax for `submit` is: + |submit + | + |The submit token is NOT YOUR LOGIN PASSWORD. + |It can be obtained from the assignment page: + |https://www.coursera.org/learn/$courseName/programming/$itemId + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + """.stripMargin + s.log.error(inputErr) + failSubmit() + } + + val base64Jar = prepareJar(jar, s) + val json = + s"""|{ + | "assignmentKey":"$assignmentKey", + | "submitterEmail":"$email", + | "secret":"$secret", + | "parts":{ + | "$partId":{ + | "output":"$base64Jar" + | } + | } + |}""".stripMargin + + def postSubmission[T](data: String): Try[HttpResponse[String]] = { + val http = Http("https://www.coursera.org/api/onDemandProgrammingScriptSubmissions.v1") + val hs = List( + ("Cache-Control", "no-cache"), + ("Content-Type", "application/json") + ) + s.log.info("Connecting to Coursera...") + val response = Try(http.postData(data) + .headers(hs) + .option(HttpOptions.connTimeout(10000)) // scalaj default timeout is only 100ms, changing that to 10s + .asString) // kick off HTTP POST + response + } + + val connectMsg = + s"""|Attempting to submit "${assignment.value}" assignment in "$courseName" course + |Using: + |- email: $email + |- submit token: $secret""".stripMargin + s.log.info(connectMsg) + + def reportCourseraResponse(response: HttpResponse[String]): Unit = { + val code = response.code + val respBody = response.body + + /* Sample JSON response from Coursera + { + "message": "Invalid email or token.", + "details": { + "learnerMessage": "Invalid email or token." + } + } + */ + + // Success, Coursera responds with 2xx HTTP status code + if (response.is2xx) { + val successfulSubmitMsg = + s"""|Successfully connected to Coursera. (Status $code) + | + |Assignment submitted successfully! + | + |You can see how you scored by going to: + |https://www.coursera.org/learn/$courseName/programming/$itemId/ + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + |and clicking on "My Submission".""".stripMargin + s.log.info(successfulSubmitMsg) + } + + // Failure, Coursera responds with 4xx HTTP status code (client-side failure) + else if (response.is4xx) { + val result = Try(Json.parse(respBody)).toOption + val learnerMsg = result match { + case Some(resp: JsObject) => + (JsPath \ "details" \ "learnerMessage").read[String].reads(resp).get + case Some(x) => // shouldn't happen + "Could not parse Coursera's response:\n" + x + case None => + "Could not parse Coursera's response:\n" + respBody + } + val failedSubmitMsg = + s"""|Submission failed. + |There was something wrong while attempting to submit. + |Coursera says: + |$learnerMsg (Status $code)""".stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera responds with 5xx HTTP status code (server-side failure) + else if (response.is5xx) { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera seems to be unavailable at the moment (Status $code) + |Check https://status.coursera.org/ and try again in a few minutes. + """.stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera repsonds with an unexpected status code + else { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera replied with an unexpected code (Status $code) + """.stripMargin + s.log.error(failedSubmitMsg) + } + } + + // kick it all off, actually make request + postSubmission(json) match { + case Success(resp) => reportCourseraResponse(resp) + case Failure(e) => + val failedConnectMsg = + s"""|Connection to Coursera failed. + |There was something wrong while attempting to connect to Coursera. + |Check your internet connection. + |${e.toString}""".stripMargin + s.log.error(failedConnectMsg) + } + + } +*/ + + def failSubmit(): Nothing = { + sys.error("Submission failed") + } + + /** + * ***************** + * DEALING WITH JARS + */ + def encodeBase64(bytes: Array[Byte]): String = + new String(Base64.encodeBase64(bytes)) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..a919a9b --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.8 diff --git a/project/buildSettings.sbt b/project/buildSettings.sbt new file mode 100644 index 0000000..8fac702 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,5 @@ +// Used for Coursera submission (StudentPlugin) +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4" +// Used for Base64 (StudentPlugin) +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..017735d --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.0") diff --git a/src/main/scala/reductions/Interfaces.scala b/src/main/scala/reductions/Interfaces.scala new file mode 100644 index 0000000..4f64d60 --- /dev/null +++ b/src/main/scala/reductions/Interfaces.scala @@ -0,0 +1,26 @@ +package reductions + +// Interfaces used by the grading infrastructure. Do not change signatures +// or your submission will fail with a NoSuchMethodError. + +trait LineOfSightInterface { + def lineOfSight(input: Array[Float], output: Array[Float]): Unit + def upsweepSequential(input: Array[Float], from: Int, until: Int): Float + def upsweep(input: Array[Float], from: Int, end: Int, threshold: Int): Tree + def downsweepSequential(input: Array[Float], output: Array[Float], startingAngle: Float, from: Int, until: Int): Unit + def downsweep(input: Array[Float], output: Array[Float], startingAngle: Float, tree: Tree): Unit + def parLineOfSight(input: Array[Float], output: Array[Float], threshold: Int): Unit +} + +trait ParallelCountChangeInterface { + def countChange(money: Int, coins: List[Int]): Int + def parCountChange(money: Int, coins: List[Int], threshold: (Int, List[Int]) => Boolean): Int + def moneyThreshold(startingMoney: Int): (Int, List[Int]) => Boolean + def totalCoinsThreshold(totalCoins: Int): (Int, List[Int]) => Boolean + def combinedThreshold(startingMoney: Int, allCoins: List[Int]): (Int, List[Int]) => Boolean +} + +trait ParallelParenthesesBalancingInterface { + def balance(chars: Array[Char]): Boolean + def parBalance(chars: Array[Char], threshold: Int): Boolean +} diff --git a/src/main/scala/reductions/LineOfSight.scala b/src/main/scala/reductions/LineOfSight.scala new file mode 100644 index 0000000..5d7ebfb --- /dev/null +++ b/src/main/scala/reductions/LineOfSight.scala @@ -0,0 +1,89 @@ +package reductions + +import org.scalameter._ + +object LineOfSightRunner { + + val standardConfig = config( + Key.exec.minWarmupRuns -> 40, + Key.exec.maxWarmupRuns -> 80, + Key.exec.benchRuns -> 100, + Key.verbose -> false + ) withWarmer(new Warmer.Default) + + def main(args: Array[String]): Unit = { + val length = 10000000 + val input = (0 until length).map(_ % 100 * 1.0f).toArray + val output = new Array[Float](length + 1) + val seqtime = standardConfig measure { + LineOfSight.lineOfSight(input, output) + } + println(s"sequential time: $seqtime") + + val partime = standardConfig measure { + LineOfSight.parLineOfSight(input, output, 10000) + } + println(s"parallel time: $partime") + println(s"speedup: ${seqtime.value / partime.value}") + } +} + +sealed abstract class Tree { + def maxPrevious: Float +} + +case class Node(left: Tree, right: Tree) extends Tree { + val maxPrevious = left.maxPrevious.max(right.maxPrevious) +} + +case class Leaf(from: Int, until: Int, maxPrevious: Float) extends Tree + +object LineOfSight extends LineOfSightInterface { + + def lineOfSight(input: Array[Float], output: Array[Float]): Unit = { + ??? + } + + /** Traverses the specified part of the array and returns the maximum angle. + */ + def upsweepSequential(input: Array[Float], from: Int, until: Int): Float = { + ??? + } + + /** Traverses the part of the array starting at `from` and until `end`, and + * returns the reduction tree for that part of the array. + * + * The reduction tree is a `Leaf` if the length of the specified part of the + * array is smaller or equal to `threshold`, and a `Node` otherwise. + * If the specified part of the array is longer than `threshold`, then the + * work is divided and done recursively in parallel. + */ + def upsweep(input: Array[Float], from: Int, end: Int, + threshold: Int): Tree = { + ??? + } + + /** Traverses the part of the `input` array starting at `from` and until + * `until`, and computes the maximum angle for each entry of the output array, + * given the `startingAngle`. + */ + def downsweepSequential(input: Array[Float], output: Array[Float], + startingAngle: Float, from: Int, until: Int): Unit = { + ??? + } + + /** Pushes the maximum angle in the prefix of the array to each leaf of the + * reduction `tree` in parallel, and then calls `downsweepSequential` to write + * the `output` angles. + */ + def downsweep(input: Array[Float], output: Array[Float], startingAngle: Float, + tree: Tree): Unit = { + ??? + } + + /** Compute the line-of-sight in parallel. */ + def parLineOfSight(input: Array[Float], output: Array[Float], + threshold: Int): Unit = { + ??? + } +} diff --git a/src/main/scala/reductions/ParallelCountChange.scala b/src/main/scala/reductions/ParallelCountChange.scala new file mode 100644 index 0000000..b5bd150 --- /dev/null +++ b/src/main/scala/reductions/ParallelCountChange.scala @@ -0,0 +1,79 @@ +package reductions + +import org.scalameter._ + +object ParallelCountChangeRunner { + + @volatile var seqResult = 0 + + @volatile var parResult = 0 + + val standardConfig = config( + Key.exec.minWarmupRuns -> 20, + Key.exec.maxWarmupRuns -> 40, + Key.exec.benchRuns -> 80, + Key.verbose -> false + ) withWarmer(new Warmer.Default) + + def main(args: Array[String]): Unit = { + val amount = 250 + val coins = List(1, 2, 5, 10, 20, 50) + val seqtime = standardConfig measure { + seqResult = ParallelCountChange.countChange(amount, coins) + } + println(s"sequential result = $seqResult") + println(s"sequential count time: $seqtime") + + def measureParallelCountChange(threshold: => ParallelCountChange.Threshold): Unit = try { + val fjtime = standardConfig measure { + parResult = ParallelCountChange.parCountChange(amount, coins, threshold) + } + println(s"parallel result = $parResult") + println(s"parallel count time: $fjtime") + println(s"speedup: ${seqtime.value / fjtime.value}") + } catch { + case e: NotImplementedError => + println("Not implemented.") + } + + println("\n# Using moneyThreshold\n") + measureParallelCountChange(ParallelCountChange.moneyThreshold(amount)) + println("\n# Using totalCoinsThreshold\n") + measureParallelCountChange(ParallelCountChange.totalCoinsThreshold(coins.length)) + println("\n# Using combinedThreshold\n") + measureParallelCountChange(ParallelCountChange.combinedThreshold(amount, coins)) + } +} + +object ParallelCountChange extends ParallelCountChangeInterface { + + /** Returns the number of ways change can be made from the specified list of + * coins for the specified amount of money. + */ + def countChange(money: Int, coins: List[Int]): Int = { + ??? + } + + type Threshold = (Int, List[Int]) => Boolean + + /** In parallel, counts the number of ways change can be made from the + * specified list of coins for the specified amount of money. + */ + def parCountChange(money: Int, coins: List[Int], threshold: Threshold): Int = { + ??? + } + + /** Threshold heuristic based on the starting money. */ + def moneyThreshold(startingMoney: Int): Threshold = + ??? + + /** Threshold heuristic based on the total number of initial coins. */ + def totalCoinsThreshold(totalCoins: Int): Threshold = + ??? + + + /** Threshold heuristic based on the starting money and the initial list of coins. */ + def combinedThreshold(startingMoney: Int, allCoins: List[Int]): Threshold = { + ??? + } +} diff --git a/src/main/scala/reductions/ParallelParenthesesBalancing.scala b/src/main/scala/reductions/ParallelParenthesesBalancing.scala new file mode 100644 index 0000000..fcf1c36 --- /dev/null +++ b/src/main/scala/reductions/ParallelParenthesesBalancing.scala @@ -0,0 +1,64 @@ +package reductions + +import scala.annotation._ +import org.scalameter._ + +object ParallelParenthesesBalancingRunner { + + @volatile var seqResult = false + + @volatile var parResult = false + + val standardConfig = config( + Key.exec.minWarmupRuns -> 40, + Key.exec.maxWarmupRuns -> 80, + Key.exec.benchRuns -> 120, + Key.verbose -> false + ) withWarmer(new Warmer.Default) + + def main(args: Array[String]): Unit = { + val length = 100000000 + val chars = new Array[Char](length) + val threshold = 10000 + val seqtime = standardConfig measure { + seqResult = ParallelParenthesesBalancing.balance(chars) + } + println(s"sequential result = $seqResult") + println(s"sequential balancing time: $seqtime") + + val fjtime = standardConfig measure { + parResult = ParallelParenthesesBalancing.parBalance(chars, threshold) + } + println(s"parallel result = $parResult") + println(s"parallel balancing time: $fjtime") + println(s"speedup: ${seqtime.value / fjtime.value}") + } +} + +object ParallelParenthesesBalancing extends ParallelParenthesesBalancingInterface { + + /** Returns `true` iff the parentheses in the input `chars` are balanced. + */ + def balance(chars: Array[Char]): Boolean = { + ??? + } + + /** Returns `true` iff the parentheses in the input `chars` are balanced. + */ + def parBalance(chars: Array[Char], threshold: Int): Boolean = { + + def traverse(idx: Int, until: Int, arg1: Int, arg2: Int) /*: ???*/ = { + ??? + } + + def reduce(from: Int, until: Int) /*: ???*/ = { + ??? + } + + reduce(0, chars.length) == ??? + } + + // For those who want more: + // Prove that your reduction operator is associative! + +} diff --git a/src/main/scala/reductions/package.scala b/src/main/scala/reductions/package.scala new file mode 100644 index 0000000..59e0438 --- /dev/null +++ b/src/main/scala/reductions/package.scala @@ -0,0 +1,58 @@ +import java.util.concurrent._ +import scala.util.DynamicVariable + +import org.scalameter._ + +package object reductions { + val forkJoinPool = new ForkJoinPool + + abstract class TaskScheduler { + def schedule[T](body: => T): ForkJoinTask[T] + def parallel[A, B](taskA: => A, taskB: => B): (A, B) = { + val right = task { + taskB + } + val left = taskA + (left, right.join()) + } + } + + class DefaultTaskScheduler extends TaskScheduler { + def schedule[T](body: => T): ForkJoinTask[T] = { + val t = new RecursiveTask[T] { + def compute = body + } + Thread.currentThread match { + case wt: ForkJoinWorkerThread => + t.fork() + case _ => + forkJoinPool.execute(t) + } + t + } + } + + val scheduler = + new DynamicVariable[TaskScheduler](new DefaultTaskScheduler) + + def task[T](body: => T): ForkJoinTask[T] = { + scheduler.value.schedule(body) + } + + def parallel[A, B](taskA: => A, taskB: => B): (A, B) = { + scheduler.value.parallel(taskA, taskB) + } + + def parallel[A, B, C, D](taskA: => A, taskB: => B, taskC: => C, taskD: => D): (A, B, C, D) = { + val ta = task { taskA } + val tb = task { taskB } + val tc = task { taskC } + val td = taskD + (ta.join(), tb.join(), tc.join(), td) + } + + // Workaround Dotty's handling of the existential type KeyValue + implicit def keyValueCoerce[T](kv: (Key[T], T)): KeyValue = { + kv.asInstanceOf[KeyValue] + } +} diff --git a/src/test/scala/reductions/ReductionsSuite.scala b/src/test/scala/reductions/ReductionsSuite.scala new file mode 100644 index 0000000..11c8250 --- /dev/null +++ b/src/test/scala/reductions/ReductionsSuite.scala @@ -0,0 +1,124 @@ +package reductions + +import java.util.concurrent._ +import scala.collection._ +import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory +import org.junit._ +import org.junit.Assert.assertEquals + +class ReductionsSuite { + /***************** + * LINE OF SIGHT * + *****************/ + + import LineOfSight._ + @Test def `lineOfSight should correctly handle an array of size 4`: Unit = { + val output = new Array[Float](4) + lineOfSight(Array[Float](0f, 1f, 8f, 9f), output) + assertEquals(List(0f, 1f, 4f, 4f), output.toList) + } + + + + + /******************************* + * PARALLEL COUNT CHANGE SUITE * + *******************************/ + + import ParallelCountChange._ + + @Test def `countChange should return 0 for money < 0`: Unit = { + def check(money: Int, coins: List[Int]) = + assert(countChange(money, coins) == 0, + s"countChang($money, _) should be 0") + + check(-1, List()) + check(-1, List(1, 2, 3)) + check(-Int.MinValue, List()) + check(-Int.MinValue, List(1, 2, 3)) + } + + @Test def `countChange should return 1 when money == 0`: Unit = { + def check(coins: List[Int]) = + assert(countChange(0, coins) == 1, + s"countChang(0, _) should be 1") + + check(List()) + check(List(1, 2, 3)) + check(List.range(1, 100)) + } + + @Test def `countChange should return 0 for money > 0 and coins = List()`: Unit = { + def check(money: Int) = + assert(countChange(money, List()) == 0, + s"countChang($money, List()) should be 0") + + check(1) + check(Int.MaxValue) + } + + @Test def `countChange should work when there is only one coin`: Unit = { + def check(money: Int, coins: List[Int], expected: Int) = + assert(countChange(money, coins) == expected, + s"countChange($money, $coins) should be $expected") + + check(1, List(1), 1) + check(2, List(1), 1) + check(1, List(2), 0) + check(Int.MaxValue, List(Int.MaxValue), 1) + check(Int.MaxValue - 1, List(Int.MaxValue), 0) + } + + @Test def `countChange should work for multi-coins`: Unit = { + def check(money: Int, coins: List[Int], expected: Int) = + assert(countChange(money, coins) == expected, + s"countChange($money, $coins) should be $expected") + + check(50, List(1, 2, 5, 10), 341) + check(250, List(1, 2, 5, 10, 20, 50), 177863) + } + + + /********************************** + * PARALLEL PARENTHESES BALANCING * + **********************************/ + + import ParallelParenthesesBalancing._ + + @Test def `balance should work for empty string`: Unit = { + def check(input: String, expected: Boolean) = + assert(balance(input.toArray) == expected, + s"balance($input) should be $expected") + + check("", true) + } + + @Test def `balance should work for string of length 1`: Unit = { + def check(input: String, expected: Boolean) = + assert(balance(input.toArray) == expected, + s"balance($input) should be $expected") + + check("(", false) + check(")", false) + check(".", true) + } + + @Test def `balance should work for string of length 2`: Unit = { + def check(input: String, expected: Boolean) = + assert(balance(input.toArray) == expected, + s"balance($input) should be $expected") + + check("()", true) + check(")(", false) + check("((", false) + check("))", false) + check(".)", false) + check(".(", false) + check("(.", false) + check(").", false) + } + + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(10 * 1000) +} +