diff --git a/.gitignore b/.gitignore index 3b5dbd594..cb25100c1 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,17 @@ TAGS # other .DS_Store .env +/out/production/learn4haskell/Chapter1.hs +/out/production/learn4haskell/Chapter2.hs +/out/production/learn4haskell/Chapter3.hs +/out/production/learn4haskell/Chapter4.hs +/out/test/learn4haskell/Test/Chapter1.hs +/out/test/learn4haskell/Test/Chapter2.hs +/out/test/learn4haskell/Test/Chapter3.hs +/out/test/learn4haskell/Test/Chapter4.hs +/out/test/learn4haskell/DoctestChapter1.hs +/out/test/learn4haskell/DoctestChapter2.hs +/out/test/learn4haskell/DoctestChapter3.hs +/out/test/learn4haskell/DoctestChapter4.hs +/out/test/learn4haskell/Spec.hs +/stack.yaml diff --git a/src/Chapter1.hs b/src/Chapter1.hs index 406deeaca..2808645ab 100644 --- a/src/Chapter1.hs +++ b/src/Chapter1.hs @@ -209,31 +209,31 @@ So, the output in this example means that 'False' has type 'Bool'. > Try to guess first and then compare your expectations with GHCi output >>> :t True - +True :: Bool >>> :t 'a' - +'a' :: Char >>> :t 42 - +42 :: Num a => a A pair of boolean and char: >>> :t (True, 'x') - +(True, 'x') :: (Bool, Char) Boolean negation: >>> :t not - +not :: Bool -> Bool Boolean 'and' operator: >>> :t (&&) - +(&&) :: Bool -> Bool -> Bool Addition of two numbers: >>> :t (+) - +(+) :: Num a => a -> a -> a Maximum of two values: >>> :t max - +max :: Ord a => a -> a -> a You might not understand each type at this moment, but don't worry! You've only started your Haskell journey. Types will become your friends soon. @@ -301,43 +301,43 @@ expressions in GHCi functions and operators first. Remember this from the previous task? ;) >>> 1 + 2 - +3 >>> 10 - 15 - +-5 >>> 10 - (-5) -- negative constants require () - +15 >>> (3 + 5) < 10 - +True >>> True && False - +False >>> 10 < 20 || 20 < 5 - +True >>> 2 ^ 10 -- power - +1024 >>> not False - +True >>> div 20 3 -- integral division - +6 >>> mod 20 3 -- integral division remainder - +2 >>> max 4 10 - +10 >>> min 5 (max 1 2) - +2 >>> max (min 1 10) (min 5 7) - +5 Because Haskell is a __statically-typed__ language, you see an error each time you try to mix values of different types in situations where you are not @@ -428,7 +428,7 @@ task is to specify the type of this function. >>> squareSum 3 4 49 -} - +squareSum :: Int -> Int -> Int squareSum x y = (x + y) * (x + y) @@ -449,7 +449,7 @@ Implement the function that takes an integer value and returns the next 'Int'. function body with the proper implementation. -} next :: Int -> Int -next x = error "next: not implemented!" +next x = x + 1 {- | After you've implemented the function (or even during the implementation), you @@ -489,9 +489,8 @@ Implement a function that returns the last digit of a given number. results. Or you can try to guess the function name, search for it and check whether it works for you! -} --- DON'T FORGET TO SPECIFY THE TYPE IN HERE -lastDigit n = error "lastDigit: Not implemented!" - +lastDigit :: Int -> Int +lastDigit n = abs n `mod` 10 {- | =⚔️= Task 6 @@ -520,7 +519,7 @@ branches because it is an expression and it must always return some value. satisfying the check will be returned and, therefore, evaluated. -} closestToZero :: Int -> Int -> Int -closestToZero x y = error "closestToZero: not implemented!" +closestToZero x y = if (abs x) < (abs y) then x else y {- | @@ -553,8 +552,11 @@ value after "=" where the condition is true. Casual reminder about adding top-level type signatures for all functions :) -} - -mid x y z = error "mid: not implemented!" +mid :: Int -> Int -> Int -> Int +mid x y z + | x <= y && y <= z = y + | x >= y && y >= z = y + | otherwise = mid y z x {- | =⚔️= Task 8 @@ -568,7 +570,19 @@ True >>> isVowel 'x' False -} -isVowel c = error "isVowel: not implemented!" +isVowel :: Char -> Bool +isVowel c + | c == 'a' = True + | c == 'e' = True + | c == 'i' = True + | c == 'o' = True + | c == 'u' = True + | c == 'A' = True + | c == 'E' = True + | c == 'I' = True + | c == 'O' = True + | c == 'U' = True + | otherwise = False {- | @@ -631,9 +645,12 @@ Implement a function that returns the sum of the last two digits of a number. Try to introduce variables in this task (either with let-in or where) to avoid specifying complex expressions. -} - -sumLast2 n = error "sumLast2: Not implemented!" - +sumLast2 :: Int -> Int +sumLast2 n = (lastDigit n) + secondLastDigit + where + lastTwo = (mod (abs n) 100) + lastD = lastDigit n + secondLastDigit = (lastTwo - lastD) `div` 10 {- | =💣= Task 10* @@ -652,9 +669,12 @@ Implement a function that returns the first digit of a given number. You need to use recursion in this task. Feel free to return to it later, if you aren't ready for this boss yet! -} - -firstDigit n = error "firstDigit: Not implemented!" - +firstDigit :: Int -> Int +firstDigit n + | absN < 10 = absN + | otherwise = firstDigit (div absN 10) + where + absN = abs n {- You did it! Now it is time to open a pull request with your changes diff --git a/src/Chapter2.hs b/src/Chapter2.hs index b98ceaf7d..3e8fc979e 100644 --- a/src/Chapter2.hs +++ b/src/Chapter2.hs @@ -136,43 +136,43 @@ functions in GHCi and insert the corresponding resulting output below: List of booleans: >>> :t [True, False] - +[True, False] :: [Bool] String is a list of characters: >>> :t "some string" - +"some string" :: String Empty list: >>> :t [] - +[] :: [a] Append two lists: >>> :t (++) - +(++) :: [a] -> [a] -> [a] Prepend an element at the beginning of a list: >>> :t (:) - +(:) :: a -> [a] -> [a] Reverse a list: >>> :t reverse - +reverse :: [a] -> [a] Take first N elements of a list: >>> :t take - +take :: Int -> [a] -> [a] Create a list from N same elements: >>> :t replicate - +replicate :: Int -> a -> [a] Split a string by line breaks: >>> :t lines - +lines :: String -> [String] Join a list of strings with line breaks: >>> :t unlines - +unlines :: [String] -> String -} @@ -186,31 +186,31 @@ Evaluate the following expressions in GHCi and insert the answers. Try to guess first, what you will see. >>> [10, 2] ++ [3, 1, 5] - +[10,2,3,1,5] >>> [] ++ [1, 4] -- [] is an empty list - +[1,4] >>> 3 : [1, 2] - +[3,1,2] >>> 4 : 2 : [5, 10] -- prepend multiple elements - +[4,2,5,10] >>> [1 .. 10] -- list ranges - +[1,2,3,4,5,6,7,8,9,10] >>> [10 .. 1] - +[] >>> [10, 9 .. 1] -- backwards list with explicit step - +[10,9,8,7,6,5,4,3,2,1] >>> length [4, 10, 5] -- list length - +3 >>> replicate 5 True - +[True,True,True,True,True] >>> take 5 "Hello, World!" - +"Hello" >>> drop 5 "Hello, World!" - +", World!" >>> zip "abc" [1, 2, 3] -- convert two lists to a single list of pairs - +[('a',1),('b',2),('c',3)] >>> words "Hello Haskell World!" -- split the string into the list of words - +["Hello","Haskell","World!"] 👩‍🔬 Haskell has a lot of syntax sugar. In the case with lists, any @@ -336,7 +336,11 @@ from it! ghci> :l src/Chapter2.hs -} subList :: Int -> Int -> [a] -> [a] -subList = error "subList: Not implemented!" +subList i j list + | i < 0 = [] + | j < 0 = [] + | i > j = [] + | otherwise = drop i (take (j + 1) list) {- | =⚔️= Task 4 @@ -348,9 +352,8 @@ Implement a function that returns only the first half of a given list. >>> firstHalf "bca" "b" -} --- PUT THE FUNCTION TYPE IN HERE -firstHalf l = error "firstHalf: Not implemented!" - +firstHalf :: [a] -> [a] +firstHalf l = subList 0 (length l `div` 2 - 1) l {- | =🛡= Pattern matching @@ -501,7 +504,9 @@ True >>> isThird42 [42, 42, 0, 42] False -} -isThird42 = error "isThird42: Not implemented!" +isThird42 :: [Int] -> Bool +isThird42 (_:_:42:_) = True +isThird42 _ = False {- | @@ -606,7 +611,8 @@ Implement a function that duplicates each element of the list -} duplicate :: [a] -> [a] -duplicate = error "duplicate: Not implemented!" +duplicate [] = [] +duplicate (x:xs) = x:x: duplicate xs {- | @@ -621,7 +627,13 @@ Write a function that takes elements of a list only in even positions. >>> takeEven [2, 1, 3, 5, 4] [2,3,4] -} -takeEven = error "takeEven: Not implemented!" +takeEven :: [a] -> [a] +takeEven l = go True l + where + go :: Bool -> [a] -> [a] + go _ [] = [] + go True (x:xs) = x : go False xs + go False (_:xs) = go True xs {- | =🛡= Higher-order functions @@ -728,7 +740,7 @@ value of the element itself 🕯 HINT: Use combination of 'map' and 'replicate' -} smartReplicate :: [Int] -> [Int] -smartReplicate l = error "smartReplicate: Not implemented!" +smartReplicate l = concat (map (\x -> replicate x x) l) {- | =⚔️= Task 9 @@ -741,7 +753,13 @@ the list with only those lists that contain a passed element. 🕯 HINT: Use the 'elem' function to check whether an element belongs to a list -} -contains = error "contains: Not implemented!" +contains :: Int -> [[Int]] -> [[Int]] +contains n l = go n l where + go :: Int -> [[Int]] -> [[Int]] + go _ [] = [] + go n (x:xs) = if n `elem` x + then x : go n xs + else go n xs {- | @@ -781,13 +799,15 @@ Let's now try to eta-reduce some of the functions and ensure that we mastered the skill of eta-reducing. -} divideTenBy :: Int -> Int -divideTenBy x = div 10 x +divideTenBy = div 10 -- TODO: type ;) -listElementsLessThan x l = filter (< x) l +listElementsLessThan :: Int -> [Int] -> [Int] +listElementsLessThan x = filter (< x) -- Can you eta-reduce this one??? -pairMul xs ys = zipWith (*) xs ys +pairMul :: Num a => [a] -> [a] -> [a] +pairMul = zipWith (*) {- | =🛡= Lazy evaluation @@ -842,7 +862,9 @@ list. 🕯 HINT: Use the 'cycle' function -} -rotate = error "rotate: Not implemented!" +rotate :: Int -> [a] -> [a] +rotate n _ | n < 0 = [] +rotate n l = take (length l) $ drop n $ cycle l {- | =💣= Task 12* @@ -858,8 +880,11 @@ and reverses it. function, but in this task, you need to implement it manually. No cheating! -} -rewind = error "rewind: Not Implemented!" - +rewind :: [a] -> [a] +rewind l = go l [] where + go :: [a] -> [a] -> [a] + go [] acc = acc + go (x:xs) acc = go xs (x:acc) {- You did it! Now it is time to open pull request with your changes diff --git a/src/Chapter3.hs b/src/Chapter3.hs index 061811064..635fdf07b 100644 --- a/src/Chapter3.hs +++ b/src/Chapter3.hs @@ -35,7 +35,6 @@ your solutions. Okay. Ready? Set. Go! -} - {- =⚗️= Language extensions* @@ -68,419 +67,530 @@ Haskell has several different ways to create entirely new data types. Let's talk about them all and master our skill of data types construction. -} -{- | -=🛡= Type aliases - -The simplest way to introduce a new type in Haskell is __type aliases__. Type -aliases are nothing more than just other names to already existing types. -A few examples: - -@ -type IntList = [Int] -type IntPair = (Int, Int) -@ - -Type aliases are just syntactic sugar and would be replaced by the real types -during compilation, so they don't bring too much to the table of data types. -However, they may make the life of a developer easier in some places. - -One of the most common type aliases is 'String' that we already mentioned in the -List section of the previous chapter. And it is defined in the following way: - -@ -type String = [Char] -@ - -Now it makes much more sense of why 'String' is related to lists. You can see -that any list function could be used on 'String's as well. - -👩‍🔬 Due to the implementation details of lists, such representation of String - is highly inefficient. It is unfortunate that the list of characters is the - default String type. Experienced Haskellers use more efficient string types - 'Text' and 'ByteString' from Haskell libraries: 'text' and 'bytestring' - correspondingly. But, for the simplicity of this training, we are using - 'String'. - -Another common type alias in the standard library is "FilePath", which is the -same as 'String' (which is the same as '[Char]'): - -@ -type FilePath = String -@ - -Usually, you are encouraged to introduce new data types instead of using aliases -for existing types. But it is still good to know about type aliases. They have a -few use-cases, and you can meet them in various Haskell libraries as well. --} - -{- | -=🛡= ADT - -Let's not limit ourselves with just type aliases and define some real new data -structures. Are we the creators of new worlds or what? +-- | +-- =🛡= Type aliases +-- +-- The simplest way to introduce a new type in Haskell is __type aliases__. Type +-- aliases are nothing more than just other names to already existing types. +-- A few examples: +-- +-- @ +-- type IntList = [Int] +-- type IntPair = (Int, Int) +-- @ +-- +-- Type aliases are just syntactic sugar and would be replaced by the real types +-- during compilation, so they don't bring too much to the table of data types. +-- However, they may make the life of a developer easier in some places. +-- +-- One of the most common type aliases is 'String' that we already mentioned in the +-- List section of the previous chapter. And it is defined in the following way: +-- +-- @ +-- type String = [Char] +-- @ +-- +-- Now it makes much more sense of why 'String' is related to lists. You can see +-- that any list function could be used on 'String's as well. +-- +-- 👩‍🔬 Due to the implementation details of lists, such representation of String +-- is highly inefficient. It is unfortunate that the list of characters is the +-- default String type. Experienced Haskellers use more efficient string types +-- 'Text' and 'ByteString' from Haskell libraries: 'text' and 'bytestring' +-- correspondingly. But, for the simplicity of this training, we are using +-- 'String'. +-- +-- Another common type alias in the standard library is "FilePath", which is the +-- same as 'String' (which is the same as '[Char]'): +-- +-- @ +-- type FilePath = String +-- @ +-- +-- Usually, you are encouraged to introduce new data types instead of using aliases +-- for existing types. But it is still good to know about type aliases. They have a +-- few use-cases, and you can meet them in various Haskell libraries as well. + +-- | +-- =🛡= ADT +-- +-- Let's not limit ourselves with just type aliases and define some real new data +-- structures. Are we the creators of new worlds or what? +-- +-- Type in Haskell is like a box of information description that the object of that +-- type should contain. +-- +-- Haskell uses Algebraic Data Types (ADT) system of types. That means that there +-- are two types of types: product and sum types. +-- +-- To give you some basic understanding of the difference between these two, let's +-- go to the book shop. A book in there represents a product type: each book has +-- the name, author, cover, pages, etc. And all of these properties are mandatory +-- and come with the book. Bookshelf, in its turn, is a sum type. Each book in a +-- shelf is a different type, and you can choose one of them at once (there is no +-- such book where two or more physical books are sewed together). +-- +-- We will show you an example now, just to illustrate all the above and then will +-- explain each concept separately. Note, this is not real syntax. +-- +-- @ +-- -- Product type +-- Book: +-- book name +-- AND book author +-- AND book cover +-- AND book pages +-- +-- +-- -- Sum type +-- BookShelf: +-- Good book 1 : {Book} +-- OR Good book 2 : {Book} +-- OR Cheap book 3 : {Book} +-- @ +-- +-- 👩‍🔬 We use AND in product types to represent the notion of having all fields at +-- the same time. In contrast, for the sum types, we use OR to tell only about a +-- single possibility. AND in logic corresponds to multiplication in math, and OR +-- corresponds to addition. You see that there is some math theory behind the +-- concept of data types in Haskell, and that's why they are called Algebraic Data +-- Types. + +-- | +-- =🛡= Product type +-- +-- Let's now see how product data types look like in Haskell. +-- +-- Product type should have a type name, one type constructor (the function that +-- lets you create the value of the type later) and the description of the fields +-- it consists of in the view of types. +-- +-- When defining a custom type in Haskell, you use the __"data"__ keyword, write +-- the __type name__ you come up with after it, and then after the "=" sign you +-- specify the __constructor name__ followed by the __fields__. +-- +-- When in action, a custom data type could be used in the following use case. +-- A definition of a data type for a knight with a name and number of victories +-- can look like this: +-- +-- @ +-- ┌─ type name +-- │ +-- │ ┌─ constructor name (or constructor tag) +-- │ │ +-- data Knight = MkKnight String Int +-- │ │ │ +-- │ └────┴─── types of fields +-- │ +-- └ "data" keyword +-- @ +-- +-- ♫ NOTE: The constructor can have the same name as the type itself. And in most +-- cases, they are indeed named identically. This is not a problem for Haskell, +-- because types live in the types namespace, and constructors live in the value +-- namespace. So there won't be any collisions and misunderstandings from the +-- compiler side. The names are unambiguous. +-- +-- You can use the constructor name, to create a value of the "Knight" type. +-- +-- @ +-- arthur :: Knight +-- arthur = MkKnight "Arthur" 100 +-- @ +-- +-- A constructor is just a function from fields to the type! You can verify this in GHCi: +-- +-- ghci> :t MkKnight +-- MkKnight :: String -> Int -> Knight +-- +-- As in a regular function, you need to provide a 'String' and an 'Int' to the +-- 'MkKnight' constructor in order to get the full-fledged 'Knight'. +-- +-- Also, you can write a function that takes a Knight and returns its name. +-- It is convenient to use pattern matching for that: +-- +-- @ +-- knightName :: Knight -> String +-- knightName (MkKnight name _) = name +-- @ +-- +-- And you can extract the name in GHCi: +-- +-- ghci> knightName arthur +-- "Arthur" +-- +-- It is comfy to have such getters for all types, so Haskell provides a syntax for +-- defining __records__ — named parameters for the product data type fields. +-- +-- Records have a similar syntax for defining in Haskell as (unnamed) ordinary +-- product types, but fields are specified in the {} separated by a comma. Each +-- field should have a name and a type in this form: 'fieldName :: FieldType'. +-- +-- The same definition of 'Knight' but with records should be written in the +-- following way: +-- +-- @ +-- data Knight = MkKnight +-- { knightName :: String +-- , knightVictories :: Int +-- } +-- @ +-- +-- The above type definition is equivalent to the one we had before. We just gave +-- names to our fields for clarity. In addition, we also automatically get +-- functions "knightName :: Knight -> String" and "knightVictories :: Knight -> +-- Int". This is what records bring us — free getters! +-- +-- The pattern matching on constructors of such records can stay the same. Besides, +-- you can use field names as getters. +-- +-- 👩‍🔬 We are using a particular naming scheme of record field names in record +-- types in order to avoid name collisions. We add the data type name prefix in +-- front of each usual field name for that. As we saw, records create getters for +-- us, which are actually top-level functions. In Haskell, all functions defined at +-- top-level are available in the whole scope within a module. But Haskell forbids +-- creating multiple functions with the same name. The Haskell ecosystem has +-- numerous ways of solving this so-called "record" problem. Still, for simplicity +-- reasons, we are going to use disambiguation by a prefix which is one of the +-- standard resolutions for the scope problem. +-- +-- In addition to getting top-level getters automatically, you get the following +-- features for free when using records over unnamed type fields: +-- +-- 1. Specify names during constructions — additional visual help for code. +-- 2. Record-update syntax. +-- +-- By default, all functions and constructors work with positional arguments +-- (unnamed, that are identified by its position in the type declaration). You +-- write a function or a constructor name first, and then you pass arguments +-- separated by spaces. But once we declare a product type as a record, we can use +-- field names for specifying constructor values. That means that the position +-- doesn't matter anymore as long as we specify the names. So it is like using +-- named arguments but only for constructors with records. +-- This is an alternative way of defining values of custom records. +-- +-- Let's introduce Sir Arthur properly! +-- +-- @ +-- arthur :: Knight +-- arthur = MkKnight +-- { knightName = "Arthur" +-- , knightVictories = 100 +-- } +-- @ +-- +-- After we created our custom types and defined some values, we may want to change +-- some fields of our values. But we can't actually change anything! Remember that +-- all values in Haskell are immutable, and you can't just change a field of some +-- type. You need to create a new value! Fortunately, for records, we can use the +-- __record update syntax__. Record update syntax allows creating new objects of a +-- record type by assigning new values to the fields of some existing record. +-- +-- The syntax for record update is the following: +-- +-- @ +-- lancelot :: Knight +-- lancelot = arthur { knightName = "Lancelot" } +-- @ +-- +-- Without records, we had to write a custom setter function for each field of each +-- data type. +-- +-- @ +-- setKnightName :: String -> Knight -> Knight +-- setKnightName newName (MkKnight _ victories) = +-- MkKnight newName victories +-- @ +-- +-- ♫ NOTE: By default, GHCi doesn't know how to display values of custom types. If +-- you want to explore custom data types in the REPL, you need to add a magical +-- "deriving (Show)" line (will be explained later in this chapter) at the end of a +-- record. Like so: +-- +-- @ +-- data Knight = MkKnight +-- { knightName :: String +-- , knightVictories :: Int +-- } deriving (Show) +-- @ +-- +-- Now GHCi should be able to show the values of your types! Try playing with our +-- knights in GHCi to get the idea behind records. +-- +-- 🕯 HINT: At this point, you may want to be able to enter multi-line strings in +-- GHCi. Start multi-line blocks by typing the ":{" command, and close such blocks +-- using the ":}" command. +-- +-- ghci> :{ +-- ghci| data Knight = MkKnight +-- ghci| { knightName :: String +-- ghci| , knightVictories :: Int +-- ghci| } deriving (Show) +-- ghci| :} +-- ghci> +-- +-- Although, it may be easier to define data types in the module, and load it +-- afterwards. + +-- | +-- =⚔️= Task 1 +-- +-- Define the Book product data type. You can take inspiration from our description +-- of a book, but you are not limited only by the book properties we described. +-- Create your own book type of your dreams! +data Book = Book + { bookName :: String, + bookAuthor :: String, + bookPages :: Int, + bookCover :: String + } + deriving (Show) + +-- | +-- =⚔️= Task 2 +-- +-- Prepare to defend the honour of our kingdom! A monster attacks our brave knight. +-- Help him to fight this creature! +-- +-- Define data types for Knights and Monsters, and write the "fight" function. +-- +-- Both a knight and a monster have the following properties: +-- +-- ✦ Health (the number of health points) +-- ✦ Attack (the number of attack units) +-- ✦ Gold (the number of coins) +-- +-- When a monster fights a knight, the knight hits first, and the monster hits back +-- only if it survives (health is bigger than zero). A hit decreases the amount of +-- health by the number represented in the "attack" field. +-- +-- Implement the "fight" function, that takes a monster and a knight, performs the +-- fight following the above rules and returns the amount of gold the knight has +-- after the fight. The battle has the following possible outcomes: +-- +-- ⊛ Knight wins and takes the loot from the monster and adds it to their own +-- earned treasure +-- ⊛ Monster defeats the knight. In that case return -1 +-- ⊛ Neither the knight nor the monster wins. On such an occasion, the knight +-- doesn't earn any money and keeps what they had before. +-- +-- ♫ NOTE: In this task, you need to implement only a single round of the fight. +data Knight = Knight + { knightName :: String, + knightHealth :: Int, + knightAttack :: Int, + knightGold :: Int + } + deriving (Show) + +data Monster = Monster + { monsterName :: String, + monsterHealth :: Int, + monsterAttack :: Int, + monsterGold :: Int + } + deriving (Show) + +fight :: Knight -> Monster -> Int +fight k m | knightAttack k >= monsterHealth m = knightGold k + monsterGold m +fight k m | knightAttack k < monsterHealth m = -1 +fight k _ = knightGold k -Type in Haskell is like a box of information description that the object of that -type should contain. - -Haskell uses Algebraic Data Types (ADT) system of types. That means that there -are two types of types: product and sum types. - -To give you some basic understanding of the difference between these two, let's -go to the book shop. A book in there represents a product type: each book has -the name, author, cover, pages, etc. And all of these properties are mandatory -and come with the book. Bookshelf, in its turn, is a sum type. Each book in a -shelf is a different type, and you can choose one of them at once (there is no -such book where two or more physical books are sewed together). - -We will show you an example now, just to illustrate all the above and then will -explain each concept separately. Note, this is not real syntax. - -@ --- Product type -Book: - book name - AND book author - AND book cover - AND book pages - - --- Sum type -BookShelf: - Good book 1 : {Book} - OR Good book 2 : {Book} - OR Cheap book 3 : {Book} -@ - -👩‍🔬 We use AND in product types to represent the notion of having all fields at - the same time. In contrast, for the sum types, we use OR to tell only about a - single possibility. AND in logic corresponds to multiplication in math, and OR - corresponds to addition. You see that there is some math theory behind the - concept of data types in Haskell, and that's why they are called Algebraic Data - Types. --} - -{- | -=🛡= Product type - -Let's now see how product data types look like in Haskell. - -Product type should have a type name, one type constructor (the function that -lets you create the value of the type later) and the description of the fields -it consists of in the view of types. - -When defining a custom type in Haskell, you use the __"data"__ keyword, write -the __type name__ you come up with after it, and then after the "=" sign you -specify the __constructor name__ followed by the __fields__. - -When in action, a custom data type could be used in the following use case. -A definition of a data type for a knight with a name and number of victories -can look like this: - -@ - ┌─ type name - │ - │ ┌─ constructor name (or constructor tag) - │ │ -data Knight = MkKnight String Int - │ │ │ - │ └────┴─── types of fields - │ - └ "data" keyword -@ - -♫ NOTE: The constructor can have the same name as the type itself. And in most - cases, they are indeed named identically. This is not a problem for Haskell, - because types live in the types namespace, and constructors live in the value - namespace. So there won't be any collisions and misunderstandings from the - compiler side. The names are unambiguous. - -You can use the constructor name, to create a value of the "Knight" type. - -@ arthur :: Knight -arthur = MkKnight "Arthur" 100 -@ - -A constructor is just a function from fields to the type! You can verify this in GHCi: - -ghci> :t MkKnight -MkKnight :: String -> Int -> Knight - -As in a regular function, you need to provide a 'String' and an 'Int' to the -'MkKnight' constructor in order to get the full-fledged 'Knight'. - -Also, you can write a function that takes a Knight and returns its name. -It is convenient to use pattern matching for that: - -@ -knightName :: Knight -> String -knightName (MkKnight name _) = name -@ - -And you can extract the name in GHCi: - -ghci> knightName arthur -"Arthur" - -It is comfy to have such getters for all types, so Haskell provides a syntax for -defining __records__ — named parameters for the product data type fields. - -Records have a similar syntax for defining in Haskell as (unnamed) ordinary -product types, but fields are specified in the {} separated by a comma. Each -field should have a name and a type in this form: 'fieldName :: FieldType'. - -The same definition of 'Knight' but with records should be written in the -following way: - -@ -data Knight = MkKnight - { knightName :: String - , knightVictories :: Int - } -@ - -The above type definition is equivalent to the one we had before. We just gave -names to our fields for clarity. In addition, we also automatically get -functions "knightName :: Knight -> String" and "knightVictories :: Knight -> -Int". This is what records bring us — free getters! - -The pattern matching on constructors of such records can stay the same. Besides, -you can use field names as getters. - -👩‍🔬 We are using a particular naming scheme of record field names in record - types in order to avoid name collisions. We add the data type name prefix in - front of each usual field name for that. As we saw, records create getters for - us, which are actually top-level functions. In Haskell, all functions defined at - top-level are available in the whole scope within a module. But Haskell forbids - creating multiple functions with the same name. The Haskell ecosystem has - numerous ways of solving this so-called "record" problem. Still, for simplicity - reasons, we are going to use disambiguation by a prefix which is one of the - standard resolutions for the scope problem. - -In addition to getting top-level getters automatically, you get the following -features for free when using records over unnamed type fields: - - 1. Specify names during constructions — additional visual help for code. - 2. Record-update syntax. - -By default, all functions and constructors work with positional arguments -(unnamed, that are identified by its position in the type declaration). You -write a function or a constructor name first, and then you pass arguments -separated by spaces. But once we declare a product type as a record, we can use -field names for specifying constructor values. That means that the position -doesn't matter anymore as long as we specify the names. So it is like using -named arguments but only for constructors with records. -This is an alternative way of defining values of custom records. - -Let's introduce Sir Arthur properly! - -@ -arthur :: Knight -arthur = MkKnight - { knightName = "Arthur" - , knightVictories = 100 - } -@ - -After we created our custom types and defined some values, we may want to change -some fields of our values. But we can't actually change anything! Remember that -all values in Haskell are immutable, and you can't just change a field of some -type. You need to create a new value! Fortunately, for records, we can use the -__record update syntax__. Record update syntax allows creating new objects of a -record type by assigning new values to the fields of some existing record. - -The syntax for record update is the following: - -@ -lancelot :: Knight -lancelot = arthur { knightName = "Lancelot" } -@ - -Without records, we had to write a custom setter function for each field of each -data type. - -@ -setKnightName :: String -> Knight -> Knight -setKnightName newName (MkKnight _ victories) = - MkKnight newName victories -@ - -♫ NOTE: By default, GHCi doesn't know how to display values of custom types. If - you want to explore custom data types in the REPL, you need to add a magical - "deriving (Show)" line (will be explained later in this chapter) at the end of a - record. Like so: - -@ -data Knight = MkKnight - { knightName :: String - , knightVictories :: Int - } deriving (Show) -@ - -Now GHCi should be able to show the values of your types! Try playing with our -knights in GHCi to get the idea behind records. - -🕯 HINT: At this point, you may want to be able to enter multi-line strings in - GHCi. Start multi-line blocks by typing the ":{" command, and close such blocks - using the ":}" command. - -ghci> :{ -ghci| data Knight = MkKnight -ghci| { knightName :: String -ghci| , knightVictories :: Int -ghci| } deriving (Show) -ghci| :} -ghci> - -Although, it may be easier to define data types in the module, and load it -afterwards. --} - -{- | -=⚔️= Task 1 - -Define the Book product data type. You can take inspiration from our description -of a book, but you are not limited only by the book properties we described. -Create your own book type of your dreams! --} - -{- | -=⚔️= Task 2 - -Prepare to defend the honour of our kingdom! A monster attacks our brave knight. -Help him to fight this creature! - -Define data types for Knights and Monsters, and write the "fight" function. - -Both a knight and a monster have the following properties: - - ✦ Health (the number of health points) - ✦ Attack (the number of attack units) - ✦ Gold (the number of coins) - -When a monster fights a knight, the knight hits first, and the monster hits back -only if it survives (health is bigger than zero). A hit decreases the amount of -health by the number represented in the "attack" field. - -Implement the "fight" function, that takes a monster and a knight, performs the -fight following the above rules and returns the amount of gold the knight has -after the fight. The battle has the following possible outcomes: - - ⊛ Knight wins and takes the loot from the monster and adds it to their own - earned treasure - ⊛ Monster defeats the knight. In that case return -1 - ⊛ Neither the knight nor the monster wins. On such an occasion, the knight - doesn't earn any money and keeps what they had before. - -♫ NOTE: In this task, you need to implement only a single round of the fight. - --} - -{- | -=🛡= Sum types - -Another powerful ambassador of ADTs is the __sum type__. Unlike ordinary records -(product types) that always have all the fields you wrote, sum types represent -alternatives of choices. Sum types can be seen as "one-of" data structures. They -contain many product types (described in the previous section) as alternatives. - -To define a sum type, you have to specify all possible constructors separated -by "|". Each constructor on its own could have an ADT, that describes -this branch of the alternative. - -There is at least one famous sum type that you have already seen — 'Bool' — the -simplest example of a sum type. - -@ -data Bool = False | True -@ - -'Bool' is a representer of so-called __enumeration__ — a special case of sum -types, a sum of nullary constructors (constructors without fields). -Sum types can have much more than two constructors (but don't abuse this)! - -Look at this one. We need more than two constructors to mirror the "Magic Type". -And none of the magic streams needs any fields. Just pure magic ;) - -@ -data MagicType - = DarkMagic - | LightMagic - | NeutralMagic -@ - -However, the real power of sum types unleashes when you combine them with -fields. As we mentioned, each "|" case in the sum type could be an ADT, so, -naturally, you can have constructors with fields, which are product types from -the previous section. If you think about it, the enumeration also contains a -product type, as it is absolutely legal to create a data type with one -constructor and without any fields: `data Emptiness = TotalVoid`. - -To showcase such sum type, let's represent a possible loot from successfully -completing an adventure: - -@ -data Loot - = Sword Int -- attack - | Shield Int -- defence - | WizardStaff Power SpellLevel -@ - -You can create values of the sum types by using different constructors: - -@ -woodenSword :: Loot -woodenSword = Sword 2 - -adamantiumShield :: Loot -adamantiumShield = Shield 3000 -@ - -And you can pattern match on different constructors as well. - -@ -acceptLoot :: Loot -> String -acceptLoot loot = case loot of - Sword _ -> "Thanks! That's a great sword!" - Shield _ -> "I'll accept this shield as a reward!" - WizardStaff _ _ -> "What?! I'm not a wizard, take it back!" -@ - - -To sum up all the above, a data type in Haskell can have zero or more -constructors, and each constructor can have zero or more fields. This altogether -gives us product types (records with fields) and sum types (alternatives). The -concept of product types and sum types is called __Algebraic Data Type__. They -allow you to model your domain precisely, make illegal states unrepresentable -and provide more flexibility when working with data types. --} - -{- | -=⚔️= Task 3 - -Create a simple enumeration for the meal types (e.g. breakfast). The one who -comes up with the most number of names wins the challenge. Use your creativity! --} - -{- | -=⚔️= Task 4 - -Define types to represent a magical city in the world! A typical city has: - -⍟ Optional castle with a __name__ (as 'String') -⍟ Wall, but only if the city has a castle -⍟ Church or library but not both -⍟ Any number of houses. Each house has one, two, three or four __people__ inside. - -After defining the city, implement the following functions: - - ✦ buildCastle — build a castle in the city. If the city already has a castle, - the old castle is destroyed, and the new castle with the __new name__ is built - ✦ buildHouse — add a new living house - ✦ buildWalls — build walls in the city. But since building walls is a - complicated task, walls can be built only if the city has a castle - and at least 10 living __people__ inside in all houses of the city in total. --} +arthur = Knight {knightName = "Arthur", knightAttack = 10, knightHealth = 10, knightGold = 0} + +dragon1 :: Monster +dragon1 = Monster {monsterName = "Dragon1", monsterAttack = 10, monsterHealth = 10, monsterGold = 100} + +win :: Int +win = fight arthur dragon1 + +tooStrongMonster :: Monster +tooStrongMonster = Monster {monsterName = "Gold Dragon", monsterAttack = 20, monsterHealth = 20, monsterGold = 10000} + +dead :: Int +dead = fight arthur tooStrongMonster + +turtleMonster :: Monster +turtleMonster = Monster {monsterName = "Turtle", monsterAttack = 1, monsterHealth = 30, monsterGold = 10} + +firstRoundResult :: Int +firstRoundResult = fight arthur turtleMonster + +-- | +-- =🛡= Sum types +-- +-- Another powerful ambassador of ADTs is the __sum type__. Unlike ordinary records +-- (product types) that always have all the fields you wrote, sum types represent +-- alternatives of choices. Sum types can be seen as "one-of" data structures. They +-- contain many product types (described in the previous section) as alternatives. +-- +-- To define a sum type, you have to specify all possible constructors separated +-- by "|". Each constructor on its own could have an ADT, that describes +-- this branch of the alternative. +-- +-- There is at least one famous sum type that you have already seen — 'Bool' — the +-- simplest example of a sum type. +-- +-- @ +-- data Bool = False | True +-- @ +-- +-- 'Bool' is a representer of so-called __enumeration__ — a special case of sum +-- types, a sum of nullary constructors (constructors without fields). +-- Sum types can have much more than two constructors (but don't abuse this)! +-- +-- Look at this one. We need more than two constructors to mirror the "Magic Type". +-- And none of the magic streams needs any fields. Just pure magic ;) +-- +-- @ +-- data MagicType +-- = DarkMagic +-- | LightMagic +-- | NeutralMagic +-- @ +-- +-- However, the real power of sum types unleashes when you combine them with +-- fields. As we mentioned, each "|" case in the sum type could be an ADT, so, +-- naturally, you can have constructors with fields, which are product types from +-- the previous section. If you think about it, the enumeration also contains a +-- product type, as it is absolutely legal to create a data type with one +-- constructor and without any fields: `data Emptiness = TotalVoid`. +-- +-- To showcase such sum type, let's represent a possible loot from successfully +-- completing an adventure: +-- +-- @ +-- data Loot +-- = Sword Int -- attack +-- | Shield Int -- defence +-- | WizardStaff Power SpellLevel +-- @ +-- +-- You can create values of the sum types by using different constructors: +-- +-- @ +-- woodenSword :: Loot +-- woodenSword = Sword 2 +-- +-- adamantiumShield :: Loot +-- adamantiumShield = Shield 3000 +-- @ +-- +-- And you can pattern match on different constructors as well. +-- +-- @ +-- acceptLoot :: Loot -> String +-- acceptLoot loot = case loot of +-- Sword _ -> "Thanks! That's a great sword!" +-- Shield _ -> "I'll accept this shield as a reward!" +-- WizardStaff _ _ -> "What?! I'm not a wizard, take it back!" +-- @ +-- +-- +-- To sum up all the above, a data type in Haskell can have zero or more +-- constructors, and each constructor can have zero or more fields. This altogether +-- gives us product types (records with fields) and sum types (alternatives). The +-- concept of product types and sum types is called __Algebraic Data Type__. They +-- allow you to model your domain precisely, make illegal states unrepresentable +-- and provide more flexibility when working with data types. + +-- | +-- =⚔️= Task 3 +-- +-- Create a simple enumeration for the meal types (e.g. breakfast). The one who +-- comes up with the most number of names wins the challenge. Use your creativity! +data Meal = Breakfast | SecondBreakfast | Lunch | TeeTime | AfternoonSnack | Dinner | MidnightSnack + +-- | +-- =⚔️= Task 4 +-- +-- Define types to represent a magical city in the world! A typical city has: +-- +-- ⍟ Optional castle with a __name__ (as 'String') +-- ⍟ Wall, but only if the city has a castle +-- ⍟ Church or library but not both +-- ⍟ Any number of houses. Each house has one, two, three or four __people__ inside. +-- +-- After defining the city, implement the following functions: +-- +-- ✦ buildCastle — build a castle in the city. If the city already has a castle, +-- the old castle is destroyed, and the new castle with the __new name__ is built +-- ✦ buildHouse — add a new living house +-- ✦ buildWalls — build walls in the city. But since building walls is a +-- complicated task, walls can be built only if the city has a castle +-- and at least 10 living __people__ inside in all houses of the city in total. +data Culture = Church | Library deriving (Show) + +newtype House = House {housePeople :: Int} deriving (Show) + +newtype Castle = Castle {name :: String} deriving (Show) + +data City where + WalledCity :: + { cityCastle :: Castle, + cityCulture :: Culture, + cityHouses :: [House] + } -> + City + CastleCity :: + { cityCastle :: Castle, + cityCulture :: Culture, + cityHouses :: [House] + } -> + City + VillageCity :: + {cityCulture :: Culture, cityHouses :: [House]} -> + City + deriving (Show) + +peopleInCity :: [House] -> Int +peopleInCity houses = sum $ map housePeople houses + +canBuildWalls :: [House] -> Bool +canBuildWalls houses = peopleInCity houses >= 10 + +buildCastle :: City -> String -> City +buildCastle city name = case city of + VillageCity {cityCulture, cityHouses} -> CastleCity {cityCastle = Castle name, cityCulture = cityCulture, cityHouses = cityHouses} + _ -> city {cityCastle = Castle name} + +createHouse :: Int -> [House] +createHouse people + | people > 0 && people < 5 = [House people] + | otherwise = [] + +buildHouse :: City -> Int -> City +buildHouse city people = city {cityHouses = createHouse people ++ cityHouses city} + +createCity :: String -> Culture -> [House] -> City +createCity name culture houses + | canBuildWalls houses = WalledCity (Castle name) culture houses + | otherwise = CastleCity (Castle name) culture houses + +buildWalls :: City -> City +buildWalls city = case city of + WalledCity {} -> city -- already has walls + CastleCity {cityHouses, cityCulture, cityCastle} | canBuildWalls cityHouses -> WalledCity cityCastle cityCulture cityHouses + _ -> city -- no castle or not enough people to build walls + +auenland :: City +auenland = VillageCity Library $ createHouse 1 + +auenlandCannotBuildWalls :: City +auenlandCannotBuildWalls = buildWalls auenland + +auenlandCannotBuildWallsEvenWithCastle :: City +auenlandCannotBuildWallsEvenWithCastle = buildWalls $ buildCastle auenlandCannotBuildWalls "Auenland Castle" + +grownAuenland :: City +grownAuenland = buildHouse auenland 4 + +auenlandWithCastle :: City +auenlandWithCastle = buildCastle grownAuenland "Auenland Castle" + +auenlandWithCastleAndWalls :: City +auenlandWithCastleAndWalls = buildWalls auenlandWithCastle {- =🛡= Newtypes @@ -562,198 +672,252 @@ introducing extra newtypes. 🕯 HINT: if you complete this task properly, you don't need to change the implementation of the "hitPlayer" function at all! -} -data Player = Player - { playerHealth :: Int - , playerArmor :: Int - , playerAttack :: Int - , playerDexterity :: Int - , playerStrength :: Int - } - -calculatePlayerDamage :: Int -> Int -> Int -calculatePlayerDamage attack strength = attack + strength - -calculatePlayerDefense :: Int -> Int -> Int -calculatePlayerDefense armor dexterity = armor * dexterity - -calculatePlayerHit :: Int -> Int -> Int -> Int -calculatePlayerHit damage defense health = health + defense - damage - --- The second player hits first player and the new first player is returned -hitPlayer :: Player -> Player -> Player -hitPlayer player1 player2 = - let damage = calculatePlayerDamage - (playerAttack player2) - (playerStrength player2) - defense = calculatePlayerDefense - (playerArmor player1) - (playerDexterity player1) - newHealth = calculatePlayerHit - damage - defense - (playerHealth player1) - in player1 { playerHealth = newHealth } - -{- | -=🛡= Polymorphic data types - -Similar to functions, data types in Haskell can be __polymorphic__. This means -that they can use some type variables as placeholders, representing general -types. You can either reason about data types in terms of such variables (and -don't worry about the specific types), or substitute variables with some -particular types. -Such polymorphism in Haskell is an example of __parametric polymorphism__. - -The process of defining a polymorphic type is akin to an ordinary data type -definition. The only difference is that all the type variables should go after -the type name so that you can reuse them in the constructor fields later. - -For example, - -@ -data Foo a = MkFoo a -@ - -Note that both product and sum types can be parameterised. - -> Actually, we've already seen a polymorphic data type! Remember Lists from Chapter Two? - -To give an example of a custom polymorphic type, let's implement a -"TreasureChest" data type. Our treasure chest is flexible, and it can store some -amount of gold. Additionally there is some space for one more arbitrary -treasure. But that could be any treasure, and we don't know what it is -beforehand. - -In Haskell words, the data type can be defined like this: -@ -data TreasureChest x = TreasureChest - { treasureChestGold :: Int - , treasureChestLoot :: x - } -@ - -You can see that a treasure chest can store any treasure, indeed! We call it -treasure 'x'. +newtype Health = Health Int -And when writing functions involving the "TreasureChest" type, we don't always -need to know what kind of treasure is inside besides gold. -We can either use a type variable in our type signature: - -@ -howMuchGoldIsInMyChest :: TreasureChest x -> Int -@ +health :: Health -> Int +health (Health value) = value -or we can specify a concrete type: +newtype Armor = Armor Int -@ -isEnoughDiamonds :: TreasureChest Diamond -> Bool -@ - -In the same spirit, we can implement a function that creates a treasure chest with some -predefined amount of gold and a given treasure: - -@ -mkMehChest :: x -> TreasureChest x -mkMehChest treasure = TreasureChest - { treasureChestGold = 50 - , treasureChestLoot = treasure - } -@ - - -Polymorphic Algebraic Data Types are a great deal! One of the most common and -useful standard polymorphic types is __"Maybe"__. It represents the notion of -optional value (maybe the value is there, or maybe it is not). -"Maybe" is defined in the standard library in the following way: - -@ -data Maybe a - = Nothing - | Just a -@ - -Haskell doesn't have a concept of "null" values. If you want to work with -potentially absent values, use the "Maybe" type explicitly. - -> Is there a good way to avoid null-pointer bugs? Maybe. © Jasper Van der Jeugt - -Another standard polymorphic data type is "Either". It stores either the value -of one type or a value of another. - -@ -data Either a b - = Left a - | Right b -@ +armor :: Armor -> Int +armor (Armor value) = value -♫ NOTE: It can help to explore types of constructors "Nothing", "Just", "Left" - and "Right". Let's stretch our fingers and blow off the dust from our GHCi and - check that! +newtype Attack = Attack Int -You can pattern match on values of the "Either" type as well as on any other -custom data type. +attack :: Attack -> Int +attack (Attack value) = value -@ -showEither :: Either String Int -> String -showEither (Left msg) = "Left with string: " ++ msg -showEither (Right n) = "Right with number: " ++ show n -@ +newtype Dexterity = Dexterity Int -Now, after we covered polymorphic types, you are finally ready to learn how -lists are actually defined in the Haskell world. Behold the mighty list type! +dexterity :: Dexterity -> Int +dexterity (Dexterity value) = value -@ -data [] a - = [] - | a : [a] -@ +newtype Strenght = Strenght Int -Immediately we know what all of that means! -The ":" is simply the constructor name for the list. Constructors in Haskell can -be defined as infix operators as well (i.e. be written after the first argument, -the same way we write `1 + 2` and not `+ 1 2`), but only if they start with a -colon ":". The ":" is taken by lists. Now you see why we were able to pattern -match on it? +strenght :: Strenght -> Int +strenght (Strenght value) = value -The type name uses built-in syntax to reserve the square brackets [] exclusively -for lists but, otherwise, is a simple polymorphic recursive sum type. +data Player = Player + { playerHealth :: Health, + playerArmor :: Armor, + playerAttack :: Attack, + playerDexterity :: Dexterity, + playerStrength :: Strenght + } -If you rename some constructor and type names, the list type could look quite -simple, as any of us could have written it: +newtype Damage = Damage Int -@ -data List a - = Empty - | Cons a (List a) -@ +damage :: Damage -> Int +damage (Damage value) = value -♫ NOTE: We use () to group "List" with "a" type variable in the second field of - the "Cons" constructor. This is done to tell the compiler that "List" and "a" - should go together as one type. --} +calculatePlayerDamage :: Attack -> Strenght -> Damage +calculatePlayerDamage a s = Damage $ attack a + strenght s -{- | -=⚔️= Task 6 +newtype Defense = Defense Int -Before entering the real world of adventures and glorious victories, we should -prepare for different things in this world. It is always a good idea to -understand the whole context before going on a quest. And, before fighting a -dragon, it makes sense to prepare for different unexpected things. So let's -define data types describing a Dragon Lair! +defense :: Defense -> Int +defense (Defense value) = value - ⍟ A lair has a dragon and possibly a treasure chest (as described in the - previous section). A lair also may not contain any treasures, but we'll never - know until we explore the cave! - ⍟ A dragon can have a unique magical power. But it can be literally anything! - And we don't know in advance what power it has. +calculatePlayerDefense :: Armor -> Dexterity -> Defense +calculatePlayerDefense a d = Defense $ armor a * dexterity d -Create data types that describe such Dragon Lair! Use polymorphism to -parametrise data types in places where values can be of any general type. +calculatePlayerHit :: Damage -> Defense -> Health -> Health +calculatePlayerHit dam def h = Health $ health h + defense def - damage dam -🕯 HINT: 'Maybe' that some standard types we mentioned above are useful for - maybe-treasure ;) --} +-- The second player hits first player and the new first player is returned +hitPlayer :: Player -> Player -> Player +hitPlayer player1 player2 = + let damage = + calculatePlayerDamage + (playerAttack player2) + (playerStrength player2) + defense = + calculatePlayerDefense + (playerArmor player1) + (playerDexterity player1) + newHealth = + calculatePlayerHit + damage + defense + (playerHealth player1) + in player1 {playerHealth = newHealth} + +-- | +-- =🛡= Polymorphic data types +-- +-- Similar to functions, data types in Haskell can be __polymorphic__. This means +-- that they can use some type variables as placeholders, representing general +-- types. You can either reason about data types in terms of such variables (and +-- don't worry about the specific types), or substitute variables with some +-- particular types. +-- Such polymorphism in Haskell is an example of __parametric polymorphism__. +-- +-- The process of defining a polymorphic type is akin to an ordinary data type +-- definition. The only difference is that all the type variables should go after +-- the type name so that you can reuse them in the constructor fields later. +-- +-- For example, +-- +-- @ +-- data Foo a = MkFoo a +-- @ +-- +-- Note that both product and sum types can be parameterised. +-- +-- > Actually, we've already seen a polymorphic data type! Remember Lists from Chapter Two? +-- +-- To give an example of a custom polymorphic type, let's implement a +-- "TreasureChest" data type. Our treasure chest is flexible, and it can store some +-- amount of gold. Additionally there is some space for one more arbitrary +-- treasure. But that could be any treasure, and we don't know what it is +-- beforehand. +-- +-- In Haskell words, the data type can be defined like this: +-- +-- @ +-- data TreasureChest x = TreasureChest +-- { treasureChestGold :: Int +-- , treasureChestLoot :: x +-- } +-- @ +-- +-- You can see that a treasure chest can store any treasure, indeed! We call it +-- treasure 'x'. +-- +-- And when writing functions involving the "TreasureChest" type, we don't always +-- need to know what kind of treasure is inside besides gold. +-- We can either use a type variable in our type signature: +-- +-- @ +-- howMuchGoldIsInMyChest :: TreasureChest x -> Int +-- @ +-- +-- or we can specify a concrete type: +-- +-- @ +-- isEnoughDiamonds :: TreasureChest Diamond -> Bool +-- @ +-- +-- In the same spirit, we can implement a function that creates a treasure chest with some +-- predefined amount of gold and a given treasure: +-- +-- @ +-- mkMehChest :: x -> TreasureChest x +-- mkMehChest treasure = TreasureChest +-- { treasureChestGold = 50 +-- , treasureChestLoot = treasure +-- } +-- @ +-- +-- +-- Polymorphic Algebraic Data Types are a great deal! One of the most common and +-- useful standard polymorphic types is __"Maybe"__. It represents the notion of +-- optional value (maybe the value is there, or maybe it is not). +-- "Maybe" is defined in the standard library in the following way: +-- +-- @ +-- data Maybe a +-- = Nothing +-- | Just a +-- @ +-- +-- Haskell doesn't have a concept of "null" values. If you want to work with +-- potentially absent values, use the "Maybe" type explicitly. +-- +-- > Is there a good way to avoid null-pointer bugs? Maybe. © Jasper Van der Jeugt +-- +-- Another standard polymorphic data type is "Either". It stores either the value +-- of one type or a value of another. +-- +-- @ +-- data Either a b +-- = Left a +-- | Right b +-- @ +-- +-- ♫ NOTE: It can help to explore types of constructors "Nothing", "Just", "Left" +-- and "Right". Let's stretch our fingers and blow off the dust from our GHCi and +-- check that! +-- +-- You can pattern match on values of the "Either" type as well as on any other +-- custom data type. +-- +-- @ +-- showEither :: Either String Int -> String +-- showEither (Left msg) = "Left with string: " ++ msg +-- showEither (Right n) = "Right with number: " ++ show n +-- @ +-- +-- Now, after we covered polymorphic types, you are finally ready to learn how +-- lists are actually defined in the Haskell world. Behold the mighty list type! +-- +-- @ +-- data [] a +-- = [] +-- | a : [a] +-- @ +-- +-- Immediately we know what all of that means! +-- The ":" is simply the constructor name for the list. Constructors in Haskell can +-- be defined as infix operators as well (i.e. be written after the first argument, +-- the same way we write `1 + 2` and not `+ 1 2`), but only if they start with a +-- colon ":". The ":" is taken by lists. Now you see why we were able to pattern +-- match on it? +-- +-- The type name uses built-in syntax to reserve the square brackets [] exclusively +-- for lists but, otherwise, is a simple polymorphic recursive sum type. +-- +-- If you rename some constructor and type names, the list type could look quite +-- simple, as any of us could have written it: +-- +-- @ +-- data List a +-- = Empty +-- | Cons a (List a) +-- @ +-- +-- ♫ NOTE: We use () to group "List" with "a" type variable in the second field of +-- the "Cons" constructor. This is done to tell the compiler that "List" and "a" +-- should go together as one type. + +-- | +-- =⚔️= Task 6 +-- +-- Before entering the real world of adventures and glorious victories, we should +-- prepare for different things in this world. It is always a good idea to +-- understand the whole context before going on a quest. And, before fighting a +-- dragon, it makes sense to prepare for different unexpected things. So let's +-- define data types describing a Dragon Lair! +-- +-- ⍟ A lair has a dragon and possibly a treasure chest (as described in the +-- previous section). A lair also may not contain any treasures, but we'll never +-- know until we explore the cave! +-- ⍟ A dragon can have a unique magical power. But it can be literally anything! +-- And we don't know in advance what power it has. +-- +-- Create data types that describe such Dragon Lair! Use polymorphism to +-- parametrise data types in places where values can be of any general type. +-- +-- 🕯 HINT: 'Maybe' that some standard types we mentioned above are useful for +-- maybe-treasure ;) +data Treasure treasureLoot = TreasureChest + { treasureChestGold :: Int, + treasureChestLoot :: treasureLoot + } + deriving (Show) + +data Dragon dM = Dragon + { dragonName :: String, + magicPower :: Maybe dM + } + deriving (Show) + +data Liar dM treasureLoot = Liar + { dragon :: (Dragon dM), + treasure :: Maybe (Treasure treasureLoot) + } + deriving (Show) {- =🛡= Typeclasses @@ -820,7 +984,6 @@ instance ArchEnemy Bool where getArchEnemy True = "False" getArchEnemy False = "True" - instance ArchEnemy Int where getArchEnemy :: Int -> String getArchEnemy i = case i of @@ -869,7 +1032,6 @@ ghci> revealArchEnemy "An adorable string that has no enemies (✿◠ω◠)" • In the expression: revealArchEnemy "An adorable string that has no enemies (✿◠ω◠)" In an equation for 'it': it = revealArchEnemy "An adorable string that has no enemies (✿◠ω◠)" - Interestingly, it is possible to reuse existing instances of data types in the same typeclass instances as well. And we also can reuse the __constraints__ in the instance declaration for that! @@ -892,26 +1054,40 @@ type exists. You can see how we reuse the fact that the underlying type has this instance and apply this typeclass method to it. -} -{- | -=⚔️= Task 7 - -Often we want to combine several values of a type and get a single value of the -exact same type. We can combine different things: treasures, adventures, groups -of heroes, etc.. So it makes sense to implement a typeclass for such a concept -and define helpful instances. - -We will call such a typeclass "Append". You can find its definition below. +-- | +-- =⚔️= Task 7 +-- +-- Often we want to combine several values of a type and get a single value of the +-- exact same type. We can combine different things: treasures, adventures, groups +-- of heroes, etc.. So it makes sense to implement a typeclass for such a concept +-- and define helpful instances. +-- +-- We will call such a typeclass "Append". You can find its definition below. +-- +-- Implement instances of "Append" for the following types: +-- +-- ✧ The "Gold" newtype where append is the addition +-- ✧ "List" where append is list concatenation +-- ✧ *(Challenge): "Maybe" where append is appending of values inside "Just" constructors +newtype Gold = Gold {goldAmount :: Int} deriving (Show, Eq) -Implement instances of "Append" for the following types: +class Append a where + append :: a -> a -> a - ✧ The "Gold" newtype where append is the addition - ✧ "List" where append is list concatenation - ✧ *(Challenge): "Maybe" where append is appending of values inside "Just" constructors +instance Append Gold where + append :: Gold -> Gold -> Gold + append (Gold a) (Gold b) = Gold (a + b) --} -class Append a where - append :: a -> a -> a +instance Append [a] where + append :: [a] -> [a] -> [a] + append a b = a ++ b +instance Append a => Append (Maybe a) where + append :: Maybe a -> Maybe a -> Maybe a + append (Just a) (Just b) = Just $ append a b + append (Just a) Nothing = Just a + append Nothing (Just b) = Just b + append Nothing Nothing = Nothing {- =🛡= Standard Typeclasses and Deriving @@ -973,6 +1149,25 @@ implement the following functions: 🕯 HINT: to implement this task, derive some standard typeclasses -} +data Weekday + = Monday + | Tuesday + | Wednesday + | Thursday + | Friday + | Saturday + | Sunday + deriving (Show, Read, Eq, Ord, Enum, Bounded) + +isWeekend :: Weekday -> Bool +isWeekend day = day == Saturday || day == Sunday + +nextDay :: Weekday -> Weekday +nextDay day = if day == maxBound then minBound else succ day + +daysToParty :: Weekday -> Int +daysToParty day = fromEnum Friday - fromEnum day + {- =💣= Task 9* @@ -1008,7 +1203,6 @@ Implement data types and typeclasses, describing such a battle between two contestants, and write a function that decides the outcome of a fight! -} - {- You did it! Now it is time to open pull request with your changes and summon @vrom911 for the review! @@ -1019,3 +1213,52 @@ and summon @vrom911 for the review! Deriving: https://kowainik.github.io/posts/deriving Extensions: https://kowainik.github.io/posts/extensions -} + +printTaskHeader :: String -> IO () +printTaskHeader taskName = do + putStrLn "" + putStrLn "---------------------------" + putStrLn $ "---------- Task-" ++ taskName ++ " ---------" + putStrLn "---------------------------" + +main :: IO () +main = do + printTaskHeader "5" + print auenland + print grownAuenland + print auenlandWithCastle + print auenlandWithCastleAndWalls + print auenlandCannotBuildWalls + print auenlandCannotBuildWallsEvenWithCastle + printTaskHeader "6" + print $ + Liar + { dragon = Dragon "Fire Dragon Eremil" $ Just "Fire Spell", + treasure = Just $ TreasureChest {treasureChestGold = 100, treasureChestLoot = "Dragon Plate"} + } + + printTaskHeader "7" + + print $ append Gold {goldAmount = 10} Gold {goldAmount = 20} + print $ append [1, 2] [3, 4, 5] + print $ append (Just "Something") Nothing + print $ append Nothing (Just "Something") + print $ append (Just "Someting and ") (Just "Something") + print $ append (Just [1]) (Just [2, 3, 4, 5]) + + printTaskHeader "8" + + print $ "isWeekend Sunday: " ++ show (isWeekend Sunday) + print $ "isWeekend Monday: " ++ show (isWeekend Monday) + + print $ "nextDay Sunday: " ++ show (nextDay Sunday) + print $ "nextDay Monday: " ++ show (nextDay Monday) + print $ "nextDay Friday: " ++ show (nextDay Friday) + print $ "daysToParty Friday: " ++ show (daysToParty Friday) + + {-variable with name allWeekDays of all enum values from Weekday -} + let allWeekDays = [minBound .. maxBound] :: [Weekday] + + let output = map (\day -> print $ "daysToParty " ++ show day ++ ": " ++ show (daysToParty day)) allWeekDays + {- print output -} + sequence_ output