Подход к дизайну: перегрузка и коммутатор?
Что касается производительности и масштабируемости в дизайне пакетов, лучше всего:
- ... 'имена функций перегрузки (позволяя Mathematica сортировать, какую версию использовать на основе шаблонов/условий/тестов и способ определения системных заказов)?
- ... или построить одну функцию с помощью Switch [] (или аналогичной команды) для прямой оценки?
Выражение Mathematicas часто путает меня с глупыми (?) проблемами, подобными этому.
Ответы
Ответ 1
Это широкий вопрос, но я воспользуюсь этой возможностью, чтобы дать широкий ответ...
Я выступаю за то, чтобы придерживаться основной парадигмы языка программирования, а не пытаться бороться с ним или писать код, который следует за идиомами другого языка. Mathematica строится вокруг понятия сопоставления шаблонов, поэтому, IMHO, мы всегда должны сначала учитывать соответствие шаблонов, пытаясь выразить себя. Следуя этому принципу, я бы одобрил определения над Switch
.
В вопросе о производительности я все больше раздражаюсь растущим акцентом на microbenchmarks при сравнении конструкций Mathematica. Хотя полезно знать затраты, связанные с конструкциями, мы должны прислушаться к Кнуту (или это было Хоар?): "Мы должны забыть о небольшой эффективности, скажем, около 97% времени: преждевременная оптимизация - корень всего зла". "Зло" - это потеря удобочитаемости в программе, которая в интересах эффективности использует какой-то неясный или косвенный подход для достижения эффекта. Вот мой контрольный список производительности:
-
Является ли производительность проблемой? Если нет, то пропустите остальную часть контрольного списка.
-
Где узкое место в производительности? Профилировщик помогает здесь, но часто узкое место можно легко найти путем осмотра или нескольких заявлений на печать. Тогда...
-
Является ли алгоритм неэффективным? Очень часто: существует ли в два раза вложенной цикл, который может быть линеаризован или помог с помощью схемы индексирования?
-
Хорошо, алгоритм хорош, поэтому я думаю, что пришло время для микрообнаружения.
Я не знаю, не слишком ли я использую Mathematica, но большую часть времени я не делаю шаг 1. А затем №3 улавливает большинство остальных. В Mathematica я нахожу, что я, как правило, очень рад, что я могу выполнить какую-то амбициозную задачу с небольшим количеством кода - общая производительность обычно не входит в изображение.
Ух-о, мне лучше убрать мыльницу. Извините "это.
Ответ 2
За исключением очень простых случаев, я предпочитаю использовать функцию с несколькими определениями вместо Switch
. Причины этого трижды:
- Мне легче читать код, пока функция хорошо известна.
- Для случая падения/ошибки проще установить соответствующее значение по умолчанию и снова вызвать функцию.
- Если вы используете функцию, вы можете использовать именованные шаблоны при расчете результата.
Edit
Вот пример, созданный в качестве примера # 2 для Sjoerd:
createNColors[fn_, Automatic, n_] := Table[Hue[i/n], {i, n}]
createNColors[fn_, colors_List, n_] := PadRight[colors, n, colors]
createNColors[fn_, color:(Hue | RGBColor | CMYKColor | GrayLevel)[__], n_] :=
Table[color, {n}]
createNColors[fn_, color_, n_] := (
Message[fn::"color", HoldForm[color]];
createNColors[fn, Automatic, n]
)
Он может использоваться для создания набора из n цветов для некоторой опции.
Ответ 3
Чтобы ответить на часть выполнения вашего вопроса, рассмотрите следующие два примера перегрузки и использования Switch[]
switchFunc[a_] := Switch[a, _String, 5, _Integer, var, _Symbol, "string"]
overloadFunc[a_String] := 5;
overloadFunc[a_Integer] := var;
overloadFunc[a_Symbol] := "string";
Это чрезвычайно упрощено, но достаточно продемонстрировать разницу в производительности
In[1] := [email protected][switchFunc, x, 1000000]
Out[1] := {3.435, "string"}
In[2] := [email protected][overloadFunc, x, 1000000]
Out[2] := {0.754, "string"}
Однако, если вы намерены перегрузить свою функцию на основе условных тестов, производительность хуже, чем Switch[]
:
switchFunc2[a_] := Switch[a < 5, True, 6, False, 4];
overloadFunc2[a_ /; a < 5] := 6;
overloadFunc2[a_ /; a > 5] := 4;
overloadFunc2[a_] := a;
In[3] := [email protected][switchFunc2, 4, 1000000]
Out[3] := {2.63146, 4}
In[4] := [email protected][overloadFunc2, 6, 1000000]
Out[4] := {4.349, 6}
EDIT: тайминги в этом ответе были сделаны с использованием Mathematica 8.0.1 на OS X 10.7.2. См. Ответ Mr.Wizard для получения некоторых дополнительных результатов, когда вышеуказанный порядок отменен. Тем не менее, я думаю, что это плохая идея, чтобы сделать логические проверки шаблонов аргументов функции.
С точки зрения дизайна, мой личный опыт заключается в том, что Switch[]
и это похоже ужасно, потому что их трудно читать и замедлять. Тем не менее, я также считаю, что использование одной и той же функции по-разному в зависимости от типа аргумента, как правило, плохо проектируется и делает следующий код намного сложнее (хотя его легче читать).
Ответ 4
Ваш вопрос довольно неопределен, как написано, и есть разные интерпретации "перегрузки", которые меняют мой ответ. Однако, если вы говорите о перегрузке своих собственных функций в отношении разных типов (головок) и шаблонов аргументов, тогда, во всяком случае, используйте тесно интегрированное сопоставление Mathematica.
Чтобы предоставить практический пример, я буду использовать это мое решение. Для справки:
f[k_, {}, c__] := If[Plus[c] == k, {{c}}, {}]
f[k_, {x_, r___}, c___] := Join @@ (f[k, {r}, c, #] & /@ Range[0, Min[x, k - Plus[c]]])
Если я переписываю f
без соответствия шаблону и вызываю его g
:
g = Function[{k, L, c},
If[L === {},
If[[email protected] == k, {c}, {}],
Join @@ (g[k, [email protected], Append[c, #]] & /@ Range[0, Min[[email protected], k - [email protected]]])
]
];
Я чувствую, что это менее понятно, и, конечно, менее удобно писать. Мне пришлось использовать явные функции Rest
и First
, и мне пришлось ввести Append
, поскольку я не могу разместить переменное количество аргументов. Это также требует использования фиктивного третьего аргумента: {}
.
Сроки показывают, что исходная форма также значительно быстрее:
f[12, {1, 5, 8, 10, 9, 9, 4, 10, 8}]; // Timing
g[12, {1, 5, 8, 10, 9, 9, 4, 10, 8}, {}]; // Timing
{0.951, Null}
{1.576, Null}
В ответ на ответ Тимо я чувствую, что полезно делиться результатами моего времени, так как они отличаются от его. (Я использую Mathematica 7 в Windows 7.) Кроме того, я считаю, что он усложнил версию DownValues вне функции версии Switch.
Во-первых, мои тайминги его функций написаны, но с использованием диапазона значений:
Array[switchFunc2, 1*^6]; // Timing
Array[overloadFunc2, 1*^6]; // Timing
{1.014, Null}
{0.749, Null}
Итак, как и написано, функция DownValues работает быстрее для меня. Но второе условие не требуется:
ClearAll[overloadFunc2]
overloadFunc2[a_ /; a < 5] := 6;
overloadFunc2[a_] := 4;
Array[overloadFunc2, 1*^6]; // Timing
{0.546, Null}
Конечно, в случае такой простой функции можно также использовать If
:
ifFunc[a_] := If[a < 5, 6, 4]
Array[ifFunc, 1*^6]; // Timing
{0.593, Null}
И если это написано как чистая функция, которую Mathematica компилирует внутри массива:
ClearAll[ifFunc]
ifFunc = If[# < 5, 6, 4] &;
Array[ifFunc, 1*^6]; // Timing
{0.031, Null}