Вот ещё задачка.
Есть какой-то список [1,1,2,3,4,5,5,2,3,5,6,7]
Надо из этого списка вывести все элементы которые повторяются больше N раз в том же порядке, в котором они впервые появились в начальном списке.
Решение на Go
Как это решается на go? Очень просто:
func CountElement(n int, list []string) []string {
var uniqList []string
resMap := map[string]int{}
for _, el := range list {
ex, ok := resMap[el]
if !ok {
uniqList = append(uniqList, el)
resMap[el] = 1
} else {
resMap[el] = ex + 1
}
}
var resList []string
for _, el := range uniqList {
val := resMap[el]
if val >= n {
resList = append(resList, el)
}
}
return resList
}
Мы сначала проходим по списку и наполняем resMap для того чтобы считать количество вхождений и uniqList для того чтобы знать порядок в котором элементы впервые появились в списке (ведь если мы просто наполним мапу, то потеряем порядок)
Кстати, наполнять мапу тоже удобно для того чтобы понимать когда элемент впервые появился в списке.
После этого мы проходим по uniqList и проверяем как часто элемент появлялся в начальном списке (дергая мапу)
Если появлялся n или больше раз, то мы можем добавить его в resList который в итоге и возвращаем обратно.
Но на go не интересно.
Haskell 1
Давайте на хаскеле.
Если задачка на go супер просто решается, то на хаскеле она должна решаться ещё проще. Поидее.
А вот и фигушки.
В обычных языках нам доступны последовательные шаги и обращение к состоянию (например к переменным), то в хаскеле нет ни первого, ни второго. Например, в го мы могли обьявить переменную перед циклом и потом её наполнять.
В хаскеле так нельзя, потому что данные имутабельны и всё является функцией.
— Как же они тут живут? - спросите вы.
Ну в основном на различного рода свертках и комбинаторике.
Например, есть такая замечательное семейство функций Fold, которые рекурсивно вызывают себя и передают состояние дальше.
Есть две главные функции foldl (идет по списку слева направо) и foldr (наоборот)
Например, мы можем написать что-то вроде
foldl (+) 0 [1,2,3,4]
Функция будет идти по списку слева направо и применять функцию (+) к стеку (который сначала 0), а потом к первому элементу, после чего вызовет себя же
foldl (+) 1 [2,3,4]
↓↓↓
foldl (+) 3 [3,4]
↓↓↓
foldl (+) 6 [4]
↓↓↓
foldl (+) 10 []
И будет вызывать себя до тех пор, пока не доберется до пустого списка. Где перестанет и позволит результатам вычислений свернуться обратно.
Если мы разобьем нашу задачу на части, то увидим что сначала мы формируем список уникальных значений и мапу с количеством повторений, а потом проходим по списку и проверяем условие "больше N повторений"
То есть первую задачу мы однозначно можем выразить через foldl
А стеком для foldl одновременно должны быть и мапа и список повторений
foldl aggregateMap (empty , []) xs
-- xs это имя переменной списка
-- aggregateMap это название нашей будущей функции
Я для этого обьявил синоним для пары
type MapList = (Map String Int, [String])
и начал определять функцию
aggregateMap :: MapList -> String -> MapList
aggregateMap (agMap,list) x
| isNothing $ lookup x agMap = (insert x 1 agMap, x:list)
| otherwise = (update (\y -> Just (y+1)) x agMap, list)
Эта функция первым аргументом принимает наш стек, а последним возвращает модифицированный стек (кстати, так как обьекты имутабельны, хаскель не создает целиком новые обьекты, но ссылается на старые, это очень элегантное решение)
Дальше, в теле мы производим паттерн матчинг agMap в мапу, list в список и x в значение которое нам передали
После чего мы выполняем проверку
Если результат поиска в мапе возвращает нам Nothing, то мы добавляем элемент в мапу и список
В всех остальных случаях (то есть когда элемент есть) мы обновляем значение в мапе (прибавляем единицу) и ничего в список не добавляем.
Теперь можно посмотреть как эта функция работает:
>>> aggregateMap (empty , []) "1"
(fromList [("1",1)],["1"])
-- А теперь передадим результат и попробуем добавить 1 или 5, то есть вручную сымитировать foldl
>>> aggregateMap (fromList [("1",1)],["1"]) "1"
(fromList [("1",2)],["1"])
>>> aggregateMap (fromList [("1",2)],["1"]) "5"
(fromList [("1",2),("5",1)],["5","1"])
Соответственно если мы уже попробуем foldl с этой функцией, то всё должно работать
>>> foldl aggregateMap (empty , []) ["1", "1", "2", "3", "2" , "2", "5" ]
(fromList [("1",2),("2",3),("3",1),("5",1)],["5","3","2","1"])
Упс. В списке значения в обратном порядке. Это потому что в aggregateMap мы добавляли элемент в список вот так x:list - то есть каждый последующий элемент добавлялся в начало списка.
А foldl шел слева направо. То есть каждый следующий уникальный элемент становился первым. Впрочем, это не важно сейчас, мы дальше его где-то развернем.
Теперь когда у нас есть мапа и список, нам надо по нему пройтись и сформировать новый список.
Для этого в хаскеле есть функция filter
showList' n (agMap,list) = filter (\x -> fromJust(lookup x agMap) >= n) list
Эта функция первым аргументом принимает фукнцию фильтрования (которая должна вернуть Bool), а вторым — список.
В функции фильтрования (я написал её в лямбда формате, потому что она маленькая) мы принимаем элемент из списка и ищем его в в мапе, после чего сравниваем результат с n
Соответственно всё что нам остается это обьеденить функции
showAggrList :: Int -> [String] -> [String]
showAggrList n xs = reverse . showList' n
$ foldl aggregateMap (empty , []) xs
Тут . используется для того чтобы сделать композицию функций для reverse . showList' n
— это очень мощная механика хаскеля, которая позволяет составлять из двух функций одну
А $ используется чтобы не ставить скобки (что значительно повышает читаемость)
А зачем мы используем reverse? а затем что нам надо развернуть список обратно.
Итоговый код выглядит вот так.
showAggrList :: Int -> [String] -> [String]
showAggrList n xs = reverse . showList' n
$ foldl aggregateMap (empty , []) xs
type MapList = (Map String Int, [String])
aggregateMap :: MapList -> String -> MapList
aggregateMap (agMap,list) x
| isNothing $ lookup x agMap = (insert x 1 agMap, x:list)
| otherwise = (update (\y -> Just (y+1)) x agMap, list)
showList' :: Int -> MapList -> [String]
showList' n (agMap,list) = filter (\x -> fromJust(lookup x agMap) >= n) list
Тааак, посмотрим. Ну... как-то это выглядит громоздко.
Haskell 2 (монады)
Я покумекал и переписал вот так:
alternativeL :: Ord a1 => Int -> [a1] -> [a1]
alternativeL n = liftA2 (>>=) nub $ (\y x -> (y ! x >= n) ?<> [x] )
. fromListWith (+) . flip zip [1,1..]
Получилось намного компактнее, но и намного непонятнее.
Что происходит?
Для начала надо разобраться что такое >>=
Вкратце - это функция. Она принимает монадическое значение первым элементом, разворачивает его и применяет к результату функцию, которая должна вернуть монадическое значение обратно.
По описанию звучит непривлекательно и фиг поймёшь зачем это надо. Но на самом деле это просто универсальный распаковыватель-запаковыватель-итератор, который может работать для разных типов.
Например:
>>> Just 3 >>= (\x -> Just (x+1))
Just 4
А для списка >>= выполняет роль итератора
>>> [1,2,3] >>= (\x -> [x+1])
[2,3,4]
Правда стоит обратить внимание, что возвращать надо не x, но монадический тип (список). Сначала это кажется неудобным, но на деле это как раз расширяет возможности.
-- Вместо одного элемента можно вернуть несколько (например три)
>>> [1,2,3] >>= (\x -> [x,x,x])
[1,1,1,2,2,2,3,3,3]
-- И зачем нужен фильтр, если есть монады? Проверяем условие и возвращаем пустой список, если оно False
>>> [1,2,3] >>= (\x -> x > 2 ? [x] ?? [])
[3]
То есть, если бы у нас уже был список уникальных значений и какая-то мапа, то всё можно было бы переписать как
-- uniqList - список уникальных значений
-- uniqMap - мапа с количеством повторений
-- filterFromMap - функция для фльтров
uniqList >>= filterFromMap uniqMap
Как же нам получить список уникальных вхождений? Для этого есть функция nub - она принимает на вход список и возвращает уникальные вхождения.
А мапу мы можем очень просто создать вот так
fromListWith (+) . flip zip [1,1..]
[1,1..]
создает бесконечный список из единиц, zip
- делает из двух списков список пар, а flip
- разворачивает аргументы для списка
>>> zip ["a", "b", "c"] [1,2..]
[("a",1),("b",2),("c",3)]
>>> flip zip ["a", "b", "c"] [1,2..]
[(1,"a"),(2,"b"),(3,"c")]
fromListWith
создает мапу из списка и применяет функцию, если включи совпали (в нашем случае это + )
а зачем нам тут flip?
да просто чтобы не прописывать xs (ведь именно из xs мы берем ключи)
createMap xs = fromListWith (+) . zip xs [1,1..]
-- это эквивалентно
createMap = fromListWith (+) . flip zip [1,1..]
-- потому что аргумент примениться автоматически
Давайте снова взглянем на нашу монаду
уникальный массив
↓↓↓
alternativeL n xs = nub xs >>= filterFromMap n ((fromListWith (+) . flip zip [1,1..]) xs)
Осталось развернуть filterFromMap в отдельную функцию
(\y x -> (y ! x >= n) ?<> [x] )
тут y это мапа, а x - элемент который приходит из монады на сравнение
К тому же, если мы уверены в алгоритме, мы можем вызывать элементы из мапы без обертки Just — через функцию (!)
>>> fromList [("1",2),("2",3),("3",1),("5",1)] ! "1"
2
>>> fromList [("1",2),("2",3),("3",1),("5",1)] ! "2"
3
>>> fromList [("1",2),("2",3),("3",1),("5",1)] ! "9"
*** Exception: Map.!: given key is not an element in the map
CallStack (from HasCallStack):
error, called at libraries/containers/containers/src/Data/Map/Internal.hs:633:17 in containers-0.6.5.1:Data.Map.Internal
Вторая интересная часть нашего фильтра - функция ?<>
Замечательное свойство монад в том что определение монады подразумевает определение пустого ответа (функция mempty) и преобразования типа к ответу (функция return)
То есть если писать длиное определение то можно написать что-то типа
>>> [1,2,3,4] >>= (\x -> if x > 2 then [x] else [])
[3,4]
>>> [1,2,3,4] >>= (\x -> if x > 2 then return x else mempty)
[3,4]
>>> [1,2,3,4] >>= (\x -> x > 2 ? [x] ?? mempty)
[3,4]
>>> [1,2,3,4] >>= (\x -> (x > 2) ?<> [x])
[3,4]
Таким образом наша финальная функция стала вот такой
alternativeL n xs = nub xs >>= (\y x -> (y ! x >= n) ?<> [x] )
. ((fromListWith (+) . flip zip [1,1..]) xs)
но мне не нравится что в обоих половинках монады у нас одно и то же xs
Как его убрать?
Есть функция liftA2 f1 f2 f3 arg
она применяет arg к f2, arg к f3, а к результатам применяет функцию f1
Таким образом у нас и получилось сократить xs
alternativeL :: Ord a1 => Int -> [a1] -> [a1]
alternativeL n = liftA2 (>>=) nub $ (\y x -> (y ! x >= n) ?<> [x] )
. fromListWith (+) . flip zip [1,1..]
-- а $ тут используется чтобы не писать лишние скобки
Впрочем, если вы не хотите пугать людей таким кодом (и не участвуете в соревновании на самой короткий код), то монады можно переписать по-человечески через (do)
alternativeZ :: Ord b => Int -> [b] -> [b]
alternativeZ n xs = do
let uniqMap = fromListWith (+) $ zip xs [1,1..]
uniqEl <- nub xs
guard (uniqMap ! uniqEl >= n)
return uniqEl
В принципе тут происходит тоже самое.
Мы определяем uniqMap так как мы делали это выше а потом для каждого элемента xs делаем проверку (guard) - которая как раз и возвращает пустое значение если проверка не проходится Если всё прошло хорошо и проверка пройдена — возвращаем ключ.
do это просто синтаксический сахар над монадами, так что код и там, и там — вполне равнозначный.
кстати, можно убрать второй проход по списку, если в мапу с повторениями сразу складывать и индекс первого вхождения. Но учитывая что количество уникальных элементов достаточно мало, я решил что лишнее прохождение не повредит производительности, но улучшит читаемость кода
Пост получился длинным, но надеюсь вам было интересно.