Notes I made, for quick reference and memory, while reading and solving exercises from Will Kurt's Get Programming in Haskell
, Monads and Parsing from Real World Haskell
- Let and Where are both ways of creating variables. Choosing to use let or where is a matter of style the vast majority of the time in Haskell.
- Lambdas can also be used to 'implement' variables example:
myFunction x y = (\ x y -> if x > y then x + y else x) (x ^ 2) (y ^ 2)
- "We are passing in a function and returning a lambda function. The function func that we passed in is captured inside the lambda function. When we capture a value inside a lambda function, this is referred to as a closure (on the function func). (From Will Kurt's Get Programming in Haskell book)
ifEvenApplyFuncClosure func = (\ x -> if even x then func x else x)
- Partial functions are critical
> add4 a b c d = a + b + c + d
> adderPlus3 = add4 3
> adderPlus3 1 2 3
9
-
Partial functions are the reason why arguments should be ordered from most to least general.
-
We can write a function which can take in a function as an argument, and flips the order of the function's argument
flipBinaryArgs myfunction = (\x y -> myfunction y x)
- A built in Haskell function also exists called
flip
to flip binary arguments. To flip negative binary operator:
> flipedNegativeSign = flip (-)
> test = flip (-) 1 10
9
- "Closures combine lambda functions and first-class functions to give you amazing power." Will Kurt.
An example of this power
ifEven func x = if even x
then func x
else x
ifEvenInc = ifEven ( \x -> x + 1)
ifEvenDouble = ifEven (\x -> x * 2 )
- "h" is a list of single characters in haskell, 'h' is a single list
['h'] == "h"
-
basically you can cons an element of type T, with a list that contains elements of type T
-
cons is not the same as combining two lists which is done using the
++
operator. -
"Haskell uses a special form of evaluation called lazy evaluation. In lazy evaluation, no code is evaluated until it’s needed. In the case of longList, none of the values in the list were needed for computation."
-
biggest disadvantage of lazy evaluation is that it’s much harder to reason about the code’s performance
-
implementing Repeat, which takes a list and repeats the list indefinitely
myRepeat list = cycle [list]
- Recusion rules (from W. Kurt's book)
- Identify the end goals
- What happens if the end goals are reached
- List alternate possibilities.
- Ensure alternate possibilities move towards the end goal
-
Pattern matching can help a lot with recursion.
-
Implementing cycle -- beautiful
> myCycle list = list ++ myCycle (list)
> take 9 (myCycle [1,2..100])
[1,2,3,4,5,6,7,8,9]
- Use foldl (reduce) to reverse a list
rcons x y = y:x
myreverse2 list = foldl rcons [] list
- implement foldl (tail recursive)
reduce_left op init [] = init
reduce_left op init (x:xs) = reduce_left op (op init x) xs
- implement foldr (stack is created
reduce_right op init [] = init
reduce_right op init (x:xs) = op x (reduce_right op init xs)
instance Eq Int -- Defined in ‘GHC.Classes’
means that Int implements Eq in GHC.Classes
-
OOP "All objects can be viewed as a collection of attributes that you send messages to"
-
Objects are constructed using:
myObject (prop1,prop2,prop2) = \message -> message (prop1,prop2,prop3)
These functions basically let us create a function -- which returns a function that is "blank" i.e. waiting for a function to be passed to it.
Basically it is an "object" which lets us pass a "message" to it. Or, it traps the arguments -- and we can pass any function (to be applied to the arguments) later. Or, "objects" here are just data members which have this extra functionality to accept a message (ie a function).
- In Haskell, new "objects" are created by modifying copies of old, existing ones.
isPrime :: Int -> Maybe Bool
isPrime n | n < 0 = Nothing
| n > (length primes) = Nothing
| otherwise = Just ( n `elem` primes)
-- where:
primes :: [Int]
primes = sieve [2..10000]
sieve :: [Int] -> [Int]
sieve [] = []
sieve (firstElement : rest ) = firstElement : sieve ( filteredRest )
where filteredRest = filter (not . (== 0 ) . (`mod` firstElement)) rest
-
"Haskell uses type inference to automatically determine the types of all values at compile time based on the way they’re used."
-
"Int" is a type which represents how machines store numbers. "Integer" is a more true representation of the mathematical integer.
-
A list of type [Char] is a string (or a list of characters) of arbitrary length. A tuple of type (Char) is a tuple with exactly one element -- fixed length.
-
To convert from an Integral type to a fractional type:
half :: Int -> Double
half x = (fromIntegral x) / 2
- To make sure you get returned an integral type:
5 `div` 2
- Use
show
andread
to convert to and from a string. - Although when doing a read you need to specify the return type
read "6" :: Int
- Type Variable: a lowercase letter in a type signature indicates that any type can be used in that place.
simple :: a -> a
simple x = x
- We can create a type synonyms by using the
type
keyword. Example
type FirstName = String
type SecondName = String
-- or
type PersonName = (FirstName, SecondName)
- Creating a new type can be used by using the
data
keyword. Example
data ABOType = A | B | AB | O
showABO :: ABOType -> String
showABO value = case value of
AB -> "AB"
A -> "A"
B -> "B"
O -> "O"
-- other examples:
type Age = Int
type FirstName = String
data Patient = Patient FirstName Age Int
-- or:
data Name = Name FirstName LastName
| NameWithMiddle FirstName MiddleName LastName
data Sex = Male | Female
data PaitientV2 = PaitientV2 { name :: Name,
sex :: Sex,
age :: Int}
- These data types created using the record syntax can generate automatic getters and setters for us
jackie :: PaitientV2
jackie = PaitientV2 { name = Name "Jackie" "Smith",
sex = Female,
age = 42,
weight = 60,
height = 170,
bloodType = BloodType AB Pos}
-- getters:
test0 = age jackie
test1 = showBloodType (bloodType jackie )
test2 = showName (name jackie )
--setter:
jackieUpdated = jackie { age = 44 }
test3 = age jackieUpdated
test4 = showBloodType ( bloodType jackieUpdated)
-
"Type classes allow you to group types based on shared behavior"
-
A type class states which functions a type must support. "Type classes require us to think in increasingly more powerful forms of abstraction and form the heart of Haskell programming"
-
A type
a
can belong to a type class (say)Num
, which generalizes the idea of a "number". There can be various other types that belong toNum
. And all of them must implement certain functions. This info about a type class can be obtained via:
:info Num
The definition is a list of functions that all members of the class must implement.
- Example:
-- here, ```a``` is a type of class Num
-- and, this fxn accepts two Nums of type "a", and returns a type "a"
addThenMultiply :: Num a => a -> a -> a
addThenMultiply num1 num2 = ( num1 + num2 ) * 2
This function would work on Int, Double etc. Or on any other type that a programmer has created, and has implemented the Num type class.
- Can define a new type like:
class Describable a where
describe :: a -> String
Now all types that choose to be a Describable have to implement this describe function.
-
If we use :info , example: :info Int, we get all the type classes for which Int is a member of.
-
Example of an existing type class Bounded:
class Bounded a where
minBound :: a
maxBound :: a
- Here minBound and maxBound are not functions, but just fixed values:
Prelude> minBound :: Int
-9223372036854775808
-
Any type can belong to a generalized Type Class. Like
:info Int
shows the type classes that Int belongs to. -
We can define new functions using the Type Classes instead of Types in our function type signature, instead of individual types, for more generalization.
-
If a type belongs to a particular type class, it must implement the functions specified by the type class...
-
... Unless it derives them from the type class. Example: Haskell automatically implemented the type class Show for MyRandomType type here:
data MyRandomType = MyRandomType ChildTypeRandom deriving (Show)
-
We can also implement our own type classes.
-
To get into about any class (type class, or type) use
:doc <type>
example:
:doc Int
- Type constructor and data constructor
data BloodType = BloodType ABOType RhType
-- type^constructor ^ data constructor
- Generally when we create our own type. We can include it as a member of one of these Type Classes by implementing the functions specified in the documentation.
data SixSidedDie = S1 | S2 | S3 | S4 | S5 | S6
-- to make elements of type SixSidedDie able to print, we need to implment Show (IE implement the method requred by Show to implement)
instance Show SixSidedDie where
show S1 = "one"
show S2 = "two"
show S3 = "three"
show S4 = "four"
show S5 = "five"
show S6 = "six"
- to make us able to equate objects of this type, we include it under the Eq type class, and implement the function(s) required
instance Eq SixSidedDie where
(==) v1 v2 = (==) (show v1) (show v2)
- To make it comparable, we need to implment Ord. We see that Ord requires a minimum implementation of
compare
*Main> :info Ord
class Eq a => Ord a where
compare :: a -> a -> Ordering
The ordering type is defined as:
*Main> :info Ordering
data Ordering = LT | EQ | GT
Let's implement Ord for SixSidedDie
toNumber :: SixSidedDie -> Int
toNumber S1 = 1
toNumber S2 = 2
toNumber S3 = 3
toNumber S4 = 4
toNumber S5 = 5
toNumber S6 = 6
instance Ord SixSidedDie where
compare S6 S6 = EQ
compare S6 _ = GT
compare v1 v2 = if ( toNumber v1 == toNumber v2 ) then EQ
else if ( toNumber v1 > toNumber v2 ) then GT
else LT
- We can also make
SixSidedDie
a part of Enum (ie make SixSidedDie an instance of TypeClass Enum) and then use the enum functions fromEnum to manually order Eq and Ord
instance Enum SixSidedDie where
toEnum 0 = S1
toEnum 1 = S2
toEnum 2 = S3
toEnum 3 = S4
toEnum 4 = S5
toEnum 5 = S6
toEnum _ = error "No such value"
fromEnum S1 = 0
fromEnum S2 = 1
fromEnum S3 = 2
fromEnum S4 = 3
fromEnum S5 = 4
fromEnum S6 = 5
The type of fromEnum is
> :t fromEnum
fromEnum :: Enum a => a -> Int
that is, it accepts an Enum type and returns an Int. Now SixSidedDie is an Enum as well. hence we can use:
> fromEnum S2
1
> toEnum 1 :: SixSidedDie
two
-
You can run
:info SixSidedDie
to check all the type classes that SixSidedDie implements -
Although deriving these Type Classes is often a better idea than implementing everything
data SixSidedDieV2 = S01 | S02 | S03 | S04 | S05 | S06 deriving (Show, Enum, Ord, Eq)
test3 = S01 > S01
test2 = [S01 .. S06]
- To DEFINE your own type class, which has requirement for one method implementation
Die a where
printSide :: a -> String
- To DEFINE your own type class, which has some super classes:
class (Eq a, Enum a) => Die a where
printSide :: a -> String
Make sure that all instances of Die (ie who ever choses to be a type of Die ) then are also implementing Eq and Enum. (i.e are also Enum and Eq themselves.)
All instances of Die, (all types that are a Die/all types that implement Die) should also implement printSide
- You can add type signatures for partial applications too. For example in Capstone02Types...hs, there is function
rottN :: (Enum a, Bounded a) => Int -> a -> a
rottN int char = .... etc etc...
-- partial application, note type signature. It is bascically like the one from the above function except the Int!
rot13x :: (Enum a,Bounded a) => a -> a
rot13x = rottN 26
-
A type signature is a description of a transformation. Types in Haskell allow us to view programs as a series of transformations.
-
By thinking about type transformations we can design an overall program in a similar way to designing a function.
-
In Haskell, we shall thing in and use types extensively. i.e Types first and using functions to flesh out the details.
-
Types that we have seen so far are algebraic data types. That is, types created by 'and'ing two data types, or 'or'ing them.
Example: a Name type is a String and another String.
A Bool type is either True
data constructor or False
data constructor.
and types == product types or types == sum types
-
Nearly all programming languages support product types. Example: Structs in C, classes in Java, etc.
-
By making new types only by combining existing types leads to a 'top-down' design. "We can only expand to an idea by adding to it" "This is the basis for designing software in terms of class hierarchies."
-
Lets say we want an abstraction for all the items being sold at a book store. That is, all items will be represented by a certain class
StoreItem
.
How do we do this if the items available in the store? i.e make this class searchable.
For example: if the store sells vinyls and books. Both have very different properties and can not be abstracted out easily.
"The big problem is that you want a single type that represents both vinyl records and books so you can make a searchable inventory. Because you can compose types only by and, you need to develop an abstraction that describes everything that records and books have in common. You’ll then implement only the differences in the separate classes. This is the fundamental idea behind inheritance"
Now we implement the code using a common class say StoreItem
. And implement product specific logic (Books and Vinyls) using conditionals.
What do we do if we want to later add a third type of item to our inventory?
What if the third type of item does not have attributes common with Book and Vinyl.
- Therefore, the design can get really complicated with using only
product types
even for simple cases.
-
They let you combine two types with an
or
-
Examples from the book:
A die is either a 6-sided die or a 20-sided die or ....
A paper is authored by either a person (String) or a group of people ([String]).
- data Bool = False | True
is an example of an
or
type.
-
The Sum Types discussed above are kind of unique to Haskell (or FP?) and lets us use design patterns not available in traditional programming languages.
-
Composability is another divergence from regular software design.
-
Composability means that you create something new by combining two like things." Like, concat two strings, two lists to get a new list, two documents. Each of these methods of combining types has a unique operator or function.
Example
example = "What" ++ " Are " ++ " the " ++ " alternatives"
- We can combine functions by using a period
.
Example:
myLast :: [a] -> a
myLast = head . reverse
- Any type can be made a type of the Semigroup type class. Example
instance Semigroup Integer where
(<>) x y = x + y
-
Monoids are semi groups with one more constraint: a type implmenting a monoid type class must also contain the identity element.
-
Even though it seems like semigroup should be a superclass of monoid in Haskell. But no, Monoid is not implemented as a subclass of Semigroup.
-
Definition of Monoid:
class Monoid a where
mempty :: a
mappend :: a -> a -> a
mconcat :: [a] -> a
- the
mempty
is the identiy element.mappend
is used in monoid as the operator instead of<>
`
- the
[]
in Haskell, also implements monoid. So:
GHCi> [1,2,3] ++ []
[1,2,3]
GHCi> [1,2,3] <> []
[1,2,3]
GHCi> [1,2,3] `mappend` mempty
[1,2,3]
- To make a type an instance of the Monoid type class, we just need to implement
mempty
andmappend
. Haskell automatically generatesmconcat
for us.
Type signature for mconcat
is:
mconcat :: Monoid a => [a] -> a
- Haskell is able to infer the definition of
mconcat
using:
mconcat = foldr mappend mempty
Why not foldl?
- From the book "Note that the reason mconcat uses foldr instead of foldl is due to the way that foldr can work with infinite lists, whereas foldl will force the evaluation."
-
Types can take arguments -- but by using type variables in their definitions. "Their arguments are other types."
-
Quote from GPWH Book "Parameterized types allow us to define generic data structures that work with a wide range of existing data."
-
Slightly similar to generics in C# or Java. Parameterized types let us create "containers" that can hold other types. Like,
List<String>
or,
Pair<Integer,String>
-
We use these generic types to constraint the type of values that the container can take.
-
Basic example
-- a box that contains other types
data Box a = Box a deriving Show
- Previously, to create a new type we would use:
data TripleV2 = TripleV2 String String String deriving Show
See, now these parameterized types are types, that basically allow us to include arbitrary data types (while creating new data types) and nothing else,
data Triple a = Triple a a a deriving Show
These are like generic types.
- Examples:
type Point3D = Triple Double
origin :: Point3D
origin = Triple 0.0 0.0 0.0
-- more types:
type FullName = Triple String
aditya :: FullName
aditya = Triple "A" "S" "Verma"
- A function to transform a Triple a
transform :: Triple a -> ( a -> a) -> Triple a
transform (Triple x y z) func = Triple (func x) (func y) (func z)
This function is different from map functions for Lists, because map lets us change the type. Transforming (as defined above) does not.
- To implement our own List container (very interesting + important)
data MyList a = Empty | Cons a (MyList a) deriving Show
-- let us create new implementation of List (which contains Integers) type from our original parameterized type
type IntList = MyList Int
testList :: IntList
testList = Cons 1 ( Cons 2 (Cons 3 Empty ))
- To implement a map for your custom list
MyList
myMap :: ( a -> b ) -> MyList a -> MyList b
myMap func (Cons ele rest) = Cons (func ele) (myMap func rest)
myMap func _ = Empty
- A tuple in Haskell is a type which is multi-parameterized. You can define your own 2-tuple like this:
data MyTuple a b = None | Tup a b deriving (Show)
type IntStringTups = MyTuple Int String
rollName1 :: IntStringTups;
rollName1 = Tup 1 "Aditya"
rollName2 = Tup 2 "Afdsa"
-
In Haskell, just like Data and Functions have their type. Types have their own type as well. The type of a type is called a Kind.
-
Kind of Type indicates the number of types that the type takes.
-
Types that take no parameters have a kind
*
. Example,
*Main> :kind Int
Int :: *
- Types that take one parameter have a type
* -> *
*Main> :kind Triple
Triple :: * -> *
Kind of a two-tuple
*Main> :kind (,)
(,) :: * -> * -> *
- Kinds are of importance when we study monads and functors.
-
This is another parameterized type in Haskell.
-
Module which has an implementation for Map
import qualified Data.Map as Map
- The function that helps us create a Map:
*Main> :t Map.fromList
Map.fromList :: Ord k => [(k, a)] -> Map.Map k a
We can see from the type signature that it takes a list of tuples, the first element of the tuple must belong to an 'Orderable' type, (ie must inherit/implement Ord).
(This is because internally the Map is implmented via a binary tree. This is different from a hash map which is implemented using a hash function)
The fromList
function then returns us a Map k a
- So to create a Map use
Map.fromList
pairs = zip list1 list2
ourMap = Map.fromList pairs
The data ourMap
will return us our Map. Example
*Main> ourMap
fromList [(2,Heart),(7,Heart),(13,Brain),(14,Spleen),(21,Spleen),(24,Kidney)]
*Main>
- How do we lookup values from the map? Via
Map.lookup
. An example:
*Main> Map.lookup 24 organMap
Just Kidney
In general:
*Main> value = Map.lookup <key> <mapName>
- When we print
value
, we get
*Main> value
Just Kidney
Why the Just
? Let us check the type of value
*Main> :t value
value :: Maybe Organ
Organ
was the type of our value.
- How to combine two Maps?
m1 = Map.fromList [(1,1),(2,2)]
m2 = Map.fromList [(3,3),(4,4)]
-- we will write a method that can insert a pair to our map
insertPair :: Ord k => Map.Map k a -> (k, a) -> Map.Map k a
insertPair myMap (key, value) = Map.insert key value myMap
-- how to combine:
m3 = foldl insertPair m1 (Map.toList m2)
-- how to convert insertPair into a lambda function?
- How to combine two lists into a single map?
mapInitial = foldl insertMaybePair Map.empty (zip keys1 values1)
updatedMap = foldl insertMaybePair mapInitial (zip keys2 values2)
-- using fold to add many mant values
-- internally we are just doing Map.insert again and again
-- insert maybe pair takes in a map, and a (key,value) pair and creates a new map from the old map and with the updated values
-- method that can insert a maybe to our map
insertMaybePair :: Ord k => Map.Map k a -> (k,Maybe a) -> Map.Map k a
insertMaybePair myMap (key,Nothing) = myMap
insertMaybePair myMap (key, Just value) = Map.insert key value myMap
-
Parameterized types (generic types, that let you create new types by "giving them" your chosen type as a parameter) are much more important in Haskell, than generics are in OOP languages.
-
Unlike Lists or Maps which represent containers of values of a certain type. A Maybe type represents a context for a value.
-
Maybe types represent values that might be missing.
Instead of using null, "...the Maybe type allows you to write much safer code. Because of the power of the Maybe type, errors related to null values are systematically removed from Haskell programs."
- Say we quickly create a Map:
groceries :: Map.Map String Int ;
groceries = Map.fromList [("Milk",1),("Candy bars",10), ("Cheese blocks",2)]
getAMaybe = Map.lookup "Randomdaskda" groceries
getMilk= Map.lookup "Milk" groceries
On checkin the types on GHCI, when we lookup we are returned a Maybe Int both times:
*Main> getAMaybe
Nothing
*Main> :t getAMaybe
getAMaybe :: Maybe Int
*Main>
*Main>
*Main> getMilk
Just 1
*Main>
*Main> :t getMilk
getMilk :: Maybe Int
-
Maybe is a type in a context. The context being that the type might be missing. This parameterized type is not like the others discusses before, they all represent containers.
-
Definition of Maybe
data Maybe a = Nothing | Just a
Any Maybe type
is either nothing or it is just a value from that type
- When looking up a value from a HashMap, the approach in a lot of programming languages is to return an error in case the value in not found.
This can go wrong easily because where ever (not just in Maps), a function possibly throws an error for such a scenario, the developer must catch the error and handle the exception, to make sure the program does not crash. These checks need to be placed everywhere.
Freedom to handle it differently (or use alternate logic) is restricted.
Example, We might have wanted to handle the error differently depending on what value (that was expected) went missing.
-
Returning a
null
might lead to even more problems. Null checks would have to be placed everywhere. If a check is missed, they propagate further and cause more issues, that might not have been handled elewhere either. Null pointer exceptions are quite common. -
Why
Maybe
? When a function returns a Maybe, the calling function can not avoid the fact that the data value is aMaybe
. "Maybe makes it impossible to forget that a value might be null." -
Maybe types are returned everywhere where there is a chance that the requested value might not exist.Example, opening files, RESTful API calls requesting a resource, reading from DB etc...
(Credit:examples from W. Kurt's Get Programming in Haskell)
- Let us say you have a list of Maybes, you can use isJust or isNothing to filter our the values from the Nothings
drawerContents drop dropWhile
*Main> filter Data.Maybe.isJust drawerContents
[Just Heart,Just Heart,Just Brain,Just Spleen,Just Spleen,Just Kidney]
IO
is also a parameterized type. Unlike a List (which is, again, a container),IO
represents a context in which the value comes from an Input/Output operation.
Just like Maybe
which represents a context in which a vaue might be missing.
- To verify the above:
GHCi> :kind Maybe
Maybe :: * -> *
GHCi> :kind IO
IO :: * -> *
-
Examples, reading user input, reading from files, writing to console.s
-
IO is inherently stateful, it changes the state of files.
-
IO is also impure, refrential integrity is not followed. Because, a function can take in user input using
getLine
and return different values each time it is called.
A function might read a value from a file whose value changes (by other programs). Calling this function multiple times will return different values each time.
IO types are created to prevent pure and impure methods from mixing.
IO is also prone to errors.
- Despite all the above, IO is important.
From the book
What good is a program that doesn’t change the state of the world in some way? To keep Haskell code pure and predictable, you use the IO type to provide a context for data that may not behave the way all of the rest of your Haskell code does.
- A method of a type
IO ()
can not return anything.
Example, main :: IO()
- Take the example
helloPerson :: String -> String
helloPerson name = "Hello" ++ " " ++ name ++ "!"
main :: IO ()
main = do
putStrLn "Hello! What's your name?"
name <- getLine
let statement = helloPerson name
putStrLn statement
From the above:
getLine
is an object of the typeIO String
name
consequently is anIO String
too- Whenever we need an IO String obejct to talk to the external world we use
let
putStrLn
returns nothing.putStrLn :: String -> IO ()
main
is not a function, it is an IO Action "Some IO actions return no value, some take no input, and others don’t always return the same value given the same input."
Other ideas directly from W. Kurt's book
- "The interesting thing about getLine is that you have a useful return value of the type IO String."
- "Because I/O is so dangerous and unpredictable, after you have a value come from I/O, Haskell doesn’t allow you to use that value outside of the context of the IO type*. For example, if you fetch a random number using randomRIO, you can’t use that value outside main or a similar IO action. "
- ". Because of this, after you’re working with data in the context of IO, it must stay there. This initially may seem like a burden. After you’re familiar with the way Haskell separates I/O logic from everything else, you’ll likely want to replicate this in other programming languages (though you won’t have a powerful type system to enforce it)."
-
Because one can not escape the IO context, one needs to find a way to perform sequence of computations within the IO context. The
do
keyword helps us with that. -
The
do
keyword allows us to use IO types as if they were regaular types. -
This is why both
let
and<-
are used in the do block. -
Variables assigned with
<-
allow us to act as though a typeIO a
is just of type a. Example
name <- getLine
We can now pretend that name
that is an IO String
is a String
- We use let statements whenever you create variables that aren’t IO types. Example
let statement = helloPerson name
Not using a let in a do block (when creating a non-IO) variable will give us a parse error.
Statement is just a normal String. We had to use it because we were getting the values from a function in a non-IO context.
- We had to use
<-
forname <- getLine
, because in the line after that we pass this IO String to a function that only accepts a String.
"Do-notation allows you to assign an IO String variable by using <-, to act like it’s an ordinary String, and then to pass it to functions that work with only regular Strings."
(How does this work?)
-
If you need to accept a
Double
or another type from the console. You can useread
-
IO can use do-notation because it’s a member of a powerful type class called
Monad
.Do
is not specific to IO, and can be used by any member ofMonad
type class. -
Maybe
is a part of theMonad
type class too. -
"...the Monad type class allows you to write general programs that can work in a wide range of contexts."
-
Haskell handles all the dangers of IO by ensuring that all I/O logic is contained in an IO type.
- To get arguments, we use
getArgs
function found in System.Environment.
The type signature of getArgs is as follows:
getArgs :: IO [String]
It is basically a list of strings but in the IO context. Hence the IO
getArgs is important to take user input.
- To be able to map over this special list of strings, we need to use a different kind of a map function.
mapM_ putStrLn args
map (+ 1 ) [1,2,3]
mapM is a modified map to map Monad
types.
mapM_ is modified mapM, such that it does not output a list. This is because the IO functions (called IO actions) do not return any values.
- Interacting with the command line, an Example
main :: IO ()
main = do
args <- getArgs
putStrLn "hi"
mapM_ putStrLn args
-- to be able to use getArgs,
-- we need to compile it in the command line
-- ghc 26ioIntro.hs
-- ./26ioIntro.hs 1 2 3
-- this outputs:
{-
hi
1
2
3
-}
Another example of using mapM,
> x = mapM ( Just . (+1 ) . read) ["1","2","3"]
> x
Just [2,3,4]
> :t x
x :: (Num b, Read b) => Maybe [b]
- To get input from ghci itself, 3 times:
main :: IO()
main = do
userInput <- mapM (\ _ -> getLine) [1,2,3]
mapM_ putStrLn myMap
<-
is used because mapM returns the values inputted by the user, all of which are the IO String,
so it is returning [IO String]
<-
will let us pretend that this is list is a normal string list.
- Instead of using the mapM and mapM_ technique above,
Haskell has a function called
replicateM
replicateM
takes a number n
and an IO action, and repeats the IO action n
number of times.
Although, one needs to import Control.Monad
before using this function.
> :t replicateM
replicateM :: Applicative m => Int -> m a -> m [a]
- Application of the above IO concepts:
-- we need to write a program that lets us sum n number of command line arguments,
-- this n is accepted dynamically from the prompt
-- then n values are read from the CLI
-- the sum of the n values is returned
main :: IO()
main = do
args <- getArgs
let linesToRead = if length args > 0
then read (head args)
else 0
numbers <- replicateM linesToRead getLine -- always remember, all the IO functions use <-, let is for normies
let sumOfInputs = sum (map read numbers :: [Int])
print (sumOfInputs)
- You can implement replicateM easily like this
myReplicateM n ioAction = mapM (\ _ -> ioAction) [1..n]
You can also run it directly on ghci as well,
> myReplicateM 4 getLine
10
11
1
2
["10","11","1","2"]
-
In our previous examples, the number of lines to be read is fixed. What if we have to keep on reading values from the console?
-
We have the IO type to separate IO functions from other Haskell functions.
main
should contain very little logic. -
In the example in the previous sub-section, we assumed that we have to deal with the inputs right away? Can we not defer it for the future?
-
Instead of assuming that the inputs are discrete IO values inputted by the user, let us treat user input like a list of characters.
-
That will make it much easier to separate the IO from the logic.
-
For the above we use
getContents
. This lets us treat the stream from the terminal as a list of characters.
Using getContents
we can completely re-write the program, assuming we have a list of characters -- and leaving the IO operations for later.
-
Important:
getContents
will keep asking for user input, until we send it an EOF, which isControl-D
-
One can then pass the contents from
getContents
to regular Haskell functions
contents <- getContents
let numbers = toInts contents
print (sum numbers)
This is the only time we have to do IO / treat our list as IO.
Rest of the logic is written without the IO ()
- Example of lAZY IO:
quoteGenerator :: Int -> String
quoteGenerator 1 = "Hi QUote 1"
quoteGenerator 2 = "Hi QUote 2"
quoteGenerator 3 = "Hi QUote 3"
quoteGenerator _ = "Hi QUote Random"
main :: IO ()
main = do
putStrLn "Enter a number"
contents <- getContents
mapM_ putStrLn (map quoteGenerator (toInts contents))
See, over here the promt can keep asking for input,
because essentially contents is inside the mapM_
and we keep adding the the contents list.
Hence it keeps invoking the putStrLn
- In this example the prompt has to close
main4 :: IO ()
main4 = do
contents <- getContents
let numbers = toInts contents
print (sum numbers)
It does keep asking the user for values basically, and keeps putting them into numbers. But once the Ctrl-D is pressed, there is no more addition to getContents
and the result gets printed.
How do we make it reactive? (not sure, attempt in 27lazyIO.hs)
-
String (a list of chars) is a really inefficient way to process Strings. There is a another type called Text which will help us with string processing.
-
Text is implemented as an array under the hood, unlike String which is implemented as a list
-
Text does not use lazy evaluation.
-
We have two important functions for String/Text conversion
T.pack :: String -> T.Text
T.unpack :: T.Text -> String
These conversion computations are not cheap.
myWord :: T.Text
myWord = "random"
This above would throw an error, because "these literals" are Strings in Haskell.
And we can not fix this issue in haskell code.
You can pass a flag before compiling a hs program, or pass it as a flag when invoking ghci.
Example:
$ ghc text.hs -XOverloadedStrings
Another way, is to add a pragma on top of our file like,
{-# LANGUAGE <Extension Name> #-}
For text,
it will be:
{-# LANGUAGE OverloadedStrings #-}
This is a language extension
- almost all the String function their corresponding version for working on Text in Data.Texts
Example. lines
-
See files, 28text.hs and 'Quick Text Operations.hs'
-
Text is a monoid, and Text values can be combined using
mconcat
-
putStrLn
does not work for Text types. However,TIO.putStrLn
does work.
TIO needs to be imported
import qualified Data.Text.IO as TIO
print
might work, but it would not print unicode into the console, it would turn the symbols into its representation like \401
etc
- To open a file, use this function:
openFile :: FilePath -> IOMode -> IO Handle
where FilePath is a string
type FilePath = String
And IO mode is,
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
It returns an IO Handle
which represents a reference to the file.
-
To close a file
hClose myFile
-
To read an write to file
hPutStrLn
andhGetLine
Example,
firstLine <- hGetLine helloFile
- To check if the
IO Handle
has reached EOF:
hIsEOF file
Whose type signature is:
*Main T> :t hIsEOF
hIsEOF :: Handle -> IO Bool
- We have some more IO functions which make reading and writing to a file easy. To read:
input <- readFile fileName
to append:
appendFile "output.txt" "blah"
- These methods above do not close the handle. So one might run into strange lazy evalutation errors here (to research more on this).
To prevent that from happening, one can do is use Text instead of String. Text is a strict data type, i.e it does not use lazy IO.
Example from W.Kurt Get Programming in Haskell:
{-# LANGUAGE OverloadedStrings #-}
import System.IO
import System.Environment
import qualified Data.Text as T
import qualified Data.Text.IO as TI
getCounts :: T.Text -> (Int,Int,Int)
getCounts input = (charCount, wordCount, lineCount)
where charCount = T.length input
wordCount = (length . T.words) input
lineCount = (length . T.lines) input
countsText :: (Int,Int,Int) -> T.Text
countsText (cc,wc,lc) = T.pack (unwords ["chars: "
, show cc
, " words: "
, show wc
, " lines: "
, show lc])
main :: IO ()
main = do
args <- getArgs
let fileName = head args
input <- TI.readFile fileName
let summary = (countsText . getCounts) input
TI.appendFile "stats.dat"
(mconcat [(T.pack fileName), " ",summary, "\n"])
TI.putStrLn summary
The author says,
"Strict evaluation means that your I/O code works just as you’d expect it to in any other programming language. Although lazy evaluation has many great benefits, for any nontrivial I/O, reasoning about its behavior can be tricky."
"As soon as your I/O becomes even moderately complex, involving reading and writing files, or operations for which order is important, stick with strict evaluation."
-
ByteString
allow us to treat binary data as if they were regular strings. -
ByteString is not for strings alone, but can deal with any sort of Binary streams.
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
Data.ByteString
does not let us use ByteStrings as char
, so we use Data.ByteString.Char8
, as below,
bcInt :: BC.ByteString
bcInt = "6"
bcToInt :: BC.ByteString -> Int
bcToInt givenByteString = (read . BC.unpack) givenByteString
- How to print unicode to the console?
import qualified Data.Text.Encoding as E
textWithUni = E.encodeUtf8 "Textƒƒƒ"
textWuthUnicode2 = E.decodeUtf8 textWithUni
TIO.putStrLn textWuthUnicode2
-
"The Functor type class allows us to apply an ordinary function to values inside a container (for example, List) or a context (for example, IO or Maybe)"
-
"Functor allow us to generalize by solving a single problem once, and automatically solves it for multiple parameterized types"
-
"The Functor type class provides a generic interface for applying functions to values in a container or context."
-
Consider these parameterized types (parameterized by Int)
[Int]
,Map String Int
,Maybe Int
,IO Int
These above are types that are in a context.
Let's say we have a function, X :: Int -> String
Now without functors, we would need to write a customized version of X
for all of the above parameterized types.
(i.e a separate version for a Maybe Int, IO Int, [Int] etc )
With functors, we will have a "uniform way" to apply a single function to all these parameterized types.
- Usefulness of tools such as
Maybe
are reduced if you have to keep implemeting for every functionFunc :: a -> b
, another function with a similar function,FuncMaybe :: Maybe a -> Maybe b
A "special" version will need to be written for each of these functions, when implementing the logic with a type in a context.
- Now, Maybe is a member of the Functor type class
> :info Maybe
.
instance Functor Maybe -- Defined in ‘GHC.Base’
.
The above means that the Maybe type class implements the Functor type class. (ie Maybe is a functor (? can we say that?))
- Let's look at Functor,
*Main> :info Functor
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
{-# MINIMAL fmap #-}
-- Defined in ‘GHC.Base’
instance Functor (Either a) -- Defined in ‘Data.Either’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘GHC.Base’
.
.
This shows that the type class Functor
is implemented by Maybe
,
and also that functor does't implement any other Type Classes (? to verify)
Another thing to note is:
For some type class to implement Functor they minimally need to implement fmap
.
<$>
is the same as fmap
- Any type class that wants to be a Functor must implement:
fmap :: Functor f => (a -> b) -> f a -> f b
fmap
accepts a function (a->b)
and an argument f a
,
here f a
is a functor of type a, example Maybe Int
-
fmap
provides an adapter, it is a binary operation -
Check
32functors.hs
for more explanation and examples. -
Maybe
is a functor. Which means we can pass aMaybe
to fmap, and a regular function, and it will apply that for uss:
Prelude> fmap (+1) (Just 10)
Just 11
Prelude> fmap (+1) (Just 10134.3)
Just 10135.3
Prelude> fmap (++ "1") (Just "as")
Just "as1"
See how we no longer would need a function like this:
incMaybe :: Maybe Int -> Maybe Int
incMaybe (Just n) = Just (n + 1)
incMaybe Nothing = Nothing
Which takes in a Type (that is in a Maybe context) and applies a normal Int function to it -- wraps it and returns the maybe; we don't need to implement this.
We can just use fmap
,
which accepts the 'normal function definition', and a 'type in a context'
-
Works on any function that works on the nested type convertLookedUpValToString = fmap show (Just 10) -- Just "10"
-
Reversing a Maybe String using functors:
reverseMaybeWithFunctors :: Maybe String -> Maybe String
reverseMaybeWithFunctors maybeStr = reverse <$> maybeStr
what is the functor here? is it the maybe?
Again,
>:t fmap
fmal :: Functor f => (a -> b) -> f a -> f b
Functor f =>
means that f
being used in the type signature is a type of Functor
seems like the Maybe
is the functor
Correct me if I am saying this incorrectly.
- A lot of these parameterized types have the kind
* -> *
Example, Maybe
which takes in a type, and creates a new type for you.
type MI = Maybe Int
-- to use it:
x :: MI ; x = Just 10
another example,
*Main> :kind []
[] :: * -> *
All functors are of the kind * -> *
.
Many parameterized types of the kind * -> *
are instances of Functor.
- Imp: "Functors are incredibly useful because they allow you to reuse a single function with any type belonging to the Functor type class."
That means a Maybe is a functor.
- The fmap converts here a Maybe part type to a Maybe HTML
partHtml :: Maybe Html
partHtml = renderHtml <$> (Just part)
- a list of parts to a list of html
allPartsHtml :: [Html]
allPartsHtml = renderHtml <$> allParts
- a map of (int,part) to a map of (int, html)
htmlPartsDB :: Map.Map Int Html
htmlPartsDB = renderHtml <$> partsDB
- an IO of part to an IO of html
htmlSnippet :: IO Html
htmlSnippet = renderHtml <$> leftArmIO
<$>
provides a common interface to apply any function to a value in a context.
It helps with IO to change context -- but can not take things out of IO conext.
- Functors can be very useful when we have one value in a context. What if two of our values are in a context.
And we want to operate on them? And simultaneous produce the result which is in the same context.
Functors's fmap literally works on single argument function
fmap :: (a->b) -> f a -> f b
(where f is any parameterized type which is a functor),
what if we want a map (?) which can work on multiple values?
like
requiredMap :: (a -> b -> c ) -> f a -> f b -> f c
map
works like this: it takes in a unary function and a list
. And applies the operator on the values inside the list.
fmap
works similarly, it takes a unary function and a Functor
. And applies the operator on the values inside the Functor.
(A functor is a List, or a Maybe, or an IO, etc)
Example: map (+ 1) [1,2,3]
, returns: [2,3,4]
.
What if we do not provide a binary function to map, and pass it a binary function
map (+) [1,2,3]
It returns a list of partially applied functions, [ (+) 1 , (+) 2, (+) 3]
The type of this is [ a -> a ]
Similarly, we can use fmap
fmap (+) (Just 10)
which returns
Just ( (+) 10 )
The type of which is Maybe (a -> a)
Thus, a function is a context is created.
Using the idea from [[03 Partial functions with FMAP]]
maybeInc = (+) <$> (Just 10)
Note its type, maybeInc
is a function in a context. (Parameterized function?)
> :t maybeInc
maybeInc :: Num a => Maybe (a -> a)
The Applicative type contains a function <*>
, whose type is
(<*>) :: f (a -> b) -> f a -> f b
Applicative’s <*> allows us to use a function in a context.
We can now use maybeInc
:
> maybeInc <*> (Just 50)
Just 60`
To re-write:
> value = (+) <$> (Just 10) <*> (Just 60)
> value
Just 70
In Summary
To create a generic function which re-uses functions, mapped from regular type and uses them with parameterized types:
applicativeFunc :: Applicative f => ( a -> b -> c ) -> f a -> f b -> f c
applicativeFunc binaryfunc p_obj1 p_obj2 = usingApplicative
where functionInAContext = fmap binaryfunc p_obj1
usingApplicative = functionInAContext <*> p_obj2
The applicativeFunc
takes in a binary function, and two parameterized types, first we create a partial application by applying fmap
with the binary operation and the first parameterized object. It creates for us an
f binary function p_obj1
, which is a "unary" function.
(Remember that <*> accept a unary function in a context, and another parameterized type.)
Now that we have our unary function in a context, and the second parameterized object, we return a functionInAContext <*> p_obj2
Testing it out,
> applicativeFunc (+) (Just 10) (Just 30)
Just 40
More examples,
> regularFunctionInParameterized (+) [1,2,3] [4,5,6]
[5,6,7,6,7,8,7,8,9]
> regularFunctionInParameterized (+) [1,2,3] [1]
[2,3,4]
We have read user input as int,
readInt :: IO Int
readInt = read <$> getLine
What if we have a function which takes not one (e.g. [[03 Partial functions with FMAP|fmap]]) argument. Not two ([[03-01 What if we want to pass two arguments to fmap|applicative]]) but an arbitrary number of arguments?
In this case, we apply the first argument to the function via fmap, thus creating a partial function in a context. Then we apply the applicator's app ( <*>
) along with another argument (parameterized), which ends up creating another partial application, after which we apply another argument (parameterized), until we have applied all the arguments, and end up with a value in a context.
Example,
take the function:
minOfThree :: (Ord a) => a -> a -> a -> a
minOfThree val1 val2 val3 = min val1 (min val2 val3)
Can we apply this function to values that are in an IO context? For example coming from readInt
.
Let us examine the partial functions (in a context) created on the way:
partialApplication1 :: IO (Int -> Int -> Int)
partialApplication1 = fmap minOfThree readInt
Now we can pass this partialApplication1
to <*>
partialApplication2 :: IO (Int -> Int)
partialApplication2 = partialApplication1 <*> readInt
Note how it takes a function in a context, and removes (?) it from context, removes the argument from the context, applies the function to the argument, puts it back in the context. (this is probably not how it works internally, although I might be wrong.)
And then we apply the <*>
again,
result :: IO Int
result = partialApplication2 <*> readInt
Alternatively, we can write:
result2 = (fmap minOfThree readInt) <*> readInt <*> readInt
Based on partial applications, another cool thing we can do with <*>
is
value = Just (+) <*> (Just 10) <*> (Just 20)
Where value becomes Just 30
!
Although Why does this not work: value = IO (+) <*> readInt <*> readInt
It throws the error: Data constructor not in scope: IO
#question How do we do the same with IO?
#question: How is <*> implemented internally?
#question: How do we put a function in an IO context?
Let us say we want to create a value of a certain type, (e.g. an 'and' type which contains some data), but all the data we have is in a context.
Example,
data User = User {name :: String, id :: Int, score :: Int} deriving (Show)
We know that the data constructor User
works as a function as well.
User's type,
> :t User
User :: String -> Int -> Int -> User
It accepts a String, an Int, another Int, and returns a User. Hence, we can use our partial application fmap and <*>
magic we talked about before.
(fmap User (Just "Aditya")) <*> (Just 100) <*> (Just 100)
To make it general:
maybeUser maybeName maybeId maybeScore =
User <$> maybeName <*> maybeId <*> maybeScore
(How does it work? User fmapped with maybeName:, the function User is applied inside the maybeName, it returns a function in a context. This function is a context is <*>
with maybeId: the maybeId's value is applied to the function (which needs 2 arguments) and ends up returning another function in a context. In the end, we 'app' it with maybeScore: the maybeScore's value is applied to the function, which returns a User, it is wrapped in a Just
and returned.)
To test,
> maybeUser (Just "A") (Just 10) (Just 10)
Just (User {name = "A", id = 10, score = 10})
- Notice that the type signature of the operator <> is almost the the same type signature as fmap, except the function argument is also in a context for <>
fmap :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
Putting data in the desired context
- The
pure
function:
*Main> pure [10] :: Maybe [Int]
Just [10]
*Main> pure 10 :: Maybe Int
Just 10
*Main> pure 10 :: IO Int
10
> pure "string" :: Maybe String
Just "string"
Putting functions in the desired context
> pure ( + 1 ) <*> (Just 10)
Just 11
Slightly more complex,
pure (+) <*> (Just 10) <*> (Just 20)
Just 30
Function in an IO Context
*Main> pure (minOfThree) <*> (readInt) <*> (readInt) <*> (readInt)
100
12
1
1
Exactly what we wanted! (note, minOfThree is just a normal Int -> Int -> Int)
- This also works
hello :: IO String
hello = pure "Hello World"
Parameterized types that represent a container are ones that represent a data structure.
Parameterized types that convey meaning beyond just the structure are types in a context, just like IO Types. Discussed [[03-04 Applicative, IO, and chaining|here]].
Looking back, for types in context can have a function in them.
Example : Maybe (+)
, IO func
,
but might not always make sense to have a function in a container (? not sure)
Note that Maybe, IO are types of applicative. Which makes sense as functions can easily find themselves contained in these, via partial application of fmap.
A Data.Map
or (, )
are not applicatives -- these are functors.
(Any type that is a member of Applicative, could be viewed as a type in a context.)
A list is both a context and a container -- it has a structure.
Lists represent context: i.e x = [1,2,3] means x can take values 1 or 2 or 3
Using functions in the list context (us`ing applicative), generates all possible values.
Example
test04 = pure (+) <*> [1,2,3,4,5] <*> [10,20]
will generate the sums of all possible combinations from the two lists above.
[11,21,12,22,13,23,14,24,15,25]
-
A monad is used when we wanna chain together functions.
-
It is a type class with the kind
(m :: * -> *)
-
For a type to be a monad, it must implement the function
bind
(>>=) :: m a -> (a -> m b) -> m b
- Note how it takes two arguments. One, a parameterized type. Two, a function which takes in a "regular" type and returns a parameterized type.
The function bind applies the function after "unwrapping" the parameterized type argument, and returns a parameterized type.
-
Because it returns a parameterized type, you can chain it using another bind
>>=
, by chaining it with another function that accepts a normal type but returns a parameterized type. -
This is especially useful to chain
Map.loopup
-
Map.lookup
accepts a key (and a map) and returns a maybek -> Maybe a
-
Example (from Stackoverflow: https://stackoverflow.com/questions/44965/what-is-a-monad)
Take an example of straightforward chaining
streetName = getStreetName (getAddress (getUser 17))
What if one of these methods returns a Nothing
?
We would have to write conditional checks over and over again.
Instead,
(((Just 117 >>= getUser) >>= getAddress ) >>= getStreetName)
getUser
would return another Maybe
.
getAddress
while normally might take a string, but has been transformed via >>=
to work on a Maybe
Example from GHCI
mapp = M.fromList [ (1,2),(2,3),(3,4),(4,5)]
getValFromMapp val = M.lookup val mapp
result = ((M.lookup 1 mapp) >>= getValFromMapp) >>= getValFromMapp
-- returns Just 4
More resources on Monads for fun:
-
http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html
-
IO Inside / IO Simplified https://wiki.haskell.org/IO_inside#I.2FO_in_Haskell.2C_simplified
-
Understanding Monads: https://web.archive.org/web/20120114225257/http://ertes.de/articles/monads.html
-
Monads for OOP https://ericlippert.com/2013/02/21/monads-part-one/#more-461
-
Alternate way to look at Monad's
>>=
function:
This >>=
bind function,
it pipes,
on the left hand side is always some data in a context,
to the right side of >>=
is a function that takes the type (without context),
and operates on the value and returns it in context.
- Example, to write an IO action which prompts the user, gets an input, and prints it out:
nameStatementIO2 :: IO ()
nameStatementIO2 = askForName >>
getLine >>= (\ n -> return (nameStatement n) >>= putStrLn
You see, >>
is used as it ignores everything on the left.
getLine
obviously returns anIO String
- the result is piped to a lambda which calls
nameStatement
wrapped as an IO - the
>>=
operates the lambda on the string wrapped inside theIO String
. and returns the results as an IO String, - The result is
bind
(or piped) to aputStrLn
which acts on theIO String
's string, and returns anIO String
Do
notation is syntactic sugar for >>
, >>=
and ```(\x -> return (func x))``
-
Monads are a great tool for code reuse.
-
In DO notaation
<-
takes an individual element out of the context.
after which you can do operations on the indivial element -- using let expressions,
these let expressions you can also return an element, it will put all these elements back in the context and return the value
Example,
-- <- is actually takes the element out of the context basically
assessCandidateList2 :: [Candidate] -> [String]
assessCandidateList2 candidates = do
candidate <- candidates
let passed = viable candidate
let statement = if passed then "passed" else "failed"
return statement
- A truly generic function to access candidate, one which can take any monad
assessCandidateX :: (Monad m) => m Candidate -> m String
assessCandidateX candidateInContext = do
candidate <- candidateInContext
let isPassed = if (viable candidate) then "passed"
else "fail"
return isPassed
See file 36monadsDoNotation.hs for examples.
-
todo: talk more about list comprehension. cover guard function.
-
how are list comprehensions a type of monad function?
how are list comprehension implemented?
they are a syntactic sugar for do notation. which is a syntactic sugar in itself for monad's bind (>>=) function, which helps us in chaining.
The type signature for bind is:
bind:: functionWhichTakesinANormalElementButReturnsAnElementInAContext -> anElementInContext -> anElementInContext
or,
bind :: (Monad m) => (a -> m b) -> m a -> m b
List comprehensions are a syntactic sugar for [[05-02 Do notation|Do notation]], The do notation is a syntactic sugar in itself for monad's bind (>>=) function, (>>) function, and returns.
These tools help us in chaining.
Why is list comprehension same as Monad's bind?
Look at bind's type signature Monad m => m a -> (a -> m b) -> m b
it takes a value in a conext; and takes a function which accepts individual elements from inside the context, applies this function of the element(s) in the first argument (that are inside the context), and returns the values put back into the context.
List comprehension is the exact same thing, list is a monad, we pass it to the list comprehension, the function acts on the individual elements inside the list, and returns their transformed version inside the list itself.
- the main function gets it's own module Main -- the IO action is handled here
- Pallindrome logic get it's own module Palindrome.
General Ideas,
-
we’d like to keep the main IO action in a separate file from the rest of the business logic. The main module should primarily be concerned with the execution of the program. Logic in a separate file, such as
Logic.hs
-
Logic gets it's own module/file.
-
When you don’t explicitly tell Haskell that you’re in a module, Haskell assumes that you’re the Main module
-
we can make this explicit by using the following line at the top of your file
module Main where
-
What do we do if we create a function or value that conflicts with one of the functions already defined in Prelude? We specify the module when using the function with the conflicted name, example
doubleLength = Main.length * 2
-
We can also hide functions inside a module, just like private methods. These methods won't be visible to the outside world.
-
Example,
module Palindrome(isPalindrome) where
means that Palindrome module exports only theisPallindrome
function. -
You can also selectively import functions too.
-
Importing
import Data.Char (toLower,isSpace,isPunctuation)
, means that we are only importing these three functions from theData.Char
module. -
Each Haskell program has a main function, sometimes implicitly created.
-
To import the logic module into your main use:
import qualified LogicModule
-
And then just compile the main, which will include the other imported modules.
-
This is for trivial builds. For more complex ones, we use "stack"
Stack is a powerful build tool. What it does:
- Provides an isolated installation of GHC
- Handles installation of packages and dependencies.
- Automates building of the project.
- Helps with organizing and running tests.
To create a new project, stack new project-name
it creates a new project in a new directory for you,
-
the project-name.cabal
is an important file, it contains all the project configurations. i.e all metadata related to the project. -
in the cabal file, under
library
, underhs-source-dirs
, is the value for the directory where the library files live, it issrc
by default. -
exposed-modules
in the cabal file tells us which libraries we are using. By default stack creates a module called 'Lib' under src. We gotta add our more values to it (in the cabal file, and in the directory) by using commas
exposed-modules: Lib,
Palindrome,
Utils
- in the cabal file, under
executable
, this lists out where the "executable" code is stored, i.e the 'Main'. Stack separates the logic (in Lib) from the code that is used to invoke that logic (Main.hs inApp
) - Stack has the opinion that the main will import the Lib, and execute its functions.
-
Re-write Lib.hs and put all our business logic in it. Export the relevant functions.
-
Re-write main, so that it calls the Lib module.
-
Modify the cabal file.
"You have to tell stack about any modules you’re depending on. For both your Main.hs file and your Lib.hs file, you’re using Data.Text
For both your library and executable sections of palindrome-checker.cabal, you need to add the text package to the list of dependencies:
library
hs-source-dirs: src
exposed-modules: Lib
build-depends: base >= 4.7 && < 5
, text
executable palindrone-checker-exe
hs-source-dirs: app
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: base
, palindrone-checker
, text
.
.
.
Note how the pallindrome-checker-exe, i.e. the main/executable depends on "palindrone-checker", what does that mean here?
there is no module called "palindrone-checker". Or is there?
-
And now we are set to build the project!
-
Note, in the cabal file, under exposed modules, you are writing down
Lib
, the name of our module with the business logic isLib
, gotta list them here so that Main can import them.
We would have to replace Lib
in the cabal file, if the module in our src
was called something else.
- After making changes to the cabal file, modifying the code etc, Execute:
stack setup
in the directory. This uses a resolver to install a version of ghc that was used when one wrote the project. - The resolver is set in stack.yaml, usually
lts-7.9
- To build the project we use
stack build
- For stack, to run the program, we use the
exec
command and pass the name of the executable (which is defined in the *.cabal file). Examplestack exec palindrone-checker-exe
Stack feature to avoid having to re-write language pragmas in each module:
- in both the library and executable sections of the *.cabal file. Below, the
default-language : Haskell2010
, add the language extension, for example, `` extensions: OverloadedStrings```
To force stack to use system GHC, instead of downloading an isolated GHC:
stack config set system-ghc --global true
We discuss three types of testing.
We wanna load our project into GHCi, so that we can manually run our functions in the CLI.
For that, we need to first execute stack setup
, and stack build
, after which we can do a stack ghci
. From which you'll be able to interact with the project's functions in ghci.
This is for manual testing
Implement your unit tests in /test/Spec.hs
, such as using asserts
, and then execute,
stack test
.
The tests can look something like this,
main :: IO ()
main = do
putStrLn "Running tests..."
assert (isPalindrome "madam") "passed 'madam'" "FAIL: 'madam'"
assert (isPalindrome "aditya") "passed 'aditya'" "FAIL: 'raadityacecar'"
assert (isPalindrome "racecar!") "passed 'racecar!'" "FAIL: 'racecar!'"
assert ((not . isPalindrome) "random") "passed 'random'" "FAIL: 'random'"
putStrLn "done!"
On executing stack test
, it runs these tests,
Running tests...
passed 'madam'
FAIL: 'raadityacecar'
FAIL: 'racecar!'
passed 'random'
done!
Looking at these results, you can continue to modify the logic in Lib (adding to it, example: making sure that "racecar!'' passes as palindrome.)
3. Automating Unit Tests / Generating Data for Unit Tests / Testing Properties of Functions (extremely cool)
Unit testing is a way to automate manual testing of the code. Property testing is a way to automate unit tests.
We wanna test if the function has a certain property.
To do that we use Quickcheck.
We supply QuickCheck with a some properties that our function is supposed to uphold. QuickCheck automatically generates values, and tests them out on the functions --- making sure that the functions uphold the properties.
Example,
for a function isPallindrome
, the following property must always hold true. Running isPallindrome
on text
, should return the same value as running it on reverse (test)
.
So we formalize this property:
prop_reverseInvariant text = isPalindrome text == (isPalindrome (reverse text))
It takes a text, and runs isPallindrome
on both the text and its reverse value.
See how this function is testing a fundamental property of isPallindrome. It's quite beautiful.
Now we give this function (property) to quick check, which can generate 1000s of test cases for us, and call test this property on all of these test cases.
Running it on 1000 test cases:
main :: IO ()
main = do
quickCheckWith stdArgs { maxSuccess = 1000} prop_reverseInvariant
Another property that the isPallindrome
function must uphold: calling this function on a string with punctuation marks must give the same result as calling it on a string without those punctuation marks. That is, it should be punctuation invariant,
property is implement as:
prop_punctuationInvariant2 text = isPalindrome text == isPalindrome noPuncText
where noPuncText = filter (not . isPunctuation) text
Why is this so cool? Because if we have not handles punctuation invariance properly in our code, QuickCheck would find out, by having tested this property on 1000s of values.
main :: IO ()
main = do
quickCheck prop_punctuationInvariant2
putStrLn "done!"
My thoughts:
these properties on functions are able to capture deep behavior of our functions. And can help us "prove" these methods flawlessly. I think this can certainly tie to Coq, or Automated Theorem Provers.
Maybe this has implications on formal verification of code too! I loved this chapter quite a lot. And for property testing, my mind is blown.
(todo: Look up property testing, and what else can be done using it. Find out how formal verification works.)
Notes on using QuickCheck
- QuickCheck might not be able to create values of all types. The type it targets must be an
Arbitrary
- To resolve this one can install
stack install quickcheck-instances
, to make QuickCheck work with a variety of types.
Implement : probabilistic prime checking https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test
- The traditional approach of throwing errors is frowned upon in Haskell, because it throws runtime errors; that a compiler can not catch.
- Haskell does allow us to throw errors, but there are other--better--ways to handle problems that show up in code. Example using Maybe.
Maybe
type can not communicate a "lot" about what "happened." So there is another more powerful type --Either
, that lets us use any value we like to communicate the error.
Albeit useful, using head
on an empty list gives us an error
Prelude> head []
*** Exception: Prelude.head: empty list
Here is the problem: if you compile a code in which head is being operated on an empty list -- GHC wont throw a compiler error. You'll realize this issue in runtime only. Where this blows up.
Moreover, the type signature of head
gives no indication that the function will throw a compiler error
Prelude> :t head
head :: [a] -> a
A trick: Using the -Wall
flag to run GHC,
Add the flag in your stack project, in the cabal file,
executable projectname-exe:
hs-source-dirs: app
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
build-depends: base
.
.
If we don't want to miss any warnings, we can compile with -error
flag which will convert warnings to errors.
head
is a partial function, partial functions are often not defined on all the possible inputs. head
is not defined on the []
.
(Claim "most errors in software are due to partial functions "Your program receives input you don’t expect, and the program has no way of dealing with it."
A scenario: a new function fooBar
is created which takes some input and internally calls some other function (that maybe had lesser scope), maybe it calls a bunch of other internal functions. fooBar believes that's scope is now widened, it begins to accept inputs that the internal functions don't have a good way of dealing with. fooBar
in this case is a partial function (if it calls other functions internally which are being sent arguments that fooBar
opinionatedly decided on.)
Throwing errors is an obvious solution, Haskell has an inbuilt function called error
. Example,
myHead :: [a] -> a
myHead [] = error "empty list" -- BAD PRACTICE
myHead (x:_) = x
Also, never use head, instead use pattern matching. As compiler can warn us if we have pattern matching issues.
We need a type that can capture when errors might happen, and we need the compiler's help in writing more error-resistant code
Note,
- maximum, succ, sum,
/
are also partial functions in prelude that fail on certain inputs. ( empty lists, maxBound , infinite lists, what does/
fail on?)
Remember that Maybe
lets us avoid the use of Null, that is used so often in other languages. Maybe
can transform a partial function into a "complete" function.
So instead of doing this,
myHead :: [a] -> a
myHead [] = error "Empty list"
myHead (x:_) = x
We can do this,
maybeHead :: [a] -> Maybe a
maybeHead [] = Nothing
maybeHead (x:_) = Just x
We can use <*>
and <$>
on this
result1 = ( * 10 ) <$> maybeHead [10,20,30]
result2 = (*) <$> maybeHead [10,2,3] <*> (Just 55)
exampleVal = (:) <$> maybeHead \[1,2,3\] <\*> Just \[1,2,3\]
Another example, To make take safer we can do,
myTakeSafer :: Int -> Maybe [a] -> Maybe [a]
myTakeSafer 0 _ = Just []
myTakeSafer n (Just xs) = (:) <$> maybeHead xs
<*> myTakeSafer (n-1) (Just (tail xs))
Although, tail is also a partial function. #CSTODO to dig into this further.
Although the Maybe
type is very [[08-02 Handling partial functions with Maybe|useful to handle edge cases]] and exception scenarios. Returning Nothing
does not convey a lot.
Example, a primality testing program, which returns Nothing
if the number given to it is more than what it can compute. Here, Nothing
does not convey anything, it is quite cryptic.
So we introduce the Either
type.
Either is defined,
data Either a b = Left a | Right b
This is beautiful, this is a type that can send the ACTUAL data, OR in case of an error, a meaningful error message.
This is an or
type, for error handling scenario we return the Left
constructor. We return the Right
constructor when things go as planned.
The Right
constructor is similar to basically similar to Just
.
This is more powerful than Maybe
because we can return the Left
constructor and it can convey an error message. If we are operating with an Either
type in a function, and the Left
constructor pops up, the GHC won't forcibly try to carry out the operation with the Left
constructor (I think),
example,
> (+12) <$> (Right 101)
Right 113
> (+12) <$> (Left 101)
Left 101
> (+12) <$> (Left "errrorrr!")
Left "errrorrr!"
Either can help us create a safer type of the head
function,
eitherHead :: [a] -> Either String a
eitherHead [] = Left "There is no head because the list is empty"
eitherHead (x:xs) = Right x
Note the type signature, Either String a
, means Left constructor takes a string, right constructor (if things go according to plan) takes a
, i.e. an element from the list.
Example,
> eitherHead [1..]
Right 1
>
> eitherHead []
Left "Can't return head -- List Empty!"
Also, Either
implements a Monad (and thus a Functor, and Applicative as well. Why? Because Monad is an Applicative, and an Applicative is a functor.)
This is clear because above we apply the <$>
and the <*>
on the Either already.
A more involved example, add the first two elements of a List using eitherHead
,
add2From :: [Int] -> Either String Int
add2From myList = (+) <$> (eitherHead myList) <*> (eitherHead (tail myList))
Examples,
>
> addTwoValuesList [1,2,3]
Right 3
>
>
> addTwoValuesList [1]
Left "Can't return head -- List Empty!"
>
>
> addTwoValuesList []
Left "Can't return head -- List Empty!"
>
>
Beautiful.
Remember that Either
lets us use any type we want to.
Also we can stick to n number of error messages using Either.
Remember how we discussed earlier how Maybe
is not sufficient enough to display nuanced error messages. [[08-03 The Either Type]] We discussed the nuance required for primality checking.
Now, Either
let us return a whole array of error messages. And display the relevant message (on failure) in our primality testing.
isPrime :: Int -> Either String Bool
isPrime n
| n < 2 = Left "Numbers less than 2 are not candidates for primes"
| n > maxN = Left "Value exceeds limits of prime checker"
| otherwise = Right (n `elem` primes)
Testing the isPrime Method
*Main> isPrime (-100)
Left "Numbers less than 2 are not candidates for primes"
*Main> isPrime 17
Right True
*Main> isPrime 10
Right False
*Main>
*Main> isPrime 1000000000
Left "Value exceeds limits of prime checker"
*Main>
So far we haven't taken advantage of the fact that the Either
type class can accept arbitrary data type.
Many programming languages have custom types for various kinds of errors. We can create similar types which model an error, and pass it to Either
to show the relevant errors.
Example, for primality testing errors, we can have,
data PrimeError = TooLarge | InvalidValue
instance Show PrimeError where
show TooLarge = "Value exceeded max bound"
show InvalidValue = "Value is not a valid candidate for primality checking"
Based on the above, we can modify our primality checking function,
isPrime :: Int -> Either PrimeError Bool
isPrime num | num < 2 = Left InvalidValue
| num > maxBound = Left TooLarge
| otherwise = Right (elem num primes)
Testing it,
>
> isPrime 13
Right True
> isPrime 10
Right False
>
> isPrime (-1)
Left Value is not a valid candidate for primality checking
>
> isPrime 0
Left Value is not a valid candidate for primality checking
>
> isPrime 10000000
Left Value exceeded max bound
>
Makes code much more readable. And returns valid information. As Jason Fried says, "even the error messages in your software are a form of marketing."
Right now our primality checker is returning a Either PrimeError Bool
type, why not return the client a String
always?
displayResult :: Either PrimeError Bool -> String
displayResult (Right True) = "Number is a prime"
displayResult (Right False) = "Number is a composite"
displayResult (Left primeError) = show primeError --can take Eithers out of ctx
A main IO action,
main :: IO ()
main = do
input <- getLine
let inputInt = read input :: Int -- or use, input <- read <$> getLine
let primeTestResults = (displayResult . isPrime) inputInt
putStrLn primeTestResults
main
Its testing,
> main
1
Value is not a valid candidate for primality checking
0
Value is not a valid candidate for primality checking
1000
Value exceeded max bound
25
Number is a composite
13
Number is a prime
4
Number is a composite
.
.
.
Thus, creation of error classes (like PrimeError
) demonstrates sophisticated ways of handling errors.
Because of the flexibility of the Either
type, i.e how the Left
constructor can be any type, the expressiveness is huge. As we can provide the Left
data constructor types like Strings, or Ints, other custom error classes, or even a function. In which case the Left
would return a function.
In summary, Either
has a dual function, it can help us safely handle errors -- providing detailed information about it. It also lets us return actual data (just like a Maybe
)
We use the Network.HTTP.Simple library for HTTP. This library is part of the http-conduit package.
The library makes it easy to make simple HTTP requests.
Generally we create an instance of the Request
data type, and pass it to httpLBS
to execute.
Example,
> response = httpLBS "http://news.ycombinator.com"
Just Defining a request object does not execute the HTTP Request (because of lazy evaluation)
The response object is actually wrapped in an IO
. It's type is
IO (Response a)
. More specifically IO (Response LC.ByteString)
To get the status from the response,
> getResponseStatusCode <$> response
An alternative solution is using <-
, which lets us take the value out of context (even inside GHCi). Example,
Prelude> response <- httpLBS "http://news.ycombinator.com"
Prelude> getResponseStatusCode response
200
We've got to
- add token to our request
- specify the host and path
- use GET
- make sure request works for "SSL Connection"
Note, defaultRequest
is provided by the library.
Note also, the type signature of these setters,
> :t setRequestPort
setRequestPort :: Int -> Request -> Request
They take in the "state" i.e. Request, and return a new Request object. (Author W. Kurt says, "Here you see one functional solution to having state. You create a new copy with the modified value" )
Using the relevant functions we build our request,
buildRequest :: BC.ByteString -> BC.ByteString -> BC.ByteString
-> BC.ByteString -> Request
buildRequest token host method path = setRequestMethod method
(setRequestHost host
(setRequestHeader "token" [myToken]
(setRequestPath path
(setRequestSecure True
(setRequestPort 443 defaultRequest )))))
This is super-cumbersome, so we use the syntactic sugar via ($)
operator
buildRequest :: BC.ByteString -> BC.ByteString -> BC.ByteString
-> BC.ByteString -> Request
buildRequest token host method path = setRequestMethod method
$ setRequestHost host
$ setRequestHeader "token" [token]
$ setRequestPath path
$ setRequestSecure True
$ setRequestPort 443
$ defaultRequest
To execute this request,
- pass
Request
tohttpLBS
- check if the status is 200.
- if it's 200, use
getResponseBody
and write the response to a file (note: you must use lazy ByteStrings - L.writeFile, instead of the function in Char8) - If there is an error, raise an alert.
main :: IO ()
main = do
response <- httpLBS request
let status = getResponseStatusCode response
if status == 200
then do
print "saving request to file"
let jsonBody = getResponseBody response
L.writeFile "data.json" jsonBody
else print "request failed with error"
note how we can have a do block inside another do block.
Full documentation at https://haskell-lang.org/library/http-client
We use the library Data.Aeson
to work with json. It allows us to translate between a haskell type and json.
We use two methods to do that,
decode
whose type signature look likes,
decode :: FromJSON a => ByteString -> Maybe a
Note how the type we want to convert to json should be a FromJSON
type. That is, it must implement certain methods to let us convert a (json) ByteString into the type. The function returns the type wrapped in a Maybe
because: parsing the json (before we can translate it into haskell object) might throw a parse error.
- Similarly, we have **
encode
. **
encode :: ToJSON a => a -> ByteString
The data type to encode must be a ToJSON
.
We also, have eitherDecode
which gives us a more detailed error statement in case there is failure to parse the JSON.
We use the language extension DeriveGeneric
, and make our object derive from Generic
, example
import GHC.Generics
data Book = Book
{ title :: T.Text
, author :: T.Text
, year :: Int
} deriving (Show,Generic)
Then we declare this type an instance of FromJSON
and ToJSON
, example:
instance FromJSON Book
instance ToJSON Book
And now we will be able to convert Book back and forth from a json bytestring.
Example,
myBook :: Book
myBook = Book {author="Will Kurt"
,title="Learn Haskell"
,year=2017}
myBookJSON :: BC.ByteString
myBookJSON = encode myBook
and,
rawJSON :: BC.ByteString
rawJSON = "{\"author\": \"Will Kurt\",\"title\": \"Learn Haskell\",\"year\": 2017 }"
bookFromJSON :: Maybe Book
bookFromJSON = decode rawJSON
I genuinely don't understand how deriving from Generic
can let us automatically make our type a FromJSON
or ToJSON
Making our type an instance of FromJSON.
We have a json, decode
it into a haskell object.
decode :: FromJSON a => ByteString -> Maybe a
Let's say we get hold of a JSON from an external source (say a GET request), example,
sampleError :: BC.ByteString
sampleError = "{\"message\":\"oops!\",\"error\": 123}"
We need to model the above json with our Haskell type,
data ErrorMessage = ErrorMessage
{ message :: T.Text
, error :: Int
} deriving Show
As we know, that the above syntax creates two functions
message :: ErrorMessage -> T.Text
and
error :: ErrorMessage -> Int
So this causes a conflict because error
is already defined in GHC.Err
.
So we change the name of the field
data ErrorMessage = ErrorMessage
{ message :: T.Text
, errorCode :: Int
} deriving Show
Now if we try to automatically make ErrorMessage
derive ToJSON
and FromJSON
, we run into another problem because the decode
function now expects an "error" field (in our ErrorMessage
), but does not find one.
decode :: FromJSON a => ByteString -> Maybe a
To make our ErrorMessage
type an instance of FromJSON
, we need to implement the parseJSON
function for ErrorMessage
instance FromJSON ErrorMessage where
parseJSON (Object v) =
ErrorMessage <$> v .: "message"
<*> v .: "error"
Here
(Object v)
is the JSON object.ErrorMessage
is the functionT.Text -> Int -> ErrorMessage
v .: "message"
a value in context. From the JSON Object v it is parsing the "message" field, returning its value in a context.v .: "error",
again, looks into the JSON object, and retrieve the key associated with "error".
And basically, extracting the values from the JSON, and giving them to the ErrorMessage
is how we implement parseJSON for ErrorMessage
.
For more clarity the type signature is,
(.:) :: FromJSON a => Object -> Text -> Parser a
data Name = Name
{ firstName :: T.Text
, lastName :: T.Text
} deriving (Show)
instance FromJSON Name where
parseJSON (Object v) = Name <$> v .: "firstName"
<*> v .: "lastName"
-- Converting,
sampleAdi :: BC.ByteString
sampleAdi = "{\"lastName\":\"Verma\",\"firstName\":\"Aditya\"}"
adiInAConextNow :: Maybe Name
adiInAConextNow = decode ( sampleAdi)
Hence the json was converted to a Haskell type.
Prelude > adiInAConextNow
Just (Name {firstName = "Aditya", lastName = "Verma"})
encode :: ToJSON a => a -> ByteString
We have a Haskell object, we wanna convert it to JSON.
We need to implement the method toJSON
.
For ErrorMessage
it looks like,
instance ToJSON ErrorMessage where
toJSON (ErrorMessage message errorCode) =
object [ "message" .= message
, "error" .= errorCode
]
The object function takes a Pair
and returns a Value
(a json object?), these are types defined in Aeson
.
The operator (.=)
is used to create a key/value pair matching the value of your data (message/errorCode) with the field name for the JSON object (message/error).
message = "hi"
error = "err"
obj = object [ "message" .= message, "error" .= error]
Where obj is
> obj
Object (fromList [("error",String "err"),("message",String "hi")])
This is how this object functions helps toJSON
convert our type into a JSON object.
Encode objects of ErrorMessage
type. Example
getMessage :: ErrorMessage
getMessage = ErrorMessage "Random Message" 200
Testing,
> encode getMessage
"{\"error\":200,\"message\":\"Random Message\"}"
Other examples,
instance ToJSON Name where
toJSON (Name firstName lastName ) = object [ "firstName" .= firstName,
"lastName" .= lastName]
adi = Name "Aditya" "Verma"
adiJSON = encode adi
-- gives us:
-- "{\"lastName\":\"Verma\",\"firstName\":\"Aditya\"}"
More info here: https://artyom.me/aeson
(for code. go to the directory 47db/* )
We use the sqllite-simple
to interact with the db.
Create you tables, add records in them, all in an sql file say build_db.sql
, then execute,
sqlite3 tools.db < build_db.sql
Now tools.db
is our db name, a new file will be generated in our directory. To load the db, execute,
sqlite3 tools.db
To test it out,
sqlite> select * from tools;
1|hammer|hits stuff|2017-01-01|0
2|saw|cuts stuff|2017-01-01|0
We added values into the table via raw SQL, like
INSERT INTO users (username) VALUES ('aditya');
How do we do this in Haskell?
- Establish a conn with the db.
conn <- open "tools.db"
- Use the
execute
function which allows us to insert values into the table, ExampleNote here that theexecute conn "INSERT INTO users (username) VALUES (?)" (Only userName)
(?)
lets us safely pass values into our string. Only is used to create single element tuples. "This is needed becauseexecute
expects us to pass a tuple of certain size for our values. - We also need to Close the connection
close conn
- We wrap this up in a
do
block in an IO action
addUser :: String -> IO ()
addUser username = do
conn <- open "tools.db"
execute conn "INSERT INTO users (username) VALUES (?)" (Only username)
print "user added"
close conn
-- takes in the db value , and an IO action
withConn :: String -> (Connection -> IO ()) -> IO ()
withConn dbName operation = do
conn <- open dbName
operation conn
close conn
The above function you can use to avoid having to deal with conn
,
addUserNew :: String -> IO ()
addUserNew user = withConn "tools.db" $
(\ conn -> do
execute conn "INSERT INTO users (username) VALUES (?)"(Only user)
putStrLn "added user")
For more brevity we can do this:
-- lambdas are tedious, so we create,
executeWrapper :: ToRow q => Query -> q -> String -> (Connection -> IO ())
executeWrapper sqlStml tuples successMsg =
(\ conn -> do
execute conn sqlStml tuples
putStrLn successMsg
)
Now we can skip the lambdas and the conn
in our db operations, example,
-- now,
addUserNew2 :: String -> IO ()
addUserNew2 user = withConn "tools.db" $
executeWrapper "INSERT INTO users (username) VALUES (?)"
(Only user)
"added the user"
-- and, a function to checkout a tool
checkoutMy :: Int -> Int -> IO ()
checkoutMy userId toolId = withConn "tools.db" $
executeWrapper "INSERT INTO checkedout (user_id,tool_id) VALUES (?,?)"
(userId,toolId)
"Checked out the tool"
The challenge here is that we want to convert rows of the DB into Haskell types. For this sqlite-simple
library offers a function called FromRow
.
If we wanna convert rows in a database table into a Haskell type a
, the type must implement FromRow
, which is done by implementing a function fromRow
. Definition of FromRow
class FromRow a where
fromRow :: RowParser a
Consequently, we will be able to transform queries into lists of our datatype.
We have to tell the RowParser
how to construct our data type. We use the field
function (implemented in SQLite.Simple) which consumes the data in the table's row and transforms it into values required by our type constructor.
To implement fromRow
for User
and Tool
,
instance FromRow User where
fromRow = User <$> field
<*> field
instance FromRow Tool where
fromRow = Tool <$> field
<*> field
<*> field
<*> field
<*> field
Now that both User
and Tool
are instances of FromRow
we can execute queries and translate their results directly to haskell types.
We use these two methods to query,
query :: (FromRow r, ToRow q) => Connection -> Query -> q -> IO [r]
query_ :: FromRow r => Connection -> Query -> IO [r]
The difference is query_
is for queries that take no arguments.
To retrieve the set of users,
printUsers :: IO ()
printUsers = do
withConn "tools.db" $
(\ conn ->
do
values <- query_ conn
"SELECT * FROM users" :: IO [User]
mapM_ print values
)
We can write a general function which can take a query and execute it,
-- can execute all queries returning a tool
printToolQuery :: Query -> IO ()
printToolQuery query = withConn "tools.db" $
( \ conn ->
do
values <- query_ conn query :: IO [Tool]
mapM_ print values)
Example use,
> printToolQuery "Select * from tools"
1.) hammer
description: hits stuff
last returned: 2021-01-01
times borrowed: 0
2.) saw
description: cuts stuff
last returned: 2021-01-01
times borrowed: 0
To update records we need to
- Retrieve the record, and convert it to a haskell object
- Update the object with the new values
- Run an
UPDATE
query via an IO Action to update the record - Write a function which calls the functions above, i.e. orchestrating the whole thing
-- first obtain the tool
selectTool :: Connection -> Int -> IO (Maybe Tool)
selectTool conn toolId = do
resp <- query conn
"SELECT * FROM tools WHERE id = (?)"
(Only toolId) :: IO [Tool]
return $ firstOrNothing resp
firstOrNothing :: [a] -> Maybe a
firstOrNothing [] = Nothing
firstOrNothing (x:_) = Just x
-- then update the tool (the Haskell type)
updateTool :: Tool -> Day -> Tool
updateTool tool date = tool
{ lastReturned = date
, timesBorrowed = 1 + timesBorrowed tool
}
-- then put this new tool values in the DB,
updateOrWarn :: Maybe Tool -> IO ()
updateOrWarn Nothing = print "id not found"
updateOrWarn (Just tool) = withConn "tools.db" $
\conn -> do
let q = mconcat ["UPDATE TOOLS SET "
,"lastReturned = ?,"
," timesBorrowed = ? "
,"WHERE ID = ?;"]
execute conn q (lastReturned tool
, timesBorrowed tool
, toolId tool)
print "tool updated"
updateToolTable toolId = do
conn <- open "tools.db"
chosenTool <- selectTool conn toolId
currentDate <- utctDay <$> getCurrentTime
let newTool = (fmap updateTool chosenTool) <*>
(pure currentDate)
-- we need a Maybe type for date ^
-- don't use Just, use pure to convert
-- nowt hat we have our updated tool,
-- we can use, updateOrWarn :: Maybe Tool -> IO ()
-- newTool is a MaybeTool,
-- and updateOrWarn takes just that as a parameter
updateOrWarn newTool
close conn
checkin :: Int -> IO ()
checkin toolId = withConn "tools.db" $
\conn -> do
execute conn
"DELETE FROM checkedout WHERE tool_id = (?);"
(Only toolId)
There is a context for performing mutation on an array by using the STUArray
type. Basically, stateful mutation.
First we look at UArray
, it is a non-lazy Array.
Lazy evaluation can get real inefficient. If you're performing operations (using map) on a List: haskell won't compute the values until absolutely needed, but it will store all sorts (what all?) data on the computations that might be done in the future. And on doing these computation, it will take a longer because lists (lazy evaluation) are linked lists.
To use the UArray type class, we import Data.Array.Unboxed
. (And add array
to the list of build-depends if we're using stack.)
It has the kind UArray :: * -> * -> *
, the two other types it accepts are
- the first, type of the index. Must be an Enum and Bounded. Example, Char, Int, Bool
- the second, is for the type of the value.
Example,
zeroIndexArray :: UArray Int Bool
To create a UArray, we use the array
fxn. Takes two arguments. One, is a tuple (lowerbound, upperbound)
. Second, the list of values you wanna put in your array [(index,value)]
.
> :t array
array :: (IArray a e, Ix i) => (i, i) -> [(i, e)] -> a i e
For values that you don't explicitly add to your array. Haskell will put in a default value.
To look up values in your UArray by using the ! operator,
n> zeroIndexArray ! 1
True
*Main> zeroIndexArray ! 2
False
*Main> zeroIndexArray ! 3
False
Creating an array whose index starts from 1 is trivial,
oneIndexArray :: Int -> UArray Int Int
oneIndexArray up = array (1,up) $ zip [1..up] [1..up]
test1 = oneIndexArray 5
Array is updated like other functional data structures. By creating a copy of the original array with the modified values.
beansInBuckets :: UArray Int Int
beansInBuckets = array (0,3) [] -- initializes everything to zero
We use the //
operator to modify a UArray, example,
newArray = beansInBuckets // [(1,9),(3,11)]
Now, newArray
equals array (0,3) [(0,0),(1,9),(2,0),(3,11)]
.
We use a function called accum
defined in Data.Array.Base
. This function takes in three arguments
- a binary operation
- a UArray
- an indexed list of values to apply to the UArray to
[(0,1),(0,2),(0,3)]
etc
Add 1000 to each element in our UArray,
newArray1000 = accum (+) updatedArray $ zip [0 .. 3] $ cycle [1000]
Efficient array algorithms require us to change state of the array.
STUArray
is a special type of UArray
. "The STUArray uses a more general type called ST. ST allows for stateful, nonlazy programming." STUArrays are a type of monad. STUArray lets us change values in a UArray.
To use STUArrays, we import
import Data.Array.ST
import Control.Monad
import Control.Monad.ST
In a STUArray context, you can perform stateful computations. Although this is not a hack that will let us disregard FP principles. We can only use STUArray only when the statefulness is indistinguishable from pure functional code for the "users" of our functions.
We implement a function listToSTUArray
, that takes a list of Ints and transforms the list into an STUArray.
To help us with that, we will use the writeArray
function, which takes an STUArray, an index, and a value. This is the crux. writeArray
performs a stateful mutation of the underlying array without creating a copy of the array!
We also use newArray
function, which takes a pair representing the bounds of the array as well as a value for initializing the array. This returns an empty STUArray the specified size.
listToSTUArray :: [Int] -> ST s (STUArray s Int Int)
listToSTUArray vals = do
let end = length vals - 1
myArray <- newArray (0,end) 0
forM_ [0 .. end] $ \i -> do
let val = vals !! i
writeArray myArray i val
return myArray
- In
newArray (0,end) 0
we are initializing the empty STUArray. - Next in a
forM_
we extract values fromvals
, and then write them to our STUArraymyArray
using the functionwriteArray
. - In the end, we return this the array back to its context.
On trying to print the STUArray, we get
GHCi> listToSTUArray [1,2,3]
<<ST action>>
The ST
context is much safer than IO
, as referential integrity still holds. So, haskell lets us take these arrays out of the context, via
runSTUArray :: ST s (STUArray s i e) -> UArray i e
Example,
> runSTUArray ( listToSTUArray [1,2,3] )
array (0,2) [(0,1),(1,2),(2,3)]
We can create a wrapper,
listToUArray :: [Int] -> UArray Int Int
listToUArray vals = runSTUArray $ listToSTUArray vals
Quote, "STUArray forces us to maintain perfect encapsulation, we can leave the context of the STUArray without violating any of the core rules of functional programming"
We can combine runSTUArray
with our original function,
listToUArray_ :: [Int] -> UArray Int Int
listToUArray_ vals = runSTUArray $ do
let end = length vals - 1
myArray <- newArray (0,end) 0
forM_ [0 .. end] $ \i -> do
let val = vals !! i
writeArray myArray i val
return myArray
How do we use UArray in the context of an STUArray?
- We use a function called
thaw
which will unfreeze our UArray. - We use
bounds
fxn, which will give us the bounds of our array. - STUArray has a function called readArray that reads a stateful value from an array.
-- author's implementation
bubbleSort :: UArray Int Int -> UArray Int Int
bubbleSort myArray = runSTUArray $ do
stArray <- thaw myArray
let end = (snd . bounds) myArray
forM_ [1 .. end] $ \i -> do
forM_ [0 .. (end - i)] $ \j -> do
val <- readArray stArray j
nextVal <- readArray stArray (j + 1)
let outOfOrder = val > nextVal
when outOfOrder $ do
writeArray stArray j nextVal
writeArray stArray (j + 1) val
return stArray
-- my implementation,
bubbleSort :: [Int] -> UArray Int Int
bubbleSort vals = runSTUArray $ do
let end = length vals - 1
stuArray <- listToSTUArray vals
forM_ [0..end] $ \ j -> do
forM_ [0..(end-1)] $ \ i -> do
val1 <- readArray stuArray i
val2 <- readArray stuArray (i+1)
if (val1 > val2) then
do
writeArray stuArray i val2
writeArray stuArray (i+1) val1
else return ()
return stuArray
We are maintaining perfect "encapsulation", even though we are changing state, it is not apparent to the external world. Because we can translate our stateful data structure STUArray back to a regular UArray. This lets us treat stateful code as pure functions.
"...three properties, and a few rules about how we can use them together, that define a monad in Haskell. Let's revisit the above list in condensed form.
-
A type constructor m.
-
A function of type m a -> (a -> m b) -> m b for chaining the output of one function into the input of another. No comments
-
A function of type a -> m a for injecting a normal value into the chain, i.e. it wraps a type a with the type constructor m.
The >>
function,
(>>) :: m a -> m b -> m b
a >> f = a >>= \_ -> f
Same as (>>=) except it ignores the output from a
.
Type signature of a Parser
myParser :: GenParser Char st [String]
We are in the GenParser context, it reads individual Chars and returns out lists of Strings.
- The
many
function
Takes a fxn as argument.
"It tries to repeatedly parse the input using the function passed to it. It gathers up the results from all that repeated parsing and returns a list of them. "
- the line parser is just a bunch on cells
All these miniparsers are bascially "splitting" input strings based on various parameters.
Check the comments on 49parsec.hs
for theory.
Examples,
x = char ',' :: GenParser Char st Char
Now,
> parse x "(source)" ",,,,,,."
Right ','
> parse x "?" "ABC"
Left "?" (line 1, column 1):
unexpected "A"
expecting ","
parse returns an either, the second example does not find a ,
and returns a Left Error. The firsr example finds a ,
and says that the parsing was successful Right ','
- The
many
function to parse a cell
cell = many (noneOf ",\n")
cell
parses input till it encounters a "," or "\n"
Many takes a parsing function, and applies it over and over again.
noneOf is a parser which parses anything except for ,
and \n
the resulting cell
parser will parse till it finds the ',' or '\n', and then returns everything it found before the ',' or '\n'
sepBy
function
The function takes two parsers.
One, is a parser that can parse "some sort of content". Two, is another parser that parses for a separator.
It tries to parse content, and then tries to parse a separator, and then back and forth; until it cant parse a separator.
And then returns a list of content that it was able to parse.
Where can we use it? To parse lines separated by "\n". To parse cells which are separated by ","
example,
cell = many (noneOf ",\n")
-- ^ extracts out a single cell from a line (basically till it reaches "," or "\n")
line = sepBy cell (char ',')
-- ^ extracts out a whole line. a line is separated by "cell" and ","
csvFile = endBy line (char '\n')
-- ^ extracts out the lines, separated by '\n'
(see 54parserExperiment.hs) for more info.
endBy
function
this is same as sepBy
, except it expects that last element should also be followed by a separator.
That is, it continues parsing content until it can't parse any more content.
We use "endBy" to parse lines, since every line must end with the end-of-line character.
Due to these both functions, the parser can be written more succintly,
import Text.ParserCombinators.Parsec
csvFile = endBy line eol
line = sepBy cell (char ',')
cell = many (noneOf ",\n")
eol = char '\n'
parseCSV :: String -> Either ParseError [[String]]
parseCSV input = parse csvFile "(unknown)" input
Testing,
> parseCSV "1,2,3\n4,5,6\n7,8,9\n"
Right [["1","2","3"],["4","5","6"],["7","8","9"]]
From the book,
-
A
csvFile
contains 0 or more lines,
each of which is terminated by the end-of-line character. -
A
line
contains 1 or more cells, separated by a comma. -
A
cell
contains 0 or more characters, which must be neither the comma nor the end-of-line character. -
The end-of-line character is the newline,
\n
You can give all these mini parsers to the parse
function and test them out,
>parse eol "(unknown)" "blahblah\n"
Left "(unknown)" (line 1, column 1):
unexpected "b"
expecting "\n"
> parse eol "(unknown)" "\n"
Right '\n'
>
- There is a parser called string that we can use to match the multi-character patterns.
> parse (string "aditya") "source:ghci" "adityaaa."
Right "aditya"
*Main> parse (string "aditya") "source:ghci" "_adityaaa."
Left "source:ghci" (line 1, column 1):
unexpected "_"
expecting "aditya"
*Main>
- What if we want a choice? We want our parsing "automata" to accept multiple sequences of characters.
We use <|>
.
Note type signature as well. In the Parsec context, we are expecting string as input, and outputting Strings too.
myParser :: Text.Parsec.Prim.Parsec String () String ;
myParser = (string "aditya") <|> (string "awesom")
The above does not work.
> parse myParser "source:ghci" "adit"Left "source:ghci" (line 1, column 1):
unexpected end of input
expecting "aditya"
Actually, the reason is, <|> only attempts the option on the right if the option on the left consumed no input.
*Main> myParser :: Text.Parsec.Prim.Parsec String () String ; myParser = (string "aditya") <|> (string "zezima")
*Main>
*Main> parse myParser "source:ghci" "zezimaaa"Right "zezima"
*Main> parse myParser "source:ghci" "adityaaa"
Right "aditya"
*Main> parse myParser "source:ghci" "ergfdsafeg"
Left "source:ghci" (line 1, column 1):
unexpected "e"
expecting "aditya" or "zezima"
*Main>
- This below also does work on multiple types of inpts, we can give this parser both "aditya", and "awesom" and it accepts both.
myParser :: Text.Parsec.Prim.Parsec String () String ;
myParser = try (string "aditya") <|> try (string "awesom")
- Note that inside these parsers you can even return all sorts of cool stuff, like,
myParser :: Text.Parsec.Prim.Parsec String () String ;
myParser = try (string "aditya" >> return "is cool")
<|> try (string "awesom" >> return "is also cool")
<|> return "your input wasnot cool, but parser wont fail"
Testing it,
> parse myParser "source:ghci" "aditya"
Right "is cool"
> parse myParser "source:ghci" "awesomeeee"
Right "is also cool"
> parse myParser "source:ghci" "what"
Right "your input wasnot cool, but parser wont fail"
- An alternative:
myParser :: Text.Parsec.Prim.Parsec String () String ;
myParser = try (string "aditya" >> return "is cool")
<|> try (string "awesom" >> return "is also cool")
<|> fail "Couldn't find coolness"
Will give us,
> parse myParser "source:ghci" "what"
Left "source:ghci" (line 1, column 1):
unexpected "w"
expecting "aditya" or "awesom"
Couldn't find coolness
- Alternatively,
myParser :: Text.Parsec.Prim.Parsec String () String ;
myParser = try (string "aditya" >> return "is cool")
<|> try (string "awesom" >> return "is also cool")
<?> "Couldn't find coolness"
More info here: http://book.realworldhaskell.org/read/using-parsec.html
(from the resource : https://en.wikibooks.org/wiki/Haskell/Monad_transformers)
Monad transformers lets us use the capabilities of several monads into one.
Like composing two monads into a single monad that shares the behaviour of both.
"We will define a monad transformer that gives the IO monad some characteristics of the Maybe monad; we will call it MaybeT
.
MaybeT
is just a wrapper around m (Maybe a)
, where m
can be any monad (IO
in our example).
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
See the file 55monadTransformers.hs
(in progress to study/experiment more on Monad Transformers)
to do - in progress