Глава 6 из книги «Mathcad 14 для студентов,
инженеров и конструкторов
» BHV-Петербург, 2007 г.

Программирование в Mathcad и для Mathcad

6.1. Немного истории

6.2. Панель программирования

6.3. Турецкий платок

6.4. Рекурсия

6.5. Сказка о рыбаках и рыбке или Проблема метки

6.6. Размерность в Mathcad-программах

6.7. Отладка программ

6.8. Локальная функция

6.9. Программирование для Mathcad на C/C++

6.9.1. Разработка пользовательской функции на C/C++ для Mathcad

6.9.2. Использование разработанной пользовательской функции

6.9.3. Дополнительные возможности программирования на языке C для Mathcad

Если в первых трех изданиях книги автор вплоть до данной, шестой главы удержался и не вставлял в книгу программы, созданные с помощью встроенных средств программирования Mathcad, то в четвертом и пятом изданиях Mathcad-программы (операторы, созданные с использованием инструментов, показанных на рис. 6.3) появились уже в гл. 1 (см. рис. 1.40). И это имеет под собой определенное обоснование. Дело в том, что раньше программирование в среде Mathcad было некой экзотикой[1]. Теперь же это рутинный этап ведения расчетов в среде данной математической программы[2].

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

6.1. Немного истории

Средства программирования появились в 6-й версии Mathcad, а некоторые их дополнения и изменения в 7-й (операторы break, continue и return), 12-й (локальная функция — см. разд. 6.8) и в 13-й версиях (средства отладки – см. разд. 6.7).

Несмотря на то, что пакет Mathcad возник как прямая альтернатива программированию, при работе в этой среде всегда ощущалась потребность в программировании для расширения и совершенствования базового набора математических инструментов — операторов и функций. Более-менее опытные пользователи решали эту проблему тремя путями.

Путь 1. В самых первых версиях Mathcad были две функции (if и until), позволявшие через различные хитрости и трюки реализовывать две основные алгоритмические конструкции — выбор (if) и повторение (until). Хитрить же приходилось из-за неспособности функций if и until иметь в качестве аргументов несколько операторов. Поэтому для реализации даже несложного алгоритма нужно было подключать механизм вложенных функций и операторов, что нередко превращало программу в настоящую криптограмму, в которой даже сам автор разбирался с трудом.

Вот как, например, выглядит поиск корня уравнения (нуля функции) методом половинного деления (рис. 6.1) с использованием функций if и until. Читатель может сравнить "программу" на рис. 6.1 с программой (без кавычек) на рис. 2.6, реализующей практически тот же алгоритм: отрезок неопределенности [а, b] делится пополам и смотрится, где расположен корень; соответствующая половинка снова делится пополам, и так до тех пор (until — рис. 6.1 или while — рис. 2.6), пока не будет достигнута нужная точность расчета.

Рис. 6.1. Поиск корня алгебраического уравнения методом половинного деления с использованием функций if и until

Кроме "зашифрованности" алгоритма (чего стят аргументы функции until на рис. 6.1) "беспрограммный" поиск корня имеет и другой недостаток: он приводит к нерациональному использованию ресурсов компьютера — к генерации векторов a и b[3], у которых нас интересуют только последние (last) элементы, между которыми зажат искомый корень (переменная корень).

Операторы if и until позволяют менять естественный порядок выполнения операторов в Mathcad-документе сверху вниз и слева направо на более сложный. Кроме того, есть еще два признака программирования: локальные переменные, а также и функции, объединение операторов в операторные блоки. Но об этом речь пойдет чуть ниже.

Примечание

Функция until в 2000-й версии перешла в разряд недокументированной, в 11-й ее совсем изъяли из Mathcad, а в 12-й она вернулась в инструментарий программирования (см. п. 13 в Предисловии к четвертому изданию книги, где перечислены новинки Mathcad 12) – см. «Блудный сын Mathcad»

Путь 2. Версии Mathcad, начиная с 4.0, — это полноценные Windows-приложения. Поэтому при решении конкретной задачи в среде Mathcad можно в статике (через файлы на диске или через буфер обмена) или в динамике (технологии DDE и OLE — см. разд. 6.9) перенести данные (скаляр, вектор или матрицу) в среду, например, Fortran и, используя богатый набор средств вычислительной математики этого языка программирования, решить задачу (этап задачи). Начиная с 5-й версии Mathcad пользователям была предоставлена возможность программирования на языке С и объявления в среде Mathcad новых встроенных функций (операторов). Код этих функций нужно откомпилировать каким-либо 32-разрядным транслятором и прикрепить к среде Mathcad через механизм DLL, опираясь на соглашение UserEFI. Но этот путь с самого начала был в чем-то тупиковым. Во-первых, Mathcad создавался как инструмент решения широкого класса задач теми, кто не хотел или не умел возиться с классическими языками программирования, и мы это уже не раз подчеркивали. При обращении же к языку C получалось так, что от чего ушли, к тому и пришли. Во-вторых, тот, кто все-таки переключался из среды Mathcad в среду языка С, как правило, там и оставался, решая всю задачу целиком. В-третьих (вернее, во-вторых с половиной), если кто-то и мог решить свою задачу на языке С, то он обычно не пользовался услугами Mathcad по неким "моральным соображениям", считая это ниже своего достоинства[4]. Но главным недостатком в технологии использования языка C для расширения возможностей Mathcad является невозможность включения в C-программу богатого математического инструментария Mathcad. Тем не менее, обращение к языку С — это одно из средств расширения списка встроенных функций, и мы в данной главе опишем, как это можно сделать (см. разд. 6.9).

Путь 3. Как отмечалось в гл. 1, последние версии Mathcad оборудованы элементами интерфейса Controls, поддерживаемыми программами на языках JScript или VBScript (см. рис. 1.16, например). Этим можно воспользоваться не только для форматирования самих Controls, но и для написания программ, особенно тем пользователям Mathcad, кто силен в языке BASIC. Так на рис. 6.2 показано, как можно в цикле определить сумму чисел S от а до b, вставив в Mathcad-документ Controls — текстовое поле с возможностью ввода двух переменных а и b (по умолчанию количество вводов Number of Inputs равно нулю) и одним (умолчание) выводом (Number of Outputs) для переменной S.

Рис. 6.2. Программа, введенная в Mathcad через текстовое окошко

Как понимает читатель, через текстовое поле, показанное на рис. 6.2, как через "игольное ушко" можно "протащить" в Mathcad-документ довольно сложные BASIC-программы и решить поставленную задачу. Недостатки здесь известные. Во-первых, и мы уже это отмечали, скрипты нежелательны в Mathcad-документах, т. к. через них можно "протащить" не только "невинные" программы (см. рис. 6.2 или 1.16), но и вирусы — "чуму" компьютеров.

Во-вторых, программы, введенные в Mathcad-документы через Controls, по-прежнему отрезаны от мощнейших инструментов Mathcad — в них нельзя вставлять функции и операторы решения уравнений и систем, линейной алгебры, оптимизации и всего того, что составляет суть данного математического пакета. Программирование в Controls, как правило, ведется только для улучшения интерфейса (см., например, рис. 1.16, где небольшая программа меняла цвет подложки выводимого числа).

6.2. Панель программирования

С первого взгляда на Mathcad возникает вопрос: почему в него не был встроен какой-либо известный и широко используемый язык программирования, а разработан новый, ни на что не похожий?

Примечание

Язык программирования Mathcad, развиваясь в 7-й версии, стал все больше приобретать черты языка С. Здесь нет ничего удивительного, т. к. сам Mathcad создается на этом языке.

Электронные таблицы Excel, текстовый редактор Word и система управления базами данных Access (Microsoft Office) используют встроенный язык BASIC[5], что кажется естественным даже для самых непримиримых противников этого языка. Но после детального знакомства с языком Mathcad удивление сменяется пониманием и даже удовлетворением. Становится очевидным, и мы это уже подчеркнули, что в рамки традиционных языков с их программами в текстовом формате невозможно втиснуть богатый набор инструментария Mathcad, который реализован не только и не столько в виде функций, сколько в общепринятом в математике виде (см., например, оператор производной на рис. 2.5, вставленный в Mathcad-программу[6]).

В Mathcad, по сути, не встроен язык программирования, а просто снято вышеупомянутое ограничение на использование составных операторов в теле алгоритмических управляющих конструкций выбора и повторения. Кроме того, как было уже отмечено, введено понятие локальной переменнойи и функции, добавлен цикл с параметром for, операторы досрочного выхода из цикла break и continue, а также оператор досрочного выхода из программы return.

Теперь о технике Mathcad-программирования.

Алгоритмические конструкции в среде Mathcad вводятся не традиционным набором через клавиатуру ключевых слов if, then, else, while и т. д., а нажатием одной из кнопок панели Programming (Программирование).

Рис. 6.3. Панель Programming Mathcad

Щелчок по одной из десяти кнопок, показанных на рис. 6.3, создает на дисплее заготовку соответствующей программной конструкции.

Опишем их.

Кнопка Add Line добавляет строку в программу, в тело цикла, в ветвь альтернативы и т. д. Этим действием снимается вышеупомянутое ограничение на число операторов во вложенных конструкциях языка:

Было

стало

Новая строка программы возникает либо выше, либо ниже текущей, где находится курсор, в зависимости от вида курсора: “” – строка появится внизу, “” – вверху. Положение курсора меняется после нажатия клавиши “I и S”.

Вертикальная линия объединяет отдельные операторы в операторный блок с одним входом и одним выходом, который выполняется как единый оператор (один из трех атрибутов структурного программирования)[7]. Какое-то подобие операторного блока пользователь Mathcad часто выделяет и в беспрограммном документе, реализуя, например, метод последовательных приближений или зажимая операторы ключевым словом Given и функцией Find (MinErr, Minimize, Maximize, Odesolve и Pdesolve).

Кнопка Add Line в чем-то подобна кнопке создания вектора, объединяющего в единый блок скалярные величины. Здесь сходство не только внешнее, но и функциональное — см. рис. 6.23, где вместо кнопки Add Line использована кнопка вставки массива. Этот прием доказывает, что без кнопки Add Line в принципе можно обойтись.

Кнопка ¬ — это оператор присвоения значения локальной переменной. На языке Pascal мы пишем А:=В+С, на языке BASIC — А=В+С, а на языке Mathcad — А¬B+С. Почему? Сначала опять же приходится недоумевать, но потом понимаешь, что без знака ¬ программа превратилась бы в нечто невразумительное, режущее глаз программиста:

r    Pascal:

A:=A:=B+C

r    BASIC:

А=А=В+С

Примечание

Первое выражение (Pascal) содержит явную синтаксическую ошибку, второе — нет, т. к. на языке BASIC символ = означает не только присвоение, но и булеву операцию "эквивалентно". В среде языка С программист написал бы

А = А == В + С

а на языке Pascal:

А := А = В + С.

В Mathcad-выражении:

A:=A¬B+C

все более-менее ясно: локальной переменной A (она в середине между символами := и ¬) присваивается значение суммы двух переменных B и C, значение которых уже задано выше в Mathcad-документе (глобальные переменные). Затем эта сумма передается глобальной переменной A (она слева от знака :=). Вернее, полуглобальной – глобальной она станет, если за ней будет стоять символ “≡”.

Благодаря локальным переменным можно создавать объемные[8] Mathcad-документы, поручая разработку отдельных функций и операторов разным программистам и не заботясь о разделении переменных: в разных программах переменные могут совпадать по имени, но при этом они не будут "перебегать друг другу дорогу" (технология программирования "сверху вниз"[9]). С локальными переменными мы, кстати, сталкивались и ранее: примеры индексы i, j и др. в операторах суммы или произведения.

С другой стороны, можно отметить и некую "вредность", а не полезность института локальных переменных в Mathcad. Эту мысль поясняет рис. 6.4.

 

Рис. 6.4. Альтернативный путь работы с локальными переменными

Локальность переменной подразумевает ее невидимость вне программы, что с одной стороны не смешивает отдельные программы, а с другой — очень затрудняет процесс отладки программ. Было бы лучше, если бы все переменные программы были видимы и вне программ (см. рис. 6.4, в центре), но при желании разработчика становились локальными и соответственно невидимыми через механизм стилей переменных Mathcad, о которых мы писали в разд. 1.2.2 (см. окончание рис. 6.4). Возникает такое чувство, что разработчики языка программирования Mathcad не знали о существовании такого мощного инструмента как стили переменных и не задействовали его. Итак, кнопка оператора ввода локальной переменной лишняя. Тут можно было спокойно обойтись кнопкой := и при необходимости сменой стиля переменной.

Нажав кнопку while, мы получим на экране заготовку цикла с предпроверкой — слово while с двумя пустыми квадратиками (placeholders) — рис. 6.5.

Рис. 6.5. Заготовка цикла while

В первый квадратик (правее while) нужно будет записать булево выражение, управляющее циклом, или просто константу, не равную нулю, чтобы получить бесконечный цикл, а во второй (ниже while) — тело цикла, операторы которого станут выполняться, пока булево выражение возвращает истинное значение (в среде Mathcad — это числовое значение, отличное от нуля). Если в теле цикла более одного оператора (а это основное отличие оператора while от вышеупомянутой функции untilсм. рис. 6.1), то нужно воспользоваться кнопкой Add Line.

Цикл, т. е. возможность выполнения какого-либо фрагмента программного кода и, в частности, Mathcad-документа — это "сердцевина" программирования — то, из-за чего в основном и приходится прибегать к программированию при решении поставленной задачи. На рис. 6.6—6.8 показано решение подобной задачи методом последовательного приближения, подвод ее к циклу while и к автоматизации этого вычислительного процесса.

Примечание

Метод последовательного приближения — очень распространенный метод решения инженерных задач — задается какое-то разумное значение искомой переменной, далее это значение расчетом по формулам уточняется. Перечисленные действия (цикл, итерация) повторяются до тех пор, пока (while) два очередных приближения будут выдавать близкие значения искомой переменной. Если такой сходимости не наблюдается, то этот метод также используют (пытаются использовать), называя его при этом по-другому — методом "научного тыка".

На рис. 6.6 в нижней его половине переменной m глобальным присваиванием дается некое значение, которое "закидывается" вверх и участвует в расчете нового значения — переменной m1. Это значение становится новым приближением (его нужно скопировать — перенести в оператор m ≡ ), и так повторяется несколько раз (у нас на рис. 6.6 три раза), пока не будет достигнута точность расчета (менее 0.1 % в нашем случае[10]). На рис. 5.8 и 5.9 читатель может увидеть подобные приближения при решении краевой задачи об эпидемии методом стрельбы (это тоже последовательные приближения: "перелет — недолет — попал").

 

Рис. 6.6. Метод последовательных приближений — начальный вариант Mathcad-документа

На рис. 6.7 операторы, участвующие в приближениях, объединены  в операторный блок с вводом в расчет локальных переменных, все из которых выводятся через векторную запись для контроля за их значениями, что очень важно при отладке программ (см. разд. 6.7).

Рис. 6.7. Метод последовательных приближений — операторы в Mathcad-программе

Примечание

В среде Mathcad 12//13/14 стало невозможно записывать левее оператора := вектор с переменными внутри, как это показано на рис. 6.7. Теперь там можно записывать только одну переменную — см. рис. 6.8.

На рис. 6.8 показан последний шаг в автоматизации метода последовательных приближений — ввод в расчет оператора while, который в паре с операторами return и if реализует цикл с выходом из середины. Это универсальный цикл программирования: если оператор return переместить в самый конец программы, то получится второй тип цикла — цикл с постпроверкой (цикл repeat...until, если вспомнить язык Pascal), которого нет среди встроенных инструментов (кнопок) программирование Mathcad — см. рис. 6.3. Правее оператора while на рис. 6.8 читатель не видит никакого оператора: там записана единица, обеспечивающая бесконечный цикл, цвет которой белый (см. разд. 1.2.3).

Рис. 6.8. Метод последовательных приближений — цикл с выходом из середины

Кнопка if, которую мы уже задействовали в цикле на рис. 6.8, позволяет вводить в программу альтернативу с одной ветвью[11]. Так, Pascal-конструкция:

if A > B then C := D

в среде Mathcad будет выглядеть несколько по-арабски (по-еврейски — записана справа налево):

С ¬ D if A > B

Но если ветвь альтернативы — составной оператор, то все почти встанет на свои места, вернее, будет записано уже по-китайски (сверху вниз).

r    Pascal:

if A>B then begin E:=F; F:=G end;

Примечание

Ключевые слова begin и end в среде языка Pascal отмечают начало и конец программных блоков (см. описание кнопки Add Line).

r    Mathcad:

if A>B

E ¬ F

F ¬ G

 

Кнопка otherwise превращает неполную альтернативу в полную.

r    Pascal:

if A > B then C := D else E := F;

r    Mathcad:

C ¬ D if A > B

E ¬ F otherwise

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

¬ if(A > B, D, F)

или

if(A > B, C¬D, E¬F)

На рис. 6.9 показан Mathcad-документ с расчетом плотности воздуха по закону идеального газа в зависимости от температуры и давления, значение которых вводятся через текстовые поля с выбором единиц измерения через переключатели. В случае температуры переключатель возвращает числа от 1 до 4 и работает в паре с функциями if, вложенными друг в друга. В случае же давления (тут возвращаются целые числа от 1 до 5) работают операторы if в паре с оператором otherwise в конце списка выбора. Второй способ записи альтернативы со множеством ветвей более удобен для анализа (поиска возможных ошибок, например) и расширения, но и первый (с функциями, а не операторами if) также имеет право на существование. Дело в том, что некая предпочтительность записи альтернативы через функцию if (выбор температурных шкал на рис. 6.9), а не через оператор if вызвана тем, что до недавнего времени считалось, что если в Mathcad-документе удалось избежать использования операторов из панели Programming (Программирование), то это хорошо. Такой документ можно было открыть и в дешевых версиях Mathcad, не имеющих средства программирования. Еще одно преимущество функции if перед оператором if — компактность записи в текстовом, а не в графическом ("операторном") формате. Читатель вправе сам выбирать, что вставлять в свои разработки — оператор if или функцию if (альтернатива с альтернативой, так сказать).

Рис. 6.9. Альтернатива — реализация через функцию и оператор

Кстати о компактности. В Mathcad-программе по умолчанию можно записать на одной строке только один оператор. Из-за этого любая более-менее объемная программа становится очень длинной и ее можно просматривать только через вертикальную прокрутку экрана дисплея или через изменение масштаба экрана (zoom). Это затрудняет работу с программой. Считается, что если уж не вся программа, но ее отдельные смысловые области (подпрограммы) должны целиком помещаться на экране дисплея стандартного размера. Основным способом повышения компактности программ является размещение на одной строке нескольких операторов.

Примечание

Основным способом повышения компактности программ является, конечно, их оптимизация, удаление из программ лишних операторов, переход к "изящным" и, как правило, кратким алгоритмам и т. д.

В среде языка программирования Mathcad[12] нет встроенных средств создания многооператорных программных строк. Но их можно ввести в программу недокументированным способом и мы этим уже неоднократно пользовались. Так на рис. 6.8, например, да и на многих других рисунках книги отображено как несколько операторов вводятся на одну программную строку в виде матрицы с одной строкой и несколькими рядами ("горизонтальный вектор", вектор-строка, элементы (компоненты) которого — отдельные операторы программы). Здесь может быть только одно ограничение — операторы, собранные в матрице, должны возвращать либо безразмерные величины, либо величины одной размерности (длины, массы, силы и т. д.). Это связано с тем, что массивы (векторы и матрицы) Mathcad не могут хранить разноразмерные величины, и мы об этом уже говорили в разд. 1.4.

Но и тут есть выход из положения. Операторы, возвращающие разноразмерные величины, можно разместить на одной программной строке в виде сомножителей произведения, сделав сам знак умножения невидимым. В такое "произведение" можно дополнительно вставить невидимые единицы-сомножители для того, чтобы отодвинуть операторы от левого края[13] или вставить между ними дополнительные пробелы. Но это будет уж слишком сильной экзотикой. И опять же, такая строка-произведение[14] застопорится, если один из операторов будет возвращать не числовое, а текстовое значение. Одним словом, несколько операторов на одной стороне — это недокументированный прием, чреватый сбоями. И если читатель видит такую строку в Mathcad-программе этой или другой книги, то он должен считать такую программу заархивированной (сжатой). При вводе такой программы в компьютер "с листа" ее следует разархивировать — оставить только по одному оператору на одной строке.

Кнопку Add Line следовало бы назвать Add Operator, т.к. через нее в программу вводится не строка (операторов), а только один оператор.

Еще один недокументированный прием — комментарии в Mathcad-программах.

Язык программирования Mathcad не имеет специальных средств (операторов) для этих целей. В Mathcad 12, как мы уже отметили ранее, появились средства скрытого комментирования отдельных операторов в том числе и программ — см. рис. 1.50. Но эти комментарии неудобны тем, что их нельзя увидеть все сразу, а только поочередно после подвода курсора к оператору и вызова соответствующего диалогового окна. Кроме того, такие комментарии не видны на распечатках Mathcad-документов. Те или иные операторы программы можно прокомментировать, поместив правее программы текстовые вставки. Но из-за возможных различий в шрифтах формул и текстовых вставок трудно бывает поставить комментарий именно там где нужно, т. е. точно у комментируемого оператора. Кроме того, такую россыпь операторов трудно двигать (вырезать и вставлять в другом месте) — что-то можно потерять по дороге.

Но комментарии можно вставлять в программы в виде отдельных операторов — текстовых констант. Можно поступать и так — записывать в программу вектор-строку с двумя элементами, первый из которых — комментарий, а второй — оператор программы, или наоборот. Недостаток тут известный — невозможность проверки орфографии таких констант, даже если они написаны по-английски. Здесь можно посоветовать поступать следующим образом: записывать комментарии, например, в Word, вылавливать возможные ошибки с помощью этого текстового процессора, а потом переносить текстовые константы в Mathcad.

Примечание

Для того чтобы можно было прописывать комментарии (текстовые константы) русским шрифтом (кириллицей), необходимо стилю Constants поставить в соответствие шрифт, работающий и с русскими буквами.

Но вернемся к альтернативе.

Альтернативу можно считать вспомогательной  структурной, управляющей конструкцией – оператором, без которого можно обойтись.

Так на рис. 6.10 показано, как в программе поиска нуля функции методом половинного деления альтернатива заменяется на два цикла while, операторы которых попеременно выполняются либо раз, либо ни разу.

Рис. 6.10. Два оператора while вместо одного оператора if

Программу, показанную на рис. 6.10, можно считать неким курьезом, программистской шуткой. А можно считать подтверждением того факта, что основная структурная теорема, гласящая, что алгоритм любой сложности можно реализовать через триаду "следование-повторение-выбор", не совсем верна — оператор if оказывается лишним. Дело в том, что…

Очень часто при решении задачи необходимо пускать расчеты по разным путям в зависимости от исходных данных. И не просто пускать, а так пускать, чтобы пользователь готовой расчетной методики видел не все эти пути (группы операторов), а только тот, по которому ведется расчет в настоящее время. На рис. 6.11 показано одно из решений этой проблемы. Мы, кстати, этим методом уже пользовались, комментируя расчет рисунком (см. рис. 1.49).

Рис. 6.11. "Визуальная" альтернатива в Mathcad-документе

На рис. 6.11 показан ход решения простенькой "альтернативно-визуальной" задачи: переменные с и d равны а·b и a2b, соответственно, при a>b, но a/b и b2+a — в противном случае. При этом после ввода значений a и b пользователь видит только те операторы, по которым в данный момент ведется расчет. Решение тут такое — пары операторов, по которым ведутся расчеты, вернее, их изображения вместе с их условием (a>b или a≤b) записываются на диск в виде двух графических файлов Calc_1.gif и Calc_2.gif (на рис. 6.11 изображен фрагмент файлового менеджера с этими фалами), которые попеременно вставляются в расчет. На рис. 6.12 показано, как такой расчет будет представлен на экране дисплея при разных значениях переменных a и b.

Рис. 6.12. "Визуальная" альтернатива в Интернете (MAS)

Конечно, расчет, показанный на рис. 6.11, не предназначен для редактирования и расширения (WorkSheet), а только для конечного использования в Интернете, например (WebSheet) — см. гл. 7.

В связи с этим (проведение всяческих "закулисных" действий с расчетом) в WorkSheet можно сравнить с театром, а WebSheet — с кинематографом, иллюзионом, где возможно то, что невозможно или "шито белыми нитками" в театре — в расчетных документах, открытых в среде Mathcad (WorkSheets), а не в Интернете (WebSheets).

Оператор if в сочетании с операторами return и otherwise — довольно гибкая конструкция, позволяющая легко программировать в среде Mathcad разного рода альтернативы с вкрапленными в них вычислительными операторами. Один из примеров — на рис. 6.13, где показана программа анализа квадратного уравнения вида A·x2+B·x+C=0.

Рис. 6.13. Конструкция "выбор"

В программе рис. 6.13 можно было обойтись без оператора return, что вернуло бы данную программу в "правовое поле" структурного программирования с главным "законом" — один вход, один выход. Но оператор return ускоряет расчет, отсекая ненужные действия.

Кнопка for вводит в программы цикл с параметром.

Когда заранее известно, сколько раз нужно выполнить какую-то часть программы (тело цикла), то используют не цикл while, а цикл for, в заголовке которого пишут не булево выражение, а параметр цикла, и указывают, какие значения он должен принимать при каждом выполнении цикла.

Цикл с параметром в среде Mathcad намного более гибок, чем его аналоги в языках программирования BASIC, Pascal или С – см. примеры на рис. 6.14.

Рис. 6.14. Три особенности цикла for

Первая особенность ("гибкость") цикла for в среде Mathcad заключается в том, то ему не нужно указывать направление шага изменения параметра в случае, когда первое значение параметра меньше второго. В такой ситуации Mathcad-цикл for сам разберется, в какую сторону вести отчет. В средах языков BASIC или Pascal отрицательный отсчет шага в цикле for требует дополнительных ключевых слов Step1 или downdo, соответственно.

Вторая особенность (опять же — "гибкость") цикла for в среде Mathcad — это возможность работы со списком своих параметров вместо переменной области (Range Variable) или вектора, который обычно пишут на этом месте.

Третья и главная особенность Mathcad-цикла for в том, что переменная цикла может принимать значение различных типов — вещественное число, комплексное число, текст, массив и т. д. Кроме того, значения параметра цикла могут выбираться и из матрицы (упакованного вектора), а не только из переменной области, вектора, или списка (см. выше).

Кнопки break и continue позволяют досрочно выходить из циклов while и for, а кнопка return — совсем из программы, что мы уже проиллюстрировали (см. рис. 6.13). О кнопках break и continue разговор особый. Сейчас же проведем такую аналогию.

Все структурные управляющие конструкции Mathcad можно усмотреть в простой житейской ситуации: потчевание гостей чаем и кофе. Хозяйка проверяет, нет ли на столе пустой чашки (булева переменная, управляющая циклом while), и наполняет ее (тело цикла) чаем или кофе (альтернатива). Добавление в чашку кусочков сахара — новый, вложенный цикл. При разливе чая чашка (стакан) может лопнуть, что прерывает цикл, из которого выходят в конец цикла (break — гости встают из-за стола и занимаются чем-то другим) или в начало цикла (continue — на столе меняется скатерть, а чаепитие возобновляется). Третий сценарий: разбитая чашка так расстраивает хозяйку, что вечеринка досрочно заканчивается (return). Хорошая хозяйка умеет переключить разговор, если он затронет тему, неприятную одному из гостей. Умеет она незаметно сглаживать и другие нештатные ситуации. Здесь очень пригодится оператор on error, обрабатывающий ошибки. Он, по сути, является альтернативой с одной ветвью и завуалированной (запрятанной) булевой переменной, управляющей альтернативой. Мы использовали оператор on error, когда рисовали "картину" поиска корней уравнений (см. рис. 2.8, 2.21 и 2.22).

Язык программирования Mathcad по своей идеологии очень похож на язык FRED интегрированного пакета Framework[15]. Говорят, что один из "погорельцев" фирмы Ashton-Tate (разработчика Framework) перешел в фирму Mathsoft и приложил руку к созданию языка программирования Mathcad. Внешне же своими вертикальными линиями, фиксирующими операторные блоки, пакет Mathcad напоминает алгоритмические конструкции из книги А. П. Брудно "Программирование в содержательных обозначениях"[16]. В свое время автор очень увлекался подобными линиями, втискивая программы в рамки структурных диаграмм[17]. Вертикальные линии программ Mathcad более наглядны (особенно для обучения структурному программированию), чем просто операторные скобки (begin...end на языке Pascal, фигурные скобки языка С, оператор list() языка FRED, конец строки BASIC-программы, круглые скобки математических выражений и т. д.).

Говоря о структурном программировании, нельзя не отметить тот факт, что разработчики языка Mathcad сразу отказались от метки и операторов условного и безусловного перехода к метке как инструмента реализации разветвленных алгоритмов. Заодно был игнорирован и цикл с постпроверкой. Для некоторого смягчения этой категоричной позиции и были введены операторы return, break и continue, двумя последними из которых автор не рекомендует пользоваться, т. к. они сильно путают программиста.

Рассмотрим еще несколько особенностей Mathcad-программ на занимательных задачах.

6.3. Турецкий платок

Очень часто, решая ту или иную проблему, мы оказываемся в шкуре буриданова осла. Задача может иметь два альтернативных решения, как две охапки сена слева и справа от упомянутого животного. Все доводы "за" и "против" уравновешены. Как в этом случае поступить? Ехать или не ехать в командировку? Покупать или не покупать еще один винчестер? Поистине, гамлетовские вопросы задает нам жизнь!

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

Есть проверенный столетиями метод принятия подобных решений. Достаточно разложить пасьянс. Сошелся — решение принято и все сомнения прочь. Можно подыскать дополнительные доводы в его пользу, и начать воплощать в жизнь. Пасьянс психологически нас на это настраивает.

Но принять решение подобным образом иногда бывает трудно, т. к. не всегда под рукой есть колода карт, да и не совсем удобно раскладывать их на рабочем месте. Но это можно сделать и на экране дисплея. В среде Windows, например, есть игры-пасьянсы ("Солитер" и "Косынка"), но мы придумаем что-нибудь новенькое, а главное, более занимательное и поучительное — разложим в среде Mathcad старинный пасьянс "Турецкий платок". На это есть три причины:

1.     Чтобы лучше освоить программную среду, нужно постараться решить в ней задачу, для этого крайне неподходящую (своего рода программистское извращение[18]— деривация).

2.     В детстве каждый нормальный человек, наигравшись, ломал игрушку, чтобы посмотреть, как она устроена. Посмотрим и мы, как тасуется колода и раскладывается пасьянс.

3.     По традиции гадать на картах и раскладывать пасьянсы разрешается только в святки[19]. Наш же пасьянс можно считать просто числовой головоломкой, которую позволительно решать круглый год.

Mathcad-документ (рис. 6.15) позволяет разложить пасьянс "Турецкий платок" по следующим правилам. Из одной перетасованной колоды в 52 листа выкладывают картинкой вверх пять рядов по 10 карт в каждом. Последние две карты кладут в шестой неполный ряд на любое место, как правило, к первому и второму столбцам[20]. Требуется распустить этот "платок", снимая из разных столбцов за один ход по две нижние одинаковые карты — тройки, дамы, тузы и т. д.

Рис. 6.15. Программа пасьянса "Турецкий платок"

Для снятия карт нужно скопировать правую часть оператора П=... (саму матрицу) на свободное место, подвести курсор к нужной текстовой константе (к карте), щелкнуть левой кнопкой мыши и нажать клавишу <Delete>. Вместо числа появится пустой квадратик.

Из разложенного пасьянса можно снять две девятки, две тройки, две пятерки или двух королей. Если удастся снять все 52 карты, то это означает, что в командировку все-таки ехать придется, а винчестер покупать надо.

Составление программы, формирующей матрицу П (раскладка пасьянса) — прекрасное и, что не менее важно, занимательное средство изучения таких базовых понятий линейной алгебры, как вектор и матрица[21]. Так, при формировании матрицы П транспонируется матрица Масть (она ставится "на поп" — матрица с одной строкой превращается в матрицу с одним столбцом, т. е. в вектор). Далее с помощью функции stack формируется новая нерастасованная колода карт, где одна отсортированная масть идет за другой (вектор Колода). Затем в цикле с параметром (for...) с помощью цикла while[22] и функции rnd идет формирование перетасованной колоды — вектора Тасованная_колода, который в конце программы двойным циклом с двумя параметрами (for... for...) складывается слоями в матрицу П.

Программу, формирующую матрицу П, можно развить: заставить программу отбраковывать явно нерешаемые раскладки — такие, например, где в одном столбце оказались три или даже четыре одинаковые карты. Еще одна тупиковая ситуация — две пары карт крест накрест закрывают друг друга. Это будет хорошим упражнением, закрепляющим навыки работы с "матричными" операторами и функциями в среде Mathcad. А вот более сложное задание читателю: доработать программу так, чтобы она сама раскладывала пасьянс, либо на худой конец сообщала, что его можно решить, просто снимая снизу первые подвернувшиеся одинаковые открытые карты. Более умная стратегия подразумевает выбор карты из трех или четырех одинаковых открытых карт.

Кроме линейной алгебры, наша программа затрагивает и другие интересные разделы математики — теорию вероятностей, статистику (см. функцию rnd, генерирующую псевдослучайные числа). Интересный вопрос: можно ли составить программу, высчитывающую вероятность сходимости того или иного пасьянса? Считается, что пасьянс "Солитер", входящий в стандартную поставку Windows, раскладывается при любых начальных раскладках. Но это только гипотеза...

На рис. 6.16 программа раскладки пасьянса переделана так, чтобы выпадали совершенно разные раскладки при открытии файла. Для этого используется функция time, возвращающая время в секундах, пошедшее с 1970 г. От этого значения отрезается дробная часть (генератор не псевдо-, а истинослучайных чисел), которая множится на 1000. Полученное целое число становится числом выполнения цикла for, в теле которого истинослучайное количество раз вызывается функция rnd. Этим и достигается случайная и неповторяющаяся раскладка пасьянса за счет тщательной тасовки колоды. Данный прием будет полезен при создании программ контроля знаний (см. гл. 7), когда необходимо генерировать случайные исходные данные для тестовых заданий студентам, например.

Рис. 6.16. Программа пасьянса "Турецкий платок" — рандомизация

Кроме того, изменен вид реализации цикла с постпроверкой (а его, повторяем, нет в Mathcad). На рис. 6.15 он ведется через бесконечный цикл и оператор break, а на рис. 6.16 через "конечный" цикл while и дублирование оператора Случайное_число ¬ floor(rnd(52)).

Часто бывает так, что человек прекрасно делает то, чему он учился играючи и с большим удовольствием: ходит, говорит, плавает, катается на велосипеде и т. д., и т.п. Автор, например, задолго до школы научился считать, играя в карты: валет — двойка, дама — тройка и т. д. И сейчас, обращаясь к своим студентам, не устает повторять, что главное, что нужно получать от учебы, — это не знания, не практические навыки, а... удовольствие. Не получая радости от учебы, от повседневного труда, человек тратит свою жизнь впустую...

Начинать изучение векторов и матриц (массивов данных) в курсе программирования (информатики) можно со знакомства со встроенными функциями и операторами конкретной программной среды. А можно раскладывать пасьянсы.

Как уже отмечалось ранее, переменные в Mathcad могут быть локальными, самообъявляющимися в программах (как, например, переменные Случайное_число, Колода, Карта, Столбец и Ряд в программе на рис. 6.15 и 6.16). Значение локальных переменных пропадает по выходу из программы. Разработчики языка Mathcad посчитали лишним обязательное объявление переменных до их использования (как это делается при работе с языком Pascal, например). Наверное, переменные не объявляются из-за того, что они все однотипные[23]. Но необъявление переменных может приводить к ошибкам, которые трудно выявить. Ввел программист в программу переменную dаy, а через пару операторов написал dey (что по произношению более соответствует английскому слову day (день); можно умудриться написать и dаy, где вторая буква будет из русского алфавита) — программа выдает неверный ответ. Кроме того, переменная day уже хранит встроенную единицу времени, что может также быть причиной ошибки. Опечатки в программе часто бывают намного страшней в плане отладки, чем ошибки алгоритма. Кроме локальных и глобальных переменных, значения которых заданы вне программы и автоматически в нее проникают, в среде Mathcad есть и системные (предопределенные) переменные и константы. Пример — числа e и p, значение которых определено самой системой (математикой), а не пользователем.

Мы уже не первый раз используем буквы кириллицы в именах переменных и функций. В программе на рис. 6.15 все переменные прописаны по-русски. Pro и Contra этого приема.

r    Pro

Полные имена переменных, написанные на родном языке программиста (Столбец, Ряд вместо i, j) делают программу более простой для понимания, но более сложной для написания (рекомендуется длинные переменные писать один раз, а потом копировать в нужных местах). Конечно, можно было написать и английские термины в качестве имен переменных, но они будут выглядеть чужеродными в пасьянсе, программу которого для русскоязычного читателя написал человек, считающий себя русским. Да и автор, честно говоря, не знает, как будет по-английски "Масть", "Колода". Лезть же в словарь не с руки. Проще написать Мast, Коloda. Проще, да некрасиво (см. ниже).

r    Contra

·       Русские имена переменных порождают "смешенье языков французского с нижегородским[24]": for Столбец?! Правильнее и грамотней писать for Солбца[25] (для Столбца); на многих программистов русское имя объекта программирования (файла, переменной, функции и т. д.) действует как красная тряпка на быка[26].

·       В латинском и русском алфавитах многие буквы (например, а, с) совпадают по написанию. Из-за этого в программе могут оказаться переменные, одинаковые для человека, но разные для Mathcad.

·       Выбор для переменных шрифта с окончанием Cyr приводит к перекодировке символов. Из-за этого, например, может пропасть восклицательный знак в факториале и др.

6.4. Рекурсия

Рекурсивная функция — это такая функция, которая вызывает сама себя в момент ее создания[27]. Рекурсия — очень мощный и удобный инструмент решения задач. Так, например, один из самых быстрых алгоритмов сортировки массива использует рекурсию. Вычислить определитель квадратной матрицы также помогает рекурсия и т. д.

Языки программирования в своем развитии обычно проходят три стадии:

r    рекурсия невозможна;

r    рекурсия не разрешена, но применяется по принципу: "Если нельзя, но очень хочется, то можно". Так, на старых версиях языка BASIC рекурсия реализовывалась через оператор ON N GOTO, передающий управление программой на N-ю строку;

r    рекурсия разрешена.

Mathcad вторую стадию проскочил.

И вот уже трещат морозы

И серебрятся средь полей...

(Читатель ждет уж рифмы розы;

На вот, возьми ее скорей!)

Читатель ждет уж примеров рекурсии в среде Mathcad? Скорее всего, нет. Традиционные примеры ("розы-морозы") ему оскомину набили. Но мы попробуем что-нибудь свеженькое. Например, двустороннюю (ретроспективную) рекурсию или, если так можно выразиться, самораскрывающуюся рекурсию.

Как запомнить число e (основание натурального логарифма) с девятью цифрами после запятой? Очень просто — две целых семь десятых плюс два раза Лев Толстой — 2.718281828, т. е. к числу 2.7 нужно приписать два раза год рождения классика (1828). Остается самая малость — запомнить, в каком году родился Лев Толстой, или, на худой конец, сообразить, что это случилось в позапрошлом веке, чтобы вспомнить хотя бы три знака числа e после запятой: 2.718. Ретроспектива, обращенная в XIX век, поможет запомнить довольно-таки точное значение одной из фундаментальных констант математики.

Как запомнить, что факториал нуля равен не нулю (типичная ошибка), а единице? Очень просто. Нужно применить ретроспективный метод поиска факториала числа: сообщить машине факториал какого-либо положительного числа N (5!=1·2·3·4·5=120, например) и то, что факториал предыдущего числа N1 равен факториалу первого (N!), разделенному на N. Факториал пяти (120) — это такая же тривиальная истина, как и то, что Лев Толстой родился в XIX веке, а грубая оценка числа е — 2.7.

Работая по программе с двусторонней рекурсией, показанной на рис. 6.17, можно не только правильно определить факториал нуля (единица), но и получить факториалы отрицательных (?!) чисел[28]. Только нужно обучить этому машину — заложить в программу двустороннюю рекурсию для поиска факториала на всем целочисленном диапазоне. "Двусторонность" здесь проявляется в том, что факториалы ищутся не только справа, но и слева от пяти.

Рис. 6.17. Расчет факториала (двусторонняя рекурсия)

Описывая в гл. 5 модель финансовой пирамиды, мы упомянули числа Фибоначчи, которые связаны с условными кроликами (табл. 6.1).

Таблица 6.1. Изменение популяции кроликов

Характеристика

Значения

Поколение

...

4

5

6

7

8

9

10

11

...

Число кроликов

...

3

5

8

13

21

34

55

89

...

Приведенный ряд специально начат не с традиционного места (первое поколение), а с четвертого поколения (три кролика), для того чтобы задать читателю вопрос, подобный тому, который стоял в задаче о факториале: "Чему равно минимальное количество кроликов в популяции — каково наименьшее число Фибоначчи?" Нормальный ответ, приводимый во всех учебниках — ноль. Но не будем спешить и напишем программу с двусторонней рекурсией, взяв за базовые числа Фибоначчи не традиционную пару 0 и 1, а 3 и 5 (четвертое и пятое поколения) — см. рис. 6.18.

Рис. 6.18. Расчет чисел Фибоначчи (двусторонняя рекурсия)

Ряд кроликов Фибоначчи в "отрицательных поколениях" зеркально отображает значения в "положительных поколениях", но с переменным знаком.

Числа Фибоначчи в наше время широко применяются в вычислительной математике, в том числе и для иллюстрации рекурсии, как, например, в нашей книге. Кроме того, метод Фибоначчи используется для поиска минимума. Частный случай метода Фибоначчи — метод золотого сечения (см. рис. 1.60): пара смежных чисел Фибоначчи при N, стремящемся к бесконечности, соотносится по золотому сечению.

Но "изюминка" программы, показанной на рис. 6.18, не только и не столько в "двусторонности", а в том, что возвращаются все рекурсивные вызовы функций. Это достигается заменой встроенных операторов сложения и вычитания, на пользовательские аналогичные операторы с маленьким секретом, который более подробно раскрыт на рис. 6.19, где представлена рекурсивная программа, возвращающая так называемую непрерывную (цепную) дробь. В этой задаче ответ повторяет исходные данные, а сама суть решения задачи сосредоточена в рекурсивных вызовах.

Рис. 6.19. Непрерывная дробь

Пользовательский оператор сложения на рис. 6.19 имеет степень у второго слагаемого. Это видно за счет того, что оператор прописан на цветном фоне. Показатель степени равен одному, что никак не влияет на вычисления, но позволяет выводить их, если заглушить численное значение переменной i. Этот же прием скрытой степени позволяет осуществить "вековую мечту" пользователей Mathcad — выводить на печать не только результат, но и все числа, участвующие в расчете (см. http://twt.mpei.ac.ru/mas/worksheets/202_All_Values.mcd).

6.5. Сказка о рыбаках и рыбке или Проблема метки

Как было уже отмечено ранее, язык программирования Mathcad не имеет меток и операторов условного и безусловного перехода к меткам.

Проблему метки, вернее, проблему избавления от метки при переписывании "меточных" программ для "безметочного" пакета Mathcad, мы рассмотрим на примере решения старинной английской задачи о рыбаках и рыбке.

"Три рыбака легли спать, не пересчитав и не поделив улова. Ночью один из рыбаков проснулся и решил уйти, забрав свою долю. Но число рыб не делилось на три. Тогда он, не долго думая, выбросил одну рыбку, а из остатка забрал треть. Второй и третий рыбаки поступили аналогичным образом — ушли по-английски и по-джентельменски, выбросив по одной рыбке и оставив спящим товарищам четное число рыб. Спрашивается, какое минимальное число рыб в улове отвечает условию задачи".

Пусть читатель сначала попробует решить задачу без компьютера. Компьютер же мы привлечем к этой работе для двух целей. Во-первых, на задаче о рыбаках и рыбке мы рассмотрим некоторые методы структурирования программ в среде Mathcad — методы освобождения программ от меток и "втискивания" их в узкие рамки структурных управляющих конструкций, описанных выше (см. рис. 6.2). Во-вторых, мы покажем, что задача о рыбаках и рыбке до сих пор решалась неправильно...

Эту задачу в среде Mathcad можно решить, не прибегая к средствам программирования, что мы уже и делали — см. рис. 2.30, где было показано, как задача решается через недокументированную функцию isolve, возвращающую целочисленные корни уравнений.

Но можно эту задачу решить и "в лоб" методом последовательных приближений — задается первое приближение к ответу (50 рыб, например), а затем от этого числа отнимается по единице до тех пор, пока убывающий улов не будет представлять собой целочисленный ряд: было 25 рыб (искомый ответ задачи), первый рыбак выбросил одну, забрал треть и оставил товарищам 16 рыб (по 8 каждому); второй рыбак (не зная, что первый ушел) оставил 10 рыб, а третий — 6. Задача решена, но с применением "ручной" работы, состоящей в наблюдении за значениями переменной Улов и в изменении (уменьшении на единицу) значения переменной Ответ.

Попробуем автоматизировать поиск ответа в задаче о рыбаках и рыбке, используя "меточный" язык BASIC.

' 1. Исходная неструктурированная Basic-программа

Input "Предположение"; Ответ

label: Улов = Ответ

  For Рыбак = 1 To 3

    Улов = Улов — 1

    Улов = Улов - Улов / 3

    If Улов > Int(Улов) Then Ответ = Ответ - 1: Goto label

  Next

Print "Ответ "; Ответ; "рыб"

 

' 2. Первый шаг структурирования - разбег

Input "Предположение"; Ответ

Ответ = Ответ + 1              ' Шаг назад

label: Ответ = Ответ - 1       ' Шаг вперед

  Улов = Ответ

  For Рыбак = 1 To 3

    Улов = Улов — 1

    Улов = Улов - Улов / 3

    If Улов > Int(Улов) Goto label

  Next

Print "Ответ "; Ответ; "рыб"

 

' 3. Второй шаг структурирования — ввод признака

Input "Предположение"; Ответ

Ответ = Ответ + 1

label: Ответ = Ответ — 1

Улов = Ответ

Поделили = "да"                ' Признак дележа улова

  For Рыбак = 1 To 3

    Улов = Улов — 1

    Улов = Улов - Улов / 3

    If Улов > Int(Улов) Then Поделили = "нет"

  Next

If Поделили = "нет" Goto label

Print "Ответ "; Ответ; "рыб"

 

' 4. Третий шаг структурирования — отказ от метки

Input "Предположение"; Ответ

Ответ = Ответ + 1

Do                             ' Начало цикла с постпроверкой

  Ответ = Ответ — 1

  Улов = Ответ

  Поделили = "да"

  For Рыбак = 1 To 3

    Улов = Улов — 1

    Улов = Улов - Улов / 3

    If Улов > Int(Улов) Then Поделили = "нет"

  Next

Loop Until Поделили = "да"     ' Конец цикла

Print "Ответ "; Ответ; "рыб"

Выше представлены три BASIC-программы, решающие задачу о рыбаках и рыбке в автоматическом режиме: оператор Input запрашивает первое приближение (те же 50 рыб, к примеру), а оператор Print выдает ответ — 25 рыб. В первой BASIC-программе один к одному реализован алгоритм "ручного" расчета: как только в теле цикла с параметром (три последовательных ухода рыбаков) переменная Улов обретает дробный "хвостик" (встроенная BASIC-функция Int этот "хвостик" обрезает; ее Mathcad-аналог — функция floor), то (Then) предположение уменьшается на единицу (Ответ=Ответ1), а управление программой передается оператору, помеченному меткой (Goto label). Прежде чем эту программу перевести на язык Mathcad, ее нужно освободить от метки[29]. И не только потому, что в арсенале средств программирования Mathcad нет метки и операторов условного и безусловного перехода к метке, но по другим причинам, не связанным с Mathcad. В нашей коротенькой программе-безделушке (см. в ней п. 1) метка вполне уместна и естественна, но если программа с метками разрастается, то в ней становится трудно разбираться и ее практически невозможно отлаживать и расширять. Программа, как справедливо подчеркивают адепты структурного программирования, становится похожа на спагетти: вытаскиваешь (вилкой) блок операторов для отдельной отладки или компиляции, а к нему намертво привязаны нити (макаронины) Goto-переходов. Кроме того, такую программу невозможно создавать группой разработчиков (технология снизу вверх). Первые реализации языка Pascal совсем не имели меток, т. к. этот язык разрабатывался Н. Виртом в первую очередь для обучения студентов структурному программированию. Метка появилась только в поздних версиях данного языка. Так от детей в период, когда они учатся жить (выживать!), прячут спички.

Первый шаг структурирования BASIC-программы — это превращение конструкции:

If Улов>Int(Улов) Then Ответ=Ответ-1: Goto label

в оператор условного перехода к метке:

If Улов>Int(Улов) Goto label

Это сделать несложно (см. п. 2), применив в программе технику разбега спортсмена перед прыжком: шаг назад от черты-метки (Ответ=Ответ+1) и разбег (Ответ=Ответ1). Структурирование программы, как правило, несколько усложняет алгоритм: "За все нужно платить!", "Красота требует жертв!" и т. д.

Второй шаг структурирования — это вытаскивание оператора безусловного перехода из тела цикла For. Для этого в программу (см. п. 3) вводится вспомогательная булева переменная-признак Поделили, а в теле цикла оператор условного перехода к метке заменяется на оператор "альтернатива с одной ветвью" (одна из основных структурных управляющих конструкций). Сам же оператор условного перехода "сползает" вниз. После этого в программе "вырисовывается" еще одна основная структурная управляющая конструкция — цикл с постпроверкой, который в третьем варианте программы реализован через метку и оператор условного перехода к метке. После этого программу наконец-то можно совсем освободить от метки, реализуя алгоритм через цикл с постпроверкой (DoLoop Until), в тело которого вписан цикл For, а в него ¾ альтернатива с одной ветвью (см. п. 4).

После всех этих манипуляций BASIC-программу можно один к одному переписать для Mathcad (рис. 6.20). Придется только оформить ее в виде функции пользователя: в языке Mathcad нет операторов Input и Print[30]. Их аналоги в среде Mathcad (операторы  :=  и  =) работают, увы, только вне программ.

Рис. 6.20. Задача о рыбаках и рыбке — проблема метки

На рис. 6.20 показаны вызовы функции Ответ при различных значениях предположений (50, 24, –3 и даже –30 рыб). Английский физик Поль Дирак придумал не только античастицы, но и "антирыбы": он сказал, что задача о рыбаках и рыбке решалась неправильно (25 рыб). Правильный ответ — минус две рыбы (плюс две антирыбы): выбрасываем одну — остается минус три, забираем треть — остается минус две и так до бесконечности. Наше компьютерное решение задачи показывает, что и Дирак ошибался: "Поль, ты не прав!" Условию задачи отвечает бесконечный ряд чисел (назовем его "рыбный ряд Дирака") с шагом 27.

Чтобы не прослыть совсем уж полным педантом (программистом-занудой), можно в конец цикла for на рис. 6.20 вставить оператор

break if Поделили = "нет"

прерывающий выполнения цикла for. Если рыбаков будет не трое, а больше (тридцать три рыбака, например — задача для читателя), то этот прием существенно ускорит работу программы.

Можно еще усложнить задачу, заставив любое количество рыбаков выбрасывать или подлавливать любое количество рыб.

Задачу о рыбаках и рыбке можно решать другим способом — перебором с другого конца: задать не начальное число рыб в улове, а предположить, что последний рыбак оставляет две рыбы (меньше не может быть), и увеличивать их число на единицу, если условия задачи не выполняются. Задача будет решаться быстрее, но  –2 рыб, а, тем более, –29 рыб мы не получим: "Keep if simple, stupid! — Делай это проще, дурачок!" Человеку психологически трудно спуститься к отрицательным числам в ответе, машина же делает это спокойно, без всяких предрассудков. Не дает отрицательного ответа и аналитическое решение задачи — поиск целочисленных корней одного уравнения с двумя неизвестными.

6.6. Размерность в Mathcad-программах

По знаменитой формуле Н. Вирта программа — это сумма алгоритма и структуры данных. Алгоритм программы в среде Mathcad задается нажатием десяти кнопок на панели программирования (см. рис. 6.3), отвечающих за циклы, альтернативы и прочее. Структура данных в среде Mathcad также проста. Там есть только вещественные переменные двойной точности, которые могут превращаться в комплексные или целочисленные, группироваться в векторы и матрицы (простые и вложенные), принимать, если надо, текстовые значения.

Тип числовой переменной в среде Mathcad, как уже было отмечено в разд. 1.4, в какой-то мере заменяется размерностью хранимой величины, что очень удобно в инженерных расчетах. По программе на рис. 6.21 рассчитываются два параметра треугольника. Аргументами функции Параметры_треуг могут быть величины с разной размерностью длины (метры — m, сантиметры — cm, дюймы — in). Система Mathcad сама в них разберется, сделает нужные пересчеты и выдаст правильный результат в выбранной пользователем системе единиц (кг-м-с, г-см-с, СИ или британская).

Рис. 6.21. Размерная Mathcad-программа

Система Mathcad, кроме того, ведет контроль размерностей и не позволяет складывать, например, метры с килограммами или секундами.

Функция в среде Mathcad, как уже отмечалось, может возвращать несколько значений, объединенных в вектор. Программно заданная пользовательская функция на рис. 6.21 должна возвращать значение периметра и площади треугольника. Но если пользователь захочет сделать размерными аргументы, чтобы функция вернула ему значение периметра и площади треугольника с соответствующими размерностями, то его ждет неудача. При этом будет выдано дезинформирующие сообщения об ошибке, которые в переводе гласят: "Несоответствие единиц измерения" (Mathcad 11), "Это значение должно быть безразмерным" (Mathcad 12), или …. Пользователь будет думать, что он складывает метры с килограммами, но причина в другом — вектор в среде Mathcad может хранить переменные только одной размерности.

Ошибок в среде Mathcad, как и в любой другой программной среде, достаточно. Автор акцентирует внимание на этой ошибке по двум причинам. Во-первых, автор когда-то потратил уйму времени, отлаживая программу, подобную программе на рис. 6.21, и не понимая, в чем суть ошибки. Во-вторых, дал об этом знать разработчикам Mathcad, но эта ошибка почему-то так и не была исправлена. Кстати, об ошибках...

Считается, что по-настоящему красивая женщина ("чертовски красивая") непременно должна иметь внешний дефект (ошибку Природы), небольшой, но сразу бросающийся в глаза: вздернутый нос, родинка, веснушки... Такие "украшения" лишний раз напоминают о том, что это не богиня, от которой лучше держаться подальше, не бездушная "кукла восковая", а земная женщина. Вот хрестоматийный пример: Наталья Николаевна Пушкина (урожденная Гончарова) — петербургская красавица, которая, тем не менее, чуть-чуть косила[31]. А вот другой пример — Шекспир воспел "смуглую леди сонетов" в те времена, когда белизна лица считалась непременным атрибутом женской красоты.

Слово "чуть-чуть", только что промелькнувшее в тексте, напоминает о кратком, но точном определении художественного вкуса: "Искусство — это чувство меры". Рафинированное произведение искусства, созданное на основе чистых канонов, находится как бы в неравновесном состоянии. Маленький щелчок ("глюк" в программе, родинка на лице красавицы или ее легкое косоглазие, кривая колокольня в Пизе, корявый автограф в углу картины или темный штрих в биографии художника или программиста[32], на худой конец) сталкивает эту шаткую балансирующую конструкцию либо в чулан поделок (кич), либо в сокровищницу шедевров.

А вот еще пример, поворачивающий проблему на новую грань, где пересекаются плоскости формы и содержания. Сергей Довлатов в своих записках упоминает об известном профессоре-филологе с такими косыми глазами, что с ним трудно было общаться — непонятно, в какой глаз нужно смотреть. Этот профессор, прикрывая рукой левый глаз, говорил собеседнику: "Смотрите в правый. На левый не обращайте внимания. Левый — это дань формализму". Хорошо дурачиться, создав предварительно целую филологическую школу — ошибки простительны только в гениальных программах.

Если читатель увидит опечатки или даже ошибки в этой книге, то автор просит, во-первых, сообщить ему об этом, а, во-вторых, перечитать то, что написано выше.

Итак. Работа с размерностями вообще требует особой аккуратности. Вот хороший совет при работе в среде Mathcad. Вводить в Mathcad-документ новую переменную лучше оператором m= (вывод значения), а не оператором m:= (ввод значения). Этим пользователь проверяет лишний раз, не занята ли уже переменная m хранением заданной ранее величины. В Mathcad 7 и выше этот прием не обременителен, т. к. в среде этих версий оператор m= автоматически (SmartOperator) превращается в оператор m:=, если переменная m свободна от хранения чего-либо, и мы об этом уже писали в гл. 1.

Предпроверку переменных особо можно рекомендовать в расчетах с использованием размерностей физических величин. В этом режиме (а он включается по умолчанию) предопределенными (системными) будет огромное число "популярных" переменных (A, c, C, F, g, H, J, K, L, m, N и т. д.), хранящих единичные значения физических величин (сила тока, скорость, заряд, емкость, ускорение, индуктивность, энергия, температура, длина, количество вещества и т. д.). В таком расчете "невинное" выражение m:=3 развалит весь стройный порядок единиц измерения: вместо метров появится, черт знает что.

Примечание

Если единицы физических величин не используются, то нужно их совсем отключить (диалоговое окно Unit System (), вызываемое командой Worksheet Options в меню Tools).

Можно заставить функцию, показанную на рис. 6.21, выдавать две величины не сразу, а попеременно — в зависимости от значения дополнительного аргумента (решение 1): если i="Периметр", то функция Параметры_треуг возвращает периметр треугольника, если i="Площадь", то — площадь. В среде Mathcad 11 такой прием был допустим, а в среде Mathcad 12 — нет.

Кардинальное решение проблемы — лишение переменных размерностей в массиве, и возврат их вне программы (решение 2 на рис. 6.21).

6.7. Отладка программ

Одна из самых слабых сторон средств программирования, встроенных в Mathcad было это практически полное отсутствие каких-либо специализированных инструментов отладки программ. Программу можно написать и за час (день, неделю, месяц...), но искать ошибку в ней можно весь оставшийся день (неделю, месяц, год...). Поэтому все традиционные среды программирования, как правило, имеют минимум три инструмента отладки — трассировка программы, пошаговое (пооператорное) выполнение программы и наблюдение за значениями переменных при выполнении программы. С другой стороны, Mathcad-программы — это обычно небольшие фрагменты, совсем не похожие на тех монстров в несколько тысяч строк, с какими приходится сталкиваться в традиционном программировании и которые без специальных средств отладить практически невозможно.

Мы уже отмечали, что механизм локальных переменных Mathcad, задуманный как средство реализации принципа программирования "снизу вверх" (см. рис. 6.4), препятствует наблюдению за значениями переменных программы вне программы. Это ограничение можно обойти, заставив Mathcad-программу возвращать не требуемое (запланированное) значение, а массив (матрицу или вектор), хранящий в качестве своих элементов все локальные переменные (см. пример на рис. 6.7). После такой отладки программы массив можно заменить на скаляр либо оставить все как есть, а оператором  [ () изымать из массива, возвращаемого программой, нужный элемент (см., например, рис. 6.8). Тут, правда, нужно быть осторожным в том случае, если переменные программы хранят разноразмерные значения (см. рис. 6.21).

Если необходимо следить за значением локальной переменной или группы переменных в цикле, то можно порекомендовать в тело цикла вставить оператор return  if , поставив за словом if управляющую булеву переменную. Все псевдоанимации, о которых было рассказано в книге (см. разд. 1.7 и рис. 1.75, 1.76, 2.2, 2.6, 3.26 и 3.33), — это по своей сути Mathcad-программы в режиме отладки, когда нажатие кнопки Submit увеличивает на единицу значение переменной rc, что влечет за собой вывод новых данных из цикла с последующей их графической иллюстрацией.

Но изменяющиеся в цикле значения переменных можно видеть и непосредственно в самой программе, если вставить в цикл тандем операторов  ¬  ® (локальное присваивание и символьный вывод), о котором мы уже упоминали в гл. 1 (см. рис. 1.11). В программе, показанной на рис. 6.22, этот тандем позволил создать в среде Mathcad... секундомер.

Рис. 6.22. Секундомер в Mathcad

Если в Mathcad-программу ввести бесконечный цикл и вызывать в нем функцию time, а разницу во времени запуска программы и вызовом функции time выводить символьно[33], то и получится секундомер — справа от символа ® будут мелькать числа, отсчитывающие секунды, дающие информацию о том, что происходит в самом цикле, а это и есть суть отладки.

Но наблюдение за значениями переменных в цикле — это скорее курьез[34], чем серьезный инструмент отладки Mathcad-программ. "По серьезному" делать трассировку цикла можно через другой недокументированный прием — через замену оператора, добавляемого нажатием кнопки Add Line, на оператор вставки массива — вектора или матрицы, как это показано на рис. 6.23.

Рис. 6.23. Программа-матрица

В верхней части рис. 6.23 показана коротенькая Mathcad-программа поиска нуля функции методом Ньютона (касательных). В нижней части рис. 6.23 представлена та же программа, но со вставленным счетчиком цикла n и телом цикла в виде матрицы (а не виде цепочки операторов, введенных через нажатие кнопки Add Line), значение которой присваиваются вектору V. По завершении работы программы этот составной массив V (вектор, элементы которого — массивы) выводится на дисплей и отображает всю историю поиска нуля функции.

На рис. 6.23а показана Mathcad-программа вычисления суммы ряда.

Рис. 6.23а. Программа вычисления суммы ряда (слева) и она же с операторами сохранения промежуточных результатов (справа)

Первый вариант программы, показанный на рис. 6.23а, возвращает только итоговый результат, второй же за счет ввода в программу дополнительных операторов и счетчика n – всю историю суммирования. Этот прием стал встроенным в среде Mathcad 13, где появились следующие инструменты отладки программы:

·         две встроенные функции trace и pause;

·         окно трассировки программы;

·         панель с четырьмя кнопками отладки;

На рис. 6.23в и 6.23с показана работа этих инструментов.

Главным недостатком пользовательских приемов отладки, показанных на рис. 6.24 и 6.24а, было то, что если программа зависнет, то нельзя будет узнать ни сам итоговый результат, ни содержание "трассировочной" матрицы.

Этот недостаток устранен с вводом в Mathcad 13 встроенных средств отладки.

Рис. 6.23a. Программа с выводом всех данных из цикла (pic)

Рис. 6.23. Трассировка программы

На рис. 6.23в показано, как в программу поиска нуля функции f(x) вставлена дополнительная отладочная строка с новой функцией trace (трассировка). Кроме того, через команду View/Trace Windows открыто одноименное окно отладки и включен режим Toggle Debugging через меню Tools. После этих манипуляций при вызове функции Root_Newton, в окне Trace Windows будут отображаться значения переменных, перечисленных в качестве аргументов функции trace.

Если же в программе окажется бесконечный цикл, то выявить причины его "бесконечности" поможет вторая отладочная функция Mathcad 13 – функция pause (пауза), работа которой показана на рис. 6.23с.

Рис. 6.23с. Трассировка программы с прерываниями

Если в "отладочную" программу вставить не функцию trace, а функцию pause, то в окне Trace Windows будет выдаваться очередная строка значений аргументов функции pause, а сам расчет прерываться (детская игровая команда "замри!"). Продолжить расчет можно, нажав на кнопку Resume новой панели инструментов, появившейся в Mathcad 13 и показанной в правом верхнем углу рис. 6.23с.

6.8. Локальная функция

В Mathcad 12 были включены локальные функции. Теперь в любой строке программы можно записать f(x,...) и нажать кнопку ¬ в панели инструментов Programming (Программирование) и ввести в программу локальную функцию, область видимости которой ограничена местом вода этой функции и концом самой программы. На рис. 6.24 показана программно созданная функция с именем Min_GR поиска минимума функциональной зависимости методом золотого сечения, содержащая локальную функцию с именем GR, делящую отрезок неопределенности в золотом соотношении (Golden Ratio). Функция GR вызывается только в теле программы-функции Min_GR и нигде более. Этим и оправдана ее локальность. Но!

Рис. 6.24. Локальная функция

Если функцию GR вызвать вне тела цикла программы Min_GR, то ее работа прервется сообщением об ошибке, в переводе гласящим: "Данная переменная или функция не определена". Хорошо ли это или плохо?! С одной стороны, хорошо. Различные Mathcad-программы, разработанные разными людьми и собранные в одном Mathcad-документе, могут иметь одноименные локальные функции (y(x), например), которые не будут "глушить" друг друга. Так был задуман и механизм локальных переменных, появившийся одновременно с появлением программирования в Mathcad (6-я версия). Тогда о локальных функциях, по-видимому, забыли или у разработчиков руки до них не дошли. С другой стороны, механизм локальности переменных и функций препятствует процессу отладки программ. Это тем более досадно, если учесть, что в среде Mathcad нет (см. выше) встроенных средств отладки, а есть набор всяких пользовательских хитростей и трюков для поиска ошибок в программах. Эта идея отражена на рис. 6.4 с простенькой программой, содержащей переменные a, b и c. Было бы хорошо, если бы эти переменные стали видимы и вне программы, и можно было бы фиксировать их текущие значения. Потом, после отладки программы эти переменные можно сделать локальными, поменяв их стиль Variables на Local Var, например (см. рис. 6.4, внизу).

Как пользователь вводит локальную функцию в программу? Он, конечно, должен писать ее не в теле программы[35], а вне ее, чтобы иметь возможность тестировать ее — изменять значение аргументов и смотреть, что она возвращает. После отладки у функции нужно будет изменить знак (оператор) := на ¬, а саму функцию перенести (перетащить) в программу, где она автоматически станет локальной и невидимой вне тела программы. А можно просто наложить GR-функцию на тело программы Min_GR, заменив знак := на º (или с самого начала иметь не знак :=, а знак º), а стиль имени функции с Variables на Local Var, например. Так, собственно, и поступал автор уже давно (см. рис. 1.60 и сайт http://twt.mpei.ac.ru/mas/worksheets/Gold_Ratio.mcd с программой поиска минимума функции методом золотого сечения (технология Mathcad Application Server), а также совет 185 из книги "Советы пользователям Mathcad" — http://twt.mpei.ac.ru/ochkov/Sovet_MC).

О знаках := и ¬. Хорошо бы было, чтобы оператор := в программах присваивал соответствующей функции или переменной стиль Variables, "видимый" и вне программы, а оператор ¬ — стиль Local Var, видимый только в программе, а может быть, и вне программы тоже. Это внесло бы существенный вклад в процесс создания встроенных инструментов отладки программ Mathcad, о которых мы говорили выше. Еще было бы лучше, если бы одной командой (переключателем) можно было бы менять с локального на глобальный стиль всех объявленных в программе переменных и функций.

6.9. Программирование для Mathcad на C/C++

Раздел написан К.Орловым

Очень удобным и мощным инструментом Mathcad, напрямую связанным с "настоящим" традиционным программированием, является перевод пользовательских функций в разряд встроенных через механизм DLL (Dynamic Link Library). Кроме того, данный инструмент открывает возможность использования в среде Mathcad функций Windows, недоступных напрямую.

На рис. 6.25 показано, как среди встроенных функций Mathcad появились специальные функции, связанные с профессиональной деятельностью автора, — функции по термодинамическим свойствам воды, водяного пара и смесей газов, необходимых для теплотехнических расчетов (см. www.wsp.ru и сами расчеты на MAS www.vpu.ru/mas).

Рис. 6.25. Пользовательская встроенная функция в Mathcad

Сами функции, созданные через механизм DLL имеют универсальный характер и могут подключаться к другим расчетным и программным средам, к Excel, например (рис. 6.26).

Рис. 6.26. Пользовательская встроенная функция в Excel

6.9.1. Разработка пользовательской функции на C/C++ для Mathcad

В данном разделе (он написан К. А. Орловым[36]) приведена минимальная последовательность действий для создания пользовательской библиотеки на C/C++ для Mathcad при помощи механизма UserEFI (интерфейс взаимодействия динамических библиотек Windows (DLL) и Mathcad). Целью будет создание пользовательской функции, вычисляющей площадь конуса по формуле:

V(d, h) = 1/3 h d2/4

Функция будет иметь имя ConeVolume и указываться в диалоговом окне Insert Function (Вставка функции) в категории (Расчет объемов тел).

В качестве среды разработки пользовательской библиотеки для Mathcad возьмем Microsoft Visual Studio .Net 2003. Выбор данной среды разработки неслучаен: именно ее используют разработчики пакета Mathcad и именно для компиляторов Microsoft в последних версиях Mathcad поставляются файлы примеров, заголовочные и lib-файлы.

В качестве имени пользовательской библиотеки будет указываться строка "mymcfunc", образованная от словосочетания "MY MathCad FUNCtions".

Архив с файлами проекта доступен в Интернете по адресу: http://twt.mpei.ac.ru/orlov/mathcad/mymcfunc.zip.

Для создания пользовательской функции Mathcad на C/C++ требуется выполнить семь шагов:

1.     Создать заготовку проекта, которую будем в дальнейшем изменять.

2.     Подключить к проекту специальные заголовочный и библиотечный файлы, идущие в комплекте поставки Mathcad.

3.     Создать и заполнить массив (таблицу) сообщений ошибок, могущих возникнуть при вызове пользовательской функции.

4.     Написать непосредственно код функции по некоторым правилам, определенным в механизме UserEFI.

5.     Создать и заполнить структуру, описывающую пользовательскую функцию для ее подключения к Mathcad.

6.     Написать код регистрации таблицы сообщений об ошибках и пользовательской функции.

7.     Создать специальный файл с описанием пользовательской функции для отображения информации о ней в диалоге Insert Function (Вставка функции).

Проделаем эти шаги.

Создание заготовки проекта

Выполним следующие шаги:

1.     В окне программы Microsoft Visual Studio выберем пункт File | New | Project (Файл | Создать | Проект).

2.     В появившемся окне укажем тип проекта Win32 Project, его имя и папку (рис. 6.27). В качестве имени будем использовать "mymcfunc". Такое имя будет иметь пользовательская библиотека.

Рис. 6.27. Создание проекта в Microsoft Visual Studio

3.     После нажатия кнопки OK появится диалог мастера создания проекта. В разделе Application Settings (Настройки приложения) необходимо указать тип приложения (Application type): DLL, и нажать кнопку Finish (Готово) — рис. 6.28. После этого будут сгенерированы исходные файлы проекта: stdafx.h, stdafx.cpp и mymcfunc.cpp.

Рис. 6.28. Настройки мастера создания проекта библиотеки

Подключение файлов mcadincl.h и mcaduser.lib

Для создания пользовательских функций для Mathcad необходимо подключить к созданному проекту заголовочный файл mcadincl.h и библиотечный файл mcaduser.lib. В файле mcadincl.h находятся определения констант, типов и прототипы функций используемых при взаимодействии Mathcad и пользовательской библиотеки. Для компиляторов фирмы Microsoft он находится в папке \UserEFI\MicroSft\Include, вложенной в папку установки Mathcad, а файл mcaduser.lib — в папке \UserEFI\MicroSft\Lib.

Для подключения заголовочного файла mcadincl.h необходимо добавить следующие строки в файл stdafx.h проекта:

// TODO: reference additional headers your program requires here

#include "D:\Program Files\Mathsoft\Mathcad 12\UserEFI\Microsft\include\mcadincl.h"

Примечание

В случае если пакет Mathcad установлен в другую папку, следует отредактировать путь к файлу mcadincl.h.

Подключить файл mcaduser.lib можно, добавив ссылку на файл в настройках проекта, и отредактировать список путей к lib-файлам проектов C/C++ или же, что несколько проще и нагляднее, добавив в файл stdafx.h следующую строку:

#pragma comment(lib, "D:\\Program Files\\Mathsoft\\Mathcad 12\\UserEFI\\Microsft\\lib\\mcaduser.lib")

Данная строка указывает компоновщику, что необходимо использовать файл mcaduser.lib. Двойной обратный слеш (\\) в пути выступает в качестве escape-последовательности C/C++, означающей обычный обратный слеш (\).

Примечание

Если пакет Mathcad установлен в другую папку, необходимо отредактировать путь к файлу mcaduser.lib.

Создание таблицы описания ошибок

Разрабатываемая нами функция будет осуществлять проверку на правильность входных данных и сигнализировать об ошибке, аналогично встроенным функциям Mathcad. Помимо всего прочего, для этого необходимо создать таблицу (массив) строк, в которой будет храниться текстовое описание ошибок.

При вызове функции, вычисляющей объем конуса, могут быть следующие ошибки:

r    аргументы функции (диаметр основания и высота конуса) содержат мнимую часть;

r    аргументы функции меньше нуля.

Создадим массив с текстовыми описаниями ошибок, добавив в файл mymcfunc.cpp после строки

#include "stdafx.h"

следующий код:

char * myErrorMessageTable[]=

{

  "Число должно быть действительным"[37],

  "Число не должно быть отрицательным",

};

Работа с таблицей описаний ошибок приведена далее.

Разработка кода функции

Следующий шаг — это создание непосредственного кода функции. Для этого добавим в файл mymcfunc.cpp после массива с описанием ошибок (см. предыдущий раздел) следующий код:

// Код функции для вычисления объема конуса

LRESULT ConeVolumeImpl(

          COMPLEXSCALAR * const Result,

          const COMPLEXSCALAR * const d,

          const COMPLEXSCALAR * const h)

{

  // Проверка на отсутствие мнимой части у первого аргумента –

  // диаметра основания конуса

  if (d->imag != 0.0)

  {

    // Возвращение значения, сигнализирующего об ошибке:

    //   код (номер) ошибки: 1

    //   номер аргумента функции, содержащего ошибку: 1

    return MAKELRESULT(1, 1);

  }

  // Проверка на отсутствие мнимой части у второго аргумента –

  // высоты конуса

  if (h->imag != 0.0)

  {

    // Возвращение значения, сигнализирующего об ошибке:

    //   код (номер) ошибки: 1

    //   номер аргумента функции, содержащего ошибку: 2

    return MAKELRESULT(1, 2);

  }

 

  // Проверка на неотрицательность первого аргумента 

  // диаметра основания конуса

  if (d->real < 0.0)

  {

    // Возвращение значения, сигнализирующего об ошибке:

    //   код (номер) ошибки: 2

    //   номер аргумента функции, содержащего ошибку: 1

    return MAKELRESULT(2, 1);

  }

  // Проверка на неотрицательность второго аргумента - высоты конуса

  if (h->real < 0.0)

  {

    // Возвращение значения, сигнализирующего об ошибке:

    //   код (номер) ошибки: 2

    //   номер аргумента функции, содержащего ошибку: 2

    return MAKELRESULT(2, 2);

  }

 

  // Вычисление объема конуса и присвоение значения

  // реальной части переменной Result

  Result->real = 1.0 / 3.0 *

    3.141592653589793 *

    h->real *

    d->real * d->real

    / 4.0;

 

  // Мнимая часть переменной Result должна быть пустой

  Result->imag = 0.0;

 

  // Функция должна возвращать 0 в случае успешного завершения

  return 0;

}

Приведенный код содержит функцию ConeVolumeImpl, тип возвращаемого значения которой — LRESULT. Все пользовательские функции для Mathcad, работающие по механизму UserEFI должны иметь такой тип возвращаемого результата.

В случае успешного выполнения функция должна вернуть нулевое значение. Иначе функция возвращает 32-х битное число: в младшем 16-ти битном слове хранится номер ошибки (начиная с 1), а в старшем — номер аргумента функции, содержащего неправильное значение (нумерация также начинается с 1). Допускается в качестве номера ошибочного аргумента возвращать 0, что означает, что ошибка относится ко всей функции, а не к отдельному аргументу.

Функция ConeVolumeImpl имеет три аргумента: Result, d, h.

В механизме UserEFI первый аргумент функции (в нашем случае с именем Result) — это результат, возвращаемый при вызове функции в Mathcad, т. е. тот результат, который мы получим в рабочем документе Mathcad. В нашем случае он имеет тип COMPLEXSCALAR * const (т. е. постоянный указатель на переменную типа COMPLEXSCALAR). Тип COMPLEXSCALAR описан в файле mcadincl.h как:

// complex scalar type

typedef struct tagCOMPLEXSCALAR {

    double real;

    double imag;

} COMPLEXSCALAR;

Это означает, что переменная типа COMPLEXSCALAR позволяет хранить комплексное число: поле real (с типом double — вещественное двойной точности) хранит  действительную часть, а поле imag (c типом double — вещественное двойной точности) — мнимую часть числа.

Использование типа COMPLEXSCALAR для первого аргумента функции означает, что в рабочем документе функция будет возвращать скалярное значение.

Второй и третий аргументы функции (с именами d и h) хранят значения диаметра основания и высоты конуса соответственно. Они также имеют тип указателя на переменную типа COMPLEXSCALAR с единственным отличием: они постоянны (const), что означает, что изменять их значения запрещено.

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

r    сравниваются мнимые части переменных d и h с нулем, путем обращения к полю imag. В случае отличия мнимой части от нуля, функция возвращает ненулевое значение, содержащее номер ошибки (1) и номер аргумента (1 — для d и 2 — для h);

r    сравниваются действительные части переменных d и h с нулем, путем обращения к полю real. В случае отрицательного значения, функция возвращает ненулевое значение, содержащее номер ошибки (2) и номер аргумента (1 — для d и 2 — для h).

После всех проверок производится непосредственно расчет объема конуса. В качестве высоты конуса используется значение действительной части аргумента h (поле h->real), а в качестве диаметра основания конуса — значение действительной части аргумента d (поле d->real). Полученное значение объема конуса заносится в действительную часть первого аргумента Result (Result->real).

В конце функции производится возврат нулевого значения (return 0;), что указывает на успешное выполнение функции.

Создание структуры с информацией о функции

Для каждой функции пользователя Mathcad, созданной по механизму UserEFI, требуется заполнить описывающую ее специальную структуру. По информации из этой структуры Mathcad определяет имя функции, ее адрес количество и типы передаваемых аргументов и возвращаемого результата.

Для создания такой структуры добавим в файл mymcfunc.cpp после кода функции ConeVolumeImpl (см предыдущий раздел) следующие строки:

FUNCTIONINFO fiConeVolume =

{

  "ConeVolume",

  "d, h",

  "объем конуса с диаметром основания d и высотой h",

  (LPCFUNCTION) ConeVolumeImpl,

  COMPLEX_SCALAR,

  2,

  {COMPLEX_SCALAR, COMPLEX_SCALAR}

};

Данный код создает глобальную переменную типа FUNCTIONINFO, который определен в файле mcadincl.h как:

typedef struct tagFUNCTIONINFO {

    char * lpstrName;

    char * lpstrParameters;

    char * lpstrDescription;

    LPCFUNCTION lpfnMyCFunction;

    long unsigned int returnType;

    unsigned int nArgs;

    long unsigned int argType[MAX_ARGS];

} FUNCTIONINFO;

Первое поле структуры — lpstrName — имя функции, которое будет использоваться в документах Mathcad и в диалоге вставки функции. Как было ранее указано, функция будет иметь имя ConeVolume.

Внимание

Имена функции в проекте на C и в Mathcad не совпадают.

Второе поле структуры — lpstrParameters — содержит строку с перечнем аргументов функции. Эта строка отображалась в диалоговом окне Insert Function (Вставки функции) в Mathcad 7. Начиная с 8-й версии, перечень аргументов указывается в xml-файле описания функций, структура и содержимое которого будут представлены далее. Если вы разрабатываете программу для последних версий Mathcad (8, ..., 11, 12 и т. д.), то для уменьшения объема кода в данном поле можно указать пустую строку ("").

Третье поле структуры — lpstrDescription — содержит строку с описанием функции. Как и предыдущее поле, эта строка отображалась в диалоговом окне Insert Function (Вставки функции) в Mathcad 7, а с 8-й версии описание функции берется из специального xml-файла. Для последних версий Mathcad в качестве значения можно задавать пустую строку.

Четвертое поле структуры — lpfnMyCFunction — указатель на функцию типа LPCFUNCTION, определенного в файле mcadincl.h как:

typedef LRESULT (* LPCFUNCTION )

  ( void * const, const void * const, ... );

Данная запись означает, что пользовательская функция для Mathcad, созданная по механизму UserEFI, на языке C всегда должна иметь тип результата LRESULT, изменяемый первый аргумент, который будет хранить возвращаемый в Mathcad результат, и не менее одного неизменяемого аргумента. Помимо всего прочего, это означает, что по механизму UserEFI невозможно создать функцию без аргументов. Даже если функция не использует аргументы, необходимо указывать хотя бы один аргумент-заглушку.

Пятое поле структуры FUNCTIONINFO — поле returnType — это число, характеризующее тип результата, возвращаемого функцией. Значения для каждого из возможных типов результатов приведены в файле mcadincl.h. В нашем случае результат вычисления функции имеет тип COMPLEXSCALAR, поэтому используется значение, определенное под именем COMPLEX_SCALAR.

Шестое поле — nArgs — содержит количество аргументов функции.

Седьмое (последнее) поле — argType[] — массив чисел, характеризующих тип аргументов функции (аналогично пятому полю returnType). В нашем случае используются два аргумента типа COMPLEXSCALAR, поэтому указываются идентификаторы COMPLEX_SCALAR.

Подключение пользовательской функции к Mathcad

Подключение пользовательской функции по механизму UserEFI происходит следующим образом:

1.     Mathcad загружает все библиотеки (файлы с расширением dll) из папки UserEFI (находится в папке установки Mathcad).

2.     При загрузке библиотека должна зарегистрировать при наличии свою таблицу описания ошибок и пользовательских функции для пакета Mathcad, определенных в ней.

3.     В случае успешной регистрации пользовательских функций они становятся аналогичны встроенным функциям пакета Mathcad.

Информация для диалога Insert Function (Вставки функции), начиная с 8-й версии Mathcad, берется, в случае наличия, из специальных xml-файлов, структура которых описана в следующем разделе. В 7-й версии пакета Mathcad использовалась информация из описывающей функцию структуры (см. предыдущий раздел).

Для обработки события загрузки библиотеки и регистрации функции необходимо внести изменения в функцию DllMain в файле mymcfunc.cpp:

BOOL APIENTRY DllMain(

      HINSTANCE hModule,

      DWORD ul_reason_for_call,

      LPVOID lpReserved)

{

  switch (ul_reason_for_call)

  {

    case DLL_PROCESS_ATTACH:

      {

        if (CreateUserErrorMessageTable(hModule, 2, myErrorMessageTable))

        {

          CreateUserFunction(hModule, &fiConeVolume);

        };

      }

      break;

  }

  return TRUE;

}

Функция DllMain вызывается операционной системой в различных ситуациях (определяется параметром ul_reason_for_call). Параметр ul_reason_for_call равен значению DLL_PROCESS_ATTACH при загрузке библиотеки. В приведенном выше коде в этом случае производится:

r    во-первых, регистрация таблицы сообщений об ошибках (см. разд. "Создание таблицы описания ошибок" ранее в этой главе) путем вызова функции CreateUserErrorMessageTable. Первый аргумент функции — hModule —дескриптор текущего модуля (библиотеки с пользовательскими функциями); второй аргумент — число сообщений об ошибках; третий аргумент — указатель на массив, содержащий описание ошибок;

r    во-вторых, в случае успешной регистрации таблицы сообщений об ошибках, регистрация пользовательской функции путем обращения к функции CreateUserFunction. Первый аргумент функции — hModule — дескриптор текущего модуля; второй аргумент — указатель на структуру, содержащую информацию о пользовательской функции (см. предыдущий раздел).

После внесения всех указанных изменений необходимо скомпилировать библиотеку и полученный файл с расширением dll (в нашем случае mymcfunc.dll) поместить в папку UserEFI, вложенную в папку установки Mathcad. В нашем случае это D:\Program Files\Mathsoft\Mathcad 12\UserEFI.

После запуска Mathcad станет возможным использование разработанной пользовательской функции. Пример ее применения показан на рис. 6.29.

Рис. 6.29. Пример применения пользовательской функции

Разработанная функция обладает одним недостатком — если пользователь не знает о ней, то он никогда и не сможет узнать. Для того чтобы описание функции было доступно пользователю, необходимо добавить информацию о ней в специальный xml-файл описания функции (см. следующий раздел).

Файл описания функции

Начиная с 8-й версии пакета Mathcad, стало возможным использование категорий функций (рис. 6.30) в диалоге "Вставка функции" (Insert Function). Использование же механизма UserEFI не позволяло указывать категорию функции (для этого необходимо было внести изменения в UserEFI и сделать его несовместимым с предыдущей версией), а также не позволяло указывать ссылку на справку по функции, которая также вызывается из диалога вставки функции.

Рис. 6.30. Диалог вставки функции в Mathcad

Для указания информации о функции в диалоговом окне Insert Function (Вставка функции) в пакете Mathcad, начиная с 8-й версии, необходимо создать специального вида xml-файл и поместить его в папку DOC\FUNCDOC папки установки Mathcad. В нашем случае имя файла — mymcfunc_en.xml и размещаться он будет в папке D:\Program Files\Mathsoft\Mathcad 12\Doc\Funcdoc\. Содержимое файла таково:

<?xml version="1.0" encoding="windows-1251"?>

<FUNCTIONS>

<help_file></help_file>

<function>

  <name>ConeVolume</name>

  <params>d, h</params>

  <category>Расчет объемов тел</category>

  <description>Вычисляет объем конуса с диаметром основания d и высотой h.</description>

  <help_topic></help_topic>

</function>

</FUNCTIONS>

Верхняя строчка в приведенном тексте стандартная для всех xml-файлов (в которых используется Windows-кодировка русских букв). На следующей строке открывается корневой элемент xml-файла: FUNCTIONS. Для файлов описания функций пакета Mathcad эта строчка должна присутствовать без изменений.

На третьей строке находится элемент xml-файла (с именем help_file), содержимое которого указывает на chm-файл справки, связанной с функциями для Mathcad, описываемыми в xml-файле. В нашем случае файл справки отсутствует и вместо его имени указана пустая строка. Если файл справки существует, то вместо пустой строки необходимо указывать путь к нему относительно данного xml-файла описания функции. В случае отсутствия файла справки используется файл справки по умолчанию — mcad.chm (или mcad_en.chm), находящийся в папке DOC папки установки Mathcad. Хотелось бы отметить, что практически во всех версиях Mathcad, начиная с 8-й и заканчивая 11-й, в обработке xml-файлов описания функций была ошибка, и файл справки mcad.chm использовался всегда, независимо от содержимого элемента help_file.

На четвертой строке открывается элемент function, который содержит информацию о функции для пакета Mathcad. Для функции указывается следующая информация:

r    в элементе name — имя функции, как оно было задано в структуре информации о функции (см. разд. "Создание структуры с информацией о функции" ранее в этой главе) и как оно используется в документах Mathcad;

r    в элементе params — список параметров функции;

r    в элементе category — категория функции при ее отображении в диалоговом окне Insert Function (Вставка функции);

r    в элементе description — краткое описание функции, ее аргументов и возвращаемого результата;

r    в элементе help_topic — раздел из файла справки, связанный с функцией (появляется при нажатии кнопки в левом нижнем углу диалогового окна Insert Function (Вставка функции)). В нашем случае элемент help_topic содержит пустую строку, т. к. справка по функции отсутствует.

На десятой и одиннадцатой строках производится закрытие элементов function и FUNCTIONS.

В том случае, если пользовательских функций несколько, то необходимо создать на каждую функцию свой элемент function. с соответствующими элементами name, params, category и т. д.

Рассмотрим вопрос об имени xml-файла. Выше было сказано, что в качестве имени будет использоваться mymcfunc_en.xml, что связано со следующими причинами.

r    В документации к Mathcad сказано, что пользовательские функции должны быть описаны в едином файле user.xml. Однако, по мнению автора, это несколько неправильно. Тогда для разных библиотек пользовательских функций необходимо иметь один единый файл справки, а для редактирования информации в xml-файле придется искать вхождения функций из одной библиотеки среди общего перечня, что может быть затруднительно. Поэтому целесообразнее использовать для каждой библиотеки пользовательских функций отдельный xml-файл описаний функций с именем, совпадающим с именем библиотеки.

r    Начиная с 11-й версии пакета Mathcad, для всех файлов описания функций необходимо добавлять к имени файла окончание "_EN". Если имя файла не заканчивалось на эту строку, то файл игнорировался. По-видимому, это связано с желанием разработчиков Mathcad создать простую поддержку многоязычных версий. Однако проверить это у автора не было возможности, и он просто констатирует, что, для того чтобы файл описания функций обрабатывался в 11-й версии Mathcad, его имя должно заканчиваться на строку "_EN". При этом предыдущие версии Mathcad обрабатывают все файл, независимо от их имени, что позволяет "безболезненно" добавить к имени файла требуемое для Mathcad 11 окончание.

После формирования xml-файла описания функции и его сохранения в требуемой папке, запустим Mathcad и обратимся к диалоговому окну Insert Function (Вставка функции). Созданная нами функция ConeVolume появилась в этом диалоге аналогично встроенным функциям пакета Mathcad (рис. 6.31).

Рис. 6.31. Диалоговое окно Insert Function с пользовательской функцией

6.9.2. Использование разработанной пользовательской функции

Далее будут рассмотрены некоторые вопросы использования разработанной ранее пользовательской функции ConeVolume.

Отладка кода пользовательской функции

Наибольший недостаток встроенного программирования в документах Mathcad заключается в отсутствии нормального отладчика — специальной программы, позволяющей выполнять программу по шагам, отслеживать значения переменных и т. д. Иногда даже бывает проще сначала написать последовательность действий с помощью обычных формул Mathcad (что позволяет просматривать значения на каждом шагам), а затем, путем копирования, свести их в единую программу.

При разработке пользовательских функций на языке C/C++ ситуация несколько другая — при отладке мы можем широко использовать мощные возможности отладчика, например, среды разработки (в нашем случае Microsoft Visual Studio). Это позволяет выявлять ошибки в коде программы, если что-то работает не так, как надо.

Для использования режима отладки необходимо:

1.     Включить конфигурацию Debug (Отладка), при которой происходит генерация отладочной информации. Для этого необходимо в среде Visual Studio выбрать пункт меню Build | Configuration Manager... (Построить | Менеджер конфигурации) и в появившемся диалоге выбрать в качестве активной конфигурацию Debug (рис. 6.32). При создании проекта эта конфигурация выбирается по умолчанию.

2.     Указать имя исполняемого (exe) файла, который будет запускаться при отладке. Основной исполняемый файл Mathcad называется mathcad.exe и находится в папке установки Mathcad. В настройках проекта следует задать путь к этому файлу в поле команды, исполняемой при отладке (рис. 6.33).

3.     Указать папку, в которую необходимо помещать скомпилированную библиотеку. Согласно механизму UserEFI библиотека с пользовательскими функциями должна находиться в папке UserEFI папки установки Mathcad. В настройках проекта в поле Output Directory (Выходная папка) задаем имя этой папки (рис. 6.33).

Рис. 6.32. Выбор отладочной конфигурации Debug в Microsoft Visual Studio

Рис. 6.33. Настройка проекта для осуществления отладки

После запуска режима отладки командой Debug | Start (Отладка | Запуск) можно производить отладку работы функции. Так, на рис. 6.34 показана отладка функции с использованием точки останова (breakpoint). Из рисунка видно, что можно легко просмотреть все значения переменных. С помощью такой отладки при написании книги была найдена ошибка в формуле вычисления объема: вместо текста 1.0 / 3.0 было записано 1 / 3, что в отладочной конфигурации приводило к целочисленному делению 1 на 3, в результате дающему 0. Это, в свою очередь, вело к тому, что при любых аргументах функция возвращала нулевое значение объема.

Рис. 6.34. Отладка работы пользовательской функции

В версии Mathcad 2001i для защиты от несанкционированного копирования пакета Mathcad использовалась разработка сторонней фирмы. Эта разработка не разрешала запускать пакет Mathcad под отладчиком, что не позволяло производить отладку пользовательских функций. В этом случае для отладки необходимо было не запускать Mathcad-процесс (launch) под отладчиком, а присоединяться (attach) к уже запущенному процессу, т. к. проверка на работу под отладчиком производилась модулем защиты только при запуске Mathcad.

Реакция на неправильные значения аргументов

При разработке кода функции была добавлена проверка на ошибочные значения аргументов. Результат работы кода обработки ошибок показан на рис. 6.35. Текст описания ошибок берется из зарегистрированной таблицы, а код ошибки возвращается в младшем слове результата, передаваемого C-функцией.

Рис. 6.35. Обработка ошибочных значений аргументов

Сравнение скорости выполнения функций, написанных на C и в Mathcad

Одно из основных преимуществ разработки пользовательских функций на языке С, а не на встроенном языке программирования Mathcad, — это увеличение скорости вычисления, связанное с тем, что встроенный язык программирования является интерпретируемым, а С — компилируемым. В данном разделе будет проведено сравнение скорости вычисления пользовательской функции, написанной на С, со скоростью вычисления аналогичной по выполняемым действиям функции, написанной на языке программирования Mathcad.

Рис. 6.36. Документ сравнения функций на С/C++ и в Mathcad

На рис. 6.36 показан документ Mathcad, в котором производится создание функции расчета объема конуса с именем ConeVolumeM(d, h) и производится замер времени, затраченного на вычисление функций, написанных на C/C++ и на встроенном языке программирования Mathcad, 1 000 000 раз. Результаты сравнения несколько необычны — время на обращение к функции, написанной на языке программирования Mathcad меньше, чем время на обращение к функции, написанной на C/C++. Это связано со следующими причинами.

r    Расчет выполнялся в Mathcad 11 в режиме ускоренных вычислений (Higher speed calculation). Это позволяет Mathcad производить значительную оптимизацию вычислений, что и приводит к более высокой скорости расчета. При отключенном режиме ускоренных вычислений — в режиме совместимости (Backward compatibility) — картина меняется и затраты на вычисления следующие: 14.121 на функцию на языке C/C++ и 21.090 на функцию, написанную на языке программирования Mathcad. Переключение между режимами производится на вкладке Calculation (Вычисление) диалогового окна Worksheet Options (Опции Mathcad-документа), вызываемого через одноименную команду меню Tools (Сервис).

r    Формула для расчета объема конуса достаточно простая и при обращении к ней относительно велики затраты на вызов функции. При более сложных формулах картина может измениться.

Создание функций с размерностями

Одним из ограничений механизма создания пользовательских функций на C/C++ является невозможность создания функций с размерными величинами. При этом результат, возвращаемый функцией при размерных аргументах, будет зависеть от выбранной системы единиц (рис. 6.37).

Как видно из рис. 6.37, при различных системах единиц наша функция возвращает разные результаты. Это связано с тем, что при размерных аргументах, перед обращением к пользовательской функции, Mathcad приводит значение аргумента к базовой размерности выбранной системы единиц. Для размерности длины: в СИ — это метр, в U.S. — фут и т. д. И в случае, представленном на рис. 6.37, при выбранной системе единиц СИ в функцию передается значение диаметра основания и высоты конуса, равное 1.0, а при выбранной системе единиц U.S. — 3.2808398950131230 (именно столько футов в одном метре).

Рис. 6.37. Результаты вызова пользовательской функции с размерными аргументами при различных системах единиц

В то же время, функция, написанная на встроенном языке программирования Mathcad (см. рис. 6.36), возвращает правильное значение при любой выбранной системе единиц.

Для решения проблемы с размерностями можно использовать подход, примененный в одной из авторских разработок. Суть подхода — в переопределении пользовательской функции с пересчетом размерностей аргументов и результатов. На рис. 6.38 показан документ Mathcad, в котором переопределяется пользовательская функция, и представлен пример обращения к ней.

Рис. 6.38. Переопределение пользовательской функции для поддержки размерных величин

При переопределении функции используется символ глобального присваивания. Это сделано для поддержки 11-й версии пакета Mathcad, обычное присваивание в которой не работает. В правой части оператора присваивания производится проверка на соответствие размерностей аргументов (оба аргумента должны иметь размерность длины), а также производится приведение размерной величины к безразмерной относительно той размерности, которую "ожидает" пользовательская функция. В данном случае функция "ожидает" аргументы в системе СИ: значения приводятся к метрам.

Результат, возвращаемый пользовательской функцией, умножается на размерность, соответствующую размерности, к которой приводятся значения аргументов. В принципе, вместо размерности метр (m) можно было с таким же успехом использовать размерность фут (ft). Основной принцип, который должен поддерживаться, — соответствие размерностей аргументов и результата, возвращаемого функцией.

Использование размерных функций позволяет осуществлять автоматический пересчет в нужную размерность незаметно как для пользователя, так и для пользовательской функции. Помимо этого, производится контроль на правильность размерностей аргументов функции (см. сообщение об ошибке на рис. 6.38).

Формулы с переопределением пользовательских функций можно вынести в отдельный документ Mathcad и делать ссылки на него из рабочих документов (меню Insert | Reference... (Вставка | Ссылка...)), использующих размерные пользовательские функции.

При работе в пакете Mathcad 12 необходимо внести дополнительные изменения в формулу переопределения функции. Это связано с тем, что в данной версии было введено понятие пространства имен (namespaces). И для ссылки на пользовательскую функцию следует в правой части оператора присвоения ввести оператор пространства имен (нажатием комбинации клавиш <Ctrl>+<Shift>+<N>) и указать пространство имен "user". Дополнительная информация приведена в документации к 12 версии пакета Mathcad в разделе "Namespaces".

6.9.3. Дополнительные возможности программирования на языке C для Mathcad

В предыдущих разделах мы рассмотрели основы создания пользовательской функции на C/C++ для пакета Mathcad на основе механизма UserEFI. Однако некоторые моменты были опущены для большей наглядности. В следующих разделах будут рассмотрены дополнительные возможности механизма UserEFI: работа с массивами, строковыми значениями, получение и освобождение памяти, проверка отмены расчетов пользователем, создание справки по пользовательской функции. При этом будут вноситься изменения и дополнения в файлы рабочего проекта по созданию пользовательской функции ConeVolume для Mathcad. В архиве, размещенном в Интернете по адресу http://twt.mpei.ac.ru/orlov/mathcad/mymcfunc.zip, находятся файлы как исходного проекта, так и измененные.

Создание пользовательской функции для работы с массивами

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

myCreateArray(rows, cols, def_value)

В качестве первого шага внесем дополнения в таблицу сообщений об ошибках (находящуюся в файле mymcfunc.cpp):

char * myErrorMessageTable[] =

{

  "Число должно быть действительным",

  "Число не должно быть отрицательным",

  "Число строк/столбцов должно быть больше либо равно 1",

  "Ошибка выделения памяти",

  "Вычисление прервано пользователем. Нажмите F9 для перерасчета"

};

Новые ошибки могут возникать в следующих случаях:

r    количество строк или столбцов не является положительным целым числом;

r    при выделении памяти может возникать ошибка, например связанная с недостатком памяти;

r    в процессе вызова функции пользователь может нажать клавишу <Esc>, останавливающую процесс вычислений. В код пользовательской функции будет добавлен специальная проверка для отслеживания этой ситуации и прерывания расчетов в этом случае.

Как и в случае создания функции ConeVolume, следующим шагом будет написание кода функции. Для этого добавим в файл mymcfunc.cpp строки:

// Код функции для создания массива с заполненными элементами

LRESULT myCreateArray(

        COMPLEXARRAY * const Result,

        const COMPLEXSCALAR * const rows,

        const COMPLEXSCALAR * const cols,

        const COMPLEXSCALAR * const def_value)

{

  // Проверка на отсутствие мнимой части у первого аргумента –

  // количество строк

  if (rows->imag != 0.0)

  {

    // Возвращение значения, сигнализирующего об ошибке:

    //   код (номер) ошибки: 1

    //   номер аргумента функции, содержащего ошибку: 1

    return MAKELRESULT(1, 1);

  }

  // Проверка на положительность первого аргумента - количество строк

  if (rows->real < 1.0)

  {

    // Возвращение значения, сигнализирующего об ошибке:

    //   код (номер) ошибки: 2

    //   номер аргумента функции, содержащего ошибку: 1

    return MAKELRESULT(3, 1);

  }

  // Проверка на отсутствие мнимой части у второго аргумента –

  // количество столбцов

  if (cols->imag != 0.0)

  {

    // Возвращение значения, сигнализирующего об ошибке:

    //   код (номер) ошибки: 1

    //   номер аргумента функции, содержащего ошибку: 1

    return MAKELRESULT(1, 2);

  }

  // Проверка на положительность второго аргумента - количество столбцов

  if (cols->real < 1.0)

  {

    // Возвращение значения, сигнализирующего об ошибке:

    //   код (номер) ошибки: 2

    //   номер аргумента функции, содержащего ошибку: 1

    return MAKELRESULT(3, 2);

  }

  // Выделение памяти под возвращаемый массив

  if (!MathcadArrayAllocate(Result, (unsigned int) rows->real,

                       (unsigned int) cols->real, TRUE , TRUE))

  {

    // При неудачном выделении возвращаем код ошибки

    return MAKELRESULT(4, 0);

  }

  // При успешном выделении заполняем элементы массива

  // значением по умолчанию

  for (unsigned int col = 0; col < Result->cols; col++)

  {

    // Проверка на прерывание пользователем вычислений

    if (isUserInterrupted())

    {

      // Освобождение памяти

      MathcadArrayFree(Result);

      return MAKELRESULT(5, 0);

    }

    for (unsigned int row = 0; row < Result->rows; row++)

    {

      Result->hReal[col][row] = def_value->real;

      Result->hImag[col][row] = def_value->imag;

    }

  }

  // Функция должна возвращать 0 в случае успешного завершения

  return 0;

}

Разберем приведенный код.

В отличие от кода функции ConeVolume функция myCreateArray имеет тип возвращаемого результата (в Mathcad) COMPLEXARRAY. Эта структура определена в файле mcadincl.h как:

// complex array type

typedef struct tagCOMPLEXARRAY {

    unsigned int rows;

    unsigned int cols;

    double **hReal;   // hReal[cols][rows],

                      // == NULL when the real part is zero

    double **hImag;   // hImag[cols][rows],  == NULL when the imaginary part is zero

} COMPLEXARRAY;

Первые два поля структуры хранят количество строк и столбцов в массиве соответственно. Третье и четвертое поля — массивы действительных и мнимых частей элементов массива. Для доступа к действительной части элемента массива в строке с номером i и столбце с номером j необходимо использовать

A->hReal[j][i]

а для доступа к мнимой части:

A->hImag[j][i]

где A — переменная типа COMPLEXARRAY.

В начале функции myCreateArray производится проверка на правильность переданных аргументов — количество строк и столбцов. При неправильных значениях функция возвращает код ошибки аналогично тому, как это было сделано в функции ConeVolume.

После проверки на правильность переданных аргументов происходит выделение памяти под возвращаемый массив. Это производится путем обращения к функции MathcadArrayAllocate, которая определена в файле mcadincl.h как:

// array allocation -- should be used to allocate

// return array

BOOL    MathcadArrayAllocate(COMPLEXARRAY * const,

                             unsigned int rows,

                             unsigned int cols,

                             BOOL allocateReal,

                             BOOL allocateImag );

Первый аргумент функции — ссылка на переменную типа COMPLEXARRAY, для элементов которой будет производиться выделение памяти. Второй и третий аргументы функции содержат число строк и столбцов в выделяемом массиве соответственно. Четвертый и пятый аргументы предназначены для частичного выделения памяти под массив: под действительные и мнимые части элементов. Если четвертый аргумент имеет значение FALSE (0), то память под действительную часть не выделяется и в этом случае поле hReal структуры COMPLEXARRAY имеет значение null. В противном случае происходит выделение памяти под действительную часть. Это также справедливо и для мнимой части, выделения памяти под которую зависит от значения пятого аргумента функции MathcadArrayAllocate. Если в вашем случае нет необходимости использовать мнимые части, то вызвав функцию MathcadArrayAllocate с пятым аргументом, равным FALSE (0), можно сэкономить половину памяти.

В нашей ситуации значение по умолчанию может содержать мнимую часть, и поэтому производится выделение памяти под мнимые части элементов массива.

При ошибке выделения памяти функция MathcadArrayAllocate возвращает нулевое значение. В этом случае выполнение функции прерывается и возвращается соответствующий номер ошибки.

После выделения памяти в вышеприведенном коде производится заполнение элементов массива значениями по умолчанию путем присвоения в двойном цикле (по строкам и столбцам):

Result->hReal[col][row] = def_value->real;

Result->hImage[col][row] = def_value->imag;

Помимо этого в код функции добавлена проверка на прерывание пользователем вычислений (при нажатии клавиши <Esc>) путем обращения к функции isUserInterrupted, которая определена в файле mcadincl.h как:

BOOL isUserInterrupted(void);

Функция возвращает нулевое значение, если пользователь не прерывал вычислений, а в противном случае — значение, отличное от нуля. Обращаться к данной функции следует как много реже, т. к. она замедляет процесс вычислений.

Если функция isUserInterrupted вернула ненулевое значение, означающее, что пользователь отменил вычисления, производится освобождение выделенной ранее памяти и возвращается соответствующий код ошибки. Освобождение памяти происходит путем обращения к функции MathcadArrayFree, которая определена в файле mcadincl.h как

void MathcadArrayFree( COMPLEXARRAY * const );

Единственный аргумент функции — указатель на структуру COMPLEXARRAY, память которой необходимо освободить.

Выделенную под возвращаемый массив память в случае успешного выполнения пакет Mathcad высвободит самостоятельно.

Структура с информацией о функции myCreateArray выглядит следующим образом (ее необходимо добавить в файл mymcfunc.cpp):

FUNCTIONINFO fiMyCreateArray =

{

  "myCreateArray",  // Имя функции в документах Mathcad

  "",      // Перечень параметров функции (для диалога вставки функции)

  "",      // Описание функции (для диалога вставки функции)

  (LPCFUNCTION) myCreateArray,  // Указатель на функцию, содержащую код

  COMPLEX_ARRAY,                // Тип возвращаемого результата

  3,                            // Количество аргументов функции

  {COMPLEX_SCALAR, COMPLEX_SCALAR, COMPLEX_SCALAR}  // Массив с типами

                                                    // аргументов функции

};

Она заполнена несколько иначе, чем структура, использованная для функции ConeVolume:

r    второе и третье поля — пустые строки, т. к. они необходимы только для 7-й версии Mathcad, которая очень редко сейчас используется;

r    тип возвращаемого результата не COMPLEX_SCALAR, а COMPLEX_ARRAY;

r    количество аргументов функции — 3, а не 2, вследствие чего дополнен массив с типами аргументов функции.

Для регистрации функции, как и в случае с ConeVolume, необходимо вызвать функцию CreateUserFunction в коде функции DllMain:

BOOL APIENTRY DllMain(

        HINSTANCE hModule,

        DWORD ul_reason_for_call,

        LPVOID lpReserved)

{

  switch (ul_reason_for_call)

  {

    case DLL_PROCESS_ATTACH:

      {

        if (CreateUserErrorMessageTable(hModule, 5, myErrorMessageTable))

        {

          CreateUserFunction(hModule, &fiConeVolume);

          CreateUserFunction(hModule, &fiMyCreateArray);

        };

      }

      break;

  }

  return TRUE;

}

Результат работы созданной функции показан на рис. 6.39. С ее помощью можно даже создавать единичные массивы, что нельзя сделать с помощью диалогового окна Insert Matrix (Вставка массива).

Рис. 6.39. Использование функции myCreateArray

Работа со строковыми значениями

Работу со строковыми значениями рассмотрим на примере пользовательской функции myReverseString, прототип которой выглядит как

myReverseString(input_string)

где input_string — строковая переменная.

Результатом работы функции myReverseString будет также строка, порядок следования символов в которой должен быть обратным порядку в строке-аргументе input_string. То есть для строки "123" она будет возвращать строку "321".

Код функции myReverseString следующий (его необходимо добавить в файл mymcfunc.cpp):

// Код функции для изменения порядка следования символов в строке

LRESULT myReverseString(

        MCSTRING * const Result,

        const MCSTRING * const input_string)

{

  // Определение количества символов в переданной строке

  unsigned int n = (unsigned int)strlen(input_string->str);

  // Выделение памяти под возвращаемую строку

  Result->str = MathcadAllocate(n + 1);

  // Если память не выделена, то возвращаем ошибку

  if (Result->str == NULL)

  {

    return MAKELRESULT(4, 0);

  }

  // Копирование строки из input_string в Result

  strcpy(Result->str, input_string->str);

  // Изменение порядка следования символов в строке

  _strrev(Result->str);

  // Функция должна возвращать 0 в случае успешного завершения

  return 0;

}

Возвращаемый результат и аргумент функции myReverseString имеют одинаковый тип — это ссылка на структуру MCSTRING, которая определена в файле mcadincl.h как:

typedef struct tagMCSTRING {

  char *str;

}MCSTRING;

Единственное поле структуры MCSTRING — это поле str, которое хранит указатель на ANSII-строку.

Первым шагом в коде функции myReverseString мы определяем количество символов в переданной строке с помощью функции strlen.

Далее производится выделение памяти под возвращаемую строку с помощью функции MathcadAllocate, которая определена в файле mcadincl.h как:

char * MathcadAllocate(unsigned int size);

где в параметре size передается требуемое количество памяти в байтах.

В случае успешного выполнения функция MathcadAllocate возвращает адрес выделенного буфера памяти, а в противном случае — значение null. В коде функции производится проверка на результат вызова функции MathcadAllocate и при возникновении ошибки осуществляется выход из функции и возврат соответствующего кода ошибки.

После выделения памяти выполняется копирование строки из input_string в Result с помощью функции библиотеки выполнения C strcpy, а затем изменяется порядок следования в строке Result с помощью другой функции библиотеки С — _strrev.

Структура, описывающая данную функцию, выглядит следующим образом:

FUNCTIONINFO fiMyReverseString =

{

  "myReverseString",  // Имя функции в документах Mathcad

  "",      // Перечень параметров функции (для диалога вставки функции)

  "",      // Описание функции (для диалога вставки функции)

  (LPCFUNCTION) myReverseString,  // Указатель на функцию, содержащую код

  STRING,                         // Тип возвращаемого результата

  1,                              // Количество аргументов функции

  {STRING}                        // Массив с типами аргументов функции

};

Тип возвращаемого результата для функции myReverseString — это STRING, количество аргументов — 1, и тип аргумента — также STRING.

Регистрация (подключение) в Mathcad функции myReverseString выполняется аналогично регистрации функций ConeVolume и myCreateArray и достигается посредством добавления следующей строчки в код функции DllMain:

CreateUserFunction(hModule, &fiMyReverseString);

На рис. 6.40 показан пример вызова разработанной функции.

Рис. 6.40. Пример обращения к пользовательской функции по работе со строками

Создание ссылки на справку по пользовательской функции

В разд. "Файл описания функции" ранее в этой главе для пользовательской функции ConeVolume был рассмотрен xml-файл с информацией о функции для диалогового окна Insert Function (Вставка функции). Помимо всего прочего, из этого диалога возможно обращение к справке о выделенной функции. Чтобы создать ссылку на справку о разработанной пользовательской функции необходимо проделать следующие шаги:

1.     Создать html-страницу со справкой о функции и "упаковать" ее в файл справки с расширением chm. Последнее это может быть сделано с помощью программы Microsoft HTML Help WorkShop, информация о которой доступна в Интернете по адресу http://msdn.microsoft.com/workshop/author/htmlhelp. В качестве примера будет использована уже существующая html-страница (Contacting_MathSoft.html), находящаяся в файле справки mcad.chm (входящего в комплект поставки пакета Mathcad).

2.     Заполнить соответствующие элементы в xml-файле с информацией для диалога Insert Function (Вставка функции). В качестве примера изменим информацию о функции ConeVolume в созданном ранее файле mymcfunc_EN.xml, который размещается в папке DOC\FUNCDOC папки установки Mathcad:

<?xml version="1.0" encoding="windows-1251"?>

<FUNCTIONS>

<help_file>..\mcad.chm</help_file>

<function>

  <name>ConeVolume</name>

  <params>d, h</params>

  <category>Расчет объемов тел</category>

  <description>Вычисляет объем конуса с диаметром основания d и высотой h.</description>

  <help_topic>Contacting_MathSoft.html</help_topic>

</function>

</FUNCTIONS>

По сравнению с приведенным ранее текстом файла были внесены следующие изменения:

·       в элементе help_file (третья строка) указана ссылка на используемый файл справки (в нашем случае на файл mcad.chm);

·       в элементе help_topic (девятая строка) указана ссылка на файл Contacting_MathSoft.html, находящийся в файле mcad.chm.

Пример обращения к справке показан на рис. 6.41.

Рис. 6.41. Обращение к справке из диалогового окна Insert Function

И последнее.

В среде Mathcad 11 и 12 появилась возможность программного управления Mathcad-документом через нажатие комбинации клавиш <Ctrl>+<Shift>+<Alt>+<S>. Такое действие выводит на экран дисплея диалоговое с именем Mathcad Scripting Host, показанное на рис. 6.42.

Рис. 6.42. Внешнее программное управление Mathcad-документом

В окне, показанном на рис. 6.42, пользователь может написать некую программу на языках VBScript или JScript и нажать клавишу Execute для ее исполнения. Так VBScript-программа, отображенная на рис. 4.42, считывает значение переменной n из Mathcad-документа и присваивает ее переменной N. Далее в цикле for несколько (N+1) раз меняется значение переменной Index Mathcad-документа, которой присваивается значение переменной i из VBScript-программы. Столько же раз раз выполняется сам Mathcad-документ. Все это приводит к тому, что переменной a Mathcad-документа попеременно присваиваются значения из списка Data. Этот простой пример показывает, что через комбинацию клавиш <Ctrl>+<Shift>+<Alt>+<S> можно при необходимости проводить итерационные вычисления, «заставляя» Mathcad-документ выполняться нужное число раз и меняя в нем значения отдельных переменных.

На рис. 6.42a показан альтернативный способ внешнего программного управления Mathcad-документом.



[1]   Кроме того, раньше инструменты программирования были только в продвинутых (и дорогих) версиях Mathcad, версиях с приставками PLUS или Pro. Сейчас же все версии Mathcad оснащаются этим инструментом.

[2]   Мольеровский Журден, очень удивился, когда узнал, что он не просто говорит, а говорит именно прозой. Так и некоторые пользователи Mathcad удивляются, когда оказывается, что они программируют в среде Mathcad, к которой обратились именно потому, что в ней не нужно программировать.

[3]   С другой стороны эти векторы хранят историю решения задачи, что полезно при отладке (см. разд. 6.7).

[4] Бытовало даже такое мнение, что математические пакеты — это средство решения задач на компьютере ленивыми. "Ленивые" же резонно на это возражали, что все блага цивилизации — это их (ленивых) изобретения.

[5]   Точнее VBA — Visual Basic for Applications.

[6]   Исключение составляют только функции, работающие в паре с ключевым словом Given. Их нельзя вставить в программу.

[7]

[8]  Объемные — в смысле большие, а не в том смысле, какой отмечался в разд. 1.5.

[9]

[10]  Решается задача, связанная с процессом обработки природной воды некими реагентами для того, чтобы не откладывалась накипь при ее нагреве в котле. Но мы сейчас обсуждаем не суть задачи, а ее реализацию в Mathcad — метод последовательных приближений.

[11] Кстати, без программирования альтернативу с одной ветвью реализовать нельзя: функция if всегда должна иметь три аргумента.

[12]  В традиционных языках программирования сначала нельзя было писать  несколько операторов на одной строке. Затем по мере развития языков такая возможность появилась.

[13]  Так повышают наглядность программы: чем правее расположен оператор, тем он глубже сидит в структуре программы.

[14]  Ее автор использовал в своих Mathcad-программах, пока не додумался заменить их на векторы-строки.

[15] Следом появится интегрированный пакет Works, а уж потом Office, интегрирующий текстовый и табличный процессоры (Word и Excel), а также базы данных (Accses).

[16] Брудно А. П.  Программирование в содержательных обозначениях. — М.: Наука, 1968.

[17] Очков В. Ф. 128 советов начинающему программисту. — М.: Энергоатомиздат, 1991 (см. http://twt.mpei.ac.ru/ochkov/128/index.htm).

[18] В первых трех изданиях этой книги гл. 3 так и называлась – "Взгляд на Mathcad шутника, дериватора и эстета".

[19]  В период между Рождеством и Крещением. Программа на рис. 6.15 написана автором (вернее, подправлена для Mathcad 8) 11 января 1998 г. Так что здесь все в порядке.

[20]  У нас они будут нулевым и первым — мы не будем трогать переменную ORIGIN.

[21] Кстати, в Mathcad 13, как заверяют разработчики, существенно улучшены инструменты работы с линейной алгеброй, базовыми понятиями (объектами) которой и являются вектора и маршруты.

[22]  Здесь более уместен цикл с постпроверкой, но его нет в среде Mathcad. Как в этом случае поступить, будет рассказано ниже.

[23]  До Mathcad 7 Pro они имели вещественный тип и при необходимости комплексный. В версиях выше 7-й тип переменных — Variant (помесь числа с текстом), если исходить из стандарта языка Visual Basic и технологии OLE.

[24] Такое "смешенье языков" имеет место в меню, когда в английский Mathcad внедряют русский Ворд, пардон, Word или Excel (см. рис. 1.49).

[25]  Такое было возможно в ранних версиях языка BASIC, где допускались длинные переменные, но они идентифицировались только по двум первым символам.

[26]  Некоторые полагают, что лет через 10—20 мы перейдем к написанию русских текстов латиницей: sejchas tak pishut teksty mejdunarodnych telegram. Молдавия, Азербайджан и некоторые другие страны СНГ уже перешли от кириллицы к латинице. Основная причина в сфере информатики. Вспомним, какие абракадабры нередко приходят к нам по e-mail.

[27]  У алхимиков есть символ: "Змея, глотающая свой хвост", который подходит и для рекурсии — маленькая программа, заглатывающая память компьютера.

[28]  Мы, кстати, это уже делали (см. рис. 1.29). Более того, на этом рисунке было показано, как факториал можно заставить выдавать значения от нецелого аргумента (операнда).

[29] Структурная революция началась со статьи Э. Дейкстры "Программирование без GOTO" ("Programming without GOTO"). Сейчас многие программисты-снобы гордятся и хвастают тем, что они написали не одну сотню серьезных программ, ни разу не обратившись к метке.

[30] В языке Visual Basic оператора Print тоже нет, а есть метод Print.

[31]  Слегка косят две наши милые современницы — актрисы Татьяна Шмыга и Людмила Максакова.

[32]  Который, например, что-то "позаимствовал" у другого программиста, но не признается в этом даже на Страшном суде.

[33]  А почему здесь нельзя вставить оператор =? Это бы существенно ускорило процесс отладки программ: символьная математика — это медленная математика.

[34]  Цифры в таком цикле часто сменяются слишком быстро. Из-за этого программу приходится спутывать как пасущуюся лошадь — вставлять в цикл новые пустые, замедляющие циклы. Кроме того, следует отметить, что тандем  ¬  ® в среде Mathcad 12 перестал выводить численное значение.

[35]  Мы не рассматриваем случай, когда локальные переменные, объявленные в программе выше места ввода локальной переменной, "проникают" в локальную переменную, минуя "вход" через список аргументов локальной функции.

[36]  Смена авторство этого раздела книги сопровождается и сменой и операционной системы: Mathcad-документы готовились в Windows 2000, программы на C/C++ — Windows XP, что отразилось на рисунках книги.

[37] В Mathcad 12 при наличии в тексте описаний русских букв, а также символов с ASCII кодом больше 127 возникает ошибка при запуске Mathcad. Этот "баг" можно обойти, например, использованием транслитерации.