simple function that shifts specific element of a list

后端 未结 2 1948
傲寒
傲寒 2021-01-16 18:06

I am new to Haskell and I am trying to figure out how to make a function:

    shift:: Eq a => a -> [a] -> Int -> [a]
    shift x (h:t) z
<         


        
相关标签:
2条回答
  • 2021-01-16 18:33

    For you are new to Haskell, it's better to break the problem into simpler subproblems. One trick to solve your problem is:

    thinking of "insert" instead of "shift". Pseudo code:
    - remove x from the list
    - insert x back to the list at the proper position based on given n
    

    You already implement delete, lets take advantage of it. Enjoy simple code:

    -- assume x is in the list, otherwise your problem would not make sense
    shift :: Eq a => a->[a]->Int->[a]
    shift x lst n = if n == 0 then delete x lst 
                    else insert x (n + (position x lst)) (delete x lst)
    

    Sub-problems are:

    1. Delete x from the list: you did that
      delete :: Eq a => a->[a]->[a]
      delete x []          = []
      delete x (head:tail) = if x == head then tail else head : delete x tail
    
    2. find the position of x in the list
      -- assume x in the list
      position :: Eq a => a->[a]->Int
      position x (head:tail) = if x == head then 1 else 1 + position x tail
    
    3. -- insert x into the list at nth position
      insert :: a->Int->[a]->[a]
      insert x n (head:tail) = if n <= 1 then x : (head:tail) 
                               else head : (insert x (n-1) tail)
      insert x n lst         = if n >= length lst then lst ++ [x] 
                               else insert x n lst
    

    Your precondition is that x is in the list, otherwise it does not make sense. If you want to include the case x is not in the list, twist the code.

    Have fun.

    0 讨论(0)
  • 2021-01-16 18:38

    As @WillemVanOnsem suggests, you may want to try writing a function that shifts a target element one space to the right. Even this simplified problem might very well be challenging!

    See if you can implement a direct recursive version. It could be similar in structure to your delete function, except it will swap two elements instead of dropping an element at the critical point. (Answer at the bottom -- look for the definition of simpleShiftRight.)

    Once you've done that, try working through this alternative approach which has the advantage that it will more easily generalize to solving your original problem.

    First, using delete isn't very helpful, because delete "forgets" where the element originally was. For example, both of the following:

    delete '.' "abc.def"
    delete '.' "abcde.f"
    

    yield "abcdef", and it's not clear how to use this result to, say, shift the period one position to the right of where it was.

    Instead, what you'd really like to do is break a string up into the parts before and after the target element. That is, you'd like to define a function split that works like this:

    > split '.' "abc.def"
    ("abc","def")
    > split '.' "abcde.f"
    ("abcde","f")
    

    With this result, shifting the period becomes much easier.

    For example, if we wanted to shift the period one position to the right, we could start by defining a function

    pairRight :: ([a], [a]) -> ([a], [a])
    

    that works like this:

    > pairRight ("abc","def")
    ("abcd","ef")
    > pairRight ("abcde","f")
    ("abcdef","")
    

    and a function

    rejoin :: a -> ([a], [a]) -> [a]
    

    that works like this:

    > rejoin '.' ("abcd","ef")
    "abcd.ef"
    > rejoin '.' ("abcdef","")
    "abcdef."
    

    and combine them:

    > rejoin '.' (pairRight (split '.' "abc.def"))
    "abcd.ef"
    > rejoin '.' (pairRight (split '.' "abcde.f"))
    "abcdef."
    

    to get a function that shifts a character one space to the right.

    Now, split can be defined in terms of the library function break, like so:

    split :: Eq a => a -> [a] -> ([a], [a])
    split x xs = let (a, _:b) = break (==x) xs in (a,b)
    

    Can you implement the functions pairRight and rejoin? They shouldn't be too hard, but if you get stuck the answer's at the bottom.

    You might also want to try defining split from scratch without using break. It's a slightly tricky recursive function. If you start with an "obvious" approach:

    split :: Eq a => a -> [a] -> ([a], [a])
    split x (y:ys) | x == y    = (..., ys)
                   | otherwise = split x ys
    split _ [] = error "split: target not found"
    

    you'll run into a problem. It's not clear how to fill in the ... because you've sort of thrown away the start of the list in the recursion. Hopefully you've already learned that one way around this is to introduce an extra parameter to keep track of the list elements already processed and define a function:

    split' :: Eq a => a -> [a] -> [a] -> ([a], [a])
    split' x ls (r:rs) = ...
    

    where x is the element we're looking for, ls is the set of elements on the left side of the list that we've already processed (where we didn't find a copy of x), and (r:rs) is the right side of the list that we're still processing.

    If you need a further hint, here's a template:

    split' x ls (r:rs) | x == r    = (..., ...)
                       | otherwise = split' x (...) rs
    split' _ _ [] = error "split: target not found"
    

    Can you fill in the ... here? If you can, then you can define:

    split :: Eq a => a -> [a] -> ([a], [a])
    split x xs = split' x [] xs
    

    Once you have split, pairRight, and rejoin defined, you should be able to combine them into a function:

    shiftRight :: Eq a => a -> [a] -> [a]
    

    that can shift a target element one position to the right.

    If you get stuck, here's a complete definition of shiftRight and its helpers:

    shiftRight :: (Eq a) => a -> [a] -> [a]
    shiftRight x xs = rejoin x (pairRight (split x xs))
    
    -- alternative definition of split:
    -- split :: Eq a => a -> [a] -> ([a], [a])
    -- split x xs = let (a, _:b) = break (==x) xs in (a,b)
    
    split :: Eq a => a -> [a] -> ([a], [a])
    split x xs = split' x [] xs
    
    split' :: Eq a => a -> [a] -> [a] -> ([a], [a])
    split' x ls (r:rs) | x == r    = (ls, rs)
               | otherwise = split' x (ls ++ [r]) rs
    split' _ _ [] = error "split: target not found"
    
    pairRight :: ([a], [a]) -> ([a], [a])
    pairRight (ls, r:rs) = (ls ++ [r], rs)
    
    rejoin :: a -> ([a], [a]) -> [a]
    rejoin x (ls, rs) = ls ++ [x] ++ rs
    

    In this version, trying to shiftRight a target that isn't in the list or that is already in the rightmost position will give an error. You might want to try fixing that. (Note that split could return a Maybe [a], yielding Nothing if the target isn't found; it also shouldn't be too tough to modify pairRight so that it does nothing if the pair is already shifted right as far as it can go.)

    If this seems like a lot of bother for a simple problem, I guess it is. In real code, an experienced Haskell programmer would probably write a direct recursive version:

    simpleShiftRight :: (Eq a) => a -> [a] -> [a]
    simpleShiftRight x (y:z:rest) | x == y    = z:y:rest
                                  | otherwise = y : simpleShiftRight x (z:rest)
    simpleShiftRight _ rest                   = rest
    

    or this one that uses break:

    simpleShiftRight :: (Eq a) => a -> [a] -> [a]
    simpleShiftRight x xs = case break (==x) xs of
      (ls, y:z:rs) -> ls ++ z:y:rs
      _ -> xs
    

    Both versions are succinct, handle all the corner cases, and are "obviously correct". The downside, as previously mentioned, is that this version isn't quite as easy to generalize to your original problem.

    The version above does generalize pretty easily -- you just need to replace pairRight with a more sophisticated pair shifting function. For example, defining:

    pairRightN :: Int -> ([a],[a]) -> ([a],[a])
    pairRightN n (ls, r:rs) | n > 0 = pairRightN (n-1) (ls ++ [r], rs)
    pairRightN _ (ls, rs)           = (ls, rs)
    

    allows you to handle all positive values for n (i.e., all right shifts, no matter how big). It's not too hard to further generalize it to handle left shifts, too.

    0 讨论(0)
提交回复
热议问题