Skip to content

Ch4 - Is it possible to solve the countEven exercise without pattern matching? #303

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
milesfrain opened this issue Jan 31, 2021 · 6 comments · Fixed by #459
Closed

Ch4 - Is it possible to solve the countEven exercise without pattern matching? #303

milesfrain opened this issue Jan 31, 2021 · 6 comments · Fixed by #459

Comments

@milesfrain
Copy link
Member

The solution for the countEven exercise relies on pattern matching, but that isn't covered until the next chapter. Should we remove that exercise, or reorganize content in the book?

countEven :: Array Int -> Int
countEven ints = countEven' ints 0
where
countEven' :: Array Int -> Int -> Int
countEven' [] count = count
countEven' ints' count = countEven' (fromMaybe [] (tail ints')) $ add count $ maybe 0 oneIfEven $ head ints'

Even if we do want to use pattern matching, this solution can be cleaned-up a bit.

@hdgarrood
Copy link

I think we can implement countEven without pattern matching by using the same patterns as in the examples just before that exercise. I think we should also avoid making it tail-recursive at least until the reader has reached the section about tail recursion. For example:

isEven :: Int -> Boolean
isEven n =
  if n == 0 then
    true
  else if n == 1 then
    false
  else
    isEven (n - (2 * (n / abs n)))

countEven :: Array Int -> Int
countEven arr =
  if null arr
    then 0
    else oneIfEven (fromMaybe 1 $ head arr) + countEven (fromMaybe [] $ tail arr)
  where
  oneIfEven x = if isEven x then 1 else 0

However, it does suck to have to do fromMaybe on both the head and the tail of the array, and in particular it sucks to have to make up default values in the cases that shouldn't ever happen in practice. I think there is an argument for waiting until after we have introduced pattern matching for these exercises. They'd also be a lot nicer if we used List, I think.

@milesfrain
Copy link
Member Author

milesfrain commented Jan 31, 2021

Edit: This message posted simultaneously with the previous one, so it doesn't make much sense as a direct reply.


isEven without pattern matching could be cleaned-up a bit to:

isEven :: Int -> Boolean
isEven n = if n == 0 then
  true
  else not $ isEven $ n - 1

But with pattern matching, we can do:

isEven :: Int -> Boolean
isEven 0 = true
isEven n = not $ isEven $ n - 1

Wondering if we should modify the exercise and tests to handle negative inputs.

And for reference, here's the best countEven I can think of without a map or fold:

countEven :: Array Int -> Int
countEven arr = case uncons arr of
  Nothing -> 0
  Just {head: x, tail: xs} -> (if isEven x then 1 else 0) + countEven xs

But for these situations, I propose we note that future chapters will show how to rewrite these solutions elegantly, then follow-up. Otherwise, having readers only write the clunky implementations reflects poorly on the language. Here's an example of a nicer countEven rewrite:

countEven :: Array Int -> Int
countEven = length <<< filter isEven

Should we even be including these introductory exercises that ask readers to write non-idomatic code? I think we could provide better exercises.

@hdgarrood
Copy link

I think asking the reader to do at least one or two exercises with explicit recursion is probably good, because you do occasionally see it in the wild even if in most cases you ought to be using maps, filters, folds, and so on. If we ask them to rewrite countEven using filter once we have covered filter, that ought to make up for us asking them to write non-idiomatic code in the first place; that could be a good way to emphasize the point about preferring folds to explicit recursion.

@hdgarrood
Copy link

I think an implementation which doesn't handle negative inputs is arguably wrong, as the exercise does ask for a function which returns true if and only if its input is an even integer, since negative even integers are still even integers.

@milesfrain
Copy link
Member Author

milesfrain commented Jan 31, 2021

Here's what I'm thinking for next steps:

  • Improve the isEven exercise description and tests for handling negative numbers.
  • Replace countEven with a different recursion exercise.

@klarkc
Copy link

klarkc commented Sep 1, 2021

Just to add my solution, without pattern matching, but preserving Maybe, which IMHO feels too complicated for a beginner - I had to search externally for what is Nothing and Just, and after a lot of research I've discovered that I can just transform Maybe values to "real" values with fromMaybe

countEven :: Array Int -> Int
countEven arr = if null arr then 0 else evenInc (fromMaybe (-1) (head arr)) $ countEven (fromMaybe [] (tail arr))
    where
        evenInc :: Int -> Int -> Int
        evenInc n = if isEven n then (+) 1 else (+) 0

https://github.com/klarkc/purescript-book/blob/4a15bc8596bcb0cf1069d598b4c5f53a16ad1e75/exercises/chapter4/test/MySolutions.purs#L15-L19

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Development

Successfully merging a pull request may close this issue.

3 participants