5

I am working on a programming assignment where I must define my own version of isPrefixOf from Data.List using only foldr, map, and cons (and thus no recursion). The hint I've been given is that the return value of foldr should itself be a function. Can someone help me understand how I could apply that fact? My guess for the structure is included below.

startsWith :: String -> String -> Bool
startsWith s1 s2 = (foldr (???) ??? s1) s2

I am allowed to define my own helper functions. For the curious this is from an assignment for CIS 552 at Penn.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 1
    Do you have any permission to solve this problem with foldl ? – S4eed3sm Feb 08 '22 at 06:13
  • 1
    @S4eed3sm, a left fold has a particular performance issue with this problem. – dfeuer Feb 08 '22 at 07:31
  • 2
    Yes, you've started along the right path. I urge you to replace the first `???` with `_c` and the second one with `_n` and compile your module. GHC will tell you the types of the values that you need to put in the "holes". – dfeuer Feb 08 '22 at 07:34
  • 1
    First, figure out which function should `foldr` return when the length of s1 is zero. (That's easy: one that always returns `True` for any argument). Replace the second set of `???` with it. Then, figure out which function should `foldr` return when the length of s1 is n+1, and you already have a function that works correctly when the length is n. – n. m. could be an AI Feb 08 '22 at 08:03
  • @dfeuer Why foldl has performance issue here ? – S4eed3sm Feb 08 '22 at 08:46
  • 1
    @s4eed3sm, `foldl` and `foldl'` for lists can't short-circuit, so they'll walk the whole prefix even if there's a mismatch early on. – dfeuer Feb 08 '22 at 15:38

1 Answers1

6

EDIT: It turns out that by folding over the pattern instead of the string, the code gets a lot simpler and shorter and something like

"" `isPrefixOf` undefined

works. Thank you @dfeuer and @WillNess. This is the updated program:

isPrefixOf pattern s = foldr g (const True) pattern s
    where
        g x r (h:t)
          | x == h    = r t
          | otherwise = False

It works almost the same way as the below program, so refer to that for the explanation.


I managed to solve it using nothing but foldr:

isPrefixOf :: String -> String -> Bool
p `isPrefixOf` s = isPrefixOfS p
    where
        isPrefixOfS 
            = foldr 
                (\c acc -> 
                    \str -> case str of
                        x:xs -> if c == x
                                then acc xs
                                else False
                        []   -> True 
                    )
                null
                s

Here's the explanation.

In order to create the function isPrefixOf, we want this:

isPrefixOf pattern s 
  = case pattern of
      [] -> True
      x:xs -> if (null s) then False
              else if (head s) /= x
                   then False
                   else xs `isPrefixOf` (tail s)
              

Well, let's simplify this - let's create a function called isPrefixOfS that only takes a pattern, which it compares to s automatically. We need to build this chain of nested functions:

-- Pseudocode, not actual Haskell

\p -> case p of 
        [] -> True
        x:xs -> if x /= (s !! 0)
                  then False
                  else <apply xs to> \q -> case q of [] -> True
                                                     x:xs -> if x /= (s !! 1) -- Note that we've incremented the index
                                                             then False
                                                             else <apply xs to> \r -> ....

This seems pretty self explanatory - let me know in a comment if it requires further explanation.
Well, we can see that this chain has a recursive property to it, where the last character of s will be compared in the most deeply nested lambda. So we need to nest lambdas from right to left. What can we use for that? foldr.

isPrefixOfS 
            = foldr -- Folding from right to left across `s`
                (\c acc -> -- `c` is the current character of `s`, acc is the chain of nested functions so far
                    \str -> -- We return a function in the fold that takes a String
                      case str of
                        -- If the string is not null:
                        x:xs -> if c == x   -- If the head of the string is equal to the current character of s,
                                then acc xs -- Then pass the tail of the string to the nested function which will compare it with subsequent characters of s
                                else False -- Otherwise we return false
                
                        -- If the string is null, we have completely matched the prefix and so return True
                        []   -> True  
                    )
                null -- Innermost nested function - we need to make sure that if the prefix reaches here, it is null, i.e. we have entirely matched it
                s

And now we use this function isPrefixOfS on p:

p `isPrefixOf` s = isPrefixOfS p

EDIT: I just found this post which uses a similar logic to implement zip in terms of foldr, you may want to look at that as well.

Vikstapolis
  • 744
  • 3
  • 13
  • 2
    Slight abbreviation: `c == x && acc xs` – dfeuer Feb 08 '22 at 08:25
  • Your answer is actually a little more complicated than it needs to be, and as a result also slightly wrong. ``[] `isPrefixOf` undefined`` should be `True`, but your code says `undefined`. Do you see why? – dfeuer Feb 08 '22 at 08:28
  • @dfeuer For your first comment - that was actually my original code but I made it a bit more verbose to expose the logic behind it. For the second comment - I didn't notice that, thanks for pointing it out. A simple pattern match in the definition of `isPrefixOf` will fix it. What in the program is too complicated? – Vikstapolis Feb 08 '22 at 08:40
  • 1
    indeed `and $ zipWith (==) [] undefined` returns `True` as does your second definition, so the first isn't its true translation. the straight-away translation of the 2d version folds over the first argument just like its prototype does: `isPrefixOf pattern s = foldr g (const True) pattern s where g x r (h:t) | x==h = r t | otherwise = False`. switching what is folded over first indeed complicates things. – Will Ness Feb 08 '22 at 13:51
  • Thanks @WillNess, your version of the function makes a lot more sense. I've edited the answer – Vikstapolis Feb 08 '22 at 14:07
  • Sorry I fell asleep and couldn't answer your question; glad you got it figured out. Patterns like `if ... then ... else False` are often seen in beginner code, but I am not convinced they're easier to understand. – dfeuer Feb 08 '22 at 14:13
  • 1
    This is a bit too much detail for someone's homework question. You want to guide them, not do their homework for them. – chepner Feb 08 '22 at 15:51