• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

soupi/haskell-study-plan: An opinionated list of resources for learning Haskell

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称(OpenSource Name):

soupi/haskell-study-plan

开源软件地址(OpenSource Url):

https://github.com/soupi/haskell-study-plan

开源编程语言(OpenSource Language):


开源软件介绍(OpenSource Introduction):

Haskell Study Plan

You might also be interested in my project-oriented online book: Learn Haskell by building a static blog generator

About This Guide

This guide is an opinionated list of resources for learning Haskell.

It is aimed at more experienced programmers that would like a denser Haskell tutorial.

If you prefer a gentler introduction, try one of these resources:

If you prefer videos:

And/or one of these resources to get started quickly:

Table of Contents

Beginning

  1. Install Haskell
  2. Learn about the core tools
  3. Reading Simple Haskell
  4. Writing Simple Haskell
  5. Indentation
  6. Haskell intro (cis194)
  7. Haskell Basics (cs240h)
  8. Substitution and Equational Reasoning
  9. Haskell and proving things
    • Read until “Everything is a Monoid” (right after “Chaining proofs”)

The basics are important, each resource here brings it’s own view on it which will help solidify this material. If there are exercises to do, do them!

Key ideas:

  • In Haskell we use a different computation model
    • Instead of “telling the machine what to do in order to change the state of the machine” we “transform data until we have the result we want”
  • Referential Transparency enables equational reasoning
  • Types help prevent errors and help model programs

More Basics Drill Down

Tools

Useful Packages

Here are a few useful packages you might want to use when building software with Haskell:

  • base - Haskell standard library. Contains large collection of useful libraries ranging from data structures to parsing combinators and debugging utilities.
  • containers - Contains efficient general-purpose implementations of various immutable container types including sets, maps, sequences, trees, and graphs.
  • vector - Efficient arrays.
  • text - An efficient unicode text type. It is much more efficient than the built in String type.
  • bytestring - An efficient vector of byte type.
  • async - API for running IO operations asynchronously.
  • network - Low-level networking interface.
  • random - random number library.

Book (paid): Haskell (Almost) Standard Libraries by Alejandro Serrano Mena

And more.

Exercises

Lists

Sort a List

Sort a list of ints by inserting all its elements into a binary search tree.

  1. Define a data type of a binary search tree
  2. Write the type signatures of the functions relevant to the task (sort, insertElementToTree, listToTree, flatten, display, etc.)
  3. Implement these functions

Think of scenarios and test your functions.

Dict

Compress and decompress a file using dict compression.

Dict compression takes text, splits it by words, and creates two things:

  1. A mapping from each word in the text to a number
  2. the original text where each word is replaced by it’s map’s number

Your task is to create an application that can either compress or decompress a text file.

There are two commands: compress and decompress, they both get a text file.

  • To compress: > dict compress file.txt
  • To decompress: > dict decompress file.txt

For the compress command, the output should be the compressed items ((1) and (2)). For the decompress command, the output should be the original text.

Note: You can use the functions read and show to convert from/to some types and String.

PPM

Create a program that will output a PPM file.

  1. The size of each “pixel” should be controlled by a parameter
  2. Your input should be a list of list of colors
  3. If a row is not long enough fill the rest of it with the color white
  4. Bonus: Choose a pallete of 8 or 16 basic colors and read a file containing numbers from 0 to 7 (or 15) separated by spaces and newlines, and output it’s image

RPN Calculator

Create a program that calculates an arithmetic expression written in reverse polish notation.

Implement the following operations:

literal integers, +, -, *, /, negate

Example execution:

$ rpn-calc 5 7 2 negate + *
25

Lambda Calculus

Overview

The lambda calculus is a minimalistic language that is in the core of functional programming.

It presents a minimalistic framework to learn about many common features in functional languages.

While this section isn’t strictly necessary, and you can skip it, it does provide some insight about the core of Haskell.

Exercises

  1. Reduce the following expressions to normal form using pen and paper
    1. λx. x
    2. (λx. x) y
    3. (λx. x x) (λy. y)
    4. (λw. λx. λz. x w z) a (λb. λc. c b) (λd. d)
  2. Use eta conversion on the following expression
    1. λx. f x
    2. λf. λy. (λx. f x) y
  3. Write the expression 2 + 3 in the lambda calculus and evaluate it using pen and paper
  4. Write the expression factorial 5 in the lambda calculus and evaluate it using pen and paper

Use this Lambda Machine to check your answers

Kinds

Overview

Every expression has a concrete type.

Kinds are the types of types.

This is a simplified view of how kinds are represented in GHC:

data Kind
  = Type -- can also be written as: *
  | KArr Kind Kind -- KArr in Haskell this is written as: ->

Think of Type being the kind of concrete (or inhabited) types, and KArr is a function from Kind to Kind.

If a type is parametarized (when defining the ADT you pass it parameters) then in order for it to be concrete you have to supply it with all the types it expects to get.

Example:

data Bool
  = True
  | False

data Maybe a
  = Just a
  | Nothing

Bool is not parametarized so it is a concrete type (which means it’s kind is Type) and has the Values True and False.

Maybe is not a concrete type, it need to be supplied with a type for a. (It has the kind Type -> Type).

Maybe Bool is a concrete type because all of the paramters for Maybe have been supplied.

An expression can only have a type with the kind Type.

Examples:

ValueTypeKindComments
TrueBoolType (also written *)a value
‘c’CharType
“Hello”StringType
not TrueBoolTypefunction application
Just TrueMaybe BoolType
[“Hello”][String]Type
NothingMaybe aTypepolymorphic
ida -> aTypea function
map(a -> b) -> [a] -> [b]Type
map not[Bool] -> [Bool]Typepartially applied function
getLineIO StringType
putStrLnString -> IO ()Type
VoidTypea concrete types with no values
MaybeType -> Typeisn’t fully supplied with parameters
IOType -> Type
EitherType -> Type -> Type
Either aType -> Typepartially supplied with parameters
Free(Type -> Type) -> Type -> Typethe first argument is of higher kind

You can use ghci to query the kind of a type using :kind

Why do we care about Kinds? It let us generalize things and create abstractions.

Let’s take a look at a data type that uses higher kinds:

data Rec f a
  = Rec a (f (Rec f a))
  • This data type has two type parameters, f and a.

From their use in the right side of the = we can see that a has the kind Type because it is placed as a field without type arguments. We can also see that f has kind Type -> Type because it is placed as a field with one type argument (which in this case, is the same data type we defined). This makes Rec kind to be (Type -> Type) -> Type -> Type.

Why is this data type interesting? Let’s try to plug some types and see. We need some a which as kind Type so let’s just choose Int for now, and let’s use Maybe for f. Let’s look at some values of our new type Rec Maybe Int.

  • x1 = Rec 1 Nothing
  • x2 = Rec 1 (Just (Rec 2 Nothing))
  • x3 = Rec 1 (Just (Rec 2 (Just (Rec 3 Nothing))))

See a pattern here? it seems like this is an encoding of a non-empty list:

  • You always have at least one value
  • Nothing is similar to Nil
  • Just is similar to Cons

Let’s take a look at another example with this type:

data Identity a
  = Identity a

Identity basically just holds a value of type a. Nothing interesting here.

Let’s try to plug it in Rec (and get Rec Identity Int) and see what kind of value we can have:

  • y1 = Rec 1 (Identity (Rec 2 (Identity (Rec 3 (Identity ...)))))
  • y2 = Rec 0 y2

As you can see we basically need to keep providing new values with no way of bailing out. So we got an infinite list of values (or a stream).

We can write all kinds of generic algorithms on this data type and reuse them for different scenarios and needs simply by pluging in a different f!

We’ll see more of those after we talk about type classes.

There is more to Haskell’s kinds system, and a really good article about it is linked later on the tutorial.

And by the way, the real name of Rec is Cofree.

Exercise

Try to plug into our Rec a different type of kind Type -> Type that you know and see what happens!

What is IO?

Overview

It is a parametarized type constructor (it has the kind Type -> Type).

IO a represents a description of a program (or subroutine) that when executed will produce some value of type a and may do some I/O effects while at it.

Evaluating an IO a is pure - the evaluation will always reduce to the same description of a program.

In an executable, you need to define main :: IO () - a description of a program to run. The Haskell runtime will execute this.

You can combine subroutine descriptions to create bigger subroutine descriptions:

  1. pure :: a -> IO a

    Produces a value without doing any I/O.

    • Example: pure True

    Which has the type IO Bool, will not do any I/O and when executed will produce a value of type Bool, specifically True.

  2. fmap :: (a -> b) -> IO a -> IO b

    Similar to map on lists, it will apply a function on the parameter of IO.

    • Example: fmap not (pure True)

    Which has the type IO Bool will not do any I/O and when executed will produce a value of type Bool by first applying the function not on the result of pure True, and so will produce the value False.

  3. (>>) :: IO a -> IO b -> IO b

    Run this first thing, discard the result, and then run the second thing.

    • Example:
      putStrLn "Hello" >> putStrLn "World"
              

    Which has the type IO (), when executed, will print the string Hello and then will print the string World and will produce a value of type (), specifically () (in this case the value has the same name as the type).

  4. (>>=) :: IO a -> (a -> IO b) -> IO b

    Run this first thing, take its result, pass it to the function which is the second argument, and then execute that.

    • Example: getLine >>= putStrLn

    Which has the type IO () will read a String from the user, apply that String to putStrLn and then execute it, thus printing the same string it got from the user. Then it will produce a value of type (), specifically ().

    Note: You can implement (>>) using (>>=) like this:

    (>>) prog1 prog2 = prog1 >>= \_ -> prog2
        
  5. join :: IO (IO a) -> IO a

    Takes a description of a program that produces a description of a program that produces a value of type a and converts it to a descrption of a program that will produce a value of type a by executing the first, and then executing the result.

    • Example: join (fmap putStrLn getLine)

    Which is the same as getLine >>= putStrLn. As you can see we can implement >>= using fmap and join

    (>>=) prog func = join (fmap func prog)
        

There are many more functions and combinators that return IO a. You can view some of them in the module System.IO.

Do notation

do notation is syntactic sugar around >> and >>=.

Example:

main = do
  putStrLn "Tell me your name."
  let greet name = "Hello, " ++ name ++ "!"
  name <- getLine
  putStrLn (greet name)

Will be desugared to:

main =
  putStrLn "Tell me your name." >>
    let
      greet name = "Hello, " ++ name ++ "!"
    in
      getLine >>= \name ->
        putStrLn (greet name)
  1. A regular line that does not create a binding will be sequenced to the next using >>
  2. A new definition can be created using let, it will be translated to let <definition> in <rest of the lines in the do>
  3. A line that creates a binding with <- will use >>= to pass the result and the lambda (\name ->) is used to bind the variable to the result
  4. The last line will remain the same - no desugar needed

This is basically CPS (continuation passing style).

codeoperatortype of the left sidetype of the right sidecomments
let gretting = “hello”=StringString= means both side are interchangeable (they both mean exactly the same thing)
let mygetline = getLine=IO StringIO StringHere we just create a new name that is identical to getLine. We are not running anything
name <- getLine<-StringIO String<- is syntactic sugar for >>= where we bind the result of the computation to the name

IO’s API fits a pattern that can be seen in more types in Haskell, which is why the type signatures of the functions presented here are more general. We’ll discuss that later.

Exercises

  • Implement a number guessing game
    • Generate a random number between 1 and 100, the user should try to guess what it is.
      • If the user guess is too high, say it’s too high.
      • If the user guess is too low, say it’s too low.
      • Hint: you can use randomRIO to generate a random number
    • Bonus: Remember the amount of times the user guesses and print that at the end of the game.
      • Hint: In pure functional programming we use recursion to emulate state
    • Bonus: Remember the user’s guesses and tell them if they already tried that guess.
  • Implement a REPL interface to your RPN Calculator
    • Create an interactive interface that lets the user repeatedly write calculations and return the evaluations for them

Type classes

Overview

We use type classes to describe groups of types that all behave in a similar way and refer to them generically.

A good type class will have operations on the type and laws attached to it - similar to abstract algebra.

Laws cannot be enforced by the compiler - a good convention in Haskell is not to define lawless type classes and not implement unlawful instances.

We define a type class like this:

class Eq (a :: *) where
  (==) :: a -> a -> Bool

We define a class of types that can implement the operation (==).

We implement an instance of a type class for a given type like this:

-- In this case we place `Bool` in place of `a` everywhere
instance Eq Bool where
  (==) b1 b2 = case (b1, b2) of
    (True, True) -> True
    (False, False) -> True
    _ -> False

Now we can implement polymorphic functions that will work on a subset of all types - all types that fill the constraint - have instances of a type class.

(/=) :: Eq a => a -> a -> Bool
(/=) x y = not (x == y)

class instances should be defined in the same place as the type class definition or at the same place as the type definitions. Failing to do that may cause Orphan Instances.

Abstractiondefinitiondifferent substitutionscomments
No polymorphismfunc1 :: Int -> Int -> Intnonewe know exactly which types are used and can do all kinds of operations on them
Parametric polymorphismfunc2 :: a -> a -> aa can be any typeWe don’t know which type a is and can’t do any type related operations on it
Type classes (ad-hoc)func3 :: Ord a => a -> a -> aa can be any type that can be ordered (Bool, Int, String)anything to the left of => is a constraint on the type

More Material

Exercise

  • Read about a few common type classes:
    • Show
    • Read
    • Eq
    • Ord
    • Num
    • Integral
    • Floating
  • Go back to Sort a List exercise and change it to work on more types than just Int

Note: We can create instances for higher kinded types (for example: Type -> Type). We will see some of those next.

Monoids, Functors, Applicative, Monads and More

Overview

Key idea:

These are abstract algebraic structures

They define operations and laws on them such as identity and associativity.

Many patterns fit these structures, making them useful as abstractions!

Type classes you should care about (at the moment):

  • Semigroup
  • Monoid
  • Functor
  • Applicative
  • Monad
  • Foldable
  • Traversable

Read about them in the typeclassopedia in this order.

After that: read The monads section in wiwik to meet some useful monad instances.

  • Haskell and proving things
    • Read from “Everything is a Monoid” (right after “Chaining proofs”) or from the beginning if you want to review it again

Instances

Make sure to meet:

  • Maybe
  • Either
  • List
  • -> (Functions)
  • IO
  • Reader
  • State
  • Writer

And understand why and how they work!

Exercises

  • Implement some instances to a few types you like.
  • Implement Functor, Foldable and Traversable instances for the Tree data type you defined at Sort a list and revised in Type Classes
  • Implement a Foldable instance for the Rec data type we defined in the section on Kinds.
    • Test your solution by using Sum, Product, Any or All from Data.Monoid.
  • Implement a Functor instance for the Rec data type we defined in the section on Kinds.
    • Test your solution by mapping and then folding

Bonus: Counterexamples of Type Classes

More

Error Handling

Using Either for errors

There are quite a few ways to indicate and handle errors in Haskell. We are going to look at one solution: using the type Either. Either is defined like this:

data Either a b
  = Left a
  | Right b

Simply put, a value of type Either a b can contain either a value of type a, or


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
haskell/HTTP: Haskell HTTP package发布时间:2022-06-18
下一篇:
sjsyrek/haskell-study-startup: Launch your own Haskell study group. Now.发布时间:2022-06-18
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap