diff --git a/haskell_102/codelab/00_setup/Makefile b/haskell_102/codelab/00_setup/Makefile new file mode 100644 index 0000000..3b4a15f --- /dev/null +++ b/haskell_102/codelab/00_setup/Makefile @@ -0,0 +1,45 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +.DEFAULT_GOAL := run +.PHONY: repl run clean + +TARGET = codelab + +TOOL = .tool +REPL = .repl_tool +BUILD_GENERATED = dist dist-newstyle .stack-work stack.yaml.lock $(TOOL) $(REPL) + +repl: $(REPL) + @`cat $(REPL)` :$(TARGET) + +run: $(TOOL) + @`cat $(TOOL)` run $(TARGET) + +clean: + @$(RM) -r $(BUILD_GENERATED) + +# +# Private rules to check which toolchain is available +# + +$(TOOL): + @([[ -e `which stack` ]] && echo stack > $(TOOL) || true) + @([[ -s $(TOOL) ]] || ([[ -e `which cabal` ]] && echo cabal > $(TOOL)) || true) + @([[ -s $(TOOL) ]] || (cat setup.md && false)) + +$(REPL): + @([[ -e `which stack` ]] && echo stack > $(TOOL) || true) + @([[ -s $(REPL) ]] || ([[ -e `which cabal` ]] && echo cabal repl > $(REPL)) || true) + @([[ -s $(REPL) ]] || (cat setup.md && false)) diff --git a/haskell_102/codelab/00_setup/Setup.hs b/haskell_102/codelab/00_setup/Setup.hs new file mode 100644 index 0000000..4dae40a --- /dev/null +++ b/haskell_102/codelab/00_setup/Setup.hs @@ -0,0 +1,16 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +import Distribution.Simple +main = defaultMain diff --git a/haskell_102/codelab/00_setup/codelab.cabal b/haskell_102/codelab/00_setup/codelab.cabal new file mode 100644 index 0000000..5644277 --- /dev/null +++ b/haskell_102/codelab/00_setup/codelab.cabal @@ -0,0 +1,32 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +name: codelab +version: 0.1.0.0 +build-type: Simple +cabal-version: >=1.10 + +executable codelab + hs-source-dirs: src + default-language: Haskell2010 + main-is: Main.hs + build-depends: + base >=4.11 && <4.15 + +executable solution + hs-source-dirs: src + default-language: Haskell2010 + main-is: Main.hs + build-depends: + base >=4.11 && <4.15 diff --git a/haskell_102/codelab/00_setup/setup.md b/haskell_102/codelab/00_setup/setup.md new file mode 100644 index 0000000..d3fe010 --- /dev/null +++ b/haskell_102/codelab/00_setup/setup.md @@ -0,0 +1,16 @@ +============================================ + + HASKELL 102 + +============================================ + +If you are reading this text, probably you don't have Haskell environment setup +on your system. This should give you enough instructions to install it. + +First, note that there are two different ways to get Haskell installed: + +1. using [Stack](https://docs.haskellstack.org/en/stable/README/) +2. getting the [Haskell Platform](https://www.haskell.org/platform/) + +This course is not taking any side, you are free to install either of these. The +links above point to the install pages for either of the toolchains. diff --git a/haskell_102/codelab/00_setup/src/Main.hs b/haskell_102/codelab/00_setup/src/Main.hs new file mode 100644 index 0000000..b3b0710 --- /dev/null +++ b/haskell_102/codelab/00_setup/src/Main.hs @@ -0,0 +1,23 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +module Main where + +main :: IO () +main = do + putStrLn "============================================" + putStrLn "" + putStrLn "Haskell toolchain is installed! All is good." + putStrLn "" + putStrLn "============================================" diff --git a/haskell_102/codelab/00_setup/stack.yaml b/haskell_102/codelab/00_setup/stack.yaml new file mode 100644 index 0000000..661db76 --- /dev/null +++ b/haskell_102/codelab/00_setup/stack.yaml @@ -0,0 +1,17 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resolver: lts-14.25 +packages: +- . diff --git a/haskell_102/codelab/01_mastermind/Makefile b/haskell_102/codelab/01_mastermind/Makefile new file mode 100644 index 0000000..747ed11 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/Makefile @@ -0,0 +1,36 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +.PHONY: run clean + +TARGET = codelab +ARGS = solve + +TOOL = .tool +BUILD_GENERATED = dist dist-newstyle .stack-work stack.yaml.lock $(TOOL) + +run: $(TOOL) + @`cat $(TOOL)` run $(TARGET) $(ARGS) + +clean: + @$(RM) -r $(BUILD_GENERATED) + +# +# Private rules to check which toolchain is available +# + +$(TOOL): + @([[ -e `which stack` ]] && echo stack > $(TOOL) || true) + @([[ -s $(TOOL) ]] || ([[ -e `which cabal` ]] && echo cabal > $(TOOL)) || true) + @([[ -s $(TOOL) ]] || (cat setup.md && false)) diff --git a/haskell_102/codelab/01_mastermind/Setup.hs b/haskell_102/codelab/01_mastermind/Setup.hs new file mode 100644 index 0000000..4dae40a --- /dev/null +++ b/haskell_102/codelab/01_mastermind/Setup.hs @@ -0,0 +1,16 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +import Distribution.Simple +main = defaultMain diff --git a/haskell_102/codelab/01_mastermind/codelab.cabal b/haskell_102/codelab/01_mastermind/codelab.cabal new file mode 100644 index 0000000..5d7c886 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/codelab.cabal @@ -0,0 +1,58 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +name: codelab +version: 0.1.0.0 +build-type: Simple +cabal-version: >=1.10 + +executable codelab + hs-source-dirs: src + default-language: Haskell2010 + main-is: Main.hs + other-modules: + Code + , Color + , ColorMap + , Do + , ErrorOr + , Game + , Internal + , Tests + build-depends: + base >=4.11 && <4.15 + , ansi-terminal >= 0.8.0.4 + , containers >= 0.6.0.1 + , optparse-applicative >= 0.14.3.0 + , random >= 1.1 + +executable solution + hs-source-dirs: src + default-language: Haskell2010 + main-is: Main.hs + other-modules: + CodeSolution + , ColorSolution + , ColorMapSolution + , DoSolution + , ErrorOrSolution + , Game + , Internal + , Tests + build-depends: + base >=4.11 && <4.15 + , ansi-terminal >= 0.8.0.4 + , containers >= 0.6.0.1 + , optparse-applicative >= 0.14.3.0 + , random >= 1.1 diff --git a/haskell_102/codelab/01_mastermind/setup.md b/haskell_102/codelab/01_mastermind/setup.md new file mode 100644 index 0000000..3e3f6ef --- /dev/null +++ b/haskell_102/codelab/01_mastermind/setup.md @@ -0,0 +1,16 @@ +============================================ + + HASKELL MTV 102 + +============================================ + +If you are reading this text, probably you don't have Haskell environment setup +on your system. This should give you enough instructions to install it. + +First, note that there are two different ways to get Haskell installed: + +1. using [Stack](https://docs.haskellstack.org/en/stable/README/) +2. getting the [Haskell Platform](https://www.haskell.org/platform/) + +This course is not taking any side, you are free to install either of these. The +links above point to the install pages for either of the toolchains. diff --git a/haskell_102/codelab/01_mastermind/src/Code.hs b/haskell_102/codelab/01_mastermind/src/Code.hs new file mode 100644 index 0000000..9d45df0 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/Code.hs @@ -0,0 +1,132 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module Code where + +import Data.Map hiding (foldr, filter, map) + +import Color +import ColorMap +import Internal (codelab) + + +-- SECTION 4: Code (combining functions) +-- +-- A code is a list of colors, that we will simply represent as... +-- a list of colors. This section aims at implementing the functions +-- that we will need to compare two codes. +-- +-- Basically, we compute three values: +-- total: the number of colors the codes have in common +-- black: the number of correct colors at the correct position +-- white: total - black + +type Code = [Color] +data Score = Score + { scoreBlack :: Int + , scoreWhite :: Int + } deriving Eq + +instance Show Score where + show s = concat + [ "black: ", show $ scoreBlack s + , ", white: ", show $ scoreWhite s + ] + +-- "allCodes" generates all possible codes of a given size. +-- allCodes 0 = [[]] +-- allCodes 1 = [[Red], [Yellow]...] +-- allCodes 2 = [[Red, Red], [Red, Yellow]...] +-- +-- We use list comprehension to generate the list of codes. The trick, +-- here, is recursion: the codes of size n are obtained by adding all +-- possible colors to all possible codes of size (n-1). The syntax of list +-- comprehension in Haskell is: +-- [result | value1 <- container1, value2 <- container2] +-- The same list comprehension in Python would be: +-- [result for value1 in container1 for value2 in container2] +-- +-- As a reminder: this guard syntax is equivalent to a series of if / else: +-- if s < 0 +-- then ... +-- else if s == 0 +-- then ... +-- else ... +allCodes :: Int -> [Code] +allCodes s + | s < 0 = error "allCodes: size was lower than 0" + | s == 0 = codelab + | otherwise = [color:code | color <- codelab, code <- codelab] + +-- Transforms a code into the corresponding map of Color to Int. To do so, +-- we fold ("reduce") the list, by using a ColorMap as the accumulator. You +-- will need the following functions: +-- +-- foldr :: (Color -> ColorMap -> ColorMap) -> ColorMap -> [Color] -> ColorMap +-- addColorToMap :: Color -> ColorMap -> ColorMap +-- empty :: ColorMap +codeToMap :: Code -> ColorMap +codeToMap code = codelab + +-- This function computes the black score of two given codes. To do so, we +-- "zip" the two lists together to compare them. +-- +-- [ R G B M ] +-- ? [ R B Y M ] +-- -> [ 1 0 0 1 ] +-- -> 2 +-- +-- To compute the result, you will need: +-- +-- zipWith :: (Color -> Color -> Bool) -> [Color] -> [Color] -> [Bool] +-- (==) :: Color -> Color -> Bool +-- map :: (Bool -> Int) -> [Bool] -> [Int] +-- fromEnum :: Bool -> Int +-- sum :: [Int] -> Int +-- +-- (If this one seems complicated, try testing zip and zipWith in GHCI!) +-- For bonus points, reimplement it with "filter" or with a list comprehension. +countBlacks :: Code -> Code -> Int +countBlacks c1 c2 = codelab $ codelab codelab $ codelab codelab c1 c2 + + +-- This one computes the total number of colors in common between two +-- codes, by using ColorMaps. For each color, we take the minimum of the +-- values in each map. You will need: +-- codeToMap :: Code -> ColorMap +-- allColors :: [Color] +-- getCount :: Color -> ColorMap -> Int +-- map :: (Color -> Int) -> [Color] -> [Int] +-- sum :: [Int] -> Int +countTotal :: Code -> Code -> Int +countTotal c1 c2 = codelab $ codelab compareColor codelab + where + compareColor :: Color -> Int + compareColor color = min (codelab) (codelab) + cmap1, cmap2 :: ColorMap + cmap1 = codelab c1 + cmap2 = codelab c2 + +-- Finally, "countScore" takes two codes and computes the score. :) +countScore :: Code -> Code -> Score +countScore c1 c2 = codelab + where + black = codelab + total = codelab + white = codelab diff --git a/haskell_102/codelab/01_mastermind/src/CodeSolution.hs b/haskell_102/codelab/01_mastermind/src/CodeSolution.hs new file mode 100644 index 0000000..9888aab --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/CodeSolution.hs @@ -0,0 +1,133 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module CodeSolution where + +import Data.Map hiding (foldr, filter, map) + +import ColorSolution +import ColorMapSolution +import Internal (codelab) + + +-- SECTION 4: Code (combining functions) +-- +-- A code is a list of colors, that we will simply represent as... +-- a list of colors. This section aims at implementing the functions +-- that we will need to compare two codes. +-- +-- Basically, we compute three values: +-- total: the number of colors the codes have in common +-- black: the number of correct colors at the correct position +-- white: total - black + +type Code = [Color] +data Score = Score + { scoreBlack :: Int + , scoreWhite :: Int + } deriving Eq + +instance Show Score where + show s = concat + [ "black: ", show $ scoreBlack s + , ", white: ", show $ scoreWhite s + ] + +-- "allCodes" generates all possible codes of a given size. +-- allCodes 0 = [[]] +-- allCodes 1 = [[Red], [Yellow]...] +-- allCodes 2 = [[Red, Red], [Red, Yellow]...] +-- +-- We use list comprehension to generate the list of codes. The trick, +-- here, is recursion: the codes of size n are obtained by adding all +-- possible colors to all possible codes of size (n-1). The syntax of list +-- comprehension in Haskell is: +-- [result | value1 <- container1, value2 <- container2] +-- The same list comprehension in Python would be: +-- [result for value1 in container1 for value2 in container2] +-- +-- As a reminder: this guard syntax is equivalent to a series of if / else: +-- if s < 0 +-- then ... +-- else if s == 0 +-- then ... +-- else ... +allCodes :: Int -> [Code] +allCodes s + | s < 0 = error "allCodes: size was lower than 0" + | s == 0 = [[]] + | otherwise = [color:code | color <- allColors, code <- allCodes (s - 1)] + +-- Transforms a code into the corresponding map of Color to Int. To do so, +-- we fold ("reduce") the list, by using a ColorMap as the accumulator. You +-- will need the following functions: +-- +-- foldr :: (Color -> ColorMap -> ColorMap) -> ColorMap -> [Color] -> ColorMap +-- addColorToMap :: Color -> ColorMap -> ColorMap +-- empty :: ColorMap +codeToMap :: Code -> ColorMap +codeToMap code = foldr addColorToMap empty code + +-- This function computes the black score of two given codes. To do so, we +-- "zip" the two lists together to compare them. +-- +-- [ R G B M ] +-- ? [ R B Y M ] +-- -> [ 1 0 0 1 ] +-- -> 2 +-- +-- To compute the result, you will need: +-- +-- zipWith :: (Color -> Color -> Bool) -> [Color] -> [Color] -> [Bool] +-- (==) :: Color -> Color -> Bool +-- map :: (Bool -> Int) -> [Bool] -> [Int] +-- fromEnum :: Bool -> Int +-- sum :: [Int] -> Int +-- +-- (If this one seems complicated, try testing zip and zipWith in GHCI!) +-- For bonus points, reimplement it with "filter" or with a list comprehension. +countBlacks :: Code -> Code -> Int +countBlacks c1 c2 = sum $ map fromEnum $ zipWith (==) c1 c2 +--countBlacks c1 c2 = length $ filter id $ zipWith (==) c1 c2 + + +-- This one computes the total number of colors in common between two +-- codes, by using ColorMaps. For each color, we take the minimum of the +-- values in each map. You will need: +-- codeToMap :: Code -> ColorMap +-- allColors :: [Color] +-- getCount :: Color -> ColorMap -> Int +-- map :: (Color -> Int) -> [Color] -> [Int] +-- sum :: [Int] -> Int +countTotal :: Code -> Code -> Int +countTotal c1 c2 = sum $ map compareColor allColors + where + compareColor :: Color -> Int + compareColor color = min (getCount color cmap1) (getCount color cmap2) + cmap1, cmap2 :: ColorMap + cmap1 = codeToMap c1 + cmap2 = codeToMap c2 + +-- Finally, "countScore" takes two codes and computes the score. :) +countScore :: Code -> Code -> Score +countScore c1 c2 = Score black white + where + black = countBlacks c1 c2 + total = countTotal c1 c2 + white = total - black diff --git a/haskell_102/codelab/01_mastermind/src/Color.hs b/haskell_102/codelab/01_mastermind/src/Color.hs new file mode 100644 index 0000000..fe679e0 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/Color.hs @@ -0,0 +1,62 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module Color where + +import Internal (codelab) + + +-- SECTION 1: Color (Enum and Bounded) + +-- Our game, being a coding exercise, works with the six dev colors: red, +-- yellow, green, cyan, blue, and magenta. +data Color + = Red -- this is a constructor, of type Color + | Yellow -- same here + | Green + | Cyan + | Blue + | Magenta + deriving + ( Ord -- the compiler automagically generates the instances for these + , Eq + , Enum + , Bounded + ) + +-- We want to have a list of all the colors. We could write such a list +-- manually, but that'd be cumbersome and error-prone. Thankfully, lists +-- support interpolation! The [a .. b] syntax is translated into a call to +-- enumFromTo (defined in the Enum typeclass). Here, all you have to do is +-- figure out which color is the minimum color, and which is the max. Some +-- other typeclass might help you? +allColors :: [Color] +allColors = [minColor .. maxColor] -- this is enumFromTo minColor maxColor + where + minColor = codelab + maxColor = codelab + +-- We should also provide a way to display values of type Color. +-- Let's make `show` return only the first letter of the color's name. +instance Show Color where + show = codelab + +-- We will not write the Read instance to convert a String to a Color because +-- read is partial and we want to handle the error case ourselves (see section +-- 3). diff --git a/haskell_102/codelab/01_mastermind/src/ColorMap.hs b/haskell_102/codelab/01_mastermind/src/ColorMap.hs new file mode 100644 index 0000000..5271a6a --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/ColorMap.hs @@ -0,0 +1,65 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module ColorMap where + +import Data.Map (Map, insert, lookup) +import Data.Maybe +import Prelude hiding (lookup) + +import Color +import Internal (codelab) + + +-- SECTION 2: ColorMap (how to use maps) +-- +-- We will use color maps to count the occurrences of each color in a code. The +-- type Map comes from Data.Map. Its documentation is here: +-- +-- https://hackage.haskell.org/package/containers/docs/Data-Map-Lazy.html +-- +-- A ColorMap is a map from a Color to an Int: the code [R, R, G, B] would be +-- represented as the map: {R: 2, G: 1, B: 1}. +type ColorMap = Map Color Int + +-- This simple helper extracts an Int value out of a Maybe. If there is +-- no value to extract, it returns 0. You can implement it by pattern +-- matching, but there is a shorter way to implement it. +-- See https://hackage.haskell.org/package/base/docs/Data-Maybe.html +getIntOr0 :: Maybe Int -> Int +getIntOr0 = codelab + +-- "getCount" extracts a color count from a color map; if the color isn't +-- in the map, it returns 0 instead. To implement it, you will need a +-- lookup function: +-- +-- lookup :: key -> Map key value -> Maybe value +getCount :: Color -> ColorMap -> Int +getCount color cmap = codelab + +-- Increase the count of a color in the map by 1. Since a map is immutable, +-- you in fact create a new one with the modification. The two functions +-- you will need are: +-- +-- getCount :: Color -> ColorMap -> Int +-- insert :: Color -> Int -> ColorMap -> ColorMap +-- +-- For a fancier version, you can look up "insertWith". +addColorToMap :: Color -> ColorMap -> ColorMap +addColorToMap color cmap = codelab diff --git a/haskell_102/codelab/01_mastermind/src/ColorMapSolution.hs b/haskell_102/codelab/01_mastermind/src/ColorMapSolution.hs new file mode 100644 index 0000000..3a66784 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/ColorMapSolution.hs @@ -0,0 +1,65 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module ColorMapSolution where + +import Data.Map (Map, insertWith, lookup) +import Data.Maybe +import Prelude hiding (lookup) + +import ColorSolution +import Internal (codelab) + + +-- SECTION 2: ColorMap (how to use maps) +-- +-- We will use color maps to count the occurrences of each color in a code. The +-- type Map comes from Data.Map. Its documentation is here: +-- +-- https://hackage.haskell.org/package/containers/docs/Data-Map-Lazy.html +-- +-- A ColorMap is a map from a Color to an Int: the code [R, R, G, B] would be +-- represented as the map: {R: 2, G: 1, B: 1}. +type ColorMap = Map Color Int + +-- This simple helper extracts an Int value out of a Maybe. If there is +-- no value to extract, it returns 0. You can implement it by pattern +-- matching, but there is a shorter way to implement it. +-- See https://hackage.haskell.org/package/base/docs/Data-Maybe.html +getIntOr0 :: Maybe Int -> Int +getIntOr0 = fromMaybe 0 + +-- "getCount" extracts a color count from a color map; if the color isn't +-- in the map, it returns 0 instead. To implement it, you will need a +-- lookup function: +-- +-- lookup :: key -> Map key value -> Maybe value +getCount :: Color -> ColorMap -> Int +getCount color cmap = getIntOr0 $ lookup color cmap + +-- Increase the count of a color in the map by 1. Since a map is immutable, +-- you in fact create a new one with the modification. The two functions +-- you will need are: +-- +-- getCount :: Color -> ColorMap -> Int +-- insert :: Color -> Int -> ColorMap -> ColorMap +-- +-- For a fancier version, you can look up "insertWith". +addColorToMap :: Color -> ColorMap -> ColorMap +addColorToMap color cmap = insertWith (+) color 1 cmap diff --git a/haskell_102/codelab/01_mastermind/src/ColorSolution.hs b/haskell_102/codelab/01_mastermind/src/ColorSolution.hs new file mode 100644 index 0000000..1769bc8 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/ColorSolution.hs @@ -0,0 +1,67 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module ColorSolution where + +import Internal (codelab) + + +-- SECTION 1: Color (Enum and Bounded) + +-- Our game, being a coding exercise, works with the six dev colors: red, +-- yellow, green, cyan, blue, and magenta. +data Color + = Red -- this is a constructor, of type Color + | Yellow -- same here + | Green + | Cyan + | Blue + | Magenta + deriving + ( Ord -- the compiler automagically generates the instances for these + , Eq + , Enum + , Bounded + ) + +-- We want to have a list of all the colors. We could write such a list +-- manually, but that'd be cumbersome and error-prone. Thankfully, lists +-- support interpolation! The [a .. b] syntax is translated into a call to +-- enumFromTo (defined in the Enum typeclass). Here, all you have to do is +-- figure out which color is the minimum color, and which is the max. Some +-- other typeclass might help you? +allColors :: [Color] +allColors = [minColor .. maxColor] -- this is enumFromTo minColor maxColor + where + minColor = Red + maxColor = Magenta + +-- We should also provide a way to display values of type Color. +-- Let's make `show` return only the first letter of the color's name. +instance Show Color where + show Red = "R" + show Yellow = "Y" + show Green = "G" + show Cyan = "C" + show Blue = "B" + show Magenta = "M" + +-- We will not write the Read instance to convert a String to a Color because +-- read is partial and we want to handle the error case ourselves (see section +-- 3). diff --git a/haskell_102/codelab/01_mastermind/src/Do.hs b/haskell_102/codelab/01_mastermind/src/Do.hs new file mode 100644 index 0000000..d592fc0 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/Do.hs @@ -0,0 +1,94 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module Do where + +import Code +import Color +import Internal (codelab) + + +-- SECTION 5: "do" notation +-- +-- While "do" notation is commonly used for "imperative" code, it can be used +-- with anything that is a monad (or even an applicative). As we have seen in +-- the slides, lists are monads, meaning we can use "do" notation to write our +-- list generators in a very flexible way! +-- +-- For example, a generator like this: +-- +-- [x + 1 | x <- [1..10]] +-- +-- can be written using "do" notation like this: +-- +-- do x <- [1..10] +-- return $ x + 1 +-- +-- Code inside the "do" block must evaluate to a "wrapped" value, which would be +-- a list in our case. If you want to return just one value, you can use the +-- "return" function to wrap a single value to produce a value in the context. + +-- Can you rewrite "allCodes" to use the "do" notation? +allCodesDo :: Int -> [Code] +allCodesDo s + | s < 0 = error "allCodes: size was lower than 0" + | s == 0 = codelab + | otherwise = do + color <- codelab + code <- codelab + return codelab + +-- Unlike generators, a "do" block can return any wrapped value. For lists, it +-- means it can return any list, not necessarily a list of length 1. Let's build +-- a "generator" using the "do" notation that would produce a list of duplicates +-- of the specified length. For example, for length 5 it should produce +-- +-- [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] +duplicatesList :: Int -> [Int] +duplicatesList len = do + i <- [1..codelab] + codelab + +-- What if we want the different "blocks" to have different lengths? Let's build +-- a "generator" similar to the previous one, but that would duplicate only odd +-- values. For example, for length 5 it should produce +-- +-- [1, 1, 2, 3, 3, 4, 5, 5] +-- +-- Do not forget about Hoogle, should you need a new function. +oddlyDuplicateList :: Int -> [Int] +oddlyDuplicateList len = do + codelab + +-- Think about the fact that when coding in "do" notation you have the full +-- power of the language, but you are building something like a generator. For +-- imperative code that uses IO, your generator is producing sequences of +-- actions that your application needs to execute to actually achieve the +-- desired result. +-- +-- This approach provides an amazing level of flexibility in terms of +-- abstraction. You can apply all the abstraction, code sharing and other +-- techniques to the code that produces imperatives actions, as you normally +-- apply to the code that is building lists. + +-- You've reached the end of the codelab ! +-- But we're not done with the game itself: we haven't seen any error handling, +-- user input, or even IO! The rest of the code is in Game.hs: though there is +-- no codelab function for you to replace there, please take the time to read it +-- and try the functions in GHCI. diff --git a/haskell_102/codelab/01_mastermind/src/DoSolution.hs b/haskell_102/codelab/01_mastermind/src/DoSolution.hs new file mode 100644 index 0000000..5527eb5 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/DoSolution.hs @@ -0,0 +1,95 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module DoSolution where + +import CodeSolution +import ColorSolution +import Internal (codelab) + + +-- SECTION 5: "do" notation +-- +-- While "do" notation is commonly used for "imperative" code, it can be used +-- with anything that is a monad (or even an applicative). As we have seen in +-- the slides, lists are monads, meaning we can use "do" notation to write our +-- list generators in a very flexible way! +-- +-- For example, a generator like this: +-- +-- [x + 1 | x <- [1..10]] +-- +-- can be written using "do" notation like this: +-- +-- do x <- [1..10] +-- return $ x + 1 +-- +-- Code inside the "do" block must evaluate to a "wrapped" value, which would be +-- a list in our case. If you want to return just one value, you can use the +-- "return" function to wrap a single value to produce a value in the context. + +-- Can you rewrite "allCodes" to use the "do" notation? +allCodesDo :: Int -> [Code] +allCodesDo s + | s < 0 = error "allCodes: size was lower than 0" + | s == 0 = [[]] + | otherwise = do + color <- allColors + code <- allCodesDo $ s - 1 + return $ color : code + +-- Unlike generators, a "do" block can return any wrapped value. For lists, it +-- means it can return any list, not necessarily a list of length 1. Let's build +-- a "generator" using the "do" notation that would produce a list of duplicates +-- of the specified length. For example, for length 5 it should produce +-- +-- [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] +duplicatesList :: Int -> [Int] +duplicatesList len = do + i <- [1..len] + [i, i] + +-- What if we want the different "blocks" to have different lengths? Let's build +-- a "generator" similar to the previous one, but that would duplicate only odd +-- values. For example, for length 5 it should produce +-- +-- [1, 1, 2, 3, 3, 4, 5, 5] +-- +-- Do not forget about Hoogle, should you need a new function. +oddlyDuplicateList :: Int -> [Int] +oddlyDuplicateList len = do + i <- [1..len] + if odd i then [i, i] else [i] + +-- Think about the fact that when coding in "do" notation you have the full +-- power of the language, but you are building something like a generator. For +-- imperative code that uses IO, your generator is producing sequences of +-- actions that your application needs to execute to actually achieve the +-- desired result. +-- +-- This approach provides an amazing level of flexibility in terms of +-- abstraction. You can apply all the abstraction, code sharing and other +-- techniques to the code that produces imperatives actions, as you normally +-- apply to the code that is building lists. + +-- You've reached the end of the codelab ! +-- But we're not done with the game itself: we haven't seen any error handling, +-- user input, or even IO! The rest of the code is in Game.hs: though there is +-- no codelab function for you to replace there, please take the time to read it +-- and try the functions in GHCI. diff --git a/haskell_102/codelab/01_mastermind/src/ErrorOr.hs b/haskell_102/codelab/01_mastermind/src/ErrorOr.hs new file mode 100644 index 0000000..755b07a --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/ErrorOr.hs @@ -0,0 +1,106 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module ErrorOr where + +import Color +import Internal (codelab) + + +-- SECTION 3: ErrorOr (handling errors in pure code) +-- +-- We know about Maybe. It is used to represent an optional value. But +-- sometimes, when a computation fails, we want to have more information than +-- just a "Nothing" value. +-- +-- For those purposes, we'll introduce here the type "ErrorOr". The error it may +-- or may not contain is simply a String. +-- +-- Interestingly, it is a Monad! We'll implement the Monad typeclass for it. +-- +-- TO GO FURTHER: there is a built-in type that does exactly the same thing: it +-- is Either a b; though there is no need to know about it for this codelab, you +-- can read about it here: +-- https://hackage.haskell.org/package/base/docs/Data-Either.html + +-- An error message is just a String. +type ErrorMsg = String + +-- ErrorOr has two constructors; a value of type ErrorOr a is either an error +-- message or a wrapped value. +data ErrorOr a + = Error ErrorMsg -- an error with a message + | Value a -- a wrapped value of type a + deriving (Show, Eq) + +-- "wrapValue" takes a value, and puts it in the context of an "ErrorOr a". +wrapValue :: a -> ErrorOr a +wrapValue = codelab + +-- "fmapValue" takes a function, and tries to apply it on the value inside the +-- "ErrorOr a". If it cannot apply the function because the "ErrorOr a" contains +-- an error, it simply returns this existing error. We do a simple pattern match +-- to decide what to do. +fmapValue :: (a -> b) -> ErrorOr a -> ErrorOr b +fmapValue _ (Error msg) = codelab +fmapValue f (Value x) = codelab + +-- "apValue" is the version of "ap" for our "ErrorOr" type. The first value is +-- an "ErrorOr (a -> b)": if we indeed have a function in it, we can apply it on +-- the second argument; if we don't, we simply keep the error. To apply the +-- function, we will need a way to apply a function on a contextual value... +apValue :: ErrorOr (a -> b) -> ErrorOr a -> ErrorOr b +apValue (Error msg) _ = codelab +apValue (Value f) eoa = codelab + +-- Finally, "bindValue" is our version of "bind". It works exactly like +-- "fmapValue", except we don't have to wrap the result. +bindValue :: (a -> ErrorOr b) -> ErrorOr a -> ErrorOr b +bindValue _ (Error msg) = codelab +bindValue f (Value x) = codelab + +-- Using the functions declared in the Codelab, we can now write the instances +-- of our three beloved typeclasses for ErrorOr. +-- +-- The functions that we need for the instances are: +-- +-- fmap :: (a -> b) -> ErrorOr a -> ErrorOr b +-- +-- pure :: a -> ErrorOr a +-- (<*>) :: ErrorOr (a -> b) -> ErrorOr a -> ErrorOr b +-- +-- return :: a -> ErrorOr a +-- (>>=) :: ErrorOr a -> (a -> ErrorOr b) -> ErrorOr b +-- +-- We need both pure and return for historical reasons, even if they do the same +-- thing. +instance Functor ErrorOr where + fmap = fmapValue + +instance Applicative ErrorOr where + pure = wrapValue + (<*>) = apValue + +instance Monad ErrorOr where + return = wrapValue + (>>=) = flip bindValue + +-- Having ErrorOr, we can define a safe function to convert a Char to a Color. +readColor :: Char -> ErrorOr Color +readColor = codelab diff --git a/haskell_102/codelab/01_mastermind/src/ErrorOrSolution.hs b/haskell_102/codelab/01_mastermind/src/ErrorOrSolution.hs new file mode 100644 index 0000000..1b42cf9 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/ErrorOrSolution.hs @@ -0,0 +1,112 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# OPTIONS_GHC -fno-warn-unused-imports #-} +{-# OPTIONS_GHC -fno-warn-unused-matches #-} +{-# OPTIONS_GHC -fno-warn-unused-binds #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module ErrorOrSolution where + +import ColorSolution +import Internal (codelab) + + +-- SECTION 3: ErrorOr (handling errors in pure code) +-- +-- We know about Maybe. It is used to represent an optional value. But +-- sometimes, when a computation fails, we want to have more information than +-- just a "Nothing" value. +-- +-- For those purposes, we'll introduce here the type "ErrorOr". The error it may +-- or may not contain is simply a String. +-- +-- Interestingly, it is a Monad! We'll implement the Monad typeclass for it. +-- +-- TO GO FURTHER: there is a built-in type that does exactly the same thing: it +-- is Either a b; though there is no need to know about it for this codelab, you +-- can read about it here: +-- https://hackage.haskell.org/package/base/docs/Data-Either.html + +-- An error message is just a String. +type ErrorMsg = String + +-- ErrorOr has two constructors; a value of type ErrorOr a is either an error +-- message or a wrapped value. +data ErrorOr a + = Error ErrorMsg -- an error with a message + | Value a -- a wrapped value of type a + deriving (Show, Eq) + +-- "wrapValue" takes a value, and puts it in the context of an "ErrorOr a". +wrapValue :: a -> ErrorOr a +wrapValue = Value + +-- "fmapValue" takes a function, and tries to apply it on the value inside the +-- "ErrorOr a". If it cannot apply the function because the "ErrorOr a" contains +-- an error, it simply returns this existing error. We do a simple pattern match +-- to decide what to do. +fmapValue :: (a -> b) -> ErrorOr a -> ErrorOr b +fmapValue _ (Error msg) = Error msg +fmapValue f (Value x) = Value $ f x + +-- "apValue" is the version of "ap" for our "ErrorOr" type. The first value is +-- an "ErrorOr (a -> b)": if we indeed have a function in it, we can apply it on +-- the second argument; if we don't, we simply keep the error. To apply the +-- function, we will need a way to apply a function on a contextual value... +apValue :: ErrorOr (a -> b) -> ErrorOr a -> ErrorOr b +apValue (Error msg) _ = Error msg +apValue (Value f) eoa = fmapValue f eoa + +-- Finally, "bindValue" is our version of "bind". It works exactly like +-- "fmapValue", except we don't have to wrap the result. +bindValue :: (a -> ErrorOr b) -> ErrorOr a -> ErrorOr b +bindValue _ (Error msg) = Error msg +bindValue f (Value x) = f x + +-- Using the functions declared in the Codelab, we can now write the instances +-- of our three beloved typeclasses for ErrorOr. +-- +-- The functions that we need for the instances are: +-- +-- fmap :: (a -> b) -> ErrorOr a -> ErrorOr b +-- +-- pure :: a -> ErrorOr a +-- (<*>) :: ErrorOr (a -> b) -> ErrorOr a -> ErrorOr b +-- +-- return :: a -> ErrorOr a +-- (>>=) :: ErrorOr a -> (a -> ErrorOr b) -> ErrorOr b +-- +-- We need both pure and return for historical reasons, even if they do the same +-- thing. +instance Functor ErrorOr where + fmap = fmapValue + +instance Applicative ErrorOr where + pure = wrapValue + (<*>) = apValue + +instance Monad ErrorOr where + return = wrapValue + (>>=) = flip bindValue + +-- Having ErrorOr, we can define a safe function to convert a Char to a Color. +readColor :: Char -> ErrorOr Color +readColor 'R' = Value Red +readColor 'Y' = Value Yellow +readColor 'G' = Value Green +readColor 'C' = Value Cyan +readColor 'B' = Value Blue +readColor 'M' = Value Magenta +readColor c = Error $ '\'' : c : "' is not a proper color." diff --git a/haskell_102/codelab/01_mastermind/src/Game.hs b/haskell_102/codelab/01_mastermind/src/Game.hs new file mode 100644 index 0000000..5303e32 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/Game.hs @@ -0,0 +1,150 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# LANGUAGE CPP #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Game (startGameAsHuman, startGameAsAI) where + +import System.IO +import System.Random + +#ifdef SOLUTION +import CodeSolution +import ColorSolution +import ErrorOrSolution +#else +import Code +import Color +import ErrorOr +#endif + +-- We only export the startGame function and the ways to play: human or AI. +startGame :: Input -> IO () +startGame = play (randomCode kSeed kCodeSize) kMaxTurns + +startGameAsHuman = startGame humanInput +startGameAsAI = startGame computerInput + + +-- Here, sequence transforms a list of monadic values into a monadic list of +-- values: it moves the context towards the outside. +-- Here on our type it does: [ErrorOr Color] -> ErrorOr [Color] +readCode :: String -> ErrorOr Code +readCode = sequence . map readColor + + +-- Let's build an automatic solver! We need to keep track of the game we're +-- playing, so we introduce a "History" type which records our guesses and their +-- score. + +type History = [(Code, Score)] + +check :: History -> Code -> Bool +check hist candidate = all consistentWithCandidate hist + where + consistentWithCandidate (code, score) = countScore code candidate == score + +-- naiveSolver codeSize history = head $ filter (check history) $ allColors codeSize +naiveSolver :: Int -> History -> Code +naiveSolver codeSize history = + let codes = allCodes codeSize -- we generate all possible codes of that size + goodCodes = filter (check history) codes -- we keep only the ones that match our history + in head goodCodes -- we simply pick the first one + + +-- This is where we link everything together. +-- We define an Input function type, which we use to abstract the player. +-- * humanInput uses getCode to read a code from stdin +-- * computerInput uses naiveSolver to guess the next code + +validateCode :: Int -> String -> ErrorOr Code +validateCode size input = do + code <- readCode input -- readCode input :: ErrorOr Code + if length code == size -- but code :: Code + then Value code + else Error $ "Expecting a code of size " ++ show size ++ "." + +getCode :: Int -> Int -> IO Code +getCode size turn = do + putStr $ "Turn " ++ show turn ++ ": " -- print the prompt + hFlush stdout -- flush stdout + line <- getLine -- read the input + case validateCode size line of -- we try to read the code + Error msg -> do -- if it wasn't a proper code + putStrLn msg -- we print the error + getCode size turn -- and we retry + Value code -> return code -- otherwise, perfect! + + +type Input = Int -> Int -> History -> IO Code + +humanInput :: Input +humanInput size turn _ = getCode size turn + +computerInput :: Input +computerInput size _ hist = return $ naiveSolver size hist + + +play :: Code -> Int -> Input -> IO () +play answer maxTurn input = do + putStrLn $ "Valid colors: " ++ show allColors + putStrLn $ "Size of the answer: " ++ show (length answer) + putStrLn $ "Number of tries: " ++ show maxTurn + putStrLn $ "Good luck!" + playTurn 1 [] + where + playTurn turn history + | turn > maxTurn = putStrLn $ "Sorry, you lost! The answer was " ++ show answer + | otherwise = do + code <- input (length answer) turn history + if code == answer + then putStrLn $ show code ++ " => well done!" + else do + let score = countScore answer code + putStrLn $ show code ++ " => " ++ show score + playTurn (turn + 1) $ (code, score) : history + + +-- Random code generation. + +-- We use a fixed seed, in order to be able to replay the same game over +-- and over again. If you read this, as an exercise you could think about +-- what it would entail to have a seed as a parameter to our program. + +-- randomCode drops the first generated element to mitigate the poor +-- implementation of System.Random. + +type Seed = Int + +instance Random Color where + random g = randomR (minBound, maxBound) g + randomR (a, b) g = let (i, n) = randomR (fromEnum a, fromEnum b) g + in (toEnum i, n) + +randomCode :: Seed -> Int -> Code +randomCode seed size = take size $ drop 1 $ randoms $ mkStdGen seed + + +-- Game constants +-- Those are the constants on which our game rely. + +kMaxTurns :: Int +kMaxTurns = 8 + +kCodeSize :: Int +kCodeSize = 4 + +kSeed :: Seed +kSeed = 756 diff --git a/haskell_102/codelab/01_mastermind/src/Internal.hs b/haskell_102/codelab/01_mastermind/src/Internal.hs new file mode 100644 index 0000000..7f22695 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/Internal.hs @@ -0,0 +1,88 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# LANGUAGE CPP #-} + +module Internal where + +import Control.Exception (ErrorCall(..), catch) +import Control.Monad (void, when) +import System.Console.ANSI + ( Color(..), ColorIntensity(..), ConsoleLayer(..), SGR(..) + , hSupportsANSI, setSGR) +import System.Exit (exitFailure) +import System.IO (stdout) +import System.Timeout (timeout) +import Text.Printf (printf) + +-- for students: what they need to implement + +codelab :: a +codelab = error "SOMETHING IS NOT IMPLEMENTED!" + + +-- tests, checking + +type Test = IO Bool +type Tests = [Test] + +check :: [Test] -> IO () +check tests = do + failing <- (length . filter not) <$> sequence tests + when (failing > 0) exitFailure + +test :: (Show a, Eq a) => String -> a -> a -> IO Bool +test name expected actual = do + void $ printf "%-42s" name + result <- runTest >>= maybe (False <$ onTimeout) return + putStrLn "" + return result + where + runTest = timeout timeLimit $ catch checkEq $ ifError $ (False <$) . onFailure + onSuccess = putTag "OK" kG >> printf " got: %s" (show actual) + onError = putTag "KO" kR >> printf " want: %s; got: %s" (show expected) (show actual) + onFailure s = putTag "!!" kR >> printf " error: %s" s + onTimeout = putTag "??" kR >> putStr " (timeout)" + timeLimit = 1000000 -- 10^6 microseconds = 1 second + checkEq + | expected == actual = True <$ onSuccess + | otherwise = False <$ onError + +ifError :: (String -> a) -> ErrorCall -> a +#if __GLASGOW_HASKELL__ < 800 +ifError f (ErrorCall s) = f s +#else +ifError f (ErrorCallWithLocation s _) = f s +#endif + + +-- colors for the terminal + +type TermColor = [SGR] + +kR = [SetColor Foreground Dull Red] +kG = [SetColor Foreground Dull Green] + +putTag :: String -> TermColor -> IO () +putTag tag color = do + supportsANSIColors <- hSupportsANSI stdout + if supportsANSIColors + then putColoredTag tag color + else putSimpleTag tag + +putColoredTag :: String -> TermColor -> IO () +putColoredTag s sgrColor = setSGR sgrColor >> putStr s >> setSGR [] + +putSimpleTag :: String -> IO () +putSimpleTag = putStr diff --git a/haskell_102/codelab/01_mastermind/src/Main.hs b/haskell_102/codelab/01_mastermind/src/Main.hs new file mode 100644 index 0000000..a3aaf3c --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/Main.hs @@ -0,0 +1,48 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +module Main where + +import Control.Monad (join) +import Options.Applicative + +import Tests (check) +import Game (startGameAsAI, startGameAsHuman) + +-- Parsing arguments uses these typeclasses: +-- - Functor: (<$>) +-- - Applicative: pure +-- - Monad: join +-- - Monoid: (<>) +-- +-- Also, we use several helpers from Options.Applicative. +-- +-- You might want to look on Hoogle/Hackage for each of them. +main :: IO () +main = join . customExecParser preferences $ info modes idm + where + -- The modes under which the program is run + modes = subparser + ( command "play" (info (pure startGameAsHuman) playHelp) + <> command "solve" (info (pure startGameAsAI) solveHelp) + <> command "check" (info (check <$> argument auto argHelp) checkHelp) + ) + -- We prefer to display help on error or if no argument is supplied + preferences = prefs (showHelpOnError <> showHelpOnEmpty) + -- For "check", we want to display a better help message + argHelp = metavar "SECTION" <> help "Section to test" + -- Help messages for every mode + playHelp = progDesc "Play using human input" + solveHelp = progDesc "Play using AI input" + checkHelp = progDesc "Check one section of codelab" diff --git a/haskell_102/codelab/01_mastermind/src/Tests.hs b/haskell_102/codelab/01_mastermind/src/Tests.hs new file mode 100644 index 0000000..7c1c8eb --- /dev/null +++ b/haskell_102/codelab/01_mastermind/src/Tests.hs @@ -0,0 +1,166 @@ +-- Copyright 2021 Google LLC +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +{-# LANGUAGE CPP #-} + +module Tests (check) where + +import Data.Function (on) +import Data.IntMap (IntMap) +import qualified Data.IntMap as IntMap +import Data.List (nub) +import qualified Data.Map as Map +import Text.Printf (printf) + +#ifdef SOLUTION +import CodeSolution +import ColorSolution +import ColorMapSolution +import DoSolution +import ErrorOrSolution +#else +import Code +import Color +import ColorMap +import Do +import ErrorOr +#endif + +import Internal (test, Tests) +import qualified Internal (check) + +-- Check one section of the codelab +check :: Int -> IO () +check x = case IntMap.lookup x testMap of + Just (file, tests) -> doCheck file tests + _ -> noSuchSection + +-- When we check a code section, we print the file under test and then the +-- results of all tests from there +doCheck :: String -> Tests -> IO () +doCheck file tests = do + putStr "Checking code from " >> putStrLn file + Internal.check tests + +-- If user supplies invalid arguments, we should print the valid arguments and +-- the files under test +noSuchSection :: IO () +noSuchSection = do + putStrLn "Requested invalid section. Available sections are:" + mapM_ displaySection $ IntMap.toAscList testMap + where + displaySection (k, (f, _)) = printf "\t%d -> %s\n" k f + +-- We record a mapping section -> (file, tests) +testMap :: IntMap (String, Tests) +testMap = IntMap.fromList + [ (1, ("src/Color.hs", colorTests)) + , (2, ("src/ColorMap.hs", colorMapTests)) + , (3, ("src/ErrorOr.hs", errorOrTests)) + , (4, ("src/Code.hs", codeTests)) + , (5, ("src/Do.hs", doTests)) + ] + +colorTests :: Tests +colorTests = + [ test "allColors contains Red" True $ elem Red allColors + , test "allColors contains Yellow" True $ elem Yellow allColors + , test "allColors contains Green" True $ elem Green allColors + , test "allColors contains Cyan" True $ elem Cyan allColors + , test "allColors contains Blue" True $ elem Blue allColors + , test "allColors contains Magenta" True $ elem Magenta allColors + , test "allColors size is 6" 6 $ length allColors + , test "show Red" "R" $ show Red + , test "concatMap show allColors" "RYGCBM" $ concatMap show allColors + , test "allColors starts with Red" Red $ head allColors + , test "allColors ends with Magenta" Magenta $ last allColors + ] + +colorMapTests :: Tests +colorMapTests = + [ test "getIntOr0 (Just 42)" 42 $ getIntOr0 (Just 42) + , test "getIntOr0 Nothing" 0 $ getIntOr0 Nothing + , test "getCount on empty map" 0 $ getCount Cyan Map.empty + , test "getCount on map" 2 $ getCount Cyan (mk Cyan 2) + , test "add color in empty map" (mk Blue 1) $ addColorToMap Blue Map.empty + , test "add color in map" (mk Blue 3) $ addColorToMap Blue (mk Blue 2) + ] + where + mk c x = Map.fromList [(c, x)] + +errorOrTests :: Tests +errorOrTests = + [ test "wrapValue on Int" (Value 42) $ wrapValue 42 + , test "wrapValue on String" (Value "foo") $ wrapValue "foo" + , test "fmapValue show on Int" (Value $ show 42) $ fmapValue show (Value 42) + , test "fmapValue length on String" (Value $ length "foo") $ fmapValue length (Value "foo") + , test "fmapValue show on Error" (Error "OH NOES") $ fmapValue id (Error "OH NOES" :: ErrorOr String) + , test "apValue function on value" (Value "42") $ apValue (Value show :: ErrorOr (Int -> String)) (Value 42) + , test "apValue function on error" (Error "WAT") $ apValue (Value show :: ErrorOr (Int -> String)) (Error "WAT") + , test "apValue error on value" (Error "OH NOES") $ apValue (Error "OH NOES" :: ErrorOr (Int -> String)) (Value 42) + , test "apValue error on error" (Error "OH NOES") $ apValue (Error "OH NOES" :: ErrorOr (Int -> String)) (Error "WAT") + , test "bindValue on good Int" (Value 42) $ bindValue fun (Value 42) + , test "bindValue on bad Int" (Error "ODD X") $ bindValue fun (Value 21) + , test "bindValue on Error" (Error "OH NOES") $ bindValue fun (Error "OH NOES") + , test "readColor 'R'" (Value Red) $ readColor 'R' + , test "readColor 'Y'" (Value Yellow) $ readColor 'Y' + , test "readColor 'G'" (Value Green) $ readColor 'G' + , test "readColor 'C'" (Value Cyan) $ readColor 'C' + , test "readColor 'B'" (Value Blue) $ readColor 'B' + , test "readColor 'M'" (Value Magenta) $ readColor 'M' + , test "readColor 'Z'" (Error "'Z' is not a proper color.") $ readColor 'Z' + ] + where + fun x = if even x then Value x else Error "ODD X" + +codeTests :: Tests +codeTests = + [ test "# codes of size 0: 1" 1 $ length $ allCodes 0 + , test "# codes of size 1: 6" 6 $ length $ allCodes 1 + , test "# codes of size 4: 1296" 1296 $ length $ allCodes 4 + , test "all codes 0 have size 0" [0] $ nub $ length <$> allCodes 0 + , test "all codes 1 have size 1" [1] $ nub $ length <$> allCodes 1 + , test "all codes 4 have size 4" [4] $ nub $ length <$> allCodes 4 + , test "no duplicated codes" True $ on (==) length (allCodes 4) (nub $ allCodes 4) + , test "empty code -> empty map" Map.empty $ codeToMap [] + , test "[C,R,C] -> {R: 1, C: 2}" (Map.fromList [(Red, 1), (Cyan, 2)]) $ codeToMap [Cyan, Red, Cyan] + , test "countBlacks [R,Y,G,B] [B,R,Y,G]" 0 $ countBlacks [Red, Yellow, Green, Blue] [Blue, Red, Yellow, Green] + , test "countBlacks [R,Y,G,B] [R,B,G,Y]" 2 $ countBlacks [Red, Yellow, Green, Blue] [Red, Blue, Green, Yellow] + , test "countBlacks [B,B,C,G] [Y,B,G,C]" 1 $ countBlacks [Blue, Blue, Cyan, Green] [Yellow, Blue, Green, Cyan] + , test "countBlacks [B,B,C,G] [B,B,C,G]" 4 $ countBlacks [Blue, Blue, Cyan, Green] [Blue, Blue, Cyan, Green] + , test "countTotal [C,R,B,M] [Y,R,G,G]" 1 $ countTotal [Cyan, Red, Blue, Magenta] [Yellow, Red, Green, Green] + , test "countTotal [C,R,B,M] [Y,Y,C,M]" 2 $ countTotal [Cyan, Red, Blue, Magenta] [Yellow, Yellow, Cyan, Magenta] + , test "countTotal [C,R,B,M] [Y,R,C,M]" 3 $ countTotal [Cyan, Red, Blue, Magenta] [Yellow, Red, Cyan, Magenta] + , test "countTotal [B,B,C,G] [Y,B,G,C]" 3 $ countTotal [Blue, Blue, Cyan, Green] [Yellow, Blue, Green, Cyan] + , test "countTotal [B,B,C,G] [B,B,C,G]" 4 $ countTotal [Blue, Blue, Cyan, Green] [Blue, Blue, Cyan, Green] + , test "countScore [B,B,C,G] [R,R,R,R]" (Score 0 0) $ countScore [Blue, Blue, Cyan, Green] [Red, Red, Red, Red] + , test "countScore [B,B,C,G] [Y,B,G,C]" (Score 1 2) $ countScore [Blue, Blue, Cyan, Green] [Yellow, Blue, Green, Cyan] + , test "countScore [B,B,C,G] [B,B,C,G]" (Score 4 0) $ countScore [Blue, Blue, Cyan, Green] [Blue, Blue, Cyan, Green] + ] + +doTests :: Tests +doTests = + [ test "# codes of size 0: 1" 1 $ length $ allCodesDo 0 + , test "# codes of size 1: 6" 6 $ length $ allCodesDo 1 + , test "# codes of size 4: 1296" 1296 $ length $ allCodesDo 4 + , test "all codes 0 have size 0" [0] $ nub $ length <$> allCodesDo 0 + , test "all codes 1 have size 1" [1] $ nub $ length <$> allCodesDo 1 + , test "all codes 4 have size 4" [4] $ nub $ length <$> allCodesDo 4 + , test "no duplicated codes" True $ on (==) length (allCodesDo 4) (nub $ allCodesDo 4) + , test "len: 0: []" [] $ duplicatesList 0 + , test "len: 3: [1, 1, 2, 2, 3, 3]" [1, 1, 2, 2, 3, 3] $ duplicatesList 3 + , test "len: 0: []" [] $ oddlyDuplicateList 0 + , test "len: 3: [1, 1, 2, 3, 3]" [1, 1, 2, 3, 3] $ oddlyDuplicateList 3 + , test "len: 5: [1, 1, 2, 3, 3, 4, 5, 5]" [1, 1, 2, 3, 3, 4, 5, 5] $ oddlyDuplicateList 5 + ] diff --git a/haskell_102/codelab/01_mastermind/stack.yaml b/haskell_102/codelab/01_mastermind/stack.yaml new file mode 100644 index 0000000..661db76 --- /dev/null +++ b/haskell_102/codelab/01_mastermind/stack.yaml @@ -0,0 +1,17 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resolver: lts-14.25 +packages: +- .