2

So, I want my function to return the index (the first if it appears multiple times in the list) of a given value. It appears to work only for an empty list or if the given element has the index 0. Could you let me know what I do wrong?

linsearch _ [] = -1
linsearch y (x:xs) = head [i | let j = length xs, i <- [0..j-1], y == x]

Thank you.

  • 1
    You seem to be starting to write a recursive algorithm but then use list notation for just the current case (`x`) and ignore the remainder of the list (`xs`). – Thomas M. DuBuisson Sep 26 '18 at 13:51

3 Answers3

5

Returning -1 to represent a failed search is something you do in a language with a bad type system. Also, there is no reason to find all the elements if you only care about the first one. Further, head will fail if the resulting list is empty.

linsearch :: Eq a => a -> [a] -> Maybe Int
linsearch _ [] = Nothing
linsearch y (x:xs) | y == x = Just 0
                   | otherwise = fmap (+ 1) (linsearch y xs)

An empty list obviously fails: return Nothing. With a non-empty list, check if the first element is the one you are looking for; if so, return Just 0. Otherwise, y may or may not be in xs; recurse to find out. You'll add 1 to that result to account for xs being the "shifted" version of the original list. You need to use fmap because you won't be adding 1 to some integer y; you'll be adding it to a "wrapped" value like Just y, or possibly Nothing if y really isn't in the list. Note

fmap (+1) Nothing == Nothing
fmap (+1) (Just 3) == Just 4
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Hello. Thank you for your response. Returning those exact values is a contraint of my assignment. I understood your way of solving this problem, but unfortunately I can't take this way in solving it. A solution that I know for sure that works is creating a new list of indexes and zipping it to the existent one of values, but I am still trying to figure out how to write that :) – Andrei Bercea Sep 26 '18 at 15:06
  • You can use the same approach; you just need to check the return value of the recursive call. If it is -1, return that. Otherwise, return that value + 1. – chepner Sep 26 '18 at 15:42
  • 1
    Or, just wrap the `Maybe` version: `maybe (-1) id . linsearch` returns -1 if `linsearch` returns `Nothing`, or extracts the wrapped `Int` from a `Just` response. – chepner Sep 26 '18 at 15:44
1

TL;DR: the solution is at the end of the post.

You don't need the list decomposition pattern in your definition, just deal with the list as a whole through the list comprehension:

linsearch :: Eq a => a -> [a] -> Int
linsearch _ [] = -1
linsearch y xs = head [i | let j = length xs, i <- [0..j-1],
                           -- y == x  -- x? what is that?
                           y == (xs!!i)]

Now linsearch 3 [0..9] returns 3, as it should. But linsearch 3 [0..] doesn't return at all -- it is lost in trying to calculate the list's length, which isn't actually needed here at all! Moreover, ditching the length calculation makes us re-arrange the algorithm from its currently quadratic form into the much better, linear one:

linsearch :: Eq a => a -> [a] -> Int
linsearch _ [] = -1
linsearch y xs = head [i | (x,i) <- zip xs [0..], y == x]

linsearch 3 [0..] now successfully returns 3, as it should.

linsearch 3 [0,2..] still diverges though (i.e. never returns), because Haskell doesn't know -- nor does it want to -- that there's no point searching on an ordered increasing list while past the very first element which is larger than the one we're searching for. That is so because [a] is the type of lists, not of ordered lists.

We could define such variant though too, as e.g.

linsearchOrdered :: Ord a => a -> [a] -> Int
linsearchOrdered y xs = linsearch y $ takeUntil (> y) xs

takeUntil :: (a -> Bool) -> [a] -> [a]
takeUntil p xs = foldr (\x r -> if not (p x) then x:r else [x]) [] xs

And sure enough, it is working now:

> linsearchOrdered 3 [0,2..]
*** Exception: Prelude.head: empty list

> takeUntil (> 3) [0,2..]
[0,2,4]
it :: (Ord a, Num a, Enum a) => [a]    

> linsearch 3 [0,2,4]
*** Exception: Prelude.head: empty list

wait, what? Where's the error coming from? It comes from your use of head: since there were no 3 found in [0,2,4], you code calls head [] which is an error.

Instead, we can use take 1, and convert its result to a Maybe a with a standard use of listToMaybe:

import Data.Maybe

linsearch :: (Eq a) => a -> [a] -> Maybe Int
linsearch y xs = listToMaybe $ take 1 [i | (x,i) <- zip xs [0..], y == x]

Ok, now it's working:

> linsearchOrdered 3 [0,2..]
Nothing    

> linsearchOrdered 4 [0,2..]
Just 2

Note the return type is different now. Instead of using a special value, we use a wrapping type to indicate the success (with Just) or failure (with Nothing):

data Maybe a = Nothing | Just a

If you really want your original design, we can code it as

linsearch y xs = head $ [i | (x,i) <- zip xs [0..], y == x] ++ [-1]

Haskell is lazy so head will (safely, now, with the ++ [-1]) stop on the first matching element (if at all possible). There's almost no duplicated effort going to waste, both with using head and takeUntil.

Both are folds though. zip can be coded as a fold as well, so linsearch is a fold too. And folds composition can be fused into one fold by fusing the reducer functions, turning it all into an explicitly one-pass algorithm, if so desired.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
1

There is a function elemIndex in Data.List which does exactly what you want. Your function could be written as follows:

import Data.List
linsearch y xs = elemIndex y xs

elemIndex returns a Maybe Int. So calling linsearch 5 [] yields Nothing while linsearch 5 [3,4,5] yields Just 2. Side note, the above implementation allows you to drop the xs: linsearch y = elemIndex y is valid. Now if you want to return only an Int we can "extract" the value from the Maybe using a case expression:

linsearch y xs = caseExtract $ elemIndex y xs
  where
    caseExtract m = case m of
                      Just a  -> a
                      Nothing -> -1

If ya wanna get really fancy, you could go "point-free" by dropping the xs and using composition(.) with fromMaybe from Data.Maybe :

import Data.List
import Data.Maybe
linsearch y = fromMaybe (-1) . elemIndex y

The benefit of using library functions like elemIndex is that they are safe, unlike head which will go boom if passed an empty list. Reference: Haskell Report LYAH Data.List reddit unsafe head fromMaybe

dopamane
  • 1,315
  • 2
  • 14
  • 27