This commit is contained in:
Garrett Mills 2021-09-29 03:14:23 -05:00
commit cc5a1154be
9 changed files with 664 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea*

80
03-starting-out.hs Normal file
View File

@ -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")]

View File

@ -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

124
05-syntax-in-functions.hs Normal file
View File

@ -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

42
06-recursion.hs Normal file
View File

@ -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

View File

@ -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.

26
08-modules.hs Normal file
View File

@ -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

View File

@ -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

6
README.md Normal file
View File

@ -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