2013-01-03 19 views
5

Chcę zastąpić element na liście nową wartością tylko przy pierwszym wystąpieniu. Napisałem poniższy kod, ale używając go wszystkie dopasowane elementy ulegną zmianie.Zastąp element na liście tylko raz - Haskell

replaceX :: [Int] -> Int -> Int -> [Int] 
replaceX items old new = map check items where 
check item | item == old = new 
      | otherwise = item 

Jak mogę zmodyfikować kod tak, aby zmiana miała miejsce tylko przy pierwszym dopasowanym elemencie?

Dzięki za pomoc!

Odpowiedz

11

Chodzi o to, że map i f (check w przykładzie) komunikować się tylko czasowo, jak przekształcić poszczególne elementy. Nie komunikują o tym, jak daleko lista zmienia elementy: map zawsze prowadzi do końca.

map :: (a -> b) -> [a] -> [b] 
map _ []  = [] 
map f (x:xs) = f x : map f xs 

Załóżmy napisać nową wersję map --- Zadzwonię go mapOnce bo nie mogę wymyślić lepszej nazwy.

mapOnce :: (a -> Maybe a) -> [a] -> [a] 

Są dwie rzeczy, aby pamiętać o tym typu podpis:

  1. Ponieważ możemy zaprzestać stosowania f niepełnym wymiarze dół listy, lista wejściowego i lista wyjściowa musi mieć ten sam typ . (Z map, ponieważ cała lista będzie zawsze odwzorowywane, typ może się zmienić.)

  2. Rodzaj f nie zmienił się a -> a, ale a -> Maybe a.

    • Nothing oznacza "opuścić ten element bez zmian, nadal w dół listy"
    • Just y będzie oznaczać "zmienić ten element i pozostawić pozostałe elementy niezmienioną"

Więc:

Jest to przykład:
replaceX :: [Int] -> Int -> Int -> [Int] 
replaceX items old new = mapOnce check items where 
    check item | item == old = Just new 
       | otherwise = Nothing 
+1

Dziękuję bardzo za dokładne wyjaśnienie! Dużo się nauczyłem. – Afflatus

4

Można łatwo napisać to jako rekurencyjnego iteracji tak:

rep :: Eq a => [a] -> a -> a -> [a] 
rep items old new = rep' items 
    where rep' (x:xs) | x == old = new : xs 
         | otherwise = x : rep' xs 
      rep' [] = [] 
+0

jestem zupełnie nowy dla Haskella. czy mogę zapytać, co oznacza "Eq a =>"? – Afflatus

+0

To ograniczenie typu; oznacza to, że 'a' musi być instancją typu Eq. Jest to potrzebne, aby można było użyć '=='. –

4

Bezpośrednia realizacja byłaby

rep :: Eq a => a -> a -> [a] -> [a] 
rep _ _ [] = [] 
rep a b (x:xs) = if x == a then b:xs else x:rep a b xs 

Lubię lista jako ostatni argument coś zrobić jak

myRep = rep 3 5 . rep 7 8 . rep 9 1 
2
nie

Może najszybsze rozwiązanie, ale łatwe do zrozumienia:

rep xs x y = 
    let (left, (_ : right)) = break (== x) xs 
    in left ++ [y] ++ right 

[Edi t]

Jak Dave skomentował, to się nie powiedzie, jeśli x nie znajduje się na liście. Bezpieczna wersja będzie:

rep xs x y = 
    let (left, right) = break (== x) xs 
    in left ++ [y] ++ drop 1 right 

[Edytuj]

Arrgh !!!

rep xs x y = left ++ r right where 
    (left, right) = break (== x) xs 
    r (_:rs) = y:rs 
    r [] = [] 
+0

Mimo że dopasowanie wzorca się nie powiedzie, 'x' nie powinno występować w' xs'. – dave4420

+0

Ponownie edytuj: czy 'x' nie występuje w' xs', twoja bezpieczna wersja zwróci 'xs ++ [y]'. Nie to robią inne rozwiązania. Nie mówię, że to jest złe (chociaż nie sądzę, że to jest to, co chce OP, bez wątpienia są pewne sytuacje, w których to zachowanie jest poprawne), ale myślę, że warto to zauważyć. – dave4420

+0

Oto hack: 'rep xs x y = niech (lewo, prawo) = przerwa (== x) xs; (tak, nie) = splitAt 1 po prawej ++ [y | _ <- yes] ++ no'. –

3

Szczerze mówiąc, nie podoba mi się większość dotychczasowych odpowiedzi. dave4420 prezentuje kilka ciekawych informacji na temat map, które zastępuję, ale też nie lubię jego rozwiązania.

Dlaczego nie lubię tych odpowiedzi? Ponieważ powinieneś uczyć się rozwiązywać takie problemy, dzieląc je na mniejsze problemy, które można rozwiązać prostszymi funkcjami, najlepiej funkcjami bibliotecznymi. W tym przypadku, biblioteka jest Data.List, a funkcja jest break:

break, stosowane do orzecznika p oraz listę xs, zwraca krotki gdzie pierwszym elementem jest najdłuższy prefiks (być może pusty) od xs elementów które nie spełniają p, a drugim elementem jest pozostała część listy.

Uzbrojony że możemy zaatakować ten problem tak:

  1. podzielić listę na dwie części: wszystkie elementy przed pierwszym występowania old i reszta.
  2. Lista "odpoczynku" będzie pusta lub jej pierwszy element będzie pierwszym wystąpieniem old. Oba te przypadki są łatwe w obsłudze.

Więc mamy tego rozwiązania:

import Data.List (break) 

replaceX :: Eq a => a -> a -> [a] -> [a] 
replaceX old new xs = beforeOld ++ replaceFirst oldAndRest 
    where (beforeOld, oldAndRest) = break (==old) xs 
      replaceFirst [] = [] 
      replaceFirst (_:rest) = new:rest 

Przykład:

*Main> replaceX 5 7 ([1..7] ++ [1..7]) 
[1,2,3,4,7,6,7,1,2,3,4,5,6,7] 

Więc moja rada dla ciebie:

  1. Dowiedz się, jak importować biblioteki.
  2. Przestudiuj dokumentację biblioteki i ucz się standardowych funkcji. Data.List to świetne miejsce na rozpoczęcie.
  3. Staraj się korzystać z tych funkcji biblioteki tak często, jak możesz.
  4. Jako ćwiczenie do samodzielnego studiowania, możesz wybrać niektóre standardowe funkcje z Data.List i napisać własną wersję ich.
  5. Gdy napotkasz problem, którego nie można rozwiązać za pomocą kombinacji funkcji bibliotecznych, spróbuj wymyślić własną, ogólną funkcję, która byłaby przydatna.

EDIT: zdałem sobie sprawę, że break jest rzeczywiście Prelude funkcji i nie muszą być importowane. Mimo to, Data.List jest jedną z najlepszych bibliotek do nauki.

2

Alternatywa przy użyciu Lens library.

>import Control.Lens 
>import Control.Applicative 

>_find :: (a -> Bool) -> Simple Traversal [a] a         
>_find _ _ [] = pure []               
>_find pred f (a:as) = if pred a             
>      then (: as) <$> f a          
>      else (a:) <$> (_find pred f as) 

Funkcja ta zajmuje (a -> Bool), która to funkcja, która powinna powrócić prawda na typ 'a', które wan zmodyfikować.

Jeśli pierwszy numer większy niż 5 potrzeby należy podwoić wtedy moglibyśmy napisać:

>over (_find (>5)) (*2) [4, 5, 3, 2, 20, 0, 8] 
[4,5,3,2,40,0,8] 

Wspaniałą rzeczą jest to, że obiektyw można łączyć je ze sobą za pomocą ich komponowania (.). Więc jeśli chcemy wyzerować pierwszy numer < 100 na liście sub 2th Mogliśmy:

>over ((element 1).(_find (<100))) (const 0) [[1,2,99],[101,456,50,80,4],[1,2,3,4]] 
[[1,2,99],[101,456,0,80,4],[1,2,3,4]] 
0

oto imperatyw sposób to zrobić, używając State monady:

import Control.Monad.State             

replaceOnce :: Eq a => a -> a -> [a] -> [a] 
replaceOnce old new items = flip evalState False $ do 
    forM items $ \item -> do 
    replacedBefore <- get 
    if item == old && not replacedBefore 
     then do 
     put True 
     return new 
     else 
     return old 
0
replaceValue :: Int -> Int -> [Int] -> [Int] 
replaceValue a b (x:xs) 
     |(a == x) = [b] ++ xs 
     |otherwise = [x] ++ replaceValue a b xs 
Powiązane problemy