From cc5a1154befc2b901e299084bc0788cb4d304b27 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Wed, 29 Sep 2021 03:14:23 -0500 Subject: [PATCH] Big bang --- .gitignore | 1 + 03-starting-out.hs | 80 +++++++++++++ 04-types-and-typeclasses.hs | 86 ++++++++++++++ 05-syntax-in-functions.hs | 124 +++++++++++++++++++++ 06-recursion.hs | 42 +++++++ 07-higher-order-functions.hs | 120 ++++++++++++++++++++ 08-modules.hs | 26 +++++ 09-types-and-typeclasses-redux.hs | 179 ++++++++++++++++++++++++++++++ README.md | 6 + 9 files changed, 664 insertions(+) create mode 100644 .gitignore create mode 100644 03-starting-out.hs create mode 100644 04-types-and-typeclasses.hs create mode 100644 05-syntax-in-functions.hs create mode 100644 06-recursion.hs create mode 100644 07-higher-order-functions.hs create mode 100644 08-modules.hs create mode 100644 09-types-and-typeclasses-redux.hs create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d4daa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea* diff --git a/03-starting-out.hs b/03-starting-out.hs new file mode 100644 index 0000000..2e9a6e6 --- /dev/null +++ b/03-starting-out.hs @@ -0,0 +1,80 @@ + +doubleMe x = x + x + +doubleUs x y = (doubleMe x) + (doubleMe y) + +doubleSmallNumber x = if x > 100 + then x + else (x * 2) + +doubleSmallNumber' x = (doubleSmallNumber x) + 1 + +listConcatNums = [1,2,3,4] ++ [9,10,11,12] + +-- Strings are lists of chars +listConcatChars = "hello" ++ " " ++ "world" + +-- Cons prepends a value to a list: +listPrependNum xs = 5:xs + +listPrependA xs = 'A':xs + +-- Access item at list index: +listFirstValue xs = xs !! 0 + +-- head, tail, last, init are useful built-ins for list operations +-- head - get first element +-- tail - get everything except first element +-- last - get last element +-- init - get everything except last element +-- length - get length of list +-- null - check if list is empty +-- reverse - reverses a list +-- take - get the first n elements of a list, if possible +-- drop - drop the first n elements of a list, if possible +-- maximum - get largest element in list +-- minimum - get smallest element in list +-- sum - add up all numbers in list +-- product - product of all numbers in list +-- elem - check if an item appears in a list: 4 `elem` [3,4,5,6] == True + +-- Texas ranges: +getRangeThru n = [1..n] + +-- getRangeThru 4 == [1,2,3,4] + +getEvensThru n = [2,4..n] + +-- getEvensThru 8 == [2,4,6,8] + +-- Can use infinite ranges. e.g., get first 10 multiples of 13: +first10of13 = take 10 [13,26,..] + +-- cycle - cycles a list infinitely - e.g. cycle [1,2,3] == [1,2,3,1,2,3,..] +-- repeat - takes an element and produces an infinite list - e.g. repeat 5 == [5,5..] +-- replicate - get a list of some item n times - e.g. replicate 3 10 == [10,10,10] + +-- List comprehension, like set comprehension in math: +first10Evens = [x*2 | x <- [1..10]] -- == [2,4,6,8,10,12,14,16,18,20] + +-- We can constrain x further using predicates: +first10EvensWhoseDoublesAreGreaterThan12 = [x*2 | x <- [1..10], x*2 >= 12] -- == [12,14,16,18,20] + +-- Say, e.g., we want to filter a list for odd numbers only, replacing those less than 10 with BOOM, else BANG +boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x ] + +-- Can have multiple predicates, and multiple domains: +multiRange = [ x*y | x <- [1..3], y <- [2..4] ] -- result will have length 9 + +-- Since strings are lists, we can use list comprehension to work on them, e.g. +removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']] + +-- Tuples +-- fst - get the first item in a 2-tuple +-- snd - get the second item in a 2-tuple + +-- zip - create pairs from 2 lists: +zipTest1 = zip [1..3] ["one", "two", "three"] -- == [(1,"one"),(2,"two"),(3,"three")] + +-- zip clips to the shortest list: +zipTest2 = zip [1..] ["one", "two"] -- == [(1,"one"),(2,"two")] diff --git a/04-types-and-typeclasses.hs b/04-types-and-typeclasses.hs new file mode 100644 index 0000000..d774e13 --- /dev/null +++ b/04-types-and-typeclasses.hs @@ -0,0 +1,86 @@ +-- in GHCi, we can use :t to get the type of something + +-- :t "Hello!" +"Hello!" :: [Char] + +-- It's good practice to give functions type declarations + +removeNonUppercase :: String -> String +removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']] + +addThree :: Int -> Int -> Int -> Int +addThree x y z = x + y + z + +-- Common types: +-- Int - normal integer +-- Integer - big integer, less efficient +-- Float - real floating point, single precision +-- Double - real floating point, double precision +-- Bool - True or False +-- Char - single character + +-- Type variables - write polymorphic functions that don't use the type-specific +-- features of the values passed in: + +-- :t fst +fst :: (a,b) -> a + +-- :t head +head :: [a] -> a + +-- `a` is the type-variable here + +-- => denotes a class constraint. e.g. :t (==) +(==) :: (Eq a) => a -> a -> Bool + +-- This says (==) takes a and a to Bool, given that the a's are members of the Eq class +-- Another example: + +-- :t elem +elem :: (Eq a) => a -> [a] -> Bool + +-- Eq - classes support equality testing +-- Ord - types have an ordering + +-- e.g. :t (>) +(>) :: (Ord a) => a -> a -> Bool + +-- To be a member of Ord, you must be a member of Eq and have defined behavior for the `compare` function. + +-- Show - members can be presented as strings +-- Read - opposite of show - takes a string and returns a type that is a member of Read + +-- e.g. +restTest1 = read "[1,2,3,4]" ++ [3] -- == [1,2,3,4,3] + +-- The way the result of `read` is used determines what typeclass the value is instanced in +-- :t read +read :: (Read a) => String -> a + +-- the `a` variable is determined by the usage. We can be explicit: +readTest2 = read "5" :: Int -- a is Int + +-- Enum - sequential types that can be enumerated - Enum types can be used in list ranges +-- Bounded - have upper and lower bounds + +-- e.g. +minBoundTest1 = minBound :: Int +maxBoundTest1 = maxBound :: Char + +-- these have type (Bounded a) => a - essentially polymorphic constants +-- the value changes depending on the typecast + +-- Num - members can act like numbers, e.g. + +-- :t 20 +20 :: (Num t) => t + +-- :t (*) +(*) :: (Num a) => a -> a -> a + +-- Integral - only whole numbers - superset including Int and Integer +-- Floating - only floating point numbers - Float and Double + +-- Useful note: fromIntegral takes an Integral to a generic Num type +-- :t fromIntegral +fromIntegral :: (Num b, Integral a) => a -> b diff --git a/05-syntax-in-functions.hs b/05-syntax-in-functions.hs new file mode 100644 index 0000000..09f8525 --- /dev/null +++ b/05-syntax-in-functions.hs @@ -0,0 +1,124 @@ +-- Pattern matching - different function bodies for different destructured type matches + +lucky :: (Integral a) => a -> String +lucky 7 = "Lucky number seven!" +lucky _ = "Sorry, out of luck..." + +-- This also makes recursion pretty nice + +factorial :: (Integral a) => a -> a +factorial 0 = 1 +factorial x => x * (factorial (x-1)) + +-- When making functions, always include a catch-all pattern match + +-- We can use pattern patching to do destructuring. e.g. + +addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a) +addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2) + +-- By the way, can also pattern match in list comprehensions + +listComprehensionTest1 = [ a+b | (a,b) <- [(1,3),(2,4)] ] + +-- Here, we note that if a pattern match fails, it will be skipped and move on to the next element! + +-- Implement head ourselves: +head :: [a] -> a +head [] = error "Empty list has no head" +head (x:_) = x + +-- We can use cons to destructure multiple elements: +firstSum :: (Num a) -> [a] -> a +firstSum [] = 0 +firstSum (x:[]) = x +firstSum (x:y:[]) = x + y +firstSum (x:y:z:_) = x + y + z + +-- What about length? +length :: (Num b) => [a] -> b +length [] = 0 +length (x:xs) = 1 + (length xs) + +-- Now, sum: +sum :: (Num a) => [a] -> a +sum [] = 0 +sum (x:xs) = 1 + (sum xs) + +-- We can also name patterns: +emptyUnless2OrMore :: [a] -> [a] +emptyUnless2OrMore [] = [] +emptyUnless2OrMore (x:[]) = [] +emptyUnless2OrMore list@(x:y:_) = list + +-- Guards are a flow-through boolean alternation +bmiTell :: (Floating a) => a -> String +bmiTell bmi + | bmi <= 18.5 = "underweight" + | bmi <= 25.0 = "normal" + | bmi <= 30.0 = "overweight" + | otherwise = "obese" + +-- Interesting: implementation of max +max :: (Ord a) => a -> a -> a +max a b + | a > b = a + | otherwise = b + +-- Implementation of compare +myCompare :: (Ord a) -> a -> a -> Ordering +a `myCompare` b -- functions can also be defined using infix-syntax + | a < b = LT + | a == b = EQ + | otherwise = GT + +-- We can use `where` to define local variables for a function block: +bmiTell :: (Floating a) => a -> a -> String +bmiTell weight height + | bmi <= skinny = "underweight" + | bmi <= normal = "normal" + | bmi <= fat = "overweight" + | otherwise = "obese" + where bmi = weight / height ^ 2 + skinny = 18.5 + normal = 25.0 + fat = 30.0 + +-- the variables in `where` need to be indented at the same level so Haskell knows how to scope them correctly +-- we can also pattern-match in `where`: + +initials :: String -> String -> String +initials first last = [f] ++ ". " ++ [l] ++ "." + where (f:_) = first + (l:_) = last + +-- We can also define local functions in the where-body +calcBmis :: (Floating a) => [(a,a)] -> [a] +calcBmis xs = [bmi w h | (w,h) <- xs] + where bmi weight height = weight / height ^ 2 + +-- We can also use `let`, which is an expression, not a syntactic construct: +cylinder :: (Floating a) => a -> a -> a +cylinder r h = let sideArea = 2 * pi * r * h in + let topArea = pi * r ^ 2 in + sideArea + 2 * topArea + +-- Because it's an expression, it can be used to shorten inline code: +squaresTest1 = [let square x = x * x in (square 5, square 3, square 2)] + +-- let is good for quickly destructuring a type structure inline: +tupleTest1 = (let (a,b,c) = (1,2,3) in a+b+c) * 100 -- == 600 + +-- Case statements +-- Turns out, pattern matching function bodies is just syntactic sugar for case statements! + +head :: [a] -> a +head [] = error "No head of empty list" +head (x:_) = x + +-- Is equivalent to: +head :: [a] -> a +head xs = case xs of + [] -> error "No head of empty list" + (x:_) -> x + diff --git a/06-recursion.hs b/06-recursion.hs new file mode 100644 index 0000000..7e6160d --- /dev/null +++ b/06-recursion.hs @@ -0,0 +1,42 @@ + +-- Example: maximum in list +maximum :: (Ord a) -> [a] -> a +maximum [] = error "maximum of empty list" +maximum [x] = x +maximum (x:xs) = max x (maximum xs) + +-- Another example, replicate +replicate (Num i, Ord i) => i -> a -> [a] +replicate n x + | n <= 0 = [] + | otherwise = x:(replicate (n-1) x) + +-- Now, take +take :: (Num i, Ord i) => i -> [a] -> [a] +take n _ + | n <= 0 = [] -- if n is 0 or less, empty list + -- if guard is non-exhaustive, matching falls through to next pattern +take _ [] = [] +take n (x:xs) = x:(take (n-1) xs) + +reverse :: [a] -> [a] +reverse [] = [] +reverse (x:xs) = (reverse xs) ++ [x] + +zip :: [a] -> [b] -> [(a,b)] +zip _ [] = [] +zip [] _ = [] +zip (x:xs) (y:ys) = (x,y):(zip xs, ys) + +elem :: (Eq a) => a -> [a] -> Bool +elem _ [] = False +elem a (x:xs) + | a == x = True + | otherwise = a `elem` xs + +-- Double recursion! +quicksort :: (Ord a) => [a] -> [a] +quicksort [] = [] +quicksort (x:xs) = let smallerSorted = quicksort [ a | a <- xs, a <= x ] in + let biggerSorted = quicksort [ a | a <- xs, a > x ] in + smallerSorted ++ [x] ++ biggerSorted diff --git a/07-higher-order-functions.hs b/07-higher-order-functions.hs new file mode 100644 index 0000000..998545a --- /dev/null +++ b/07-higher-order-functions.hs @@ -0,0 +1,120 @@ +-- Partial application is awesome, since haskell functions are curried: +compareWithHundred :: (Num a, Ord a) => a -> Ordering +compareWithHundred x = compare 100 x + +-- this is equivalent to: +compareWithHundred :: (Num a, Ord a) => a -> Ordering +compareWithHundred = compare 100 + +-- Infix functions can also be partially-applied: +divideByTen :: (Floating a) => a -> a +divideByTen = (/10) + +-- (/10) 200 == 200 / 10 + +isUpperAlphanum :: Char -> Bool +isUpperAlphanum = (`elem` ['A'..'Z']) + +-- Functions can take functions as params: +applyTwice :: (a -> a) -> a -> a +applyTwice f x = f (f x) + +-- Example implementation of zipWith: +zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] +zipWith _ [] _ = [] +zipWith _ _ [] = [] +zipWith f (x:xs) (y:ys) = (f x y):(zipWith f xs ys) + +flip :: (a -> b -> c) -> b -> a -> c +flip f x y = f y x + +-- map built-in: +map (a -> b) -> [a] -> [b] +map _ [] = [] +map f (x:xs) = (f x):(map f xs) + +-- filter built-in: +filter (a -> Bool) -> [a] -> [a] +filter _ [] = [] +filter f (x:xs) + | f x = x:(filter xs) + | otherwise = filter xs + +-- Largest # under 100000 divisible by 3829: +-- (this stops after finding the first matching value, because of lazy evaluation) +largestDivisible :: (Integral a) => a +largestDivisible = head (filter p [100000,99999,..]) + where p x = x `mod` 3829 == 0 + +takeWhile :: (a -> Bool) -> [a] -> [a] +takeWhile _ [] = [] +takeWhile p (x:xs) + | p x = x:(takeWhile p xs) + | otherwise = [] + +-- Collatz sequence builder +chain :: (Integral a) => a -> [a] +chain 1 = [1] +chain n + | even n = n:(chain (n `div` 2)) + | odd n = n:(chain (n*3 + 1)) + +numLongChains :: Int +numLongChains = length (filter isLong (map chain [1..100])) + where isLong xs = length xs > 15 + +-- Instead, with lambdas! +numLongChains :: Int +numLongChains = length (filter (\xs -> length xs > 15) (map chain [1..100])) + +-- You can pattern-match in lambdas, but only on one case. If a value doesn't match the pattern, +-- you get a runtime error. So, make sure your pattern is exhaustive. + +-- Folds - foldl - left fold +sum :: (Num a) => [a] -> a +sum xs = foldl (\acc x -> acc + x) 0 xs + +-- Or, more succinctly with currying: +sum xs = foldl (+) 0 xs + +-- Generally, because of currying, a function `foo a = bar b a` can be written as `foo = bar b` +-- Another fold example: +elem :: (Eq a) => a -> [a] -> Bool +elem y ys = foldl (\acc x -> if x == y then True else acc) False ys + +-- Right folds `foldr` work similarly, but from the right side, and the acc x are reversed. e.g. +map :: (a -> b) -> [a] -> [b] +map f xs = foldr (\x acc -> (f x):acc) [] xs + +-- We could have done this with `foldl` and `++`, but cons is much cheaper than `++`, so we generally +-- use `foldr` when building lists from lists. + +-- `foldl1` and `foldr1` work the same as `foldl` and `foldr`, but they use the starting element in +-- the list as the initial accumulator (the left-most or right-most, respectively) + +-- `scanl` and `scanr` are like their fold* counterparts, but return an array of all the states of the accumulator: +scanl1Test = scanl1 (\acc x -> if x > acc then x else acc) [3,4,5,3] -- == [3,4,5,5] + +-- These have a `scanl1` and `scanr1` equivalent. The final result of `scanl*` is the last element, and the head +-- element for `scanr1` + +-- Function application +-- Take: +($) :: (a -> b) -> a -> b +f $ x = f x + +-- Right-associative function application! +sum (map sqrt (1 + 2 + 3)) == sum $ map $ sqrt $ 1 + 2 + 3 + +-- This also means we can use function application... as a function: +mapTest1 = map ($ 3) [(4+), (^2)] -- == [4 + 3, 3 ^ 2] + +-- Also, function composition: +(.) :: (b -> c) -> (a -> b) -> a -> c +f . g = \x -> f (g x) + +-- This means more concise code! TFAE +mapTest2 = map (\x -> negate $ abs x) [1..3] +mapTest3 = map (negate . abs) [1..3] + +-- If we want to compose functions with multiple parameters, we need to partially apply them first. diff --git a/08-modules.hs b/08-modules.hs new file mode 100644 index 0000000..d01efbe --- /dev/null +++ b/08-modules.hs @@ -0,0 +1,26 @@ +-- Splat into global namespace +import Data.List + +-- Splat specific items into global namespace +import Data.List (nub, sort) + +-- Splat everything but specific items +import Data.List hiding (nub) + +-- Import, but not in global namespace +import qualified Data.List + +-- Import, but not in global namespace, but with alias +import qualified Data.List as L + +-- In Geometry.hs +module Geometry ( + sphereVolume, + cubeArea -- exported functions +) where + +sphereVolume :: Float -> Float +sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3) + +cubeVolume :: Float -> Float +cubeVolume side = side * side * side diff --git a/09-types-and-typeclasses-redux.hs b/09-types-and-typeclasses-redux.hs new file mode 100644 index 0000000..606ab8f --- /dev/null +++ b/09-types-and-typeclasses-redux.hs @@ -0,0 +1,179 @@ +-- data keyword defines a new data type. e.g. from prelude: + +data Bool = False | True + +data Shape = + Circle Float Float Float + | Rectangle Float Float Float Float + deriving (Show) + +-- Value constructors are actually functions returning the data type +-- :t Circle +Circle :: Float -> Float -> Float -> Shape + +surface :: Shape -> Float +surface (Circle _ _ r) = pi * r ^ 2 +surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1) + +-- Note the signature. We can't do `surface :: Circle -> Float` because `Circle` is NOT a type, `Shape` is. +-- Because value constructors are functions, we can partially apply them! + +map (Circle 10 20) [4,5] -- == [Circle 10 20 4, Circle 10 20 5] + +-- Let's make the data type more clear +data Point = + Point Float Float + deriving (Show) + +data Shape = + Circle Point Float + | Rectangle Point Point + deriving (Show) + +-- now, we pattern match: +surface :: Shape -> Float +surface (Circle _ r) = pi * r ^ 2 +surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1) + +-- To export a data type from a module, export Shape(..) for all constructors, or, e.g., Shape(Circle,Rectangle) + +-- We can use the record syntax to get named fields with automatic lookup functions: + +data Person = + Person { + firstName :: String, + lastName :: String, + age :: Int + } deriving (Show) + +personTest1 = Person {firstName="Garrett", lastName="Mills", age=21} + +-- This also creates automatic attribute access functions: +personTest2 = firstName personTest1 -- == "Garrett" + +-- Types can have zero or more parameters: +data Maybe a = Nothing | Just a + +-- We can pattern match on the record type: +sayHi :: Person -> String +sayHi (Person {firstName=f, lastName=l}) = "Hello, " ++ [f, ' ', l] + +-- 3D vector type example: +data Vector a = + Vector a a a + deriving (Show) + +vplus :: (Num t) => Vector t -> Vector t -> Vector t +(Vector i j k) `plus` (Vector l m n) = Vector (i+l) (j+m) (k+n) + +-- In this case, it is best to put the typeclass restriction on the functions +-- where it matters, instead of the data declaration itself, to avoid unnecessary restrictions. + +-- Example of a nullary, enumerable typeclass: +data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday + deriving (Eq, Ord, Show, Read, Bounded, Enum) + +-- The `type` keyword can be used to alias types. e.g. +type String = [Char] + +-- Aliases can be parameterized: +type AssocList k v = [(k,v)] + +-- We can also partially-apply type constructors as aliases. TFAE: +type IntMap v = Map Int v +type IntMap = Map Int + +-- Data types can be recursive. e.g. implementing our own cons-list: +data List a = + Empty + | Cons a (List a) + deriving (Show, Read, Eq, Ord) + +-- We can make an op infix by using only special chars. We also define the fixity to determine the tightness of the bind: +infixr 5 :-: +data List a = + Empty + | a :-: (List a) + deriving (Show, Read, Eq, Ord) + +-- Let's implement a binary search tree: +data Tree = + Empty + | Node a (Tree a) (Tree a) + deriving (Show, Read, Eq) + +singleton :: a -> Tree a +singleton x = (Node x Empty Empty) + +treeInsert :: (Ord a) => a -> Tree a -> Tree a +treeInsert x Empty = singleton x +treeInsert (Node a left right) + | x == a = Node x left right + | x < a = Node a (treeInsert x left) right + | x > a = Node a left (treeInsert x right) + +treeElem :: (Ord a) => a -> Tree a -> Bool +treeElem _ Empty = False +treeElem x (Tree a left right) + | x == a = True + | x < a = (treeElem x left) + | otherwise = (treeElem x right) + +-- Defining typeclasses by hand +-- Here's the typeclass Eq: + +class Eq a where + (==) :: a -> a -> Bool + (/=) :: a -> a -> Bool + x == y = not (x /= y) + x /= y = not (x == y) + +-- Now, we can make our custom data types instances of a typeclass by hand: + +data TrafficLight = Red | Yellow | Green + +instance Eq TrafficLight where + Red == Red = True + Green == Green = True + Yellow == Yellow = True + _ == _ = False + +-- Defining (==) and (/=) in terms of mutual recursion means that we only need to +-- specify one of them in our instance declarations. + +-- Similarly, we can do: +instance Show TrafficLight where + show Red = "Red light" + show Yellow = "Yellow light" + show Green = "Green light" + +-- We can force typeclasses to be subclasses of others. Partial example: +class (Eq a) => Num a where + -- ... + +-- In this case, Num instances must satisfy Num AND Eq. +-- :info TypeClass in GHCi will show the functions for a particular typeclass or constructor + +-- Functors +-- Consider: +class Functor f where + fmap :: (a -> b) -> f a -> f b + +-- e.g. how map is a functor +instance Functor [] where + fmap = map + +-- Notably, we need f to be a type constructor, not a concrete type. [a] is concrete, [] is a constructor +-- Similarly, Maybe: + +instance Functor Maybe where + fmap f (Just x) = Just (f x) + fmap f Nothing = Nothing + +-- What about Either? It takes 2 parameters. Partially apply it! +data Either a b = Left a | Right b +instance Functor (Either a) where + fmap f (Right x) = Right (f x) + fmap f (Left x) = Left x + +-- Use :k Type to get the kind of a type or type constructor in GHCi diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbeb2b4 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# haskell + +Some notes and tinkerings from my adventures in Haskell. + +Most notes heavily based on Learn You Haskell for a Great Good: http://learnyouahaskell.com +