Цикл ForEach в Mathematica

Я хотел бы что-то вроде этого:

each[i_, {1,2,3},
  Print[i]
]

Или, в более общем смысле, чтобы деструктурировать произвольные вещи в списке, который вы зацикливаете, например:

each[{i_, j_}, {{1,10}, {2,20}, {3,30}},
  Print[i*j]
]

Обычно вы хотите использовать Map или другие чисто функциональные конструкции и избегать нефункционального стиля программирования, в котором вы используете побочные эффекты. Но вот пример, где я думаю, что конструкция for-each в высшей степени полезна:

Скажем, у меня есть список опций (правил), которые соединяют символы с выражениями, например

attrVals = {a -> 7, b -> 8, c -> 9}

Теперь я хочу создать хеш-таблицу, где я делаю очевидное сопоставление этих символов с этими числами. Я не думаю, что есть более чистый способ сделать это, чем

each[a_ -> v_, attrVals, h[a] = v]

Дополнительные тестовые случаи

В этом примере мы преобразуем список переменных:

a = 1;
b = 2;
c = 3;
each[i_, {a,b,c}, i = f[i]]

После вышеизложенного {a,b,c} должно оцениваться как {f[1],f[2],f[3]}. Обратите внимание, что это означает, что второй аргумент each должен оставаться невычисленным, если это список.

Если невычисленная форма не является списком, она должна оценивать второй аргумент. Например:

each[i_, Rest[{a,b,c}], Print[i]]

Это должно напечатать значения b и c.

Дополнение: для правильной работы for-each он должен поддерживать Break[] и Continue[]. Я не уверен, как это реализовать. Возможно, это нужно будет каким-то образом реализовать в терминах For, While или Do, поскольку это единственные конструкции цикла, которые поддерживают Break[] и Continue[].

И еще одна проблема с ответами: они едят Return[]s. То есть, если вы используете цикл ForEach в функции и хотите вернуться из функции из цикла, вы не можете этого сделать. Выдача возврата внутри цикла ForEach, похоже, работает как Continue[]. Это просто (подождите) сбило меня с толку.


person Community    schedule 01.10.2008    source источник


Ответы (7)


Я опоздал на вечеринку на несколько лет, и это, возможно, скорее ответ на «мета-вопрос», но многие люди поначалу испытывают затруднения, когда программирование на Mathematica (или других функциональных языках) приближается к проблеме из функциональной, а не структурной точки зрения. Язык Mathematica имеет структурные конструкции, но по своей сути он функционален.

Рассмотрим ваш первый пример:

ForEach[i_, {1,2,3},
  Print[i]
]

Как отметили несколько человек, функционально это может быть выражено как Scan[Print, {1,2,3}] или Print /@ {1,2,3} (хотя по возможности следует отдавать предпочтение Scan, а не Map, как объяснялось ранее, но иногда это может раздражать, поскольку для Scan нет инфиксного оператора).

В Mathematica обычно есть дюжина способов сделать все, что иногда красиво, а иногда разочаровывает. Имея это в виду, рассмотрим второй пример:

ForEach[{i_, j_}, {{1,10}, {2,20}, {3,30}},
  Print[i*j]
]

... что более интересно с функциональной точки зрения.

Одним из возможных функциональных решений является использование замены списка, например:

In[1]:= {{1,10},{2,20},{3,30}}/.{i_,j_}:>i*j
Out[1]= {10,40,90}

... но если бы список был очень большим, это было бы излишне медленным, поскольку мы делаем так называемое «сопоставление с образцом» (например, ищем экземпляры {a, b} в списке и присваиваем их i и j) без необходимости.

Учитывая большой массив из 100 000 пар, array = RandomInteger[{1, 100}, {10^6, 2}], мы можем посмотреть на некоторые тайминги:

Замена правила выполняется довольно быстро:

In[3]:= First[Timing[array /. {i_, j_} :> i*j;]]
Out[3]= 1.13844

... но мы можем сделать немного лучше, если воспользуемся структурой выражения, в которой каждая пара действительно List[i,j], и применим Times в качестве заголовка каждой пары, превратив каждое {i,j} в Times[i,j]:

In[4]:= (* f@@@list is the infix operator form of Apply[f, list, 1] *)
    First[Timing[Times @@@ array;]]
Out[4]= 0.861267

Как используется в реализации ForEach[...] выше, Cases явно неоптимален:

In[5]:= First[Timing[Cases[array, {i_, j_} :> i*j];]]
Out[5]= 2.40212

... так как Cases выполняет больше работы, чем просто замена правила, необходимо построить вывод совпадающих элементов один за другим. Оказывается, мы можем сделать намного лучше, разложив задачу по-другому и воспользовавшись тем фактом, что Times равно Listable и поддерживает векторизованную операцию.

Атрибут Listable означает, что функция f будет автоматически обрабатывать любые аргументы списка:

In[16]:= SetAttributes[f,Listable]
In[17]:= f[{1,2,3},{4,5,6}]
Out[17]= {f[1,4],f[2,5],f[3,6]}

Итак, поскольку Times равно Listable, если бы вместо этого у нас были пары чисел в виде двух отдельных массивов:

In[6]:= a1 = RandomInteger[{1, 100}, 10^6];
        a2 = RandomInteger[{1, 100}, 10^6];

In[7]:= First[Timing[a1*a2;]]
Out[7]= 0.012661

Вау, немного быстрее! Даже если ввод не был предоставлен как два отдельных массива (или у вас более двух элементов в каждой паре), мы все равно можем сделать что-то оптимальное:

In[8]:= First[Timing[Times@@Transpose[array];]]
Out[8]= 0.020391

Мораль этой эпопеи не в том, что ForEach не является ценной конструкцией вообще или даже в Mathematica, а в том, что вы часто можете получить те же результаты более эффективно и изящно, если работаете с функциональным, а не структурным мышлением. .

person Community    schedule 05.03.2010
comment
Красиво сказано. Спасибо за этот вклад. Примером того, где я считаю ForEach полезным, является создание хеш-таблицы. Скажем, у меня есть список структур данных; тогда я мог бы пройтись по ним, скажем, ForEach[{i_, {j_, k_}}, listofstuff, hash[f[i,k] = g[j,k]]. - person dreeves; 07.03.2010

Более новые версии Mathematica (6.0+) имеют обобщенные версии Do[] и Table[], которые почти точно делают то, что вам нужно, используя альтернативную форму аргумента итератора. Например,

Do[
  Print[i],
  {i, {1, 2, 3}}]

точно такой же, как твой

ForEach[i_, {1, 2, 3,},
  Print[i]]

В качестве альтернативы, если вам действительно нравится конкретный синтаксис ForEach, вы можете создать функцию HoldAll, которая его реализует, например:

Attributes[ForEach] = {HoldAll};

ForEach[var_Symbol, list_, expr_] :=
  ReleaseHold[
    Hold[
      Scan[
        Block[{var = #},
         expr] &,
      list]]];

ForEach[vars : {__Symbol}, list_, expr_] :=
  ReleaseHold[
    Hold[
      Scan[
        Block[vars,
          vars = #;
          expr] &,
      list]]];

Здесь в качестве имен переменных используются символы, а не шаблоны, но именно так работают различные встроенные управляющие структуры, такие как Do[] и For[].

Функции HoldAll[] позволяют создавать довольно широкий спектр пользовательских структур управления. ReleaseHold[Hold[...]] — обычно самый простой способ собрать кучу кода Mathematica для последующего вычисления, а Block[{x = #}, ...]& позволяет привязать переменные в теле выражения к любые значения, которые вы хотите.

В ответ на вопрос dreeves ниже вы можете изменить этот подход, чтобы разрешить более произвольную деструктуризацию с использованием DownValues ​​​​уникального символа.

ForEach[patt_, list_, expr_] := 
  ReleaseHold[Hold[
     Module[{f}, 
       f[patt] := expr; 
       Scan[f, list]]]]

На данный момент, однако, я думаю, вам лучше создать что-то поверх кейсов.

ForEach[patt_, list_, expr_] :=
  With[{bound = list},
    ReleaseHold[Hold[
       Cases[bound,
         patt :> expr]; 
       Null]]]

Мне нравится делать Null явным, когда я подавляю возвращаемое значение функции. EDIT: я исправил ошибку, указанную ниже; Мне всегда нравится использовать With для интерполяции оцененных выражений в Hold* формы.

person Community    schedule 11.08.2009
comment
Я считаю, что это не делает деструктурирование, хотя. То есть вы не можете перебирать структуры данных, когда вы даете частям структур данных их собственные имена, как в моем примере перебора пар {i,j}. Я нахожу это супер полезным. - person dreeves; 12.08.2009
comment
Есть ли какое-то преимущество в вашем методе Hold/ReleaseHold по сравнению с моей версией, использующей Evaluate? Я думаю, в любом случае вам нужно установить атрибут HoldAll? По сути, это версия макросов Lisp для Mathematica. Было бы неплохо иметь более общий/канонический способ сделать это. Может быть, это то, что вы дали? - person dreeves; 12.08.2009
comment
Второе правило выполняет деструктуризацию, потому что Set[] деструктурирует автоматически: {i, j} = {3, 4} установит i в 3, а j в 4. Я обычно предпочитаю подход Hold/ReleaseHold, когда это возможно, особенно если я могу напрямую подставлять вещи в качестве параметров функции, потому что это избавляет меня от необходимости думать о том, что удерживается, а что нет. И да, это в основном несколько неуклюжий способ выполнения макросов Лиспа. - person Pillsy; 12.08.2009
comment
Спасибо, это поучительная дискуссия! Я полагаю, что моя версия еще более универсальна в том смысле, что она может деструктурировать произвольные вложенные структуры. Можно ли пропатчить вашу версию для этого? - person dreeves; 26.08.2009
comment
Да, я отредактировал свой ответ, чтобы разрешить произвольную деструктуризацию на основе шаблонов. Однако чем больше я об этом думаю, тем больше мне кажется, что Кейсы — это именно то, что вам нужно. - person Pillsy; 26.08.2009
comment
Вау, я только что провел тест скорости, и ваша версия с удержанием и делами кажется более чем в 4 раза быстрее, чем моя версия со сканированием/заменой/оценкой. Большое спасибо! Я делаю ваш принятый ответ сейчас. - person dreeves; 03.03.2010
comment
К сожалению, я нашел ошибку в вашей версии Hold/Cases. Если у меня есть заранее определенный список, который я хочу перебрать, то из-за удержаний он не работает. Например, nums = {1,23}; ForEach[i_, nums, Print[i]]; я думаю, вам просто нужно сделать Evaluate[list] как я сделал в моей версии. Хотите обновить свой ответ? - person dreeves; 05.03.2010
comment
@Pillsy: возможно, я сделал неверный вывод. У меня возникли проблемы с воспроизведением предполагаемой ошибки. Знаете ли вы случай, который терпит неудачу без только что добавленного вами блока With? - person dreeves; 05.03.2010
comment
@Pillsy: теперь я считаю, что предполагаемая ошибка была ложной тревогой. Я изменил LHS определения ForEach, чтобы сказать ForEach[pat_, lst_List, bod_], что вы не можете сделать для функции HoldAll. Например, SetAttributes[f, HoldAll]; f[x_List]:=0; abc={1,2,3}; f[abc] не оценивает. - person dreeves; 06.03.2010

Встроенная функция Scan в основном делает это, хотя и уродливее:

    Scan[Print[#]&, {1,2,3}]

Это особенно уродливо, когда вы хотите деструктурировать элементы:

    Scan[Print[#[[1]] * #[[2]]]&, {{1,10}, {2,20}, {3,30}}]

Следующая функция позволяет избежать уродства, преобразовывая pattern в body для каждого элемента list.

SetAttributes[ForEach, HoldAll];
ForEach[pat_, lst_, bod_] :=  Scan[Replace[#, pat:>bod]&, Evaluate@lst]

который можно использовать, как в примере в вопросе.

PS: Принятый ответ побудил меня переключиться на это, что я использую с тех пор, и, похоже, он отлично работает (за исключением оговорки, которую я добавил к вопросу):

SetAttributes[ForEach, HoldAll];             (* ForEach[pattern, list, body]   *)
ForEach[pat_, lst_, bod_] := ReleaseHold[    (*  converts pattern to body for  *)
  Hold[Cases[Evaluate@lst, pat:>bod];]];     (*   each element of list.        *)
person Community    schedule 01.10.2008
comment
Даниил, Evaluate здесь не нужен, как и пара ReleaseHold/Hold. Извините за повторение, но похоже, что вы не большой поклонник лишнего набора текста. Evaluate не требуется, так как Cases не содержит аргументов, а HoldAll for ForEach гарантирует только хранение аргументов внутри ForEach. Cases инициирует новый цикл оценки, на который HoldAll не влияет. ReleaseHold@Hold[] в общем случае не имеет значения (как я уже отмечал в другом месте), и здесь в частности, поскольку SetDelayed не оценивает свою r.h.s. На самом деле, вы можете захотеть наоборот: оберните свой список в Unevaluated - для меня это имеет больше смысла. - person Leonid Shifrin; 14.01.2011
comment
@Leonid: Это звучит правильно, хотя я чувствую, что была какая-то причина - возможно, тонкая ошибка, - по которой я в конечном итоге добавил дополнительную оценку и удержание и еще много чего. Хотите добавить свою упрощенную версию в качестве ответа, и я проверю ее и отмечу, что она принята, если я смогу убедить себя, что она лучше? Еще раз спасибо за помощь в этом! - person dreeves; 14.01.2011
comment
Конечно, почему бы и нет. Я гораздо меньше озабочен тем, чтобы заработать за это признание, чем убедиться, что неправильные представления не распространятся, поэтому я был бы просто счастлив, если бы (как только вы убедитесь, что это правильно) вы могли бы сделать это более заметным для других, посещающих эту страницу. , любым способом, не обязательно отмечая его галочкой. Позвольте мне просто признать, что я использовал ReleaseHold[Hold[]] и Evaluate аналогичным образом в течение некоторого времени, пока я не понял оценку Mathematica намного лучше и не понял, что они не нужны (по крайней мере, для таких случаев использования). - person Leonid Shifrin; 14.01.2011

Встроенная функция Map делает именно то, что вам нужно. Его можно использовать в длинной форме:

Карта[Печать, {1,2,3}]

или сокращенно

Печать / @ {1,2,3}

Во втором случае вы должны использовать "Print[Times@@#]&/@{{1,10}, {2,20}, {3,30}}"

Я бы порекомендовал прочитать справку Mathematica по Map, MapThread, Apply и Function. К ним можно немного привыкнуть, но как только вы это сделаете, вы никогда не захотите вернуться!

person Community    schedule 23.11.2008
comment
Спасибо! Карта действительно почти всегда то, что вам нужно для таких вещей. Scan на самом деле идентичен Map, за исключением того, что он используется исключительно для побочных эффектов — он не возвращает список. - person dreeves; 23.11.2008

Вот небольшое улучшение, основанное на последнем ответе dreeves, которое позволяет указать шаблон без пробела (что делает синтаксис похожим на другие функции, такие как Table или Do) и использует аргумент уровня Cases

SetAttributes[ForEach,HoldAll];
ForEach[patt_/; FreeQ[patt, Pattern],list_,expr_,level_:1] :=
   Module[{pattWithBlanks,pattern},
      pattWithBlanks = patt/.(x_Symbol/;!MemberQ[{"System`"},Context[x]] :> pattern[x,Blank[]]);
      pattWithBlanks = pattWithBlanks/.pattern->Pattern;

      Cases[Unevaluated@list, pattWithBlanks :> expr, {level}];
      Null
   ];

Тесты:

ForEach[{i, j}, {{1, 10}, {2, 20}, {3, 30}}, Print[i*j]]
ForEach[i, {{1, 10}, {2, 20}, {3, 30}}, Print[i], 2]
person Community    schedule 31.08.2011

В Mathematica есть функции отображения, поэтому, допустим, у вас есть функция Funcпринимающая один аргумент. Тогда просто напиши

Func /@ list

Print /@ {1, 2, 3, 4, 5}

Возвращаемое значение представляет собой список функций, примененных к каждому элементу в списке.

PrimeQ /@ {10, 2, 123, 555}

вернется {False,True,False,False}

person Community    schedule 24.08.2009
comment
Спасибо, да, карта (/@) обычно то, что вы хотите. Смотрите мой комментарий к ответу, опубликованному 23 ноября. - person dreeves; 26.08.2009

Благодаря Pillsy и Леонид Шифрин, вот что я сейчас использую:

SetAttributes[each, HoldAll];               (* each[pattern, list, body]      *)
each[pat_, lst_List, bod_] :=               (*  converts pattern to body for  *)
  (Cases[Unevaluated@lst, pat:>bod]; Null); (*   each element of list.        *)
each[p_, l_, b_] := (Cases[l, p:>b]; Null); (* (Break/Continue not supported) *)
person Community    schedule 15.01.2011
comment
Дэниел, поскольку каждый из них — это HoldAll, использование таких шаблонов, как lst_List, подведет вас, если список хранится в переменной или является результатом вычисления какой-либо другой функции, например: a = {b,c,d}; {b,c,d} = {1,2,3}; каждый[x_,a,x=1]. На самом деле, в этом случае ваш первый деф не будет совпадать, а второй деф вас подведет. Причина, по которой сопоставление с образцом мешает атрибутам Hold, я обсуждал здесь: mathprogramming-intro. org/book/node408.html. Я бы просто придерживался SetAttributes[each, HoldAll]; each[p_, l_, b_] := (Cases[Unevaluated[l], p :› b];); , и нет Null в конце - person Leonid Shifrin; 17.01.2011
comment
@Leonid: Я думал, что просто не ожидал, что элементы в списке останутся неоцененными, если я не передам явный список. В вашем примере меня не подводят, как вы говорите, потому что, передавая a, я действительно передаю {1,2,3}, поэтому имеет смысл, что он не может установить эти элементы. - person dreeves; 17.01.2011
comment
Ты прав. На этот раз я поторопился с комментариями и не совсем понял семантику вашей функции. Кроме того, в моей версии это не сработало бы так, как хотелось бы, так как очень сложно частично оценить, так что a оценивается в {b,c,d}, но они не оцениваются дальше, и даже это было бы неоднозначно. Я могу удалить свой комментарий. Что лучше всего делать с такими неправильными комментариями на SO - оставить их или удалить? - person Leonid Shifrin; 17.01.2011
comment
@Леонид: Фу! Спасибо! Что касается комментариев, если бы они были в вопросе или ответе, я бы определенно сказал удалить. Но комментарии я не уверен. Я должен добавить еще несколько пояснений к ответу, чтобы в первую очередь предотвратить такую ​​путаницу. (Или не стесняйтесь делать это, если у вас достаточно магических очков, чтобы редактировать ответы других людей!) - person dreeves; 17.01.2011