0410 (Friday, Week 1)

Edits after class look like this.

Q&A

Questions, comments, complaints?

Nested Patterns

negate : List Bool -> List Bool
negate xs =
  case xs of
    [] ->
      []
    x :: tail ->                -- x is a variable pattern
      not x :: negate tail

The variable pattern x here matches any Bool. Instead, could choose to pattern match on Bool data constructors, as follows. The second and third branches are nested patterns, because a (non-variable) Bool pattern appears within the (non-variable) List pattern.

negate : List Bool -> List Bool
negate xs =
  case xs of
    [] ->
      []
    True :: tail ->
      False :: negate tail
    False :: tail ->
      True :: negate tail

Both True and False cases are handled in the same way (i.e. not x :: negate tail), so arguably the first version, with the variable pattern, is preferable. But this is really a stylistic choice.

Short-Circuiting

How about the variable and nested pattern style for the following function?

all : List Bool -> Bool
all xs =
  case xs of
    []        -> True
    x :: tail -> x && all tail

all : List Bool -> Bool
all xs =
  case xs of
    []            -> True
    False :: tail -> False
    True :: tail  -> all tail

Maybe the latter version performs better, because the former evaluates all tail even when x is False? No, they're both the same, because Elm short-circuits the evaluation of the second argument to (&&) if the first evaluates to False. You may enjoy doing some investigative journalism to test this out for yourself (Hint: Debug.log). If you do, also try ((&&) x (all tail)).

Mutually Recursive Functions

All definitions at the top-level are mutually recursive. Notice how isOdd refers to isEven even though it appears later in the file.

isOdd : Int -> Bool
isOdd n =
  if n == 0 then
    False
  else
    isEven (n-1)

isEven : Int -> Bool
isEven n =
  if n == 0 then
    True
  else
    isOdd (n-1)

Similar thing, but where the mutually recursive functions are defined within a let:

odd : Int -> Bool
odd =
  let
    isOdd : Int -> Bool
    isOdd n =
      if n == 0 then
        False
      else
        isEven (n-1)
{-
  in
  let       
-}
    isEven : Int -> Bool
    isEven n =
      if n == 0 then
        True
      else
        isOdd (n-1)
  in
    isOdd

However, if we split the above into two separate let-expressions (by removing the comment), we get a variable-not-found error.

In general, the structure of a let-expression is

let
  x1 = e1
  x2 = e2
    ...
  xn = en
in
  body

where n >= 1. The variables x1 through xn are in scope for each of the definitions e1 and en, hence, the equations are mutually recursive. (And, of course, the variables are in scope in the body expression.)

Exercises

We'll work in randomly assigned Zoom breakout rooms for ~30 minutes. Try having one person at a time share their screen, while discussing and working the tasks as a group.

1. List.map, revisited

In the Intro ML in Elm (continued) video, we saw (essentially) the following examples of List.map:

map : (a -> b) -> List a -> List b
map f xs =
  case xs of
    [] ->
      []
    x :: tail ->
      f x :: map f tail
negate : List Bool -> List Bool
negate xs =
  List.map {- Bool Bool -} (\x -> not x) xs

double : {- forall number \in {Int,Float}. -} List number -> List number
double xs =
  List.map {- number number -} (\x -> 2*x) xs

nullify : {- forall a. -} List a -> List ()
nullify xs =
  List.map {- a () -} (\_ -> ()) xs

Recall that the stutter : List a -> List a function...

stutter : List a -> List a
stutter xs =
  case xs of
    [] ->
      []
    x :: tail ->
      x :: x :: stutter tail
> stutter [1, 0]
[1,1,0,0] : List number

... could not be defined in terms of List.map. Why not?

Define a more general version of List.map, called foo below, that allows stutter — in addition to anything that can be defined with List.map — to be defined nicely. Fill in the type and expression holes.

foo : ?? -> List a -> List b
foo f xs = ??

negate xs = foo ?? xs
double xs = foo ?? xs
nullify xs = foo ?? xs
stutter xs = foo ?? xs 

What is a reasonable choice for the "name hole" foo?

Now define a new function, called bar below, that allows foo to be redefined in terms of bar and List.map:

bar : ??
bar = ??

foo f xs = bar (List.map ?? xs)

What is a reasonable name for bar?

[scroll down to see answers]

2. List.fold{l,r}

[didn't get to this in class]

Let's observe the difference in behavior of folding from the left and right...

List.foldl : (a -> b -> b) -> b -> List a -> b
List.foldr : (a -> b -> b) -> b -> List a -> b

... on a small example:

> List.foldl (::) [] [1,2,3]
[3,2,1] : List number

> List.foldr (::) [] [1,2,3]
[1,2,3] : List number

How are these results produced? First, write down the definitions of foldl and foldr. Then, "play computer": manually write out how the evaluator steps through the computations (in a level of detail that you find useful).

[scroll down to see answers]

Before We Meet Again

  • Read and watch Intro to MVC
  • Start HW1
  • Prepare any topics for Q&A next time














Answers

1

Here's the recursive case for List.map and for the direct-recursion versions of the four functions we are considering:

--   f x        :: map f tail       -- List.map
--
--   not x      :: negate tail
--   2 * x      :: double tail
--   (\_ -> ()) :: nullify tail
--   x :: x     :: stutter tail     -- doesn't fit the pattern
--   ^^^^^^

The expression x :: x in the corresponding position does not make sense on its own:

> bad x = x :: x
-- INFINITE TYPE ----------------------------------------------------------- elm

I am inferring a weird self-referential type for x:

3| bad x = x :: x
       ^
Here is my best effort at writing down the type. You will see ∞ for parts of the
type that repeat something already printed out infinitely.

    List ∞

Staring at this type is usually not so helpful, so I recommend reading the hints
at <https://elm-lang.org/0.19.0/infinite-type> to get unstuck!

We have something more along the lines of x :: x :: [] — that is, [x, x] — but this can't be cons-ed on to the result of the recursive call. Instead:

mapMany : (a -> List b) -> List a -> List b
mapMany f xs =
  case xs of
    [] ->
      []
    x :: tail ->
      f x ++ mapMany f tail

stutter xs = mapMany (\x -> [x,x])                   xs 
negate  xs = mapMany (List.singleton << not)         xs
double  xs = mapMany (List.singleton << ((*) 2))     xs
nullify xs = mapMany (List.singleton << (always ())) xs

Alternate definition of mapMany, in two steps:

concat : List (List a) -> List a
concat xss =
  case xss of
    [] ->
      []
    xs :: tail ->
      xs ++ concat tail

mapMany : (a -> List b) -> List a -> List b
mapMany f xs =
  concat (map f xs)

"mapMany" is known as List.concatMap. You might hear concat sometimes referred to as "flatten".

2

Left:

foldl : (a -> b -> b) -> b -> List a -> b
foldl f acc xs =
  case xs of
    [] ->
      acc
    x :: tail ->
      let newAcc = f x acc in
      foldl newAcc tail

An illustrative unfolding (pun intended) of evaluation, though not one that corresponds exactly to Elm, or OCaml, or Haskell, or Racket...

    foldl (::) [] [1,2,3]
==>
    let newAcc1 = (::) 1 [] in
    foldl (::) newAcc1 [2,3]
==>
    let newAcc1 = (::) 1 [] in
    let newAcc2 = (::) 2 newAcc1 in
    foldl (::) newAcc2 [3]
==>
    let newAcc1 = (::) 1 [] in
    let newAcc2 = (::) 2 newAcc1 in
    let newAcc3 = (::) 3 newAcc2 in
    foldl (::) newAcc3 []
==>
    let newAcc1 = (::) 1 [] in
    let newAcc2 = (::) 2 newAcc1 in
    let newAcc3 = (::) 3 newAcc2 in
    newAcc3
==>
    [3, 2, 1]

Right:

foldr : (a -> b -> b) -> b -> List a -> b
foldr f init xs =
  case xs of
    [] ->
      init
    x :: tail ->
      let result =
        foldr f init tail
      in
      f x result

An illustrative unfolding:

    foldr (::) [] [1,2,3]
==>
    let result1 =
      foldr (::) [] [2,3]
    in
    (::) 1 result1
==>
    let result1 =
      let result2 =
        foldr (::) [] [3]
      in
      (::) 2 result2
    in
    (::) 1 result1
==>
    let result1 =
      let result2 =
        let result3 =
          foldr (::) [] []
        in
        (::) 3 result3
      in
      (::) 2 result2
    in
    (::) 1 result1
==>
    let result1 =
      let result2 =
        let result3 =
          []
        in
        (::) 3 result3
      in
      (::) 2 result2
    in
    (::) 1 result1
==>
    [1, 2, 3]