Edits after class look like this.
Questions, comments, complaints?
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.
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))
.
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.)
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.
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]
[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]
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
".
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]