diff --git a/TODO.md b/TODO.md index 781494f..b012931 100644 --- a/TODO.md +++ b/TODO.md @@ -1,18 +1,12 @@ # Structure -* Should be numbered, but also have a short title - * Subfolders have numbers and names -* Every koan has an executable, and a test -* How to do koans for different backends? Separate packages? Or rather separate sublibraries, like basic, advanced etc.? * The test should maybe show a green check mark * Each Koan should have a Haddocked substructure +* If bug in HLS can't be solved, provide my own hie.yaml ## CI -* Separate branch where all koans are solved - * CI check to make sure the diff is restricted `Koan.hs` files in the `koans` subdirectory -* Formatter -* cabal outdated wöchentlich checken +* Check cabal outdated # Content ## Basic @@ -20,11 +14,13 @@ * stdin clock * char count per line * line count & total char count + * but summing char counts doesn't count newlines, so need all 3 * exception for eof & final summary * cat a file in the tool * secondly progress report * using the current time * behaviours: reusability across clocks + * E.g. revisit some of the earlier koans and refactor them * infer clock interval from component, or vice versa? if not possible, add type signature? * Arrow notation @@ -36,6 +32,9 @@ * feedback, building up state, delay, sum * exception handling for control flow +* common mistakes? + * arrM vs arrMCl + ## Theory track * LiftClock diff --git a/koans/basic/1-hello-rhine/Koan.hs b/koans/basic/1/1-hello-rhine/Koan.hs similarity index 100% rename from koans/basic/1-hello-rhine/Koan.hs rename to koans/basic/1/1-hello-rhine/Koan.hs diff --git a/koans/basic/1-hello-rhine/solution/Koan.hs b/koans/basic/1/1-hello-rhine/solution/Koan.hs similarity index 100% rename from koans/basic/1-hello-rhine/solution/Koan.hs rename to koans/basic/1/1-hello-rhine/solution/Koan.hs diff --git a/koans/basic/1-hello-rhine/test/Test.hs b/koans/basic/1/1-hello-rhine/test/Test.hs similarity index 100% rename from koans/basic/1-hello-rhine/test/Test.hs rename to koans/basic/1/1-hello-rhine/test/Test.hs diff --git a/koans/basic/2-fix-the-bug/Koan.hs b/koans/basic/1/2-fix-the-bug/Koan.hs similarity index 100% rename from koans/basic/2-fix-the-bug/Koan.hs rename to koans/basic/1/2-fix-the-bug/Koan.hs diff --git a/koans/basic/2-fix-the-bug/solution/Koan.hs b/koans/basic/1/2-fix-the-bug/solution/Koan.hs similarity index 100% rename from koans/basic/2-fix-the-bug/solution/Koan.hs rename to koans/basic/1/2-fix-the-bug/solution/Koan.hs diff --git a/koans/basic/2-fix-the-bug/test/Test.hs b/koans/basic/1/2-fix-the-bug/test/Test.hs similarity index 100% rename from koans/basic/2-fix-the-bug/test/Test.hs rename to koans/basic/1/2-fix-the-bug/test/Test.hs diff --git a/koans/basic/3-faster/Koan.hs b/koans/basic/1/3-faster/Koan.hs similarity index 100% rename from koans/basic/3-faster/Koan.hs rename to koans/basic/1/3-faster/Koan.hs diff --git a/koans/basic/3-faster/solution/Koan.hs b/koans/basic/1/3-faster/solution/Koan.hs similarity index 100% rename from koans/basic/3-faster/solution/Koan.hs rename to koans/basic/1/3-faster/solution/Koan.hs diff --git a/koans/basic/3-faster/test/Test.hs b/koans/basic/1/3-faster/test/Test.hs similarity index 100% rename from koans/basic/3-faster/test/Test.hs rename to koans/basic/1/3-faster/test/Test.hs diff --git a/koans/basic/4-compose/Koan.hs b/koans/basic/1/4-compose/Koan.hs similarity index 100% rename from koans/basic/4-compose/Koan.hs rename to koans/basic/1/4-compose/Koan.hs diff --git a/koans/basic/4-compose/solution/Koan.hs b/koans/basic/1/4-compose/solution/Koan.hs similarity index 100% rename from koans/basic/4-compose/solution/Koan.hs rename to koans/basic/1/4-compose/solution/Koan.hs diff --git a/koans/basic/4-compose/test/Test.hs b/koans/basic/1/4-compose/test/Test.hs similarity index 100% rename from koans/basic/4-compose/test/Test.hs rename to koans/basic/1/4-compose/test/Test.hs diff --git a/koans/basic/5-compose-more/Koan.hs b/koans/basic/1/5-compose-more/Koan.hs similarity index 100% rename from koans/basic/5-compose-more/Koan.hs rename to koans/basic/1/5-compose-more/Koan.hs diff --git a/koans/basic/5-compose-more/solution/Koan.hs b/koans/basic/1/5-compose-more/solution/Koan.hs similarity index 100% rename from koans/basic/5-compose-more/solution/Koan.hs rename to koans/basic/1/5-compose-more/solution/Koan.hs diff --git a/koans/basic/5-compose-more/test/Test.hs b/koans/basic/1/5-compose-more/test/Test.hs similarity index 100% rename from koans/basic/5-compose-more/test/Test.hs rename to koans/basic/1/5-compose-more/test/Test.hs diff --git a/koans/basic/6-compose-signal-functions-and-clocks/Koan.hs b/koans/basic/1/6-compose-signal-functions-and-clocks/Koan.hs similarity index 100% rename from koans/basic/6-compose-signal-functions-and-clocks/Koan.hs rename to koans/basic/1/6-compose-signal-functions-and-clocks/Koan.hs diff --git a/koans/basic/6-compose-signal-functions-and-clocks/solution/Koan.hs b/koans/basic/1/6-compose-signal-functions-and-clocks/solution/Koan.hs similarity index 100% rename from koans/basic/6-compose-signal-functions-and-clocks/solution/Koan.hs rename to koans/basic/1/6-compose-signal-functions-and-clocks/solution/Koan.hs diff --git a/koans/basic/6-compose-signal-functions-and-clocks/test/Test.hs b/koans/basic/1/6-compose-signal-functions-and-clocks/test/Test.hs similarity index 100% rename from koans/basic/6-compose-signal-functions-and-clocks/test/Test.hs rename to koans/basic/1/6-compose-signal-functions-and-clocks/test/Test.hs diff --git a/koans/basic/7-compose-on-the-same-clock/Koan.hs b/koans/basic/1/7-compose-on-the-same-clock/Koan.hs similarity index 100% rename from koans/basic/7-compose-on-the-same-clock/Koan.hs rename to koans/basic/1/7-compose-on-the-same-clock/Koan.hs diff --git a/koans/basic/7-compose-on-the-same-clock/solution/Koan.hs b/koans/basic/1/7-compose-on-the-same-clock/solution/Koan.hs similarity index 100% rename from koans/basic/7-compose-on-the-same-clock/solution/Koan.hs rename to koans/basic/1/7-compose-on-the-same-clock/solution/Koan.hs diff --git a/koans/basic/7-compose-on-the-same-clock/test/Test.hs b/koans/basic/1/7-compose-on-the-same-clock/test/Test.hs similarity index 100% rename from koans/basic/7-compose-on-the-same-clock/test/Test.hs rename to koans/basic/1/7-compose-on-the-same-clock/test/Test.hs diff --git a/koans/basic/8-compose-on-different-clocks/Koan.hs b/koans/basic/1/8-compose-on-different-clocks/Koan.hs similarity index 100% rename from koans/basic/8-compose-on-different-clocks/Koan.hs rename to koans/basic/1/8-compose-on-different-clocks/Koan.hs diff --git a/koans/basic/8-compose-on-different-clocks/solution/Koan.hs b/koans/basic/1/8-compose-on-different-clocks/solution/Koan.hs similarity index 100% rename from koans/basic/8-compose-on-different-clocks/solution/Koan.hs rename to koans/basic/1/8-compose-on-different-clocks/solution/Koan.hs diff --git a/koans/basic/8-compose-on-different-clocks/test/Test.hs b/koans/basic/1/8-compose-on-different-clocks/test/Test.hs similarity index 100% rename from koans/basic/8-compose-on-different-clocks/test/Test.hs rename to koans/basic/1/8-compose-on-different-clocks/test/Test.hs diff --git a/koans/basic/9-input/Koan.hs b/koans/basic/2/1-input/Koan.hs similarity index 100% rename from koans/basic/9-input/Koan.hs rename to koans/basic/2/1-input/Koan.hs diff --git a/koans/basic/9-input/solution/Koan.hs b/koans/basic/2/1-input/solution/Koan.hs similarity index 100% rename from koans/basic/9-input/solution/Koan.hs rename to koans/basic/2/1-input/solution/Koan.hs diff --git a/koans/basic/9-input/test/Test.hs b/koans/basic/2/1-input/test/Test.hs similarity index 100% rename from koans/basic/9-input/test/Test.hs rename to koans/basic/2/1-input/test/Test.hs diff --git a/koans/basic/2/2-count-the-words/Koan.hs b/koans/basic/2/2-count-the-words/Koan.hs new file mode 100644 index 0000000..bb13113 --- /dev/null +++ b/koans/basic/2/2-count-the-words/Koan.hs @@ -0,0 +1,29 @@ +{- | Count the words. + +With any clock, you can treat the 'Tag' like any other data. +For example, you can apply any function the console input. +-} +module Koan where + +-- text +import Data.Text (Text) +import Data.Text as Text (words) + +-- rhine +import FRP.Rhine + +-- | A line of user input. +userInput :: ClSF IO StdinClock () Text +userInput = tagS + +-- | Output the number of words of the line that was just entered. +wordCount :: ClSF IO StdinClock () Int +-- Do you remember how to convert a pure function into a ClSF? +wordCount = userInput >-> _ (Text.words >>> length) + +-- | Print the number of words of the line that was just entered. +printWordCount :: ClSF IO StdinClock () () +printWordCount = wordCount >-> arrMCl print + +main :: IO () +main = flow $ printWordCount @@ StdinClock diff --git a/koans/basic/2/2-count-the-words/solution/Koan.hs b/koans/basic/2/2-count-the-words/solution/Koan.hs new file mode 100644 index 0000000..0e562ac --- /dev/null +++ b/koans/basic/2/2-count-the-words/solution/Koan.hs @@ -0,0 +1,29 @@ +{- | Count the words. + +With any clock, you can treat the 'Tag' like any other data. +For example, you can apply any function the console input. +-} +module Koan where + +-- text +import Data.Text (Text) +import Data.Text as Text (words) + +-- rhine +import FRP.Rhine + +-- | A line of user input. +userInput :: ClSF IO StdinClock () Text +userInput = tagS + +-- | Output the number of words of the line that was just entered. +wordCount :: ClSF IO StdinClock () Int +-- Do you remember how to convert a pure function into a ClSF? +wordCount = userInput >-> arr (Text.words >>> length) + +-- | Print the number of words of the line that was just entered. +printWordCount :: ClSF IO StdinClock () () +printWordCount = wordCount >-> arrMCl print + +main :: IO () +main = flow $ printWordCount @@ StdinClock diff --git a/koans/basic/2/2-count-the-words/test/Test.hs b/koans/basic/2/2-count-the-words/test/Test.hs new file mode 100644 index 0000000..1b62659 --- /dev/null +++ b/koans/basic/2/2-count-the-words/test/Test.hs @@ -0,0 +1,25 @@ +module Main where + +-- text +import Data.Text (Text) +import Data.Text as Text (words) + +-- koan +import Koan qualified (main) + +-- test-io +import TestIO + +testLines :: [Text] +testLines = + [ "Hello Rhine" + , "this is a" + , "test" + ] + +main :: IO () +main = testForSecondsInput 1 testLines Koan.main $ \output -> + case output of + [] -> ["Weird, your program didn't produce any output!"] + _ | output == (tshow . length . Text.words <$> testLines) -> [] + _ -> ["The program produced output, but the lines had the wrong lengths."] diff --git a/koans/basic/2/3-count-the-lines/Koan.hs b/koans/basic/2/3-count-the-lines/Koan.hs new file mode 100644 index 0000000..c92c8d8 --- /dev/null +++ b/koans/basic/2/3-count-the-lines/Koan.hs @@ -0,0 +1,26 @@ +{- | Count the lines. + +Signal functions can have information about the past by storing internal state. +For example, we can count the number of lines that have been entered so far. +-} +module Koan where + +-- rhine +import FRP.Rhine + +{- | The number of lines of input so far. + +The 'count' signal function has internal state, the current count. +Every time it is called (because 'StdinClock' has ticked), +the count is incremented and returned. +-} +lineCount :: ClSF IO StdinClock () Integer +lineCount = count -- This is part of the library! + +-- | Print the number of the line that was just entered. +printLineCount :: ClSF IO StdinClock () () +printLineCount = lineCount >-> arrMCl print + +main :: IO () +-- Recap: Do you remember how to make a 'Rhine' from a 'ClSF'? +main = flow _ diff --git a/koans/basic/2/3-count-the-lines/solution/Koan.hs b/koans/basic/2/3-count-the-lines/solution/Koan.hs new file mode 100644 index 0000000..08d9e39 --- /dev/null +++ b/koans/basic/2/3-count-the-lines/solution/Koan.hs @@ -0,0 +1,26 @@ +{- | Count the lines. + +Signal functions can have information about the past by storing internal state. +For example, we can count the number of lines that have been entered so far. +-} +module Koan where + +-- rhine +import FRP.Rhine + +{- | The number of lines of input so far. + +The 'count' signal function has internal state, the current count. +Every time it is called (because 'StdinClock' has ticked), +the count is incremented and returned. +-} +lineCount :: ClSF IO StdinClock () Integer +lineCount = count -- This is part of the library! + +-- | Print the number of the line that was just entered. +printLineCount :: ClSF IO StdinClock () () +printLineCount = lineCount >-> arrMCl print + +main :: IO () +-- Recap: Do you remember how to make a 'Rhine' from a 'ClSF'? +main = flow $ printLineCount @@ StdinClock diff --git a/koans/basic/2/3-count-the-lines/test/Test.hs b/koans/basic/2/3-count-the-lines/test/Test.hs new file mode 100644 index 0000000..ef614a3 --- /dev/null +++ b/koans/basic/2/3-count-the-lines/test/Test.hs @@ -0,0 +1,24 @@ +module Main where + +-- text +import Data.Text (Text) + +-- koan +import Koan qualified (main) + +-- test-io +import TestIO + +testLines :: [Text] +testLines = + [ "Hello Rhine" + , "this is a" + , "test" + ] + +main :: IO () +main = testForSecondsInput 1 testLines Koan.main $ \output -> + case output of + [] -> ["Weird, your program didn't produce any output!"] + _ | output == take (length testLines) (tshow @Int <$> [1 ..]) -> [] + _ -> ["The program produced output, but it wasn't quite right."] diff --git a/koans/basic/2/4-count-all-the-words/Koan.hs b/koans/basic/2/4-count-all-the-words/Koan.hs new file mode 100644 index 0000000..8dc2548 --- /dev/null +++ b/koans/basic/2/4-count-all-the-words/Koan.hs @@ -0,0 +1,67 @@ +{- | Count the all the words! + +There are a number of ways how you can create internal state yourself. +One of them is 'feedback'. Let's look at its type signature: + +@ +feedback :: s -> ClSF m (a, s) (b, s) -> ClSF m a b +@ + +'feedback' takes an internal state @s@, +and a signal function with two inputs and two outputs. +The two inputs are @a@ and the current internal state @s@. +The two outputs are @b@ and the modified internal state. +'feedback' then hides this state, which is why at the end, +the return type is @ClSF m a b@. + +If you call 'feedback', you can pass an arbitrary signal function as second argument. +It allows you to use and modify the internal state. +-} +module Koan where + +-- text +import Data.Text (Text) +import Data.Text as Text (words) + +-- rhine +import FRP.Rhine hiding (currentInput) + +-- | A line of user input. +userInput :: ClSF IO StdinClock () Text +userInput = tagS + +-- | Output the number of words of the line that was just entered. +wordCount :: ClSF IO StdinClock () Int +wordCount = userInput >-> arr (Text.words >>> length) + +{- | Compute the sum of all input numbers so far, including the current one. + + /----------/--/-- Let's solve this problem in general, for any number type, any monad and any clock. + | | | + v v v +-} +sumClSF :: (Monad m, Num a) => ClSF m cl a a +sumClSF = + feedback -- We're using internal state + 0 -- As long as no input has arrived, this is the internal state we start with + $ arr aggregator -- No side effects or further state needed: We manipulate the state with a pure function. + where + aggregator :: (Num a) => (a, a) -> (a, a) + aggregator (currentInput, currentSum) = + let + nextSum = _ -- What should be the state after a further line of input has arrived? + in + -- The missing part is the final output of the signal function. + -- If we have summed up to a certain number, what should it be? + (_, nextSum) + +-- | The number of words of input so far. +totalWordCount :: ClSF IO StdinClock () Int +totalWordCount = wordCount >-> sumClSF + +-- | Print the number of total words so far. +printTotalWordCount :: ClSF IO StdinClock () () +printTotalWordCount = totalWordCount >-> arrMCl print + +main :: IO () +main = flow $ printTotalWordCount @@ StdinClock diff --git a/koans/basic/2/4-count-all-the-words/solution/Koan.hs b/koans/basic/2/4-count-all-the-words/solution/Koan.hs new file mode 100644 index 0000000..ba433b3 --- /dev/null +++ b/koans/basic/2/4-count-all-the-words/solution/Koan.hs @@ -0,0 +1,67 @@ +{- | Count the all the words! + +There are a number of ways how you can create internal state yourself. +One of them is 'feedback'. Let's look at its type signature: + +@ +feedback :: s -> ClSF m (a, s) (b, s) -> ClSF m a b +@ + +'feedback' takes an internal state @s@, +and a signal function with two inputs and two outputs. +The two inputs are @a@ and the current internal state @s@. +The two outputs are @b@ and the modified internal state. +'feedback' then hides this state, which is why at the end, +the return type is @ClSF m a b@. + +If you call 'feedback', you can pass an arbitrary signal function as second argument. +It allows you to use and modify the internal state. +-} +module Koan where + +-- text +import Data.Text (Text) +import Data.Text as Text (words) + +-- rhine +import FRP.Rhine hiding (currentInput) + +-- | A line of user input. +userInput :: ClSF IO StdinClock () Text +userInput = tagS + +-- | Output the number of words of the line that was just entered. +wordCount :: ClSF IO StdinClock () Int +wordCount = userInput >-> arr (Text.words >>> length) + +{- | Compute the sum of all input numbers so far, including the current one. + + /----------/--/-- Let's solve this problem in general, for any number type, any monad and any clock. + | | | + v v v +-} +sumClSF :: (Monad m, Num a) => ClSF m cl a a +sumClSF = + feedback -- We're using internal state + 0 -- As long as no input has arrived, this is the internal state we start with + $ arr aggregator -- No side effects or further state needed: We manipulate the state with a pure function. + where + aggregator :: (Num a) => (a, a) -> (a, a) + aggregator (currentInput, currentSum) = + let + nextSum = currentInput + currentSum -- What should be the state after a further line of input has arrived? + in + -- The missing part is the final output of the signal function. + -- If we have summed up to a certain number, what should it be? + (nextSum, nextSum) + +-- | The number of words of input so far. +totalWordCount :: ClSF IO StdinClock () Int +totalWordCount = wordCount >-> sumClSF + +-- | Print the number of total words so far. +printTotalWordCount :: ClSF IO StdinClock () () +printTotalWordCount = totalWordCount >-> arrMCl print + +main :: IO () +main = flow $ printTotalWordCount @@ StdinClock diff --git a/koans/basic/2/4-count-all-the-words/test/Test.hs b/koans/basic/2/4-count-all-the-words/test/Test.hs new file mode 100644 index 0000000..58fe926 --- /dev/null +++ b/koans/basic/2/4-count-all-the-words/test/Test.hs @@ -0,0 +1,30 @@ +module Main where + +-- text +import Data.Text + +-- koan +import Koan qualified (main) + +-- test-io +import TestIO + +testLines :: [Text] +testLines = + [ "Hello Rhine" + , "this is a" + , "test" + ] + +main :: IO () +main = testForSecondsInput 1 testLines Koan.main $ \output -> + case output of + [] -> ["Weird, your program didn't produce any output!"] + _ | output == (tshow @Int <$> [2, 5, 6]) -> [] + _ + | output == (tshow @Int <$> [0, 2, 5]) -> + [ "Your program seems to be counting the words, but only the past ones!" + , "Can you make sure it includes the current line as well?" + ] + _ | output == (tshow @Int <$> [2, 3, 1]) -> ["Your program seems to be counting the words, but it doesn't return their sum!"] + _ -> ["The program produced output, but it wasn't quite right."] diff --git a/koans/basic/2/5-count-all-the-chars/Koan.hs b/koans/basic/2/5-count-all-the-chars/Koan.hs new file mode 100644 index 0000000..b3c4a10 --- /dev/null +++ b/koans/basic/2/5-count-all-the-chars/Koan.hs @@ -0,0 +1,69 @@ +{- | Count the all the chars! + +If we can count words, we surely can count characters as well. +But how do we output them _both_? + +'ClSF's have an instance for the 'Arrow' type class. +This means that they can be composed sequentially (as you did already with '>>>'), +but also in parallel. +Let's look at one combinator that allows this: + +@ +(&&&) :: Monad m => ClSF m cl a b -> ClSF m cl a c -> ClSF m cl a (b, c) +@ + +If two signal functions are on the same monad, the same clock, and receive the same input, +we can combine them in parallel and execute both after each other. +Both outputs are combined in a tuple. +-} +module Koan where + +-- text +import Data.Text (Text) +import Data.Text qualified as Text (length, words) + +-- rhine +import FRP.Rhine hiding (currentInput) + +-- | A line of user input. +userInput :: ClSF IO StdinClock () Text +userInput = tagS + +-- | Output the number of words of the line that was just entered. +wordCount :: ClSF IO StdinClock () Int +wordCount = userInput >-> arr (Text.words >>> length) + +-- | Output the number of characters of the line that was just entered. +charCount :: ClSF IO StdinClock () Int +charCount = userInput >-> arr Text.length + +-- | Compute the sum of all input numbers so far, including the current one. +sumClSF :: (Monad m, Num a) => ClSF m cl a a +sumClSF = feedback 0 $ arr aggregator + where + aggregator :: (Num a) => (a, a) -> (a, a) + aggregator (currentInput, currentSum) = + let + nextSum = currentInput + currentSum + in + (nextSum, nextSum) + +-- | The number of words of input so far. +totalWordCount :: ClSF IO StdinClock () Int +totalWordCount = wordCount >-> sumClSF + +-- | The number of characters of input so far. +totalCharCount :: ClSF IO StdinClock () Int +-- Reuse your sum utility! +totalCharCount = charCount >-> _ + +-- | The number of total words and characters so far. +totalWordAndCharCount :: ClSF IO StdinClock () (Int, Int) +totalWordAndCharCount = _ &&& _ + +-- | Print the number of total words and characters so far. +printAllCounts :: ClSF IO StdinClock () () +printAllCounts = totalWordAndCharCount >-> arrMCl (\(words_, chars) -> print words_ >> print chars) + +main :: IO () +main = flow $ printAllCounts @@ StdinClock diff --git a/koans/basic/2/5-count-all-the-chars/solution/Koan.hs b/koans/basic/2/5-count-all-the-chars/solution/Koan.hs new file mode 100644 index 0000000..cd22916 --- /dev/null +++ b/koans/basic/2/5-count-all-the-chars/solution/Koan.hs @@ -0,0 +1,69 @@ +{- | Count the all the chars! + +If we can count words, we surely can count characters as well. +But how do we output them _both_? + +'ClSF's have an instance for the 'Arrow' type class. +This means that they can be composed sequentially (as you did already with '>>>'), +but also in parallel. +Let's look at one combinator that allows this: + +@ +(&&&) :: Monad m => ClSF m cl a b -> ClSF m cl a c -> ClSF m cl a (b, c) +@ + +If two signal functions are on the same monad, the same clock, and receive the same input, +we can combine them in parallel and execute both after each other. +Both outputs are combined in a tuple. +-} +module Koan where + +-- text +import Data.Text (Text) +import Data.Text qualified as Text (length, words) + +-- rhine +import FRP.Rhine hiding (currentInput) + +-- | A line of user input. +userInput :: ClSF IO StdinClock () Text +userInput = tagS + +-- | Output the number of words of the line that was just entered. +wordCount :: ClSF IO StdinClock () Int +wordCount = userInput >-> arr (Text.words >>> length) + +-- | Output the number of characters of the line that was just entered. +charCount :: ClSF IO StdinClock () Int +charCount = userInput >-> arr Text.length + +-- | Compute the sum of all input numbers so far, including the current one. +sumClSF :: (Monad m, Num a) => ClSF m cl a a +sumClSF = feedback 0 $ arr aggregator + where + aggregator :: (Num a) => (a, a) -> (a, a) + aggregator (currentInput, currentSum) = + let + nextSum = currentInput + currentSum + in + (nextSum, nextSum) + +-- | The number of words of input so far. +totalWordCount :: ClSF IO StdinClock () Int +totalWordCount = wordCount >-> sumClSF + +-- | The number of characters of input so far. +totalCharCount :: ClSF IO StdinClock () Int +-- Reuse your sum utility! +totalCharCount = charCount >-> sumClSF + +-- | The number of total words and characters so far. +totalWordAndCharCount :: ClSF IO StdinClock () (Int, Int) +totalWordAndCharCount = totalWordCount &&& totalCharCount + +-- | Print the number of total words and characters so far. +printAllCounts :: ClSF IO StdinClock () () +printAllCounts = totalWordAndCharCount >-> arrMCl (\(words_, chars) -> print words_ >> print chars) + +main :: IO () +main = flow $ printAllCounts @@ StdinClock diff --git a/koans/basic/2/5-count-all-the-chars/test/Test.hs b/koans/basic/2/5-count-all-the-chars/test/Test.hs new file mode 100644 index 0000000..240e266 --- /dev/null +++ b/koans/basic/2/5-count-all-the-chars/test/Test.hs @@ -0,0 +1,33 @@ +module Main where + +-- text +import Data.Text as Text + +-- koan +import Koan qualified (main) + +-- test-io +import TestIO + +testLines :: [Text] +testLines = + [ "Hello Rhine" + , "this is a" + , "test" + ] + +main :: IO () +main = testForSecondsInput 1 testLines Koan.main $ \output -> + case output of + [] -> ["Weird, your program didn't produce any output!"] + _ | output == (tshow @Int <$> [2, 11, 5, 20, 6, 24]) -> [] + _ + | output == (tshow @Int <$> [11, 2, 20, 5, 24, 6]) -> + ["Nearly there, it seems you've swapped characters and words around."] + _ -> + [ "The program produced output, but it wasn't quite right." + , "It received the following input:" + ] + ++ testLines + ++ ["And it returned:"] + ++ output diff --git a/rhine-koans.cabal b/rhine-koans.cabal index afc986e..9dce729 100644 --- a/rhine-koans.cabal +++ b/rhine-koans.cabal @@ -56,146 +56,210 @@ library test-io silently ^>= 1.2 , temporary ^>= 1.3 -common basic-1-hello-rhine +common basic-1-1-hello-rhine if flag(solution) - hs-source-dirs: koans/basic/1-hello-rhine/solution + hs-source-dirs: koans/basic/1/1-hello-rhine/solution else - hs-source-dirs: koans/basic/1-hello-rhine + hs-source-dirs: koans/basic/1/1-hello-rhine -executable basic-1-hello-rhine - import: exec, basic-1-hello-rhine +executable basic-1-1-hello-rhine + import: exec, basic-1-1-hello-rhine main-is: Main.hs -test-suite basic-1-hello-rhine-test - import: test, basic-1-hello-rhine +test-suite basic-1-1-hello-rhine-test + import: test, basic-1-1-hello-rhine type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/1-hello-rhine/test + hs-source-dirs: koans/basic/1/1-hello-rhine/test -common basic-2-fix-the-bug +common basic-1-2-fix-the-bug if flag(solution) - hs-source-dirs: koans/basic/2-fix-the-bug/solution + hs-source-dirs: koans/basic/1/2-fix-the-bug/solution else - hs-source-dirs: koans/basic/2-fix-the-bug + hs-source-dirs: koans/basic/1/2-fix-the-bug -executable basic-2-fix-the-bug - import: exec, basic-2-fix-the-bug +executable basic-1-2-fix-the-bug + import: exec, basic-1-2-fix-the-bug main-is: Main.hs -test-suite basic-2-fix-the-bug-test - import: test, basic-2-fix-the-bug +test-suite basic-1-2-fix-the-bug-test + import: test, basic-1-2-fix-the-bug type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/2-fix-the-bug/test + hs-source-dirs: koans/basic/1/2-fix-the-bug/test -common basic-3-faster +common basic-1-3-faster if flag(solution) - hs-source-dirs: koans/basic/3-faster/solution + hs-source-dirs: koans/basic/1/3-faster/solution else - hs-source-dirs: koans/basic/3-faster + hs-source-dirs: koans/basic/1/3-faster -executable basic-3-faster - import: exec, basic-3-faster +executable basic-1-3-faster + import: exec, basic-1-3-faster main-is: Main.hs -test-suite basic-3-faster-test - import: test, basic-3-faster +test-suite basic-1-3-faster-test + import: test, basic-1-3-faster type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/3-faster/test + hs-source-dirs: koans/basic/1/3-faster/test -common basic-4-compose +common basic-1-4-compose if flag(solution) - hs-source-dirs: koans/basic/4-compose/solution + hs-source-dirs: koans/basic/1/4-compose/solution else - hs-source-dirs: koans/basic/4-compose + hs-source-dirs: koans/basic/1/4-compose -executable basic-4-compose - import: exec, basic-4-compose +executable basic-1-4-compose + import: exec, basic-1-4-compose main-is: Main.hs -test-suite basic-4-compose-test - import: test, basic-4-compose +test-suite basic-1-4-compose-test + import: test, basic-1-4-compose type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/4-compose/test + hs-source-dirs: koans/basic/1/4-compose/test -common basic-5-compose-more +common basic-1-5-compose-more if flag(solution) - hs-source-dirs: koans/basic/5-compose-more/solution + hs-source-dirs: koans/basic/1/5-compose-more/solution else - hs-source-dirs: koans/basic/5-compose-more + hs-source-dirs: koans/basic/1/5-compose-more -executable basic-5-compose-more - import: exec, basic-5-compose-more +executable basic-1-5-compose-more + import: exec, basic-1-5-compose-more main-is: Main.hs -test-suite basic-5-compose-more-test - import: test, basic-5-compose-more +test-suite basic-1-5-compose-more-test + import: test, basic-1-5-compose-more type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/5-compose-more/test + hs-source-dirs: koans/basic/1/5-compose-more/test -common basic-6-compose-signal-functions-and-clocks +common basic-1-6-compose-signal-functions-and-clocks if flag(solution) - hs-source-dirs: koans/basic/6-compose-signal-functions-and-clocks/solution + hs-source-dirs: koans/basic/1/6-compose-signal-functions-and-clocks/solution else - hs-source-dirs: koans/basic/6-compose-signal-functions-and-clocks + hs-source-dirs: koans/basic/1/6-compose-signal-functions-and-clocks -executable basic-6-compose-signal-functions-and-clocks - import: exec, basic-6-compose-signal-functions-and-clocks +executable basic-1-6-compose-signal-functions-and-clocks + import: exec, basic-1-6-compose-signal-functions-and-clocks main-is: Main.hs -test-suite basic-6-compose-signal-functions-and-clocks-test - import: test, basic-6-compose-signal-functions-and-clocks +test-suite basic-1-6-compose-signal-functions-and-clocks-test + import: test, basic-1-6-compose-signal-functions-and-clocks type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/6-compose-signal-functions-and-clocks/test + hs-source-dirs: koans/basic/1/6-compose-signal-functions-and-clocks/test -common basic-7-compose-on-the-same-clock +common basic-1-7-compose-on-the-same-clock if flag(solution) - hs-source-dirs: koans/basic/7-compose-on-the-same-clock/solution + hs-source-dirs: koans/basic/1/7-compose-on-the-same-clock/solution else - hs-source-dirs: koans/basic/7-compose-on-the-same-clock + hs-source-dirs: koans/basic/1/7-compose-on-the-same-clock -executable basic-7-compose-on-the-same-clock - import: exec, basic-7-compose-on-the-same-clock +executable basic-1-7-compose-on-the-same-clock + import: exec, basic-1-7-compose-on-the-same-clock main-is: Main.hs -test-suite basic-7-compose-on-the-same-clock-test - import: test, basic-7-compose-on-the-same-clock +test-suite basic-1-7-compose-on-the-same-clock-test + import: test, basic-1-7-compose-on-the-same-clock type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/7-compose-on-the-same-clock/test + hs-source-dirs: koans/basic/1/7-compose-on-the-same-clock/test -common basic-8-compose-on-different-clocks +common basic-1-8-compose-on-different-clocks if flag(solution) - hs-source-dirs: koans/basic/8-compose-on-different-clocks/solution + hs-source-dirs: koans/basic/1/8-compose-on-different-clocks/solution else - hs-source-dirs: koans/basic/8-compose-on-different-clocks + hs-source-dirs: koans/basic/1/8-compose-on-different-clocks -executable basic-8-compose-on-different-clocks - import: exec, basic-8-compose-on-different-clocks +executable basic-1-8-compose-on-different-clocks + import: exec, basic-1-8-compose-on-different-clocks main-is: Main.hs -test-suite basic-8-compose-on-different-clocks-test - import: test, basic-8-compose-on-different-clocks +test-suite basic-1-8-compose-on-different-clocks-test + import: test, basic-1-8-compose-on-different-clocks type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/8-compose-on-different-clocks/test + hs-source-dirs: koans/basic/1/8-compose-on-different-clocks/test -common basic-9-input +common basic-2-1-input if flag(solution) - hs-source-dirs: koans/basic/9-input/solution + hs-source-dirs: koans/basic/2/1-input/solution else - hs-source-dirs: koans/basic/9-input + hs-source-dirs: koans/basic/2/1-input -executable basic-9-input - import: exec, basic-9-input +executable basic-2-1-input + import: exec, basic-2-1-input main-is: Main.hs -test-suite basic-9-input-test - import: test, basic-9-input +test-suite basic-2-1-input-test + import: test, basic-2-1-input type: exitcode-stdio-1.0 main-is: Test.hs - hs-source-dirs: koans/basic/9-input/test + hs-source-dirs: koans/basic/2/1-input/test + +common basic-2-2-count-the-words + if flag(solution) + hs-source-dirs: koans/basic/2/2-count-the-words/solution + else + hs-source-dirs: koans/basic/2/2-count-the-words + +executable basic-2-2-count-the-words + import: exec, basic-2-2-count-the-words + main-is: Main.hs + +test-suite basic-2-2-count-the-words-test + import: test, basic-2-2-count-the-words + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/basic/2/2-count-the-words/test + +common basic-2-3-count-the-lines + if flag(solution) + hs-source-dirs: koans/basic/2/3-count-the-lines/solution + else + hs-source-dirs: koans/basic/2/3-count-the-lines + +executable basic-2-3-count-the-lines + import: exec, basic-2-3-count-the-lines + main-is: Main.hs + +test-suite basic-2-3-count-the-lines-test + import: test, basic-2-3-count-the-lines + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/basic/2/3-count-the-lines/test + +common basic-2-4-count-all-the-words + if flag(solution) + hs-source-dirs: koans/basic/2/4-count-all-the-words/solution + else + hs-source-dirs: koans/basic/2/4-count-all-the-words + +executable basic-2-4-count-all-the-words + import: exec, basic-2-4-count-all-the-words + main-is: Main.hs + +test-suite basic-2-4-count-all-the-words-test + import: test, basic-2-4-count-all-the-words + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/basic/2/4-count-all-the-words/test + +common basic-2-5-count-all-the-chars + if flag(solution) + hs-source-dirs: koans/basic/2/5-count-all-the-chars/solution + else + hs-source-dirs: koans/basic/2/5-count-all-the-chars + +executable basic-2-5-count-all-the-chars + import: exec, basic-2-5-count-all-the-chars + main-is: Main.hs + +test-suite basic-2-5-count-all-the-chars-test + import: test, basic-2-5-count-all-the-chars + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/basic/2/5-count-all-the-chars/test