{- | Description: Tutorial for solving problems Copyright: Copyright (C) 2023 Yoo Chung License: GPL-3.0-or-later Maintainer: dev@chungyc.org A tutorial for implementing and testing solutions for 99 Haskell "Problems". -} module Problems.Tutorial where import qualified Solutions.Tutorial as Solution -- * Tutorial {- $ The "Problems" module contains exactly 99 Haskell problems which you can try to solve. One way to use the module is simply to use its documentation as a standalone list of problems for which you can implement solutions from scratch. If this is how you intend to solve the problems, then you can stop reading the tutorial here. These modules were written in a way so that solutions can be tested and benchmarked. The rest of the tutorial will guide you as to how this can be done. -} -- ** Solving a problem {- $ Problems are given in the form of function documentation. They will explain what is expected to be implemented for the problem. They will also usually include examples for what the implemented functions should return. For example, we can have a problem such as the following: -} {- | Add the integers from 1 to a given number @n@. === Examples >>> sumNumbers 5 == 1 + 2 + 3 + 4 + 5 True >>> sumNumbers 100 5050 >>> sumNumbers 1000000 500000500000 -} sumNumbers :: Integer -> Integer sumNumbers :: Integer -> Integer sumNumbers = Integer -> Integer Solution.sumNumbers {- $ If you look at the source, you will see something like this: @ sumNumbers :: Integer -> Integer sumNumbers = Solution.sumNumbers @ This is the function you would be implementing for the problem. Initially, it will point to a function which already solves the problem. You will be replacing this with your own solution. Let's say that you decide to implement a recursive solution which adds numbers as they are counted down: @ sumNumbers :: Integer -> Integer sumNumbers 1 = 0 sumNumbers n = n + sumNumbers (n-1) @ You can then run tests to verify whether the solution is correct. This can be done by passing a @--match@ flag with the problem number to @stack test@ inside a @--test-arguments@ flag. For instance, with the problem in the @Problems.P01@ module, you could pass in @--match=P01@. The problem in this tutorial is in the @Problems.Tutorial@ module, so its tests can be executed as follows: @ $ stack test --test-arguments="--match=Tutorial" ... ninetynine> test (suite: examples-test, args: --match=Tutorial) Progress 1/2: ...: failure in expression `sumNumbers 5 == 1 + 2 + 3 + 4 + 5' expected: True but got: False ^ ... ninetynine> test (suite: ninetynine-test, args: --match=Tutorial) Problems.Tutorial sumNumbers is one when adding just one [✘] ... @ Uh-oh, tests fail. Turns out @sumNumbers 1@ should have been 1, not 0, so you can fix it to: @ sumNumbers :: Integer -> Integer sumNumbers 1 = 1 sumNumbers n = n + sumNumbers (n-1) @ Running the tests again, this time they pass: @ $ stack test --test-arguments="--match=Tutorial" ... Examples: 3 Tried: 3 Errors: 0 Unexpected output: 0 ninetynine> Test suite examples-test passed ... Problems.Tutorial sumNumbers is one when adding just one [✔] +++ OK, passed 1 test. adds a number to the sum [✔] +++ OK, passed 100 tests. ... 10 examples, 0 failures ninetynine> Test suite ninetynine-test passed Completed 2 action(s). @ You have now correctly implemented a solution to this problem. Now you can move on to another one, such as "Problems.P01". If you are only interested in implementing a single solution for each problem, you can stop reading the tutorial here. -} -- ** Trying multiple solutions {- $ There may be times when you would like to try more than one solution to a problem. For example, you may have remembered the apocryphal story of Gauss figuring out how to quickly calculate the sum of the numbers from 1 to 100, thwarting his math teacher who wanted to take a break while the students spent their time on a menial calculation. Let's say you would like to compare this approach to the naive approach of adding numbers one by one. So in addition to the @sumNumbers@ function above, you implement the @sumNumbers'@ function: @ sumNumbers' :: Integer -> Integer sumNumbers' n = n * (n+1) \`div\` 2 @ We can test this function as well without removing the @sumNumbers@ function. We can also run benchmarks to compare how the two functions perform. -} -- *** Tests {- $ The tests for this tutorial problem is in the @Problems.TutorialSpec@ module. If you inspect the source, you will see this function: @ spec :: Spec spec = parallel $ do properties Problem.sumNumbers \"sumNumbers\" examples ... @ You can update this function to also test @sumNumbers'@ by adding another line: @ spec :: Spec spec = parallel $ do properties Problem.sumNumbers \"sumNumbers\" __properties Problem.sumNumbers' \"sumNumbers'\"__ examples ... @ -} -- *** Benchmarks {- $ Benchmarks are provided for most problem modules. For this tutorial problem, you can add a benchmark for @sumNumbers'@ by adding another line to the @Problems.TutorialBench@ module: @ group = bgroup \"Tutorial\" [ subgroup \"sumNumbers\" Problem.sumNumbers __, subgroup \"sumNumbers'\" Problem.sumNumbers'__ , bgroup \"Solutions\" @ You can run the benchmarks for a problem by including the problem number in the benchmark arguments. For example, you would run the benchmarks for the tutorial problem by including "@Tutorial@" in the benchmark arguments, after which you may see output such as this: @ $ stack bench --benchmark-arguments=\"Tutorial\" ... benchmarking Tutorial\/sumNumbers\/1000000 time 222.9 ms (206.2 ms .. 237.9 ms) ... benchmarking Tutorial\/sumNumbers'\/1000000 time 56.75 ns (56.43 ns .. 57.06 ns) ... @ From this, you can see that @sumNumbers'@ is much faster than @sumNumbers@ when adding the numbers from 1 to 1,000,000. For another example of running benchmarks, execute the following command to run the benchmarks for "Problems.P11": @ $ stack bench --benchmark-arguments=\"P11\" @ -} -- ** In conclusion {- $ This is not an exam; you do not have to use this set of problems in any particular way. Feel free to solve problems in any order, or skip those you find uninteresting. You can implement solutions from scratch using nothing but the most basic functions that Haskell provides, or almost trivially by using a sophisticated library to do most of the work. If you do not want to solve any problems but would simply like to see how solutions to a problem can be implemented, you can do that, too. You may not even be interested in solving any problems, but instead use the solutions already available to practice writing tests in your favorite testing framework. Whatever you do, I hope this will have proven to be of some benefit, whether by providing enjoyment, educational value, or anything else. -}