Задание: последовательно обработать объекты, каждый из которых принадлежит определенному юзеру, но своих обработать только первые столько-то (потому что сам юзер мог задействовать undo). В C-подобном синтаксисе получается элементарно:
Но известно, что побочные эффекты — зло. Если потребуется заменить условие на чуть более сложное, есть опасность наделать ошибок. Как выглядит эквивалентный, но идеологически правильный код на вашем любимом языке?
RO>Но известно, что побочные эффекты — зло. Если потребуется заменить условие на чуть более сложное, есть опасность наделать ошибок. Как выглядит эквивалентный, но идеологически правильный код на вашем любимом языке?
R>но здесь два прохода по коллекции, чего, возможно, захочется избежать
Да невнимательно условия прочитал, такое на потоки плохо ложится.
Можно попробовать сложный фильтр возвращающий кортеж (element type, bool) где
второй элемент это флаг сигнализирующий что это родной элемент и в конце
take_while с условием. но сложновато получается по сравнению с императивным
циклом да.
Здравствуйте, Roman Odaisky, Вы писали:
RO>Но известно, что побочные эффекты — зло. Если потребуется заменить условие на чуть более сложное, есть опасность наделать ошибок. Как выглядит эквивалентный, но идеологически правильный код на вашем любимом языке?
ИМХО наиболее идеологически правильно просто не допускать таких задач и таких условий, в которых есть опасность наделать ошибок. Для данной задачи ИМХО лучше не сделаешь. Можно конечно сделать изврат с созданием однократно используемого предиката с внутренним состоянием, и фильтрацией с использованием этого предиката, но это извратище из извратов.
То есть псевдокод:
Predicate p = new AllUsersButOnlyLimitedMine(limit);
processByFilter(items, p, processFunction);
Может идеологически и более правильно, но тут настолько легко ошибиться, забыв перед вызовом создать новый предикат, отсчитывающий с нуля, что ну их нахрен, такие извраты, лучше по старинке .
Здравствуйте, rameel, Вы писали:
R>но здесь два прохода по коллекции, чего, возможно, захочется избежать
Зачем избегать? Избегать, это уже начинаются оптимизации. А хочется же наиболее чистого кода с минимальной вероятностью ошибки. Соответственно наиболее идеологически правильно дробить сложное навороченное правило на 2 простых правила, и выполнять их по отдельности. Время то будет один черт линейное. А когда появится проблема, что хотелось бы пробегать по коллекции один раз, уже придется простотой и идеологической правильностью жертвовать. На двух стульях сразу не усидеть!
Здравствуйте, Roman Odaisky, Вы писали:
RO>Но известно, что побочные эффекты — зло. Если потребуется заменить условие на чуть более сложное, есть опасность наделать ошибок. Как выглядит эквивалентный, но идеологически правильный код на вашем любимом языке?
Побочные эффекты здесь не только в условии. processItem — тоже не без них, судя по всему.
Здравствуйте, elmal, Вы писали:
R>>но здесь два прохода по коллекции, чего, возможно, захочется избежать E>Зачем избегать? Избегать, это уже начинаются оптимизации. А хочется же наиболее чистого кода с минимальной вероятностью ошибки. Соответственно наиболее идеологически правильно дробить сложное навороченное правило на 2 простых правила, и выполнять их по отдельности. Время то будет один черт линейное. А когда появится проблема, что хотелось бы пробегать по коллекции один раз, уже придется простотой и идеологической правильностью жертвовать. На двух стульях сразу не усидеть!
Задание было — обойти всё последовательно. Там графические объекты один на другой накладываются, надо рисовать в правильном порядке.
Здравствуйте, samius, Вы писали:
RO>>Но известно, что побочные эффекты — зло. Если потребуется заменить условие на чуть более сложное, есть опасность наделать ошибок. Как выглядит эквивалентный, но идеологически правильный код на вашем любимом языке? S>Побочные эффекты здесь не только в условии. processItem — тоже не без них, судя по всему.
processItem — это нечто вроде canvas.drawShape. Но никто не мешает переписать чисто функционально наподобие foldl getCanvasCombinedWithShape originalCanvas itemsSubset. Как только сформировать этот itemsSubset?
Здравствуйте, Kogrom, Вы писали:
K>Если я верно понял задание, то:
К сожалению, нет: считает все элементы, а не только свои. Если limit = 2, а items = [чужое, чужое, свое, свое], не сработает.
K>for i, item in enumerate(items): K> if(item.owner != myself or i < limit): K> processItem(item)
Здравствуйте, Roman Odaisky, Вы писали: RO>К сожалению, нет: считает все элементы, а не только свои. Если limit = 2, а items = [чужое, чужое, свое, свое], не сработает.
class Item:
def __init__(self, owner):
self.owner = owner
def processItem(item):
print item.owner
items = [Item(a) for a in [1, 1, 0, 0]]
limit = 2
myself = 0
for i, item in enumerate(items):
if(item.owner != myself or i < limit):
processItem(item)
Вывод:
1
1
Что требовалось?
Тем, кому нравится запутывать читателя можно вместо нормального цикла написать:
[processItem(item) for i, item in enumerate(items) if (item.owner != myself or i < limit)]
Здравствуйте, Roman Odaisky, Вы писали:
RO>Здравствуйте, samius, Вы писали:
S>>Побочные эффекты здесь не только в условии. processItem — тоже не без них, судя по всему.
RO>processItem — это нечто вроде canvas.drawShape. Но никто не мешает переписать чисто функционально наподобие foldl getCanvasCombinedWithShape originalCanvas itemsSubset. Как только сформировать этот itemsSubset?
я считаю что правильно формировать с помощью свертки, где состоянием пойдет счетчик таких что (owner != myself), и последующей фильтрации.
Если последовательность/язык не ленивы, то накапливать нужные элементы прямо в передаваемом состоянии свертки.
Здравствуйте, Roman Odaisky, Вы писали:
RO>limit должен распространяться только на свои элементы.
В общем, я тупо взял Ваш алгоритм и засунул счётчик внутрь него. Ничего в работе алгоритма не изменилось. Если у меня работает неверно, то значит в Вашем исходном алгоритме ошибка
Кстати, вывод и прорисовка кругов — это побочный эффект.
Здравствуйте, Kogrom, Вы писали:
RO>>limit должен распространяться только на свои элементы. K>В общем, я тупо взял Ваш алгоритм и засунул счётчик внутрь него. Ничего в работе алгоритма не изменилось.
Ленивость оператора «||» приводит к увеличению счетчика только при обработке своих (owner == myself) элементов, в отличие от enumerate, который возвращает увеличенное значение для всех объектов.
K>Кстати, вывод и прорисовка кругов — это побочный эффект.
Если рассматривать обсуждаемый кусок как функцию (canvas, items) → canvas, то можно считать, что побочных эффектов нет, см. сообщение выше
Здравствуйте, Roman Odaisky, Вы писали:
RO>Ленивость оператора «||» приводит к увеличению счетчика только при обработке своих (owner == myself) элементов, в отличие от enumerate, который возвращает увеличенное значение для всех объектов.
Да, я ошибался. На C++ можно использовать такое жульничество:
функция Merge переписывает переданные в нее выражения для последовательной обработки коллекции в один проход
например,
— (.Where(condition-A), .Where(condition-B)) заменяется на .Where(condition-A || condition-B)
— .Where(condition-A).Where(condition-B) заменяется на .Where(condition-A && condition-B)
— .Take(limit) при необходимости заменяется на Where(wave:i=0, {i < limit; i = i + 1})
и т.д.
пометка wave перед переменной означает, что состояние данной переменной разделяется между итерациями ("движется" вместе с курсором текущего элемента)
Foreach умеет комбинироваться вместе с Merge ("пожирая" его)
в итоге, всё это транслируется в
items.Foreach(wave:i = 0)
item =>
if (item.owner != myself || item.owner == myself && i < limit)
processItem(item);
if (item.owner != myself)
i = i + 1;
Вариант, который первым пришел в голову (пишу на своем языке, динамика, но скорее всего можно и типизировать):
let comb p max n e | p e = Some (comb p max n)
| n < max = Some (comb p max (n+1))
| else = None
let filter _ [] = []
filter comb (x::xs) | res is Some comb = x :: filter comb xs
| else = []
where res = comb x
filter ((==0) << (%2) `comb` 10 <| 0) [1..100]
Первоначальное условие для наглядности немного поменял на — берем из списка четные числа или n нечетных.
Проверить код можно здесь: http://elalang.net/elac.aspx
Принцип простой — строим такой предикат, который возвращает результат (в виде варианта) и самого себя — состояние при этом протаскивается через стек. Однако использовать стандартный фильтр с таким предикатом нельзя — тот ждет, что предикат будет возвращать булевое. Поэтому приходится писать еще и свой фильтр, который декомпозирует предикат. Проще говоря на каждой итерации приходится создавать новый предикат.
Этого можно избежать, если написать другую функцию фильтр, в которую захардкодить все проверки и также тащить состояние через стек. Фактически ваш код просто переписывается в рекурсивную функцию — это достаточно просто. Но от ФП тут фактически уже вообще ничего не остается. В общем, не знаю, надо еще подумать
Здравствуйте, samius, Вы писали:
RO>>processItem — это нечто вроде canvas.drawShape. Но никто не мешает переписать чисто функционально наподобие foldl getCanvasCombinedWithShape originalCanvas itemsSubset. Как только сформировать этот itemsSubset?
S>я считаю что правильно формировать с помощью свертки, где состоянием пойдет счетчик таких что (owner != myself), и последующей фильтрации.
S>Если последовательность/язык не ленивы, то накапливать нужные элементы прямо в передаваемом состоянии свертки.
Пришлось ввести функцию гибрид свертки и проекции, которая каждый элемент проецирует в кортеж где на первом месте элемент, а на втором — соответствующее состояние от свертки.
// F#
type MyType = { owner : obj }
// foldm : ('State -> 'a -> 'State) -> 'State -> seq<'a> -> seq<'a * 'State>
let rec foldm (f: 'State -> 'a -> 'State) s xs =
if Seq.isEmpty xs
then Seq.empty
else let h = Seq.head xs
seq { yield (h, f s h)
yield! foldm f s (Seq.skip 1 xs) }
let limit = 10
let filter self =
foldm (fun i x -> if (x.owner = self) then 0 else i+1) 0
>> Seq.takeWhile (fun (_, s) -> s < limit)
>> Seq.map fst
let comb p max n e | p e = Some (comb p max n)
| n < max = Some (comb p max (n+1))
| else = None
let filter _ [] = []
filter comb (x::xs) | res is Some comb = x :: filter comb xs
| else = filter comb xswhere res = comb x
filter ((==0) << (%2) `comb` 10 <| 0) [1..100]
open core
let comb p max n e | p e = Some (comb p max n)
| n < max = Some (comb p max (n+1))
| else = None
let apply f c e | c e is Some c' = f e $ c'
| else = c
let _ = foldl (apply id) ((==0) << (%2) `comb` 5 <| 0) [1..100]
Через обычный foldl. В качестве "действия" задан просто id.
Здравствуйте, Roman Odaisky, Вы писали:
RO>Задание: последовательно обработать объекты, каждый из которых принадлежит определенному юзеру, но своих обработать только первые столько-то (потому что сам юзер мог задействовать undo). В C-подобном синтаксисе получается элементарно: RO>
RO>Но известно, что побочные эффекты — зло. Если потребуется заменить условие на чуть более сложное, есть опасность наделать ошибок. Как выглядит эквивалентный, но идеологически правильный код на вашем любимом языке?
Один вариант — foldl, в котором в аккумулятор загоняем наш счетчик. Минус — придется пройти по всему списку, даже если счетчик перевалил
Process = fun(Element, Acc) ->
if
not Element#item.own andalso Limit >= Acc ->
process_element(Element),
Acc
true ->
Acc + 1
end
end,
lists:foldl(Process, 0, Items).
Второй вариант — передавать счетчик в вызываемую функцию. Предполагаю, что объекты — record'ы, из которых можно вытянуть информацию о «нашести»:
process_items(Items) ->
Limit = get_limit(), % получаем откуда-то предел
do_process_items(Items, Limit, 0).
%% Описание function clauses:
%% 1. элементы закончились. Завершаем
%% 2. количество наших элементов больше лимита. Завершаем
%% 3. следующий элемент в списке наш, мы его пропускаем, увеличиваем счетчик на один
%% вызываем себя же рекурсивно с остатком списка
%% 4. айтем не наш, мы его процессим
вызываем сами себя с остатком списка
do_process_items([], _, _) ->
ok;
do_process_items(_, Limit, Acc) when Acc >= Limit ->
ok;
do_process_items([#item{own=Own} | Rest], Limit, Acc) when Own =:= true ->
do_process_items(Rest, Limit, Acc + 1);
do_process_items([#item{} = Element | Rest], Limit, Acc) ->
do_something(Element),
do_process_items(Rest, Limit, Acc);
Здравствуйте, Roman Odaisky, Вы писали:
RO>Но известно, что побочные эффекты — зло. Если потребуется заменить условие на чуть более сложное, есть опасность наделать ошибок. Как выглядит эквивалентный, но идеологически правильный код на вашем любимом языке?
Haskell
algo2 myself limit processItem xs = evalStateT (mapM_ f xs) 0
where f x = do cOwnSeen <- get
when (owner x /= myself || cOwnSeen < limit) (lift $ processItem x)
when (owner x == myself) $ modify (+1)
Здравствуйте, Roman Odaisky, Вы писали:
RO>Но известно, что побочные эффекты — зло. Если потребуется заменить условие на чуть более сложное, есть опасность наделать ошибок. Как выглядит эквивалентный, но идеологически правильный код на вашем любимом языке?
Убрать изменяемую переменную то несложно, вот пример на D
Здравствуйте, Kogrom, Вы писали:
K>Но красоты тут нет, конечно.
Посмотрел как "функциональщики" решают. Рекурсия безусловно рулит. Но я продолжу версию с циклом:
for counter, item in zip([[limit]]*len(items), items):
if item.owner != myself or counter[0]:
processItem(item)
if item.owner == myself:
counter[0] -= 1
Здравствуйте, FR, Вы писали:
FR>Здравствуйте, Буравчик, Вы писали:
FR>Хаскел лучший императивный язык?
Задача императивная, потому и код такой.
Как минимум устранен хак, который может привести к ошибке (это я про использование порядка вычисления элементов в операции ИЛИ).
Здравствуйте, Буравчик, Вы писали:
Б>Задача императивная, потому и код такой. Б>Как минимум устранен хак, который может привести к ошибке (это я про использование порядка вычисления элементов в операции ИЛИ).
А почему вы считаете, что задача императивная? Если, к примеру, processItems представить просто как проекцию.
Неужели такую простую задачу декларативно уже нормально не решить?
Здравствуйте, Воронков Василий, Вы писали:
ВВ>А почему вы считаете, что задача императивная? Если, к примеру, processItems представить просто как проекцию. ВВ>Неужели такую простую задачу декларативно уже нормально не решить?
Можно решить и начальную задачу. Вот, например, более "функциональное" решение.
Здесь каждый элемент заворачивается в Maybe — в Just, если его надо обрабатывать или Nothing, если нет.
algo3 myself limit processItem xs = mapM_ processItem $ catMaybes $ snd $ mapAccumL f 0 xs
where f cOwnSeen x | owner x /= myself = (cOwnSeen, Just x)
| cOwnSeen < limit = (cOwnSeen+1, Just x)
| otherwise = (cOwnSeen, Nothing)
Функция mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y]) есть в Data.List. Позволяет пробегать по массиву с аккумулятором.
Вопрос в том, какой вариант будет более понятен и что легче отлаживать.
Здравствуйте, Roman Odaisky, Вы писали:
RO>Задание: последовательно обработать объекты, каждый из которых принадлежит определенному юзеру, но своих обработать только первые столько-то (потому что сам юзер мог задействовать undo). В C-подобном синтаксисе получается элементарно:
А почему бы не добавить что-то типа getObjects() к классу юзера ? Тогда от юзера получаем его объекты и их обрабатываем.
Здравствуйте, Буравчик, Вы писали:
Б>Вопрос в том, какой вариант будет более понятен и что легче отлаживать.
Смотря для кого понятен. Императивщику, возможно, будет понятнее всего изначальный вариант, просто переписанный в явно рекурсивную функцию-всемогутер. Типа такого:
let map _ _ _ _ [] = []
map p f max n (x::xs) =
if p x then
x :: map p f max n xs
else if n < max then
x :: map p f max (n+1) xs
else
map p f max n xs
Но это бессмысленно, как, собственно, и любое подобное решение, включая монады. При таком подходе и первоначальное продемонстрированное решение уже достаточно неплохое. И на самом деле проблем-то с чистотой там нет — если рассматривать cOwnSeen как локальное имя, на которое существует единственная ссылка — это по сути же никакая не грязь и бояться тут нечего.
Здравствуйте, _Obelisk_, Вы писали:
_O_>А почему бы не добавить что-то типа getObjects() к классу юзера ? Тогда от юзера получаем его объекты и их обрабатываем.
Порядок нарушится, надо чередовать объекты разных юзеров, в той последовательности, в которой они хранятся.
Здравствуйте, Воронков Василий, Вы писали:
ВВ>При таком подходе и первоначальное продемонстрированное решение уже достаточно неплохое. И на самом деле проблем-то с чистотой там нет — если рассматривать cOwnSeen как локальное имя, на которое существует единственная ссылка — это по сути же никакая не грязь и бояться тут нечего.
Оно плохое не из-за наличия дополнительного элемента изменяемого состояния, а из-за ошибкоопасности в условии. Вдруг понадобится добавить дополнительное требование item.isGood(), куда его сунуть, чтобы не нарушить основанный на ленивости подсчет cOwnSeen? (Оно-то ясно, куда, но ошибиться слишком легко.)
Здравствуйте, Roman Odaisky, Вы писали:
RO>Оно плохое не из-за наличия дополнительного элемента изменяемого состояния, а из-за ошибкоопасности в условии. Вдруг понадобится добавить дополнительное требование item.isGood(), куда его сунуть, чтобы не нарушить основанный на ленивости подсчет cOwnSeen? (Оно-то ясно, куда, но ошибиться слишком легко.)
Ну вы можете просто явно расписать if — ну примерно, как у меня в предыдущем примере.
Здравствуйте, AndrewVK, Вы писали:
AVK> .Select(i => new {Item = i, Cnt = i.Owner == myself ? ownCnt++ : 0})
А чем это лучше по сравнению с изначальным i.Owner != myself || ownCnt++ < limit? Ты тоже пользуешься ленивостью, возникает та же опасность при возможном изменении условия в будущем.
Здравствуйте, AndrewVK, Вы писали:
ВВ>> Двойной проход AVK>Какой такой двойной проход?
Ну предполагается, что получив отобранную таким образом коллекцию, мы с ней что-то сделаем, т.е. совершим проход по ней, так ведь? Первоначальный код делает все в один проход. В твоем варианте я *предполагаю*, что ты просто забыл дописать в конце ToList. Вот тебе и двойной проход. А ToList там, мягко говоря, нужен.
И, кстати, его необходимость как раз и выдает главную проблему этого кода. Автору топика поведение (||) кажется неочевидным. Ты вместо этого заменил все на non-strict последовательность (без мемоизации!), причем с побочным эффектом.
Здравствуйте, Воронков Василий, Вы писали:
ВВ>Ну предполагается, что получив отобранную таким образом коллекцию, мы с ней что-то сделаем, т.е. совершим проход по ней, так ведь?
Ну да. Ровно один проход.
ВВ> Первоначальный код делает все в один проход. В твоем варианте я *предполагаю*, что ты просто забыл дописать в конце ToList.
Ну ты крут. Ничего я не забывал, и никаких двойных проходов нет.
ВВ> Вот тебе и двойной проход. А ToList там, мягко говоря, нужен
Зачем? Следующей строкой будет foreach. А если ты собрался результат отдавать наружу, то надо просто вынести каунтер в специальное хранилище, чтобы интерференции не было.
... << RSDN@Home 1.2.0 alpha 5 rev. 52 on Windows 7 6.1.7601.65536>>
Здравствуйте, AndrewVK, Вы писали:
ВВ>>Ну предполагается, что получив отобранную таким образом коллекцию, мы с ней что-то сделаем, т.е. совершим проход по ней, так ведь? AVK>Ну да. Ровно один проход. ВВ>> Первоначальный код делает все в один проход. В твоем варианте я *предполагаю*, что ты просто забыл дописать в конце ToList. AVK>Ну ты крут. Ничего я не забывал, и никаких двойных проходов нет.
Нет, я просто пытаюсь представить, как должен выглядеть твой код *на самом деле*. Ведь не совсем так, как ты написал, правда? Теперь вот оказывается, что надо каунтер выносить в некое "специальное хранилище" (это, кстати, что такое?). При этом вообще-то проблемой первоначального варианта называлась неочевидность.
В общем можно полный код в студию. Со специальными хранилищами и всем прочим. У меня вот все же есть определенное подозрение, что твой вариант все же содержит некоторое количество изврата.
Здравствуйте, AndrewVK, Вы писали:
AVK>Нет никакого полного кода. Идею я продемонстрировал, а флудить тебе придется без меня.
Ну ради бога. Мне вот просто показалось немного странным, что в ветке, где все пытаются привести чистое, без побочных эффектов и прочего вселенского зла решение, ты показал отличный способ, каким локальную мутируемую переменную (что само по себе не проблема) неявным образом превратить в нелокальную мутируемую переменную (что уже очень даже проблема). А в конце поставил жирное троеточие.
Я лишь просто поинтересовался у тебя, в чем, собственно, заключалась продемонстрированная идея. А теперь, оказывается, у меня с восприятием проблемы. Ну пусть так
Знаю, что говорю сам с собой, но из любви к искусству ещё вариант с циклом:
def gen(items, myself, limit):
for item in items:
if item.owner != myself:
yield item
elif limit:
limit -= 1
yield item
else:
return
for item in gen(items, myself, limit): processItem(item)
Внешнего счётчика нет, функция (processItem), которая теоретически может дать побочный эффект, не находится в дебрях кода. Поэтому её в тестах легко можно заменить на подделку.
Здравствуйте, Roman Odaisky, Вы писали:
RO>А вот и нет. Проблема осталась: при добавлении условий легко напутать и поставить их не туда, сбив подсчет limit. Ну и ветка else лишняя.
else не лишний — без него цикл пройдёт по всему списку. Проблема с условиями сомнительна: почему её не будет при других подходах?
RO>Интересно подобрать декларативный подход, не императивный.
Здравствуйте, Kogrom, Вы писали:
RO>>А вот и нет. Проблема осталась: при добавлении условий легко напутать и поставить их не туда, сбив подсчет limit. Ну и ветка else лишняя. K>else не лишний — без него цикл пройдёт по всему списку. Проблема с условиями сомнительна: почему её не будет при других подходах?
Так он и должен идти по всему списку. Задача же обработать все элементы, у которых owner != myself (плюс доп. условие). У вас же получается, что как только лимит кончается и попадается первый элемент, у которого owner == myself, то вы выходите из функции. А это неправильно, т.к. в списке могут быть еще элементы, удовлетворяющие первому условию.
RO>>Интересно подобрать декларативный подход, не императивный. K>С этого надо было начинать.
Я так понимаю, идея ТС в том, чтобы максимально упростить добавление новых условий. Здесь декларативный подход напрашивается сам собой — чтобы была возможность эти условия просто комбинировать. Вариант, который я привел, тоже, конечно, не идеален:
open core
let apply f c e | c e is Some c' = f e $ c'
| else = c
let comb p max n e | p e = Some (comb p max n)
| n < max = Some (comb p max (n+1))
| else = None
let process act comb xs = foldl (apply act) comb xs
process processItem (comb even 5 0) [1..100]
Однако в нем по крайней мере можно ввести дополнительный комбинатор и делать так:
let orelse p a e | p e = a e | else = ()
process (processItem |> orelse (<25) |> orelse (>5) ) (comb even 5 0) [1..100]
Здравствуйте, Воронков Василий, Вы писали:
ВВ>Так он и должен идти по всему списку.
Да, действительно. Значит код будет ещё короче. Тогда условия не влияют друг на друга и нет проблемы в добавлении новых (elif limit легко можно поменять на if item.owner == myself and limit).
Но, конечно, если бы я не ошибся в понимании условия задачи, и не требовалось проходить по всему списку, то порядок условий имел бы значение.
Ладно, извините, что я вторгся в клуб любителей ФП со своими циклами и за мою пониженную внимательность.