From 9378053264d52d9814dbbe800c0f8be8dfd41c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Tue, 16 Jan 2024 17:38:00 +0100 Subject: [PATCH 1/5] Fix Koan basic-2-5 --- koans/basic/2/5-count-all-the-chars/Koan.hs | 9 +++++++-- koans/basic/2/5-count-all-the-chars/solution/Koan.hs | 9 +++++++-- koans/basic/2/5-count-all-the-chars/test/Test.hs | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/koans/basic/2/5-count-all-the-chars/Koan.hs b/koans/basic/2/5-count-all-the-chars/Koan.hs index b3c4a10..5fa5a5e 100644 --- a/koans/basic/2/5-count-all-the-chars/Koan.hs +++ b/koans/basic/2/5-count-all-the-chars/Koan.hs @@ -33,9 +33,14 @@ userInput = tagS wordCount :: ClSF IO StdinClock () Int wordCount = userInput >-> arr (Text.words >>> length) --- | Output the number of characters of the line that was just entered. +{- | Output the number of characters of the line that was just entered. + +The newline character is not part of 'userInput', +therefore +1 is added for it. +-} charCount :: ClSF IO StdinClock () Int -charCount = userInput >-> arr Text.length +-- Yes, you can use >>> to compose ordinary functions as well! +charCount = userInput >-> arr (Text.length >>> (+ 1)) -- | Compute the sum of all input numbers so far, including the current one. sumClSF :: (Monad m, Num a) => ClSF m cl a a 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 index cd22916..db609e7 100644 --- a/koans/basic/2/5-count-all-the-chars/solution/Koan.hs +++ b/koans/basic/2/5-count-all-the-chars/solution/Koan.hs @@ -33,9 +33,14 @@ userInput = tagS wordCount :: ClSF IO StdinClock () Int wordCount = userInput >-> arr (Text.words >>> length) --- | Output the number of characters of the line that was just entered. +{- | Output the number of characters of the line that was just entered. + +The newline character is not part of 'userInput', +therefore +1 is added for it. +-} charCount :: ClSF IO StdinClock () Int -charCount = userInput >-> arr Text.length +-- Yes, you can use >>> to compose ordinary functions as well! +charCount = userInput >-> arr (Text.length >>> (+ 1)) -- | Compute the sum of all input numbers so far, including the current one. sumClSF :: (Monad m, Num a) => ClSF m cl a a 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 index 240e266..99bcd72 100644 --- a/koans/basic/2/5-count-all-the-chars/test/Test.hs +++ b/koans/basic/2/5-count-all-the-chars/test/Test.hs @@ -20,9 +20,9 @@ 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 <$> [2, 12, 5, 22, 6, 27]) -> [] _ - | output == (tshow @Int <$> [11, 2, 20, 5, 24, 6]) -> + | output == (tshow @Int <$> [12, 2, 22, 5, 27, 6]) -> ["Nearly there, it seems you've swapped characters and words around."] _ -> [ "The program produced output, but it wasn't quite right." From 1a2322567bfbb53a0a2e364b975a0bdc99618dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Tue, 16 Jan 2024 18:10:30 +0100 Subject: [PATCH 2/5] Add koan basic-2-6 --- koans/basic/2/6-count-everything/Koan.hs | 70 +++++++++++++++++++ .../2/6-count-everything/solution/Koan.hs | 70 +++++++++++++++++++ koans/basic/2/6-count-everything/test/Test.hs | 30 ++++++++ rhine-koans.cabal | 16 +++++ 4 files changed, 186 insertions(+) create mode 100644 koans/basic/2/6-count-everything/Koan.hs create mode 100644 koans/basic/2/6-count-everything/solution/Koan.hs create mode 100644 koans/basic/2/6-count-everything/test/Test.hs diff --git a/koans/basic/2/6-count-everything/Koan.hs b/koans/basic/2/6-count-everything/Koan.hs new file mode 100644 index 0000000..0545b6d --- /dev/null +++ b/koans/basic/2/6-count-everything/Koan.hs @@ -0,0 +1,70 @@ +{- | Count everything! + +If we can count words, characters, and lines, let's just put it all together. +This is a bit finicky, but give it a try nevertheless! +Remember the combinator for parallel composition: + +@ +(&&&) :: Monad m => ClSF m cl a b -> ClSF m cl a c -> ClSF m cl a (b, c) +@ +-} +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. + +The newline character is not part of 'userInput', +therefore +1 is added for it. +-} +charCount :: ClSF IO StdinClock () Int +-- Yes, you can use >>> to compose ordinary functions as well! +charCount = userInput >-> arr (Text.length >>> (+ 1)) + +-- | 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 lines of input so far. +lineCount :: ClSF IO StdinClock () Integer +lineCount = count + +-- | 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 +totalCharCount = charCount >-> sumClSF + +-- | The number of total lines, words and characters so far. +totalCount :: ClSF IO StdinClock () _ -- What will the type of this be? +totalCount = _ &&& _ &&& _ + +-- | Print the number of total words and characters so far. +printAllCounts :: ClSF IO StdinClock () () +-- On what do you need to pattern match here to bring lines_, words_ and chars into scope? +printAllCounts = totalCount >-> arrMCl (\_ -> print lines_ >> print words_ >> print chars) + +main :: IO () +main = flow $ printAllCounts @@ StdinClock diff --git a/koans/basic/2/6-count-everything/solution/Koan.hs b/koans/basic/2/6-count-everything/solution/Koan.hs new file mode 100644 index 0000000..8eb84c3 --- /dev/null +++ b/koans/basic/2/6-count-everything/solution/Koan.hs @@ -0,0 +1,70 @@ +{- | Count everything! + +If we can count words, characters, and lines, let's just put it all together. +This is a bit finicky, but give it a try nevertheless! +Remember the combinator for parallel composition: + +@ +(&&&) :: Monad m => ClSF m cl a b -> ClSF m cl a c -> ClSF m cl a (b, c) +@ +-} +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. + +The newline character is not part of 'userInput', +therefore +1 is added for it. +-} +charCount :: ClSF IO StdinClock () Int +-- Yes, you can use >>> to compose ordinary functions as well! +charCount = userInput >-> arr (Text.length >>> (+ 1)) + +-- | 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 lines of input so far. +lineCount :: ClSF IO StdinClock () Integer +lineCount = count + +-- | 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 +totalCharCount = charCount >-> sumClSF + +-- | The number of total lines, words and characters so far. +totalCount :: ClSF IO StdinClock () (Integer, (Int, Int)) -- What will the type of this be? +totalCount = lineCount &&& totalWordCount &&& totalCharCount + +-- | Print the number of total words and characters so far. +printAllCounts :: ClSF IO StdinClock () () +-- On what do you need to pattern match here to bring lines_, words_ and chars into scope? +printAllCounts = totalCount >-> arrMCl (\(lines_, (words_, chars)) -> print lines_ >> print words_ >> print chars) + +main :: IO () +main = flow $ printAllCounts @@ StdinClock diff --git a/koans/basic/2/6-count-everything/test/Test.hs b/koans/basic/2/6-count-everything/test/Test.hs new file mode 100644 index 0000000..b26db5b --- /dev/null +++ b/koans/basic/2/6-count-everything/test/Test.hs @@ -0,0 +1,30 @@ +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 <$> [1, 2, 12, 2, 5, 22, 3, 6, 27]) -> [] + _ -> + [ "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 9dce729..f6f0e5f 100644 --- a/rhine-koans.cabal +++ b/rhine-koans.cabal @@ -263,3 +263,19 @@ test-suite basic-2-5-count-all-the-chars-test type: exitcode-stdio-1.0 main-is: Test.hs hs-source-dirs: koans/basic/2/5-count-all-the-chars/test + +common basic-2-6-count-everything + if flag(solution) + hs-source-dirs: koans/basic/2/6-count-everything/solution + else + hs-source-dirs: koans/basic/2/6-count-everything + +executable basic-2-6-count-everything + import: exec, basic-2-6-count-everything + main-is: Main.hs + +test-suite basic-2-6-count-everything-test + import: test, basic-2-6-count-everything + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/basic/2/6-count-everything/test From 5fc7172a8d7dfcdf30f0e94cb99547df056ded1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Tue, 16 Jan 2024 19:10:56 +0100 Subject: [PATCH 3/5] Todos --- TODO.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO.md b/TODO.md index b012931..edf39a4 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,7 @@ ## CI * Check cabal outdated +* Check in diffs between solution and problem and make sure it is stable (such that every fix in a solution gets propagated to the problem and vice versa) # Content ## Basic @@ -46,6 +47,9 @@ * Complicated clock errors * Order of components (including a RB) is not the same as order of clocks in Rhine, this gives a weird error that `HasClock cl '[]` cannot be satisfied * Write your own backend to some library +* Complicated Arrow syntax + * scoping of live and static values + * Nested if then else & do blocks ## Data analysis track From 4a15d99ac739fe2708460b7ba2086f28f273eb2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Tue, 16 Jan 2024 19:11:07 +0100 Subject: [PATCH 4/5] Change Integer -> Int --- koans/basic/2/3-count-the-lines/Koan.hs | 2 +- koans/basic/2/3-count-the-lines/solution/Koan.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/koans/basic/2/3-count-the-lines/Koan.hs b/koans/basic/2/3-count-the-lines/Koan.hs index c92c8d8..6fd3b0b 100644 --- a/koans/basic/2/3-count-the-lines/Koan.hs +++ b/koans/basic/2/3-count-the-lines/Koan.hs @@ -14,7 +14,7 @@ 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 :: ClSF IO StdinClock () Int lineCount = count -- This is part of the library! -- | Print the number of the line that was just entered. diff --git a/koans/basic/2/3-count-the-lines/solution/Koan.hs b/koans/basic/2/3-count-the-lines/solution/Koan.hs index 08d9e39..b97f741 100644 --- a/koans/basic/2/3-count-the-lines/solution/Koan.hs +++ b/koans/basic/2/3-count-the-lines/solution/Koan.hs @@ -14,7 +14,7 @@ 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 :: ClSF IO StdinClock () Int lineCount = count -- This is part of the library! -- | Print the number of the line that was just entered. From ca8b11f1516974c3b4d995e3d7888b588e288562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Tue, 16 Jan 2024 19:11:28 +0100 Subject: [PATCH 5/5] Add Koan basic-2-7 --- .../basic/2/7-count-everything-nicer/Koan.hs | 81 +++++++++++++++++++ .../7-count-everything-nicer/solution/Koan.hs | 71 ++++++++++++++++ .../2/7-count-everything-nicer/test/Test.hs | 30 +++++++ rhine-koans.cabal | 16 ++++ 4 files changed, 198 insertions(+) create mode 100644 koans/basic/2/7-count-everything-nicer/Koan.hs create mode 100644 koans/basic/2/7-count-everything-nicer/solution/Koan.hs create mode 100644 koans/basic/2/7-count-everything-nicer/test/Test.hs diff --git a/koans/basic/2/7-count-everything-nicer/Koan.hs b/koans/basic/2/7-count-everything-nicer/Koan.hs new file mode 100644 index 0000000..28dfd02 --- /dev/null +++ b/koans/basic/2/7-count-everything-nicer/Koan.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE CPP #-} + +-- Disabling formatter and linter because it would fail on the syntax error otherwise. +#ifndef __HLINT__ +{- FOURMOLU_DISABLE -} + +-- Start reading here +-- vvvvvvvvvvvvvvvvvv + +{- | Count everything nicer. + +The last problem got quite verbose, and fiddling around with nested tuples isn't fun. +Fortunately, Haskell has a language extension that provides very useful syntax +for data flow constructs like signal functions! +It is called "arrow notation", and you can read a bit more about it here: https://www.haskell.org/arrows/. +Have a look how the code of the previous koan can be cleaned up with it. +-} +module Koan where + +-- text +import Data.Text qualified as Text (length, words) + +-- rhine +import FRP.Rhine hiding (currentInput) + +-- | 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) + +-- | Print the number of total words and characters so far. +printAllCounts :: ClSF IO StdinClock () () +-- proc is a keyword. Think of it like a lambda expression! +-- But why does GHC spit out a nasty parse error here? +-- Read through the following to find out! +printAllCounts = proc () -> do + -- This is nearly like do notation, except it also has syntax for input, the -<. + + -- /------/--- Everything left from a <- is the output _signal_ of a signal function. + -- | | It is a value that can depend on the current tick of the clock. + -- | | + -- | | /--- Signal functions can be used between <- and -<. + -- | | | + -- | | | /--- This is the input to the signal function. (tagS needs none.) + -- | | | | + -- v v v v + userInput <- tagS -< () + + -- We can apply ordinary functions to signals. + let wordCount = length $ Text.words userInput + charCount = Text.length userInput + 1 + + lineCount <- count @Int -< () + + -- Signals can be inputs to signal functions. + -- This way we can aggregate signals. + totalWordCount <- sumClSF -< wordCount + totalCharCount <- sumClSF -< charCount + + -- If a signal function has trivial output (), the <- is not needed. + arrMCl print -< lineCount + arrMCl print -< totalWordCount + arrMCl print -< _ -- Which one is missing here? + +-- As you've seen, arrow notation introduces two new syntactic constructions, +-- the proc keyword an the -< operator. +-- You need to turn on a GHC language extension so that they can be parsed! +-- Can you uncomment the following line, and move to the top of the file? +-- {-# LANGUAGE Arrows #-} + +main :: IO () +main = flow $ printAllCounts @@ StdinClock + +-- Ignore the next line ;) +#endif diff --git a/koans/basic/2/7-count-everything-nicer/solution/Koan.hs b/koans/basic/2/7-count-everything-nicer/solution/Koan.hs new file mode 100644 index 0000000..69be230 --- /dev/null +++ b/koans/basic/2/7-count-everything-nicer/solution/Koan.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE Arrows #-} + +{- | Count everything nicer. + +The last problem got quite verbose, and fiddling around with nested tuples isn't fun. +Fortunately, Haskell has a language extension that provides very useful syntax +for data flow constructs like signal functions! +It is called "arrow notation", and you can read a bit more about it here: https://www.haskell.org/arrows/. +Have a look how the code can be cleaned up with it. +-} +module Koan where + +-- text +import Data.Text qualified as Text (length, words) + +-- rhine +import FRP.Rhine hiding (currentInput) + +-- | 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) + +-- | Print the number of total words and characters so far. +printAllCounts :: ClSF IO StdinClock () () +-- proc is a keyword. Think of it like a lambda expression! +-- But why does GHC spit out a nasty parse error here? +-- Read through the following to find out! +printAllCounts = proc () -> do + -- This is nearly like do notation, except it also has syntax for input, the -<. + + -- /------/--- Everything left from a <- is the output _signal_ of a signal function. + -- v v It is a value that can depend on the current tick of the clock. + -- v v + -- v v /--- Signal functions can be used between <- and -<. + -- v v v + -- v v v /--- This is the input to the signal function. (tagS needs none.) + -- v v v v + -- v v v v + userInput <- tagS -< () + + -- We can apply ordinary functions to signals. + let wordCount = length $ Text.words userInput + charCount = Text.length userInput + 1 + + lineCount <- count @Int -< () + + -- Signals can be inputs to signal functions. + -- This way we can aggregate signals. + totalWordCount <- sumClSF -< wordCount + totalCharCount <- sumClSF -< charCount + + -- If a signal function has trivial output (), the <- is not needed. + arrMCl print -< lineCount + arrMCl print -< totalWordCount + arrMCl print -< totalCharCount + +-- As you've seen, arrow notation introduces two new syntactic constructions, +-- the proc keyword an the -< operator. +-- You need to turn on a GHC language extension so that they can be parsed! +-- Can you uncomment the following line, and move to the top of the file? +-- {-# LANGUAGE Arrows #-} + +main :: IO () +main = flow $ printAllCounts @@ StdinClock diff --git a/koans/basic/2/7-count-everything-nicer/test/Test.hs b/koans/basic/2/7-count-everything-nicer/test/Test.hs new file mode 100644 index 0000000..b26db5b --- /dev/null +++ b/koans/basic/2/7-count-everything-nicer/test/Test.hs @@ -0,0 +1,30 @@ +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 <$> [1, 2, 12, 2, 5, 22, 3, 6, 27]) -> [] + _ -> + [ "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 f6f0e5f..0875af6 100644 --- a/rhine-koans.cabal +++ b/rhine-koans.cabal @@ -279,3 +279,19 @@ test-suite basic-2-6-count-everything-test type: exitcode-stdio-1.0 main-is: Test.hs hs-source-dirs: koans/basic/2/6-count-everything/test + +common basic-2-7-count-everything-nicer + if flag(solution) + hs-source-dirs: koans/basic/2/7-count-everything-nicer/solution + else + hs-source-dirs: koans/basic/2/7-count-everything-nicer + +executable basic-2-7-count-everything-nicer + import: exec, basic-2-7-count-everything-nicer + main-is: Main.hs + +test-suite basic-2-7-count-everything-nicer-test + import: test, basic-2-7-count-everything-nicer + type: exitcode-stdio-1.0 + main-is: Test.hs + hs-source-dirs: koans/basic/2/7-count-everything-nicer/test