COMP3161/9164 Concepts of Programming Languages
Term 3, 2024

Code and Notes (Week 9 Tuesday)

Table of Contents

These are the extra notes and code I presented in today's lecture. Many thanks to Johannes Åman Pohjola for the original.

For the introductory monad demo, I've added notes to the demo code below that should hopefully improve on the explanation I tried to give in today's lecture. Thanks for your patience. -Rob

1. Notes

Modules
-------

Instead of `foo`, we want an expression of this form instead:

  X ⇒ Bool

(Where Bool was the return type of the slide example `foo`)


Via Curry-Howard
----------------

Important laws for this derivation:

  A ⇒ B ⇒ C  ≡    A ∧ B ⇒ C

  ∀x. (P(x) ⇒ Q) ≡  (∃x. P(x)) ⇒ Q

The first law is a bit like uncurrying:

 uncurry :: (a -> b -> c) -> (a,b) -> c
 uncurry f (a,b) = f a b

Example of the second law:

 P(x)   it rains on day x
 Q      rain has happened

 ∀x. (P(x) ⇒ Q)   On every day, if it rains on that day,
                  rain has happened

 (∃x. P(x)) ⇒ Q   If there is a day when it rains,
                  rain has happened


Existential Types
-----------------

In Haskell types, there's always an implicit top-level
universal quantification of all type variables

  duplicate :: a -> [a]
  duplicate a = [a,a]

is actually

  duplicate :: forall a. a -> [a]

- When we implement duplicate,
   we don't get to decide what type a is
  Instead, we must write our function
   so that it works for every a
- When we call duplicate, we get to
   instantiate a to be anything we want!

Universally quantified types give:
- a lot of freedom to the user (the consumer)
- a lot of constraints to the implementer (the producer)

Existentially quantified types flip this dynamic around:
- the constraints are all on the user's side (the consumer)
- the freedom is all on the implementer's side (the producer)

Intuitively:
- When you implement a module, you get to decide the
  implementation details
- When you use the module, you can't use it in a way that
  depends on the implementation details


Existential Type Example
------------------------

Yet another example. Here we want three interfaces:
- a starting value
- a way to add two to the current value
- a way to extract the current value

Let's represent the value as an Int
  body = (0, λx. x+2, λx. x)
  body :: Int × (Int -> Int) × (Int -> Int)

  Pack Int body :: ∃a. a × (a -> a) × (a -> Int)


Making a Module
---------------

Same example as above:

  ⊢ Int ok    ⊢ body :: (a × (a -> a) × (a -> Int))[a:=Int]
  _________________________________________________________
      ⊢ Pack Int body :: ∃a. a × (a -> a) × (a -> Int)

Note: The decision of which Ints to turn into a's is
entirely up to the developer's intentions for the module!


Importing a Module
------------------

This time, for the BagModule example from the slides.

An Open that passes the typecheck:

  Open bagModule
    (bag. (empty,add,average).
       average (add (add empty 4) 3)
    ) :: Int

An Open that fails the typecheck:

  Open bagModule
    (bag. (empty,add,average).
       (add (add empty 4) 3) :: bag
    ) -- not well typed

2. Haskell code

module W9 where

-- A Brief Introduction to Monads

data Exp =
  Num Int
  | Add Exp Exp
  | Div Exp Exp
  deriving (Eq,Show)

-- Haskell's inbuilt Maybe type is defined something like this:
-- data Maybe a = Nothing | Just a

-- But to define a function that takes an instance of such a
-- data type (e.g. to express the return type of a partial
-- function, as for eval below) we have to pattern match so
-- it's defined for all of the data type's constructors.
-- This leads to long-winded, tedious nested pattern matches:

eval :: Exp -> Maybe Int
eval (Num n) = Just n
eval (Add e1 e2) =
  case eval e1 of
    Nothing -> Nothing
    Just n ->
      case eval e2 of
        Nothing -> Nothing
        Just m ->
          Just $ n + m
eval (Div e1 e2) =
  case eval e1 of
    Nothing -> Nothing
    Just n ->
      case eval e2 of
        Nothing -> Nothing
        Just m ->
          if m == 0 then
            Nothing
          else
            Just $ n `div` m

{- Monads in general

  A monad is a type constructor m equipped with two functions:
     return :: a -> m a
     bind   :: m a -> (a -> m b) -> m b

  The bind function has infix (>>=) as syntactic sugar:
     bind m f == (m >>= f)

  Return and bind must satisfy three algebraic laws:
     m >>= return    =  m       (right identity)
     return x >>= k  =  k x     (left identity)
     (m >>= k1) >>= k2 = m >>= \x -> k1 x >>= k2 (associativity)
 -}

{- Haskell's Maybe type as a monad -}

-- Say we define bind and return for Maybe as follows:
-- (>>=)  :: m a -> (a -> m b) -> m b
bind :: Maybe a -> (a -> Maybe b) -> Maybe b
bind Nothing k = Nothing
bind (Just v) k = k v
-- (NB: avoid naming collision with Prelude.return)
return' :: a -> Maybe a
return' v = Just v

-- Then does it satisfy the algebraic laws?
--
-- (right identity? yes)
-- m >>= return
--   = bind m Just
--   = case m of
--       Nothing -> Nothing
--       Just v -> Just v
--   = m -- as required
--
-- (left identity? yes)
-- return x >>= k
--   = bind (Just x) k
--   = k x -- as required
--
-- (associativity? yes)
-- LHS
--   = (m >>= k1) >>= k2
--   = bind (bind m k1) k2
--   Case 1: when m = Nothing
--     bind (bind Nothing k1) k2
--     = bind Nothing k2
--     = Nothing
--     = RHS -- as required, see below
--   Case 2: when m = Just v
--     bind (bind (Just v) k1) k2
--     = bind (k1 v) k2
--     = RHS -- as required, see below
-- RHS
--   = m >>= \x -> k1 x >>= k2
--   = bind m (\x -> bind (k1 x) k2)
--   Case 1: when m = Nothing
--     bind Nothing (\x -> bind (k1 x) k2)
--     = Nothing
--     = LHS -- as required, see above
--   Case 2: when m = Just v
--     bind (Just v) (\x -> bind (k1 x) k2)
--     = (\x -> bind (k1 x) k2) v
--     = bind (k1 v) k2
--     = LHS -- as required, see above

{- Monads as a design pattern -}

-- Two kinds of syntactic sugar for bind can give us much
-- more readable code for our use of the Maybe type than before.

eval' :: Exp -> Maybe Int
eval' (Num n) = Just n
{- Original:
eval' (Add e1 e2) =
  case eval' e1 of
    Nothing -> Nothing
    Just n ->
      case eval' e2 of
        Nothing -> Nothing
        Just m ->
          Just $ n + m
 -}
{- With monads with bind as we defined above:
eval' (Add e1 e2) =
  bind (eval' e1)
  (\n ->
    bind (eval' e2)
    (\m ->
      (return (n + m))))
 -}
{- With monads with bind's infix (>>=) syntax, note this works
 - because (>>=) is Haskell's built-in bind operator and
 - Haskell recognises Maybe as a monad through typeclassing:
eval' (Add e1 e2) =
  eval' e1 >>=
  \n ->
    eval' e2 >>=
    \m -> return $ n + m
 -}
{- With Haskell's inbuilt monad do-syntax: -}
eval' (Add e1 e2) =
  do -- this is not a loop!
     -- implicitly, there is a >>= between each line
    n <- eval' e1
    m <- eval' e2
    return $ n + m
{- And we can do the same for Div -}
eval' (Div e1 e2) =
  do
    n <- eval' e1
    m <- eval' e2
    if m == 0 then
      Nothing
    else
      return (n `div` m)

3. Python code

This was part of a demo to illustrate the lack of abstraction facilities in Python.

class myClass:
    __mySecret = 55

myObject = myClass()
# Attempting to access myObject.__mySecret yields an error,
# but myObject._myClass__mySecret yields the "hidden" value.

2024-11-28 Thu 20:06

Announcements RSS