CIS 552: Advanced Programming

Fall 2017

  • Home
  • Schedule
  • Homework
  • Resources
  • Style guide
  • Syllabus
Note: this is the stubbed version of module Lec1. You should download the lhs version of this module and replace all parts marked undefined. Eventually, the complete version will be made available.

Basics

A literate Haskell file is one where the file name ends in .lhs, and all lines prefixed by > are Haskell source code. All other lines are ignored by the Haskell compiler. The html is generated directly from the lhs version of the lecture notes. Feel free to download the source code and play with it yourself after class. You'll need to turn in a version of this file for the zeroth homework assignment.

Every Haskell file begins with a few lines naming the module (this name must start with a capital letter and be the same as the file name) and (optionally) importing definitions from other modules.

> module Lec1 where      -- comments can begin with two dashes
> import Test.HUnit      -- library imports must come at the beginning

To load this file into ghci, you will need to install the hunit library first.

Understanding a Haskell program is about substituting equals by equals

Functional programming means that the semantics of a program can be described mathematically. One principle of mathematics is called Leibniz equality: in any context, we can replace an object with something equal to it. Therefore, in Haskell, we reason about computation by reasoning about the equality of (sub-)programs.

So, if we want to know the value of an arithmetic expression, we only need to find some number that is equal to it.

  3 * (4 + 5)

  { 4+5 is equal to 9, by addition so we can replace it }

  3 * 9

  { by multiplication }

  27

That's it!

We can give names to Haskell expressions:

> x = 3 * (4 + 5)

and ask ghci to calculate their values, just as we did above.

What is Abstraction?

Pattern Recognition

  31 * (42 + 56)

  70 * (12 + 95)

  90 * (68 + 12)

Generalize to a function by defining an equation

> pat x y z = x * (y + z)

The important question is not "What does this function do?" but, instead "What does this function mean?" We can reason about that meaning using what we know about equality.

 pat 31 42 56

 { function call }

 31 * (42 + 56)

 { addition }

 31 * 98

 { multiplication }

 3038

Functions, like pat, are the core abstraction mechanisms in functional programming.

The GHC System

Interactive shell "ghci"

 :load Lec1.hs
 expression
 :type expression (:t)
 :info variable   (:i)

Emacs with Haskell-mode

 load file ^C-c ^C-l

Package management: "cabal"

Download and install libraries:

 cabal install hunit

Download and install Haskell applications:

 cabal install hlint

Elements of Haskell

  • Everything is an expression
  • Expressions evaluate to values
  • Every expression has a type

Basic types

It is good style to annotate the type of every declaration in a Haskell program.

> i :: Int
> i = 31 * (42 + 56)         -- word-sized integers
> ii :: Integer
> ii = 31 * (42 + 56)        -- arbitrarily large integers
> d :: Double
> d = 3.1 * (42 + 5)        -- double precision floating point

(Note that numeric constants, +, and * are overloaded. Type annotations resolve ambiguity.)

Can also annotate the type of the expression directly.

> c = 'a'    :: Char    -- characters
> s = "abcd" :: String  -- strings
> b = True   :: Bool    -- boolean values
> u = ()     :: ()      -- 'unit' (both type and constant have same syntax)

Function types

  A -> B

Function taking an input of type A and yielding an output of type B

> pos :: Int -> Bool
> pos x = x > 0

Multi-argument function types

   A1 -> A2 -> A3 -> B

Function taking inputs of type A1, A2, and A3 and returning a result of type B

> arith :: Int -> Int -> Int -> Int
> arith x y z = x * (y + z)

Symbolic vs. alphabetic names

Symbolic identifiers (i.e. + and *) are infix by default.

Parens around a symbolic name turn it into a regular name.

> plus :: Int -> Int -> Int
> plus = (+)
> p0 :: Int
> p0 = (+) 2 ((*) 3 4)

Likewise we can use alphabetic name in backquotes as infix.

> p1 :: Int
> p1 = 2 `plus` 2

Making Haskell DO something

Programs often interact with the world:

  • Read files
  • Display graphics
  • Broadcast packets

They don't just compute values.

How does this fit with values & equalities above?

Note, we've gotten far without doing any I/O. That's fairly standard in Haskell. Working with GHCi means that we can see the answers directly, we don't need an action to print them out. However, a standalone executable needs to do something, so we demonstrate that here.

I/O via an "Action" Value

"IO actions" are a new sort of sort of value that describe an effect on the world.

 IO a  --  Type of an action that returns an `a`  (a can be anything!)

Obligatory Hello World

Actions that do something but return nothing have the type "IO ()".

putStr :: String -> IO ()

So putStr takes in a string and returns an action that writes the string to stdout.

GHCi can execute actions interactively.

> hw :: IO ()
> hw = putStr "Hello World! \n"
ghci> hw

Alternatively, we can compile our program as an executable and run it. The entry

The only way to "execute" the action (without using ghci), is to make it the value of name "main".

> main :: IO ()
> main = hw

The batch compiler "ghc" compiles and run large programs. There must be a definition of 'main' somewhere.

Compile and run: ghc -o hello -main-is Lec1 Lec1.lhs

(Note: There can also be multiple source files in a Haskell application, and if the one that includes 'main' is called 'Main.lhs' you can leave off the '-main-is' flag. There are also much more sophisticated ways to manage the compilation of Haskell applications, especially those built from libraries and multiple source files.)

Just 'do' it

How can we do many actions? By composing small actions.

The do syntax allows us to create a compound action that sequences one action after another. The definition of many below is a compound action that outputs the three strings in order. (Try it out in ghci!)

> many :: IO ()
> many = do putStr "Hello"     -- each line in the sequence
>           putStr " World!"   -- must be an IO action
>           putStr "\n"

Note: white-space is significant here. The do notation sequences actions, but each action in the sequence must start at the same character offset.

Sometimes people put the do on a line by itself and then start the list of actions on the next line.

> many' :: IO ()
> many' = do
>   putStr "Hello"
>   putStr " World!"
>   putStr "\n"

Example: Input Action

Actions can also return a value.

getLine :: IO String

This action reads and returns a line from stdin. We can name the result as part of a do sequence, with this notation

x <- action

Here x is a variable that can be used to refer to the result of the action in later code.

> query :: IO ()
> query = do putStr "What is your name? "
>            n <- getLine
>            let y :: String
>                y = "Welcome to CIS 552 " ++ n
>            putStrLn y

Note, when we sequence actions of type IO () there is no need to name the result. These actions do not the return interesting results. We could name the result if we wanted (such as m below); but because of its type we know that m will always be ().

> query' :: IO ()
> query' = do m <- putStr "What is your name? "
>             n <- getLine
>             putStrLn ("Welcome to CIS 552 " ++ n)
>             st <- query2
>             return ()

Note that you cannot name the last action in a sequence. The name is there so that you can use the result later. If you want to return the value instead, the last action should be a return.

> query2 :: IO String
> query2 = do putStr "What is your name? "
>             n <- getLine
>             return n

There is no need to name a value if it is just going to be returned. This version is equivalent.

> query2' :: IO String
> query2' = do putStr "What is your name? "
>              getLine

Example: Testing Actions

The hunit library contains definitions for constructing unit tests for your programs. To use this library you must first install it with the cabal tool. This library defines the Test type for test cases.

> t1 :: Test
> t1 = 3 ~?= 1 + 2      -- check that the expected value `3`
>                       -- matches the result of the computation

To run the test case, we need to use the function

       runTestTT :: Test -> IO Counts
> numTest :: IO Counts
> numTest = runTestTT t1

This is an action that runs the test case(s) and returns a data structure recording which ones pass and fail.

> dotest :: IO ()
> dotest = do c <- runTestTT (3 ~?= 3)
>             print c

Homework Zero

First, follow the homework instructions for HW #0 online. Then replace undefined below with your answers.

> name :: String
> name = undefined
> pennkey :: String
> pennkey = undefined

Your github userid.

> githubUserId :: String
> githubUserId = undefined

What do you hope to get from CIS 552? What do you expect to learn from the course?

> desiredOutcome :: String
> desiredOutcome = undefined

Don't forget to read Lec2 before the next class.

Design adapted from Minimalistic Design | Powered by Pandoc and Hakyll