Scilab/Программирование

Материал из Викиучебника — открытых книг для открытого мира
Перейти к навигации Перейти к поиску

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

Написание сценариев составляет большую часть работы в среде.

Содержание

Основные понятия и определения[править]

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

Сценарием мы будем называть любую программу Scilab. Любой сценарий состоит из инструкций, которые описывают конкретные действия с объектами Scilab. Инструкция может быть представлена несколькими операндами, связанные каким-либо образом операторами. Также вызов программируемой функции мы тоже будем называть инструкцией.

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

Язык Scilab[править]

В Scilab имеется встроенный язык программирования, который необходимо знать для эффективной работы в среде. Данный язык является структурным с поддержкой объектов.

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

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

  • Ветвление — однократное выполнение одной из двух или более операций в зависимости от заданного условия;
  • Цикл — многократное выполнение одной или нескольких инструкций до тех пор, пока не выполнится некоторое условие. Один проход по всем инструкциям внутри цикла называется итерацией.

Данные конструкции кодируются с помощью специальных служебных слов, которые не могут быть использованы для имен объектов.

Сценарии Scilab, как правило, пишутся пользователем во встроенном редакторе Scinotes, тем не менее, не запрещается использовать любой другой доступный текстовый редактор. Чтобы открыть редактор можно:

  • Ввести команду scinotes в командное окно;
  • Сделать активным командное окно, а затем на панели инструментов нажать на кнопку Открыть SciNotes, либо открыть меню Инструменты и выбрать пункт Текстовый редактор SciNotes.

Перед вами появится следующее окно

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

Для примера попробуем написать простой сценарий. Введите в Scinotes следующий код

s='Hello, World!'
disp(s)

Таким образом вы написали сценарий, состоящий из двух инструкций: объявление строки и вывода строки в командное окно с помощью встроенной функции disp(). Сохраним наш сценарий, для чего необходимо:

  1. В раскрывающемся меню Файл выбрать пункт Сохранить как, либо нажать комбинацию <Ctrl>+<Shift>+<S>;
  2. В диалоговом окне выбрать доступную директорию, затем ввести имя файла сценария, например myScript, и выбрать расширение *.sce (от англ. Scenario);
  3. Нажать кнопку Сохранить.

Файлы сценариев имеют расширение *.sce и могут хранить в себе программируемые функции и исполняемые участки кода, т.е. собственно сценарий. Также есть и другое расширение — *.sci. Файлы с таким расширением должны хранить в себе только программируемые функции, которые можно будет подгрузить в текущую сессию для вызова их либо пользователем, либо каким либо сценарием. Эти два расширения являются часто используемыми, однако кроме них Scinotes позволяет создавать файлы с рядом других специфичных расширений, о которых мы поговорим чуть позже.

Теперь попытаемся выполнить наш сценарий, для чего нужно выполнить одно из следующих действий:

  • Сделать активным окно Scinotes и нажать на панели инструментов кнопку Выполнить (обратите также на кнопку команды Сохранить и выполнить);
  • Выбрать в раскрывающемся меню Выполнить желаемую команду;
  • Нажать горячую клавишу <F5>.

В командном окне вы увидите следующее

-->exec('D:\myScript.sce', -1)
 
 Hello, World!

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

Сделав активным окно Scinotes, нажмите комбинацию <Ctrl>+<N>, либо нажмите на кнопку Новый на панели инструментов. Вы увидите, что под внутренним заголовком окна появится вкладка нового безымянного документа. Нажмите на комбинацию <Ctrl>+<Shift>+<S> и сохраните файл под именем myFunc с расширением sci. В самом файле запишите следующий код

function y=cube(x)
    y=x^3
endfunction

function y=pow(x)
    y=x^2
endfunction

Во время объявления функций вы могли заметить, что при вводе ключевого слова function, ключевое слово endfunction вводится автоматически. Эта возможность включается и отключается в меню Настройки->Автоопределение включить (комбинация <Ctrl>+<Shift>+<H>). Туда же входит не менее полезная функция автоматического закрывания скобок.

Сохраните изменения. Теперь для загрузки ваших функций в окружение можно нажать кнопку <F5>. Вы увидите как функция exec() подгрузит в среду ваши функции. Теперь вы можете свободно вызывать их из командной строки

-->cube(3),pow(2)
 ans  =
    27.  
 ans  =
    4.

Далее мы рассмотрим основы программирования на встроенном языке Scilab и по ходу знакомится с методами написания сценариев.

Для начала введем несколько правил относительно стиля написания сценариев. Это необходимо для того, чтобы уменьшить число ошибок на первых порах и сохранить удобочитаемость кода.

  • Каждая инструкция должна писаться с новой строки.
  • В сценариях можно допускать объявление переменных по мере их необходимости, но в функциях мы будем придерживаться правила объявления всех используемых объектов строго сверху.
  • Все вложенные конструкции следует выделять табуляцией (пробельными отступами слева), чтобы видеть где они начинаются и заканчиваются.
  • Нетривиальные куски кода следует комментировать.
  • Правилом хорошего тона считается явное удаление объектов, если не предполагается их дальнейшее использование в сценарии.

В языке Scilab существует небольшой набор базовых конструкций, реализующих циклы и ветвления. Рассмотрим их по порядку.

Ветвления[править]

В языке Scilab две конструкции, организующие ветвление:

  1. Конструкция if ... else;
  2. Конструкция select ... case.

Общий синтаксис конструкции if ... else имеет следующий вид

if <условие 1> then
<операторы 1>
elseif <условие 2> then
<операторы 2>
...
elseif <условие n> then
<операторы n>
else
<операторы>
end

На позициях <условие> записываются логические выражения, которые возвращают логический или числовой тип данных. Общий алгоритм работы данной конструкции выглядит так:

  1. Проверяется <условие 1>. Если условие истинно или, если это число, оно больше нуля, то выполняются <операторы 1> и ветвление завершается, в противном случае переход к пункту 2;
  2. Проверяется <условие 2> Если условие истинно или, если это число, оно больше нуля, то выполняются <операторы 2> и ветвление завершается, в противном случае переход к следующему elseif и так далее;
  3. Если ни одно из условий elseif не выполнилось, то выполняются операторы по ветке else и на этом ветвление завершается.

Общий синтаксис второй конструкции имеет вид

select <значение>
case <значение 1>
<операторы 1>
case <значение 2>
<операторы 2>
...
case <значение n>
<операторы n>
else
<операторы>
end

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

В первой конструкции объявление ветвей elseif не является обязательным, но должна быть по крайней мере ветвь по if. Ветвь else в обоих конструкциях также не является обязательной.

Упражнение

С одним из примеров ветвления можно столкнуться при решении квадратных уравнений на множестве действительных чисел. Если дискриминант уравнения больше нуля, то мы можем сказать, что уравнение имеет два действительных корня; если дискриминант меньше нуля, то действительных корней уравнение не имеет и, наконец, если дискриминант равен нулю, то уравнение имеет два совпадающих по значению действительных корня.

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

//функция принимает в качестве аргумента вектор коэффициентов уравнения, начиная с коэффициента при самой младшей степени
function [y1,y2]=rootsOfBinomial(cf)
    c=cf(1)
    b=cf(2)
    a=cf(3)
    D=b^2-4*a*c
    if D<0 then
        disp("Уравнение не имеет действительных корней")
        y1=%nan
        y2=y1
    elseif D==0 then
        y1=-b/(2*a)
        y2=y1
    else
        y1=(-b+sqrt(D))/(2*a)
        y2=(-b-sqrt(D))/(2*a)
    end
endfunction

Разберем код функции подробнее. Первым делом мы для удобства скопировали значения из передаваемого вектора cf в переменные a, b и c. Затем мы рассчитали дискриминант. Дальше идет ветвление if ... else: если дискриминант будет меньше нуля, то пользователь увидит в командной строке сообщение, а функция возвратит константы %nan в качестве ответа; если дискриминант будет равен нулю (вторая ветвь), то корень будет рассчитан и записан в y1, а затем скопирован в y2, после чего пользователю будет возвращен ответ; наконец, если ни первое, ни второе условие не выполняется, то дискриминант в любом случае больше нуля и корни будут рассчитаны по соответствующим формулам.

Подгрузите функцию в текущий сеанс, а затем вызовите для следующих векторов

// В этом примере выполняются действия первой ветви
-->[ans ans]=rootsOfBinomial([1 2 3])
  Уравнение не имеет действительных корней   
 ans  =
     Nan  
 ans  =
     Nan  
// Выполняются действия по ветви ''else''
-->[ans ans]=rootsOfBinomial([6 3 -10])
 ans  =
    0.9389867  
 ans  =
  - 0.6389867
// Выполняются действия по ветви ''elseif''
-->[ans ans]=rootsOfBinomial([1 2 1])
 ans  =
  - 1.  
 ans  =
  - 1.

Замечание: хотя вам это должно уже быть известно из главы Объекты, но еще раз обратите внимание на то, что во встроенном языке не обязательна инструкция возврата результата (как, например, инструкция return в языке программирования Си), потому что синтаксически это уже прописано в первой строке с прототипом функции. Возврат результата происходит неявно, когда решатель «натыкается» на ключевое слово endfunction, если до этого он не встретил инструкции преднамеренного возврата результата.

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

function nameOfMonth(number)
    select number
    case 1
        disp("Январь")
    case 2
        disp("Февраль")
    case 3
        disp("Март")
    case 4
        disp("Апрель")
    case 5
        disp("Май")
    case 6
        disp("Июнь")
    case 7
        disp("Июль")
    case 8
        disp("Август")
    case 9
        disp("Сентябрь")
    case 10
        disp("Октябрь")
    case 11
        disp("Ноябрь")
    case 12
        disp("Декабрь")
    else
        disp("Такого месяца нет. Введите число от 1 до 12")
    end
endfunction

Теперь попробуем вызвать функцию

-->nameOfMonth(4)
 Апрель   
-->nameOfMonth(11)
 Ноябрь  
-->nameOfMonth(-1)
 Такого месяца нет. Введите число от 1 до 12

Заметим, что последний пример можно реализовать и конструкцией if ... else, однако, она будет более громоздкой и неудобной для изучения.

Циклы[править]

В языке Scilab всего два вида циклов:

  1. Цикл for;
  2. Цикл while.

Общий синтаксис для цикла for имеет вид

for <счетчик>=<начальное значение>:<шаг>:<конечное значение>
<операторы>
end

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

Для цикла while синтаксис имеет следующий вид

while <условие>
<операторы>
end

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

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

Управление циклом в его теле возможно с помощью следующих специальных операторов:

  • continue — передача управления следующей итерации;
  • break — прекращение текущей итерации и выход из цикла.
Упражнение

Только для примера напишем функцию, решающую задачу поиска минимального и максимального значения в массиве данных. Под массивом данных мы будем понимать числовую матрицу и числовой вектор. Будем использовать самый примитивный алгоритм — «Перебор»:

  1. Положить, что первый элемент массива есть искомый минимум (максимум) и записать его во временную переменную;
  2. Сравнить значение i-го элемента (начиная со второго) со значением во временной переменной: если i-ый элемент меньше (больше) него, то записать i-ый элемент во временную переменную, иначе перейти на следующий шаг;
  3. Увеличить значение счетчика i. Если счетчик вышел за размер массива, то закончить, иначе перейти на шаг 2.

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

Вообще говоря, для решения этой задачи достаточно применения цикла for, но мы, чтобы «убить двух зайцев сразу» и посмотреть сразу два цикла, для обработки вектора будем применять цикл while. Итак, код такой функции будет выглядеть следующим образом

// Функция возвращает максимальное и минимальное значение матрицы или вектора
function [Min,Max]=explorer(in)
    i=1 // счетчик для циклов
    j=1 // счетчик для цикла for
    Max=in(i) // полагаем, что первый элемент массива и есть искомый максимум
    Min=Max
    if or([size(in,'r')==1,size(in,'c')==1]) then
        i=i+1
        while i<=length(in)
            if in(i)<Min then
                Min=in(i)
            end
            if in(i)>Max then
                Max=in(i)
            end
            i=i+1
        end
    else
        for i=1:size(in,'r')
            for j=1:size(in,'c')
                if in(i,j)<Min then
                    Min=in(i,j)
                end
                if in(i,j)>Max then
                    Max=in(i,j)
                end
            end
        end
    end
endfunction

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

-->[a,b]=explorer([-5 6 8;-45 5 8;1 3 5])
 b  =
    8.  
 a  =
  - 45.
-->[a b]=explorer([-100 6 8 -45 5 456 1 3 5])
 b  =
    456.  
 a  =
  - 100.

На данном этапе попытайтесь самостоятельно разобрать написанный нами код.

В предыдущем примере вы наверное обратили внимание на конструкцию

for i=1:size(in,'r')
     for j=1:size(in,'c')
        ...
     end
end

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

При переборах матриц возьмите за правило использовать символ 'i' для перебора строкового индекса, а символ 'j' — столбцового. Также хотим обратить внимание на строку внутри цикла while, в которой происходит увеличение значения индекса i на единицу. Если ее убрать, то произойдет зацикливание, так как условие будет истинным всегда. Всегда проверяйте условие выхода из цикла while при написании сценариев. В случае, если произошло зацикливание, используйте комбинацию клавиш <Ctrl>+<X> или <Ctrl>+<C>, чтобы прервать сценарий. Также сценарий можно прервать из раскрывающегося меню Управление при активном командном окне.

Обратите также внимание на то, что при передаче матрицы, запись

Max=in(i)

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

Таким образом, для решения данной задачи цикл

// in — матрица
for i=1:length(in)
    ...
end

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

Локальные и глобальные переменные[править]

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

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

Для работы с глобальными переменными существуют специальные функции:

  • global() — определение глобальной переменной; в скобках перечисляются имена переменных в виде строк. Также допускается перечисление переменных без скобок и кавычек, как в функции clear;
  • clearglobal() — уничтожает глобальные переменные;
  • isglobal() — проверяет является ли переменная глобальной;
  • gstacksize() — установить/получить размер стека глобальных переменных.

Чтобы прочувствовать разницу между глобальными и локальными примерами, рассмотрим следующие примеры. Создайте глобальную переменную

-->global a

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

Если вы обратите внимание на Обозреватель переменных, то увидите две переменных с именем а, но разной областью видимости. Значение хранится именно в переменной с локальной видимостью, а глобальная переменная это всего лишь указатель на нее.

Теперь продемонстрируем как работает свойство глобальности на следующем примере. В командной строке объявите следующие функции

-->function f()
-->global a
-->a=a+1
-->endfunction
 
-->function g()
-->a=a+1
-->endfunction

Вызовите функцию g() и обратитесь к переменной a

-->g()
-->a
 a  =
    1.

Очевидно, что вызов функции никак не повлиял на значение глобальной переменной. Теперь вызовите два раза функцию f() и снова обратитесь к переменной a

-->f()
-->f()
-->a
 a  =
    3.

Если вы обратите внимание на коды функций f() и g(), то увидите, что для функции f() переменная a видима, поэтому каждый вызов функции увеличивал значение именно ее. Ключевым словом global мы дали понять функции, что обращаемся именно к глобальной переменной. В этом случае был проверен в первую очередь стек глобальных переменных. Если бы в стеке такой переменной не оказалось, то из функции была бы создана новая глобальная переменная точно также, как вы это сделали в командной строке ранее.

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

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

Удалять глобальные переменные несколько сложнее, чем локальные, так как функция clearglobal удаляет ссылку с глобальной видимостью, но не удаляет дубликат с локальной видимостью. Дубликат в свою очередь нужно удалять через функцию clear, предварительно проинициализировав его значением, иначе функция clear не сработает (к сожалению функция clearglobal иногда оставляет после себя «битую» ссылку на переменную с локальной видимостью, но это происходит не всегда, и эта проблема до версии 5.4.1 еще не решена).

Так, чтобы удалить нашу переменную a нужно выполнить следующие инструкции

-->clearglobal a, a=0, clear a

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

Рекурсия[править]

Язык Scilab поддерживает рекурсивный вызов, т.е. когда функция вызывает саму себя в своем же теле. Классическим примером рекурсии является вычисление факториала. Так, на языке Scilab функция будет иметь такой вид

function y=fact(n)
    if n<=1 then
        y=1
    else
        y=n*fact(n-1)
    end
endfunction

Примечание: для вычисления факториала используйте функцию factorial().

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

Например, если мы передадим функции fact() в качестве аргумента число 5, таблица рекурсии примет вид

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

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

Алгоритмы записанные циклами for и while всегда выглядят более понятными, чем с рекурсиями, и не имеют каких-либо ограничений, однако занимают много места, из-за чего листинг быстро растягивается вниз.

Самостоятельно

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

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

Несложно видеть, что использование цикла for здесь естественно, но задачу можно решить и рекурсиями.

Подсказка: глядя на листинг рекурсивной функции факториала, попробуйте организовать подобную рекурсию и для этой задачи, т.е. начните вход в рекурсию с правой части ряда. Для вычисления факториала используйте библиотечную функцию. Обязательно сравните свой ответ с ответом, который дает Scilab. Чем больше членов ряда вы возьмете (например, 100), тем точнее будет ответ вашей функции.

Вывод и ввод данных[править]

Ввод и вывод в командное окно[править]

В небольших сценариях обычно ввод данных производится через командную строку так, как мы это делали ранее. Однако, с ростом сложности проекта такой ввод становится неудобным и недружественным, т.е. пользователь должен знать какие данные и каких типов требует от него сценарий. Представьте себе, что вы получили в руки проект Scilab'а, который вы видите впервые и который требует инициализации большого количества переменных, но не предоставляет никаких пояснений. В этом разделе мы рассмотрим инструменты, позволяющие сделать ввод и вывод данных более дружественным для пользователя. Здесь будут рассмотрены только инструменты командного окна, но еще более дружественным вариантом является разработка графического интерфейса пользователя, которую мы рассмотрим позже в главе Визуализация.

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

-->a=input('Введите целое число: ')   // Эта команда выведет в Командное окно поясняющую надпись
Введите целое число: 15               // Число, которое введет пользователь, будет присвоено переменной 'а'
 a  =
    15.

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

  • x_dialog() — вызывает диалоговое окно, в которое будет выведено поясняющее сообщение и поле для ввода. Возвращает все, что введет пользователь, но в виде строки, поэтому требуется после этого производить семантический разбор.
  • x_mdialog() — вызывает диалоговое окно, которое позволяет ввести несколько значений сразу в вектор. Возвращает вектор строковых значений.
  • x_choose() — вызывает окно, в котором пользователю будет предложено сделать выбор из ограниченного числа вариантов.
  • x_matrix() — вызывает окно для ввода матрицы. Возвращает строковый тип данных.
  • getvalue() — более гибкая версия диалогового окна, позволяющая ввести несколько значений сразу, где можно определить типы данных заранее.

Для вывода в командное окно также небольшое количество функций:

  • disp() — функция реализует самый простой вывод в командное окно. Для этого нужно передавать функции в качестве аргументов объекты, которые вы хотите вывести. Выводить результаты функция будет в обратном порядке.
  • mprintf() — функция реализует форматный вывод в командное окно. Если вы знакомы с языком Си, то наверняка знаете библиотечную функцию printf(). Функция mprintf() по сути является аналогом printf() в Scilab, т.е. сначала нужно передать сначала так называемую форматную строку, а потом перечислить выводимые переменные. С полным перечнем форматов можно познакомиться в справке, либо в приложении к этому учебнику.
  • msprintf() — работает как mprintf(), но в отличие от последней возвращает строку выводимых аргументов. Обычно используется, когда выводимый результат нужно еще где-то сохранять.

Обычно форматный вывод предпочтительнее, потому что позволяет делать вывод более привлекательным для пользователя. Для овладения форматным выводом нужна практика. Если вы слышите о форматном выводе впервые, то прочитайте следующие общие правила:

  • Форматная строка всегда имеет тип данных String, т.е. оформляется в кавычках.
  • Внутри форматной строки вы можете писать поясняющие слова, управляющие последовательности и форматы для данных, которые выводятся. Так, например, в этой форматной строке
    'Корни уравнения:\na1=%5.3f\na2=%5.3f'
    

Корни уравнения: a1= a2= — это поясняющие слова

\n — управляющая последовательность
%5.3f — формат

  • Любая управляющая последовательность начинается с символа слэша ' \ ' и указывает функции порядок вывода. Например, последовательность \n делает перенос строки и возврат каретки. Все управляющие последовательности состоят всегда из одного символа после слэша и их не очень много. Часто используемые из них это \n и \t — табуляция.
  • Любой формат начинается с символа ' % ' и всегда после этого символа должно идти его имя. В отдельных случаях идут дополнительные параметры формата для получения эстетичных выводов. Например, указанный выше формат %5.3f указывает, что необходимо вывести число с плавающей точкой (имя формата f), выделить под него 5 разрядных единиц, включая разделяющую точку, а для дробной части выделить 3 разрядных знака. Если для целой части разрядных знаков недостаточно, то они будут выделены автоматически. Имена форматов, а также их синтаксис можно найти в справочной информации, либо в приложении к этому учебнику.
  • После форматной строки необходимо перечислить выводимые объекты. Их число должно быть равно числу форматов в форматной строке, а типы данных не должны противоречить именам форматов, в противном случае результат вывода может быть непредсказуем.

Например, для форматной строки выше (дополненной табуляцией) функция mprintf() сработает так

-->root1=1.12;
-->root2=45.6521;
-->mprintf('Корни уравнения:\n\ta1=%5.3f\n\ta2=%5.3f',root1,root2)
Корни уравнения:
	a1=1.120
	a2=45.652
Упражнение

Вывод и ввод с помощью файлов[править]

Во время работы очень часто требуется сохранять результаты расчетов, которые иногда и являются целью сценария. Окончательные результаты или большие объемы информации лучше всего сохранять в файлы. Если вдруг эти результаты понадобятся вам потом, вы всегда сможете их извлечь в том количестве, в котором вам нужно, и не засорять общий стек. Кроме того, файлы могут использоваться как своего рода шлюзы для работы приложений, которые не имеют другого интерфейса со Scilab.

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

  • файлы с пользовательской структурой,
  • файлы с предопределенной структурой (электронные таблицы).

Файлы с пользовательской структурой подразумевают, что пользователь сам решает как ему сохранять данные. В этом случае от него требуется сначала мысленно разработать некоторый шаблон файла, а затем постоянно ему следовать при чтении из файла. Для этого могут использоваться txt-файлы или другие с совместимой кодировкой.

Другой способ сохранять данные это выводить их в электронные таблицы. Такой способ удобен для хранения, например, точек, координаты которых очень легко «укладываются» в таблицы. Scilab поддерживает на запись формат csv — таблицы, записанные в текстовые файлы, а чтобы отличать строки и столбцы таблицы используются зарезервированные символы. Преимущества таблиц очевидны для однотипных массивов; их легко читать не прибегая к программированию собственных функций.

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

Ввод и вывод в csv-файлы[править]

Так как электронные таблицы имеют строгую структуру, то практически всю работу по записи выполняет Scilab, а пользователю требуется указать источник данных и место на жестком диске. Функций для работы с электронными таблицами немного:

  • csvDefault() — позволяет настроить работу функций, работающих с csv, в частности точность записи чисел и зарезервированный разделитель.
  • csvRead() — позволяет читать файлы.
  • csvWrite() — позволяет писать в файлы.

В Scilab также реализованы функции для совместимости с Matlab:

  • read() — позволяет читать файлы.
  • write() — позволяет писать в файлы.

Данные функции читают файлы как текстовые и не привязаны к расширению. Вы можете использовать любое нравящееся вам расширение для файла. В справочной информации применяется расширение csv (от comma-separated value file — файл со значениями, отделенными запятыми). Как вы увидите позже в этом разделе, значения можно отделять не только запятыми, а любыми символами.

Упражнение

Для начала откройте блокнот или любой доступный текстовый редактор и создайте текстовый файл с именем practice.dat. Заполните файл следующими данными:

1 2 3
4 5 6
7 8 9

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

Перейдите в консоль Scilab и введите команду

csvDefault

В этом примере файл был создан в Windows и вывод этой команды такой

-->csvDefault
 ans  =
 
!separator   ,        !
!                     !
!decimal     .        !
!                     !
!conversion  double   !
!                     !
!precision   %.17lg   !
!                     !
!regexp               !
!                     !
!eol         windows  !
!                     !
!encoding    utf-8    !
!                     !
!blank       on       !

Обратите внимание, что вывод команды это типизованный список, который назначает командам csvRead и csvWrite настройки по умолчанию. Вы можете видеть, что для данной системы разделителем значений в файле по умолчанию является символ запятой, разделителем десятичных разрядов является символ точки, все прочитанные числа будут переводиться в вещественные числа double при этом число будет прочитано с точностью не менее 171 значащего разряда после запятой. Поле regexp является шаблоном регулярного выражения, по которому следует искать строки в файле и по умолчанию шаблон пустой. Далее следуют такие правила, как стиль окончания строки (в windows строка оканчивается двумя непечатаемыми символами: CR (возврат каретки) и LF (перевод строки)), предполагаемая кодировка (здесь предполагается Unicode 8. Также поддерживается iso-latin) и, наконец, опция позволяющая игнорировать пустые строки (по умолчанию включена). Вы можете редактировать произвольные поля вызывая функцию, например так

-->csvDefault("separator", ' ')

В этом примере мы заменили разделитель значений на символ пробела. Пользуясь этим, давайте прочитаем файл

// Здесь предполагается, что файл хранится на диске "D:\"
-->A=csvRead("D:\practise.dat")
 A  =
    1.    2.    3.  
    4.    5.    6.  
    7.    8.    9.

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

-->csvDefault("separator", ',')
 ans  = 
  T  
 
-->A=csvRead("D:\practiсe.dat")
 A  = 
    123.  
    456.  
    789.

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

Например, чтобы использовать другой разделитель, вы можете ввести команду так

-->A=csvRead("D:\practiсe.dat", ' ')
 A  =
    1.    2.    3.  
    4.    5.    6.  
    7.    8.    9.

Запись в файл похожа на чтение за исключением того, что вы указываете первым аргументом в csvWrite, что вы записываете. В остальном прототип функции csvWrite похож на прототип csvRead. По умолчанию, если файла, указанного вторым аргументом не будет существовать, то он будет создан, а существующий будет перезаписан. Не указанные при вызове параметры будут браться из списка, отображаемого csvDefault.

Например,

-->B=[9 8 7; 2 2 2; 3 3 3]
 B  =
    9.    8.    7.  
    2.    2.    2.  
    3.    3.    3.  
 
-->csvWrite(B, "D:\practiсe.dat", ' ')
-->A=csvRead("D:\practiсe.dat", ' ')
 A  =
    9.    8.    7.  
    2.    2.    2.  
    3.    3.    3.

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

write_csv(M, filename [,sep, dec])
// M — записываемая матрица
// filename — имя файла
// sep — разделитель
// dec — знак, отделяющий целую часть от дробной

M = read_csv(fname [,sep])

К сожалению, в Scilab нет стандартных функций для дописывания файла, поэтому вы должны перед новой записью в файл сохранить старые данные, затем прикрепить к ним новые и, наконец, записать результат в файл.

Чтение xls-таблиц[править]

Кроме обычных файлов, вы можете использовать в качестве источника данных электронные таблицы Excel. К сожалению, на данный момент поддерживается формат типа BIFF8 (от 2003 года). Электронные таблицы читаются в два этапа:

  • сначала открывается файл, которому назначается дескриптор;
  • производится чтение.

Для этих целей используются функции:

  • xls_open() — открывает файл на чтение
  • xls_read() — производит чтение из открытого файла; позволяет назначить позицию
  • readxls() — производит чтение открытого файла целиком

Приемы программирования в Scilab[править]

Простое проектирование функций[править]

Любой алгоритм удобно оформлять в виде функции. Как оформляется программируемая функция на языке Scilab вы уже знаете: для нее должно быть выбрано уникальное имя и выведены аргументы, от которых зависит поведение алгоритма. Строка с именем функции, возвращаемым типом и входными аргументами мы будем называть прототипом функции.

Функции, написанные на языке Scilab, иногда называют еще макросами (англ. macros), а на любом другом языке — примитивами. Узнать о том, с чем вы имеете дело, можно с помощью функций type() и typeof(). Возврат этих функции перечислены в таблице ниже.

type typeof Пояснение
11 'function' Некомпилированный макрос
13 'function' Компилированный макрос
130 'fptr' Примитив

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

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

// Функция myDer численно вычисляет производную в точке x для функции f
// f — ссылка на функцию одной переменной x,
// h — приращение.
-->function y=myDer(f,x,h)
-->y=(f(x+0.5*h)-f(x-0.5*h))/h
-->endfunction

-->type(myDer)
 ans  =
    13. 
-->deff('y=g(x)','y=x^2')
-->myDer(g,4,0.001)         // мы передали функцию как аргумент
 ans  =
    8.

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

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

Гибкие функции[править]

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

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

function varargout = FunctionName(varargin)
[lhs,rhs]=argn()
...
<операторы>
...
endfunction

Переменные varargout и varargin предопределены в системе и предназначены для хранения соответственно выходных значений и аргументов функции.

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

Для понимания этого процесса введите следующий пример

-->function varargout = Example (varargin)
-->[lhs,rhs]=argn()    // lhs хранит число выходных переменных
                       // rhs хранит число передаваемых аргументов
-->mprintf("Число выходных переменных = %d\nЧисло входных переменных=%d\n",lhs,rhs)
// Следующий цикл нужен, чтобы не спровоцировать некритичную ошибку.
// Дело в том, что функция должна что-то возвращать,
// а никаких других операций над выходными переменными не происходит
-->for i=1:lhs         
-->varargout(i)=1
-->end
-->endfunction

Теперь сделайте следующие вызовы

-->Example(1)
Число выходных переменных = 1
Число входных переменных=1
 ans  =
    1.  
-->Example(1,85,65,4,25,88,63,45)
Число выходных переменных = 1
Число входных переменных=8
 ans  =
    1.
-->[x,y,z,q,w,e]=Example(1,85,65,4,25,88,63,45)
Число выходных переменных = 6
Число входных переменных=8
 e  =
    1.  
 w  =
    1.  
 q  =
    1.  
 z  = 
    1.  
 y  =
    1.  
 x  = 
    1. 
-->x=Example()
Число выходных переменных = 1
Число входных переменных=0
 x  =
    1.

Из этого примера вы должны запомнить следующее:

  • входные переменные не обязательны и их может не быть вовсе;
  • напротив, любая гибкая функция имеет по крайней мере одну выходную переменную, которая должна быть обязательно проинициализирована во время работы функции;
  • если выходная переменная одна и ее имя не задано явно, то значение записывается в ans.

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

Для обработки возможных ошибок пользователя используются следующие функции:

  • error() — останавливает выполнение программы и выводит в командную строку сообщение об ошибке;
  • warning() — посылает сообщение-предупреждение, но выполнение не останавливает. Обычно используется, когда ошибка не синтаксическая, а смысловая;
  • gettext() — возвращает локализованный текст из словаря Scilab. Используется, когда вы не хотите привязывать сообщения к конкретной локализации программы.

Эти функции мы разберем на следующем примере, а пока представим приближенный вид каркаса гибкой функции.

// Любая гибкая функция обычно всегда имеет такой каркас
function varargout = FunctionName(varargin)
[lhs,rhs]=argn()                                // получаем число выходных переменных и аргументов
///////////////////////
// Блок проверок

if(rhs<1 | rhs>3) then                          // проверка на число аргументов
<действия, возможно прекращение из-за ошибки, возможно предупреждение>
else
<часто корректирующие действия, возможно предупреждение, может отсутствовать>
end
if(lhs>1) then                                           // проверка на число выходных переменных
<действия>
else
<чаще всего это предупреждение>
end

if(type(varargin(1)) == 1 & length(varargin(1))>1) then  // проверка на целостность данных. В данном случае мы проверили, что передаваемый аргумент —
                                                         // это матрица определенного размера
<действия>
else
<обычно прекращение выполнения>
end
...
// Конец блока проверок
///////////////////////
// Блок инициализации
// Примечание: в этом блоке следует объявлять часто используемые в функции переменные —
// счетчики, буферы и т.д. Все объявления следует комментировать.
i=1
j=1
temp=0
...
// Конец блока инициализации
///////////////////////

<основные операторы>
endfunction

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

Упражнение

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

Алгоритмов сортировки большое множество. Мы пока не будем рассматривать эффективность самих алгоритмов, а сконцентрируемся только на способе упорядочивания. Таким образом, с точки зрения пользователя мы предусматриваем два способа вызова будущей функции (пусть у нее будет имя ToSort()):

  • ToSort(mas) — сортирует массив по умолчанию по возрастанию.
  • ToSort(mas,way) — сортирует массив по возрастанию, если логическая переменная way истина, и по убыванию — если ложь.

Откройте Scinotes и подготовьте sci-файл с именем sorting.sci . Пусть сама функция ToSort() будет головной и не будет привязываться к конкретному алгоритму. Это позволит модернизировать алгоритм и внедрить в прототип еще один параметр, позволяющий менять алгоритм сортировки.

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

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

Объявляем функцию ToSort()

//////////////////////////////////////
// Файл: sorting.sci
// Автор: ВикиУчебник
// Обозначения:
//    mas - целевой массив из чисел
//    way - направление сортировки
//      way = %t - по возрастанию
//      way = %f - по убыванию
// Вызовы:
// ToSort(mas) - сортировка mas по возрастанию
// ToSort(mas,way) - сортировка mas по way
//////////////////////////////////////

function varargout=ToSort (varargin)
    [lhs,rhs]=argn()
    target=-1                    // для защиты от неверного типа данных
    way=-1
    //////////////////////////////////
    // Блок проверок
    if(and(rhs<>[1 2])) then     // проверка на аргументы
        error(39)                // 39 стандартная ошибка числа аргументов
    end
    if(rhs==2) then              // Найдем массив для сортировки
        for i=1:rhs
            if(type(varargin(i))==1) then
                target=varargin(i)
                if(i>1) then
                    if(varargin(1)<>%f) then   // корректируем аргумент way
                        way=%t                 // если mas вторым аргументом
                    else
                        way=%f
                    end
                else
                    if(varargin(2)<>%f) then   // корректируем аргумент way
                        way=%t                 // если mas первым аргументом
                    else
                        way=%f
                    end
                end
                break
            end
        end
        // После цикла вероятно массив мы нашли, но если это не так
        if(target==-1) then
            error(52,1)      // сообщение, что матрицы среди аргументов нет
        end
    end
    if(rhs==1) then          // если аргумент один, то вероятно это массив
        if(type(varargin(1))==1) then
            target=varargin(1)
            way=%t
        else
            error(52,1)      // если нет массива, то ошибка
        end
     end
    // Конец блока проверок
    //////////////////////////////////

    // Основной блок
    varargout(1)=target 
    varargout(2)=way
endfunction

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

  • вводить аргументы в любом порядке, так как функция сначала попытается сама отыскать целевую матрицу. Однако, в дальнейшем мы не рекомендуем так делать, во-первых, потому что вы и сами видите как быстро разрастается блок проверок в зависимости от числа аргументов, во-вторых, пользователь строго должен быть ограничен прототипом функции, т.е. если функция ожидает целевую матрицу на первой позиции, значит так и должно быть;
  • вводить way любым типом данных, так как наша проверка не строгая: она реагирует только на логическую переменную %f.

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

// Следующий вызов спровоцирует обработчик ошибки 52 "Неверный входной тип данных"
-->ToSort('Error')
 !--error 52 
Неверный тип параметра 1: ожидалась матрица вещественных чисел.
at line      40 of function ToSort called by :  
ToSort('Error')

// Следующие два вызова спровоцирует ошибки неверного числа аргументов
-->ToSort()
 !--error 39 
Неправильное количество входных аргументов.
at line       8 of function ToSort called by :  
ToSort()
-->ToSort([1],%f,'Здесь три аргумента')
 !--error 39 
Неправильное количество входных аргументов.
at line       8 of function ToSort called by :  ри аргумента

// Следующие вызовы абсолютно корректны
-->ToSort(%t,[1 2 3])
 ans  =
    1.    2.    3.  
-->ToSort('неважно, все равно %t',[1 2 3])
 ans  =
    1.    2.    3. 
-->ToSort(1,%f)
 ans  =
    1.

Обратите внимание, что все проверки работают корректно. Теперь научим функцию сортировке. Так как ToSort() является головной, т.е. она не занимается непосредственно сортировкой, а только подготавливает данные для обработки, нам нужен по крайней мере один сортировочный алгоритм, который будет ей вызываться из Основного блока. Для простоты изложения, мы не будем вникать в сами алгоритмы, а будем довольствоваться уже готовыми реализациями. Например, пусть первым алгоритмом сортировки будет Пузырьковая сортировка (реализацию можно взять здесь).

После функции ToSort() запишите алгоритм пузырьковой сортировки, написанный на языке Scilab и приспособленный к вызову со стороны головной функции.

///////////////////////////////////////
// BubbleSort
// Назначение: сортирует mas пузырьковым алгоритмом
///////////////////////////////////////
function out = BubbleSort(mas,way)
    n=length(mas)                       // узнаем размер
    tmp=0                               // временная переменная
    i=0
    j=0
    for i=n-1:-1:1
        for j=1:i
            if(way==%t) then            // по возрастанию
                if(mas(j)>mas(j+1)) then
                    tmp=mas(j)
                    mas(j)=mas(j+1)
                    mas(j+1)=tmp
                end
            end
            if(way==%f) then            // по убыванию
                if(mas(j)<mas(j+1)) then
                    tmp=mas(j)
                    mas(j)=mas(j+1)
                    mas(j+1)=tmp
                end
            end
        end
    end
    out=mas
endfunction

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

//////////////////////////////////////
// Файл: sorting.sci
// Автор: ВикиУчебник
// Обозначения:
//    mas - целевой массив из чисел
//    way - направление сортировки
//      way = %t - по возрастанию
//      way = %f - по убыванию
// Вызовы:
// ToSort(mas) - сортировка mas по возрастанию
// ToSort(mas,way) - сортировка mas по way
//////////////////////////////////////

function varargout=ToSort (varargin)
    [lhs,rhs]=argn()
    target=-1                    // для защиты от неверного типа данных
    way=-1
    //////////////////////////////////
    // Блок проверок
    if(and(rhs<>[1 2])) then     // проверка на аргументы
        error(39)                // 39 стандартная ошибка числа аргументов
    end
    if(rhs==2) then              // Найдем массив для сортировки
        for i=1:rhs
            if(type(varargin(i))==1) then
                target=varargin(i)
                if(i>1) then
                    if(varargin(1)<>%f) then   // корректируем аргумент way
                        way=%t                 // если mas вторым аргументом
                    else
                        way=%f
                    end
                else
                    if(varargin(2)<>%f) then   // корректируем аргумент way
                        way=%t                 // если mas первым аргументом
                    else
                        way=%f
                    end
                end
                break
            end
        end
        // После цикла вероятно массив мы нашли, но если это не так
        if(target==-1) then
            error(52,1)      // сообщение, что матрицы среди аргументов нет
        end
    end
    if(rhs==1) then          // если аргумент один, то вероятно это массив
        if(type(varargin(1))==1) then
            target=varargin(1)
            way=%t
        else
            error(52,1)      // если нет массива, то ошибка
        end
     end
     
     if(length(target)==1) then             // Защита от матриц единичным размером
         warning("Вы пытаетесь отсортировать массив с одним элементом")
         return                             // Функция прерывает выполнение программы
     end
     
    // Конец блока проверок
    //////////////////////////////////

    // Основной блок
    varargout(1) = BubbleSort(target,way)
endfunction
///////////////////////////////////////
// BubbleSort
// Назначение: сортирует mas пузырьковым алгоритмом
///////////////////////////////////////
function out = BubbleSort(mas,way)
    n=length(mas)                       // узнаем размер
    tmp=0                               // временная переменная
    i=0
    j=0
    for i=n-1:-1:1
        for j=1:i
            if(way==%t) then            // по возрастанию
                if(mas(j)>mas(j+1)) then
                    tmp=mas(j)
                    mas(j)=mas(j+1)
                    mas(j+1)=tmp
                end
            end
            if(way==%f) then            // по убыванию
                if(mas(j)<mas(j+1)) then
                    tmp=mas(j)
                    mas(j)=mas(j+1)
                    mas(j+1)=tmp
                end
            end
        end
    end
    out=mas
endfunction

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

// сортируем по возрастанию
-->ToSort(%t,[28 32 64 56 85 103 31])
 ans  =
    28.    31.    32.    56.    64.    85.    103. 
// или так
-->ToSort([28 32 64 56 85 103 31])
 ans  =
    28.    31.    32.    56.    64.    85.    103.  
// сортируем по убыванию
-->ToSort([28 32 64 56 85 103 31],%f)
 ans  =
    103.    85.    64.    56.    32.    31.    28. 
// провоцируем ошибку
// Не обращайте внимание на ошибку 21 — это несовершенство функции return
-->ToSort(2,%f)
ВНИМАНИЕ: Вы пытаетесь отсортировать массив с одним элементом
 !--error 21 
Неправильный индекс.
at line      46 of function ToSort called by :  
ToSort(2,%f)
// Наша реализация может сортировать и матрицы, правда только по столбцам
-->ToSort([1 2 3; 4 5 6; 7 8 9],%f)
 ans  =
 
    9.    6.    3.  
    8.    5.    2.  
    7.    4.    1.

Хотим обратить ваше внимание на нескольких моментах:

Логические проверки
Обратите внимание, что логические проверки осуществляются с помощью ветвлений. Саму логическую конструкцию можно при этом вводить по разному, например, использовавшаяся нами запись

if(and(rhs<>[1 2])) then
...

полностью эквивалентна

if(rhs<>1 & rhs<>2) then
...

Обратите внимание, что эта проверка могла быть и такой

if(rhs>2) then
...

Последняя конструкция в данном контексте (проверить, что аргумента два) более предпочтительна, потому что она короче. Однако в другом контексте, например, «если аргументов 1, 5 или 6, делать так» предпочтительнее использовать конструкцию с and() (перебор по ограниченному множеству). Таким образом, пытайтесь ставить условия так, чтобы сократить запись.

Циклы
Чтобы поставить проверки на конвейер, используйте циклы. Однако, наша запись

for i=1:rhs
...

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

if varargin(1)==1 then
<тогда под индексом 2 у нас way>
else
<тогда way под индексом 1>
end

Если бы мы добавили в прототип еще аргументов, то ветвление пришлось бы полностью переписывать, когда как с циклом пришлось бы менять только его тело.

Обработчики ошибок
Вы наверное обратили внимание, например, на следующий вызов

error(52,1)

В данном контексте функция error() выведет заранее заготовленное сообщение для ошибки 52 «Неверный тип параметра». Аргумент со значением 1 указывает на номер этого неверного параметра. Некоторые типовые ошибки рекомендуется обрабатывать именно через error(), потому что шаблон, который увидит пользователь, автоматически будет локализован. Это означает, что если вашу программу запустил человек, разговаривающий на другом языке и допустил ошибку, то, если Scilab поддерживает этот язык, шаблон будет выведен именно на нем.

Функцию error() можно вызывать по-разному

// Следующий вызов выведет произвольное сообщение, а ошибке будет присвоен номер 10000
-->error("Это моя ошибка")
                                    !--error 10000 
Это моя ошибка

// Этот вызов присвоит ошибке номер, который вы хотите
-->error(52,"Моя ошибка с номером 52")
                                                     !--error 52 
Моя ошибка с номером 52

// Следующая инструкция вызывает шаблон для типовой ошибки 31
-->error(31)
          !--error 31 
Неправильная строка.

// Следующая инструкция вызывает шаблон для типовой ошибки, в который нужно передавать параметр
-->error(52,5)
            !--error 52 
Неверный тип параметра 5: ожидалась матрица вещественных чисел.

Обратите внимание на то, что если ваш номер совпадет с номером типовой ошибки, то он его перекроет, поэтому для своих ошибок рекомендуется выбирать номера именно после 10000. Для некоторых типовых ошибок необходимо передавать параметры, как например для ошибки 52, где параметр это число, стоящее после фразы «Неверный тип параметра...». С типовыми ошибками можно ознакомиться в справочной информации или в приложении к этому учебнику.

Самостоятельно

Теперь немного модернизируйте функцию ToSort() (сохраните это в новый файл). Пусть ее прототип будет таким

  • ToSort(way,mas1,mas2,mas3)

Т.е. теперь функция ToSort() может сортировать несколько переданных массивов (в этой реализации пусть их будет строго 3). Предусмотрите следующее:

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

Использование именованных параметров в прототипах[править]

Быстрая перегрузка[править]

В Scilab предусмотрен другой способ перегрузки функций и операторов, которую можно условно назвать быстрой, потому что все процедуры проверки аргументов среда берет на себя. Эта перегрузка может показаться сложной из-за специфичного синтаксиса.

Начнем с перегрузки операторов. Некоторые поддерживаемые операторы могут быть перегружены. Это значит, что, например, оператор ' + ' будет способен выполнять не только операцию сложения двух чисел, но и любые другие действия в зависимости от типов операндов. Чтобы перегрузить оператор, нужно сначала убедиться, что он поддерживается быстрой перегрузкой (полный список поддерживаемых операторов можно посмотреть в справке Scilab или в приложении к этому учебнику). После этого необходимо запомнить его код для перегрузки.

Синтаксис для бинарного оператора выглядит так

// пусть а — левый операнд
//       b — правый операнд
function out = %<тип левого операнда>_<код бинарного оператора>_<тип правого операнда>(а,b)
<инструкции>
endfunction

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

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

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

// символ 'c' кодирует строковый тип данных
// символ 's' кодирует числовой тип данных
// символ 'a', ВНИМАНИЕ только в конструкции %c_a_s, кодирует оператор «сложения» '+'
-->function out=%c_a_s(a, b)     // предполагается, что операнд 'a' строковый, а 'b' — числовой
-->out = a + string(b)           // функция string преобразует в строковый тип данных
-->endfunction

Теперь оператор сложения перегружен и не требует от операндов одинакового типа данных. Попробуйте следующие вызовы

-->'str'+5
 ans  = 
 str5   

// В этом примере первый оператор выполнился по инструкциям, определенным средой,
// т.к. складывать строки не запрещено, а второй оператор выполняется по вашей перегрузке
-->'str'+' '+5
 ans  =
 str 5 

// Однако такой вызов окажется некорректным и для этого случая нужна отдельная перегрузка
-->5+'str'
    !--error 144 
Операция для заданных операндов не определена.
отметьте или определите функцию %s_a_c как перегружаемую.

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

Для перегрузки унарных операторов необходимо использовать следующую конструкцию

function out = %<тип операнда>_<код оператора>(а)
<действия>
endfunction

Для примера перегрузим унарный оператор инверсии ' ~ '

// символ '5' кодирует символ инверсии
-->function out=%c_5(a)
-->out=a+'1'
-->endfunction

// Тогда оператор теперь приобретает новый смысл
-->~'a'
 ans  =
 a1

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

function out = %<тип единственного аргумента>_<имя функции>(a)   // a - единственный аргумент
операторы
endfunction

Например, такая примитивная функция как exp(), которая ожидает в качестве аргумента матрицу с вещественными числами, может реагировать иначе

-->exp(1)
 ans  =
 
    2.7182818  
 
-->function out = %c_exp(a)                        // символ 'c' кодирует строковый тип данных
-->out = 'Вы передали строковую переменную '+ a
-->endfunction
 
-->exp('привет')
 ans  =
 Вы передали строковую переменную привет

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

Имитация объектов[править]

Для читателей, не знакомых с объектно-ориентированным программированием, следующий материал может показаться трудным, поэтому следующие несколько абзацев подготовят их для понимания. Объектно-ориентированное программирование (далее для краткости ООП) — это парадигма программирования, в которой основными концепциями являются понятия класса и объекта. ООП является развитием процедурного программирования и призвано для упорядочивания и структурирования частей программы. Вы могли заметить, что данные и функции, которые ими оперируют, существуют независимо друг от друга. Это означает, что функциям в сущности «все равно» какую смысловую природу имеют передаваемые им данные, главное чтобы ожидаемый тип совпадал.

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

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

Упражнение

Рассмотрим в качестве примера класс, объекты которого хранят сведения о некотором человеке[1].

Пусть класс будет иметь имя «person» со следующими полями:

  • имя,
  • фамилия,
  • возраст.

Так как инкапсуляции в Scilab нет, то все методы и конструкции по сути будут являться обыкновенными функциями. Откройте Scinotes и объявите конструктор класса. Конструктором класса в ООП принято называть метод, который инициализирует поля экземпляра класса некоторыми начальными значениями. Для объявления мы используем типизованный список, который, напомним, создается через функцию tlist().

//Файл: Class.sci
////////////////////////////////////
//person_new
//Назначение: конструктор класса "person"
function out = person_new ()
    out = tlist(["person","name","firstname","age"])   // создаем типизованный список
    // инициализируем поля пустыми строками
    out.name = ""
    out.firstname = ""
    out.age = ""
endfunction

Теперь мы можем создавать объект класса вызовом его конструктора. Теперь нужно написать несколько типовых методов, которые позволят работать с полями экземпляра класса. Очевидно, что нужны такие типовые методы, как:

  • пользовательская инициализация полей;
  • форматный вывод экземпляра;
  • и, для примера, мы перегрузим оператор '==', чтобы два экземпляра этого класса можно было бы сравнивать.

Инициализирующий поля метод может выглядеть следующим образом

///////////////////////////////////
//person_get - метод класса person
//Назначение: инициализирует поля объекта
///////////////////////////////////
function out = person_get(varargin)
    [lhs,rhs]=argn()
    if rhs == 0 then                                        // Защита от пустых аргументов
        error("Вы не передали ни одного аргумента")
    end
    if typeof(varargin(1)) <> 'person' then                 // Проверка на принадлежность к классу
        error("Ожидался экземпляр класса person")
    else
        out = varargin(1)
    end
    
    if rhs < 2 then                                         // Защита от пустого вызова 
        error("За один вызов нужно проинициализовать хотя бы одно поле")
    end
    i = 2
    while i <= rhs                                          // Основной цикл
        select varargin(i)
        case "-name" then
            if i+1 <= rhs  then
                out.name = varargin(i+1)
                i=i+2
                continue
            end
        case "-firstname" then
            if i+1 <= rhs  then
                out.firstname = varargin(i+1)
                i=i+2
                continue
            end
        case "-age" then
            if i+1 <= rhs  then
                out.age = varargin(i+1)
                i=i+2
                continue
            end
        end
        i=i+1
    end
endfunction

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

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

Теперь объявите функции в сеансе и попробуйте следующие вызовы.

-->a=person_new()                          // вызываем конструктор класса person
 a  =
       a(1)
 
!person  name  firstname  age  !
 
       a(2)
 
       a(3)
 
       a(4)
-->a=person_get(a,'-name','Стеклов','-firstname','Борис','-age',23)  // инициализируем поля
 a  =
 
       a(1)
 
!person  name  firstname  age  !
 
       a(2)
 
 Стеклов   
 
       a(3)
 
 Борис   
 
       a(4)
 
    23.

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

В предыдущих вызовах можно видеть, что объект нашего класса выводится по заложенному в Scilab формату для списков, который не очень удобен для чтения. Для типизованных списков можно использовать особую перегрузку, которая позволяет настраивать формат так, как нужно вам. Синтаксис этой перегрузки имеет следующий вид

// здесь a — это передаваемый экземпляр класса  
function [] = %<имя класса>_p(a)
<действия по выводу>
endfunction

Для нашего класса оформим перегрузку так.

///////////////////////////
// Перегрузка вывода для объектов класса person
///////////////////////////
function [] = %person_p(a)                         
    mprintf("Экземпляр класса %s\n",a(1)(1,1))      // помним, что a(1) это строковый вектор
    mprintf("========================\n")
    mprintf("Фамилия: %s\n",string(a.name))
    mprintf("Имя: %s\n",string(a.firstname))
    mprintf("Возраст: %s\n",string(a.age))
endfunction

Снова прогрузите нашу перегрузку в среду. Теперь при любом упоминании любого объекта класса person у нас будет следующий форматный вывод, заданный нами в перегрузке.

// простой вызов
-->a
 a  =
 
Экземпляр класса person
========================
Фамилия: Стеклов
Имя: Борис
Возраст: 23

// присваиваем нашему объекту значение вручную
-->a.age = 25
 a  =
 
Экземпляр класса person
========================
Фамилия: Стеклов
Имя: Борис
Возраст: 25

// создаем другой объект класса person
-->b = person_new()
 b  =
 
Экземпляр класса person
========================
Фамилия: 
Имя: 
Возраст:

Теперь вспомните как мы создавали окно Figure через scf(), которая является конструктором Figure. Именно такая же перегрузка типизованного списка вызывается при обращении к дескриптору окна. Наконец, давайте перегрузим оператор '=='.

/////////////////////////////////////
// Особая операция над объектами класса person
// Возвращает ИСТИНУ если два объекта person имеют одинаковый возраст, иначе ЛОЖЬ
function equ = %person_o_person(a,b)      // Обратите внимание, что ваш тип данных может быть спокойно быстро перегружен
                                          // здесь символ 'o' кодирует оператор '=='
    if a.age <> b.age then
        equ = %f
    else
        equ = %t
    end
endfunction

Теперь мы можем выполнять логическую операцию над объектами созданного нами класса. Например,

-->a == b
 ans  =
 
  F  
-->b.age = 25;
 
-->a == b
 ans  =
 
  T

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

mlist для создания объектов[править]

В плане организации пользовательского класса tlist и mlist списки ничем друг от друга не отличаются. Тогда возникает вопрос: почему же не использовать только tlist? Одним из преимуществом mlist перед tlist является способ выделения и вставки элементов, а именно по индексам как в матрицах (именно поэтому mlist и называется матричноориентированным типизованным списком). Например, вы можете присваивать или извлекать значения из полей не пользуясь их именами, а используя только индексы. Такой подход мотивирован тем, что допустить ошибку при написании имени намного легче, чем использовать индексацию по номерам. С другой стороны, индексы «обезличивают» объект, так как именами можно давать интуитивно понятные подсказки. В этом случае нет никакой рекомендации, какой из списков использовать для создания класса, и решение обычно приходит из удобства в обращении к объектам.

Для примера покажем как перегрузить операцию выделения элемента списка по его индексу (эта операция для mlist списков по умолчанию не определена).

// Перегрузка операции выделения для mlist списка
// Здесь предполагается, что список имеет два поля field1 и field2
// запомните эту перегрузку по символу 'e' (от extraction)
// 'e' на самом деле кодирует конструкцию (i1,i2,...in,a), где
// a — передаваемый объект (в данном случае это список)
function out = %<имя mlist-класса>_e(varargin)
// здесь в varargin будут передаваться желаемые индексы и собственно сам mlist-объект
// varargin — это вектор, в котором передаваемый mlist-объект записан в конец
tg = varargin($)
out = 'Имя поля: ' + tg.field1(varargin(1:$-1)) + ' — ' + string(tg.field2(varargin(1:$-1)))
endfunction

Определите в среде mlist-класс и перегрузку индексации для него

-->a=mlist(['Box','name','val'],['Яблоки' 'Груши';'Апельсины' 'Ананасы'],[15 13; 8 6]);
-->function out = %Box_e(varargin)
-->tg=varargin($)
-->out = 'Имя поля: ' + tg.name(varargin(1:$-1)) + ' - ' + string(tg.val(varargin(1:$-1)))
-->endfunction

В этом примере мы создали mlist-список Box (коробка). Допустим мы хотим узнать сколько в «коробке» апельсинов.

При обычной работе со списком это происходит так

-->a.name(2,1)
 ans  =
 Апельсины   
 
-->a.val(2,1)
 ans  =
    8.

А теперь, когда формат вывода задан перегрузкой

-->a(2)
 ans  =
 Имя поля: Апельсины - 8   
 
-->a(4)
 ans  =
 Имя поля: Ананасы - 6  

// вы даже можете использовать два индекса как в матрице
// например, выведем все содержимое ящика
-->a(:,:)
 ans  =
!Имя поля: Яблоки - 15       Имя поля: Груши - 13     !
!                                                                              !
!Имя поля: Апельсины - 8  Имя поля: Ананасы - 6  !

Отметим, что принципиально такое объявление класса Box ничем не отличается от такого

-->b=tlist(['box1' 'name' 'val'],['Яблоки' 'Апельсины' 'Груши' 'Ананасы'],[15 8 13 6]);
-->b.name(2)
 ans  = 
 Апельсины   

-->b.val(2)
 ans  =
    8.

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

// на самом деле 'i' кодирует запись a(i1,i2,...,in)=b
// a — цель с возможностью индексации
// b — источник; любой объект в зависимости от того, что вы хотите сделать
// в этом примере мы предполагаем, что это список list с двумя индексами
function target = %<источник>_i_<имя mlist-класса: цель>(varargin)
// здесь в varargin записывается вектор [i1,i2,...,in,b,a]
target = varargin($)
source = varargin($-1)
// здесь предполагается, что наш класс имеет поля field1 и field2
// зная источник и цель мы просто выполняем правильное присваивание
target.field1(varargin(1:$-2)) = source(1)
target.field2(varargin(1:$-2)) = source(2)
endfunction

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

// здесь 'l' кодирует объект list
-->function target = %l_i_Box(varargin)
-->target=varargin($)
-->source=varargin($-1)
-->target.name(varargin(1:$-2))=source(1)
-->target.val(varargin(1:$-2))=source(2)
-->endfunction

Теперь, пусть у нас вынули из коробки все яблоки и положили 18 киви

-->a(1) = list('Киви',18);
-->a(1)
 ans  =
 Имя поля: Киви - 18

Наконец, наш класс Box также можно перегрузить форматным выводом. Делается это также как и для tlist-списков

-->function [] = %Box_p(a)
-->disp(a.name+': '+string(a.val))
-->endfunction

Однако, обратите внимание, что определенный нами формат будет выводится только, если вы обратитесь к объекту целиком, когда как обращаясь к элементу по индексу, вы получите вывод, определенный в перегрузке %<имя mlist-класса>_e.

-->a
 a  =
 
!Киви: 18         Груши: 13     !
!                                          !
!Апельсины: 8  Ананасы: 6  !
 
-->a(1)
 ans  =
 
 Имя поля: Киви - 18

Подводя короткий итог, tlist-списки и mlist-списки используются в одинаковой мере для имитации объектов. Если предполагается обращаться к объектам по именам полей, то вполне подойдет tlist-реализация. Если предполагается адресовать элементы по индексам, то вполне подойдет mlist-реализация, которая немного облегчает написание методов.

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

Перегрузка для tlist- и mlist-списком осуществляется по одинаковым правилам, важно только следить за правильным определением прототипа перегрузки.

Самостоятельно

Воспользуйтесь классом person, который мы до этого создавали в упражнении. Создайте еще один класс "home", который как бы имитирует жилой дом. Пусть у этого класса будут следующие поля:

  • address (адрес) — строка, в которую записывается адрес дома;
  • flats (количество квартир) — количество квартир в доме (предполагается, что дом с одним подъездом);
  • total_people — число жильцов в доме;
  • people (жильцы) — list-список из жильцов, записи которого принадлежат классу person.

Реализуйте следующие методы:

  • конструктор класса home;
  • функцию, в которую вкладывается конструктор home, которая позволит вводить новый дом пользователю. Используйте форматный ввод данных;
  • метод, позволяющий вкладывать в поле people экземпляры класса person. Предусмотрите, чтобы конструктор класса person вызывался в этом методе, и пользователь мог определить нового человека на месте. Также предусмотрите, чтобы при добавлении каждого нового жильца поле total_people обновлялось автоматически;
  • объявите перегрузку вывода для класса home и определите формат вывода экземпляров этого класса;
  • метод, позволяющий таблицей выводить жильцов указанного дома. Формат шапки представлен ниже.
Дом
Адрес: ул. Вечнозеленая — 23
Количество квартир: 12
=======================================
№     Фамилия       Имя        Возраст
=======================================
1.    Стеклов       Борис           25
2.    Соколова      Анна            31
3.    Ященко        Алексей         20
=======================================
Всего жильцов: 3

Проблема защиты полей классов[править]

Сценарии[править]

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

Сценарии в Scilab занимаются всевозможными задачами. Так, есть сценарии подготавливающие окружение для предстоящих расчетов (обычно они занимаются присвоением значений и некоторыми логическими проверками); вспомогательные сценарии (обычно эти сценарии подгружают в среду библиотеки или наоборот выгружают, объявляют классы, рисуют графический интерфейс и т.п.); сценарии, которые участвуют в расчетах и автоматизируют процесс (типичными для них действиями является вызов функций, ожидание ввода, вывод расчетов в виде графиков или как то еще и т.п.).

Сценарии оформляются в *.sce файлах. Существует также два особых вида сценария: strart-сценарий и quit-сценарий, которые оформляются обычно для модулей, и занимаются соответственно подгрузкой и выгрузкой модуля из сеанса. Об особых сценариях мы будем говорить далее, когда начнем учиться писать модули, а сейчас речь пойдет именно о sce-сценариях.

Некоторые общие правила в оформлении сценариев[править]

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

  • Вывод команд сценария в командное окно можно включать и отключать с помощью функции mode(). У этой функции несколько режимов при обработке этого процесса.
  • В сценариях действуют абсолютно все правила, которые мы применяли при вводе команд в командное окно. В частности, для сценариев можно не придерживаться строго правила «одна строка — одна инструкция» для некоторых случаев. Важно, чтобы при этом код можно было легко прочитать.
// При объявлении переменных в сценарии можно использовать запятую для получения компактной записи

// Следующая запись предпочтительнее
a = 5, b = 6, c = 7
str = 'abc', mrx = [1 2; 3 4]

// чем эта
a = 5
b = 6
c = 7
str = 'abc'
mrx = [1 2; 3 4]
  • Любой сценарий рекомендуется начинать сопроводительным пояснением. Это правило настоятельно рекомендуется использовать всегда. Обычно сопроводительное пояснение содержит: назначение сценария в общих словах, последнее обновление сценария, авторы сценария, условия на которых распространяется код сценария. Обычно для отлаженных сценариев еще вводится просьба о не редактировании кода.
  • Настоятельно рекомендуется в сценариях использовать только одноразовые функции. Сценарий должен быть незаметен для пользователя, т.е. выполнить все, что от него требуется, «за кулисами». Отсюда нет необходимости хранить функции, которые использует сценарий в своей работе. Как только сценарий вызвал функцию и получил от нее результат, он должен сразу после этого ее выгрузить. Для многоразовых функций существуют библиотеки, которые, кстати, сценариями подгружаются.
  • Сценарий не должен оставлять после себя мусор, т.е. все временные объекты сценария должны быть полностью выгружены к концу его выполнения. Не стоит путать при этом временные объекты сценария с объектами, которые он объявляет и инициализирует для пользователя.
  • Все ключевые шаги сценария всегда следует комментировать.

Ниже приведен в качестве примера основной start-сценарий пакета Scilab, который подготавливает окружение для нового сеанса. Внимательно изучите его.

// Scilab ( http://www.scilab.org/ ) - This file is part of Scilab
// Copyright (C) 2007-2008 - INRIA
// Copyright (C) 2009-2011 - DIGITEO
//
// This file must be used under the terms of the CeCILL.
// This source file is licensed as described in the file COPYING, which
// you should have received as part of this distribution.  The terms
// are also available at
// http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt

// Main Scilab initialisation file

mode(-1);  // silent execution mode

// clean database when restarted ======================================
predef("clear"); //unprotect all variables
clear  // erase all variables
clearglobal();

// Set stack size   ===================================================
function setStackSize()
  defaultstacksize = 10000000;
  old = stacksize()
  params = sciargs();
  nparam = find(params == "-mem");
  if (nparam) then
    ierr = execstr("newstacksize=" + params(nparam + 1), "errcatch");
    if (ierr == 0) then
      if old(1) <> newstacksize then
        stacksize(newstacksize);
      end
    else
      if old(1) <> defaultstacksize then
        stacksize(defaultstacksize);
      end
    end
  else
    if old(1) <> defaultstacksize then
      stacksize(defaultstacksize);
    end
  end
endfunction
setStackSize();
clear setStackSize;

// Special variables definition =======================================
oldieee=ieee()
ieee(2);
%inf = 1/0;
ieee(0);
%nan = %inf-%inf;
// boolean variables
%T = %t;
%F = %f;
%tk = (with_module("tclsci") & getscilabmode() <> "NWNI");

ieee(oldieee);
clear oldieee

// Default Obsolete Warning policy  ===================================
global %modalWarning;
// False -> Scilab will only display a Warning message in the console
//          if warnings are enabled [warning("on"/"off")].
// True  -> Scilab will show a blocking popup.
%modalWarning = %F;
clear %modalWarning;

// Create some configuration variables ================================
PWD = pwd();

// Startup message  ===================================================
if (sciargs() <> "-nb") & ~fromjava() & ~fromc() & getscilabmode() == "STD" then
  write(%io(2),[" ";gettext("Startup execution:")]);
  write(%io(2),gettext("  loading initial environment"));
end

if ((getscilabmode() == "NWNI" | getscilabmode() == "NW") & ~fromjava() & ~fromc() & sciargs()<>"-nb")
   [v, opts] = getversion()
   write(%io(2), strsubst(v, "scilab-", "Scilab ") + " (" + opts($-1) + ", " + opts($) + ")");
   clear v opts;
end

// loads modules ======================================================
modules    = getmodules();

// Map 'load' to 'old binary files load' called %_load to be able to load Scilab libraries
warning("off");
load = %_load;
warning("on");

for i=1:size(modules,"*")
  exec("SCI/modules/" + modules(i) + "/etc/" + modules(i) + ".start", -1);
end
clear modules i load;

// Create some configuration variables ================================
home = getenv("HOME", SCI);
if getos() <> "Windows" then
  if getenv("PRINTERS", "ndef") == "ndef" then
    setenv("PRINTERS", "lp");
  end
end
setenv("VERSION", getversion());

// ATOMS ===============================================================
if with_module("atoms") then
  atomsSystemInit();
  if sciargs() <> "-noatomsautoload" then
    atomsAutoload();
    clear atomsAutoload;
  end
  clear atomsSystemInit;
end


// Protect variable previously defined  ================================
predef("all");

// At startup, no interactive vertical paging by default. ==============
lines(0);

// load contrib menu if present ========================================
function loadContrib()
  if isfile(SCI+"/contrib/loader.sce") then
    global %toolboxes;
    global %toolboxes_dir;
    exec(SCI+"/contrib/loader.sce");
  end
endfunction
loadContrib();
clear loadContrib;

// calling user initialization =========================================
if sciargs()<>"-nouserstartup" then

  startupfiles = [ SCIHOME + filesep() + ".scilab"     ; .. // Home directory startup
                   SCIHOME + filesep() + "scilab.ini" ];    //  ""      ""    startup

  if SCIHOME <> pwd() then
    startupfiles = [ startupfiles     ; ..
                     ".scilab"        ; .. // Working directory startup
                     "scilab.ini" ]   ;    //  ""         ""    startup
  end

  for i = 1:size(startupfiles, "*")
    if isfile(startupfiles(i)) then
      exec(startupfiles(i),-1);
    end
  end

  clear i;
  clear startupfiles;
end

// Menus/toolbar can now be enabled ====================================
if getscilabmode() == "STD" then
  setmenu(gettext("&File"));
  setmenu(gettext("&Edit"));
  setmenu(gettext("&Preferences"));
  setmenu(gettext("&Control"));
  setmenu(gettext("&Applications"));
  if ~with_module("scinotes") then // Desactivate Editor menu
    unsetmenu(gettext("&Applications"), 1);
  end
  if ~with_module("xcos") then // Desactivate xcos menu
    unsetmenu(gettext("&Applications"), 3);
  end
  if ~with_module("m2sci") then // Desactivate mfile2sci menu
    unsetmenu(gettext("&Applications"), 5);
  end
  if ~with_module("atoms") then // Desactivate atoms menu
    unsetmenu(gettext("&Applications"), 7);
  end
  setmenu(gettext("&?"));
  setmenu(gettext("&Toolboxes"));
  toolbar(-1, "on");
end
// ====================================================================
clear ans

Этот пример хорошо иллюстрирует как следует писать типичный вспомогательный сценарий. Разберем его по частям. Сценарий состоит из 13 смысловых частей. Самой первой инструкцией mode() сценарий переключается в тихий режим, когда команды не выводятся в командное окно. Тем не менее, по желанию можно переключить режим и регулировать выводимые инструкции с помощью символа ';'. Обычно режим с отображением команд используется, когда необходимо знать в какой точке находится сейчас сценарий.

  • Сначала сценарий подготавливает стеки. Для этого он объявляет одноразовую функцию setStackSize(), которая сначала проверяет не было ли изменено стандартное значение размера стека. Если значение было изменено, то функция постарается выделить память, используя новое значение. Если у нее это не получается, то будет выделена память под значение по умолчанию, которое равно 10000000. Сразу же за объявлением этой функции идет ее вызов, а потом очистка. Это и логично, потому что стек сеанса подготавливается один раз.
  • Затем сценарий объявляет псевдонимы для констант и включает режим обработки неопределенностей.
  • %modalWarning определяет политику предупреждений. Этот кусок как вы можете видеть не используется.
  • В блоке Create some configuration variables, как несложно догадаться, определяются переменные. Здесь объявляется только одна переменная PWD, которая хранит полный путь до директории с документами текущего пользователя компьютера.
  • В следующем блоке выводится сообщение в командное окно о загрузке окружения. Теперь вы знаете в какой точке находится сценарий, когда появляется это сообщение.
  • Следующий блок является наиболее ответственным, так как он занимается загрузкой ядра и модулей. Перечень того, что нужно подгрузить, сценарий получает вызовом функции getmodules(). У каждого модуля есть свой start-сценарий, который и занимается непосредственно подгрузкой функций, а сценарий Scilab просто отдает им распоряжение сделать это. На ваших глазах выполняется правило «разделяй и властвуй», когда одна большая задача дробиться на более мелкие.
  • После того, как ядро и модули подгружены, снова происходит определение переменных окружения: home — путь до рабочей папки пользователя, определенной операционной системой; PRINTERS — для драйвера работы с принтерами; VERSION — версия пакета.
  • Далее происходит подгрузка модуля ATOMS, о котором речь пойдет ниже.
  • После подгрузки ATOMS можно считать, что среда на 90% готова к работе. Далее идут более мелкие инструкции типа защиты стека, загрузка шаблонов разработки модулей, уточнение пользовательских директорий и отрисовка строки меню вверху окна (серия вызовов setmenu()). Наконец, очищается переменная ans и управление передается пользователю.

Также отметим, что до самого запуска этого сценария, произошел еще запуск скомпилированной части ядра, включающий интерпретатор Scilab (когда вы кликаете по exe-файлу Scilab), после которого и становится возможным запуск основного start-сценария.

Особые конструкции для сценариев[править]

Как и функции, сценарии тоже должны обладать свойством устойчивости. Ранее мы показывали, как организуются проверки аргументов или данных внутри функций через ветвления. В алгоритмах, как правило, все известно о множествах возможных значений аргументов, поэтому для проверок достаточно ветвлений. В сценариях процессы протекают более свободно и к каждой инструкции проверку не поставишь. Поэтому для перехватов ошибок в сценариях удобно использовать особую конструкцию try...catch. Эта конструкция может использоваться и функциях, но на практике обычно хватает ветвлений.

Конструкция try...catch имеет следующий синтаксис:

try
<инструкции сценария>
catch
<инструкции, выполняемые в случае ошибки в одной из инструкций основного блока>
end

Другие языки программирования[править]

Отладка и оптимизация[править]

Создание пользовательских библиотек[править]

Библиотека (англ. library) — это организованный набор функций, написанных на языке Scilab (т.е. являющихся макросами), хранящийся на нескольких файлах. Библиотека является самой простой организацией программируемых функций в среде. Обычно библиотеки являются «кирпичиками» при построении модулей, которые помимо всего прочего могут также содержать файлы справки и файлы с примитивами.

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

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

Сам этап сборки библиотеки довольно прост и состоит главным образом из вызова функции genlib(). Сборка должна проводиться в отдельном каталоге, в котором вы должны разместить sci-файлы с функциями. Полный прототип функции genlib() имеет вид

// Первый прототип
genlib(lib_name [[,dir_name, [ Force [,verb [,Names]]]])
// Второй прототип
genlib(lib_name [,path=dir_name] [,verbose=verb] [,force=Force] [,names=Names])
//
// Обозначения //
// lib_name — строка; имя библиотеки
// dir_name — строка; полный путь до каталога в используемой файловой системе, в котором хранятся sci-файлы функций
// Force — логическая константа; в случае %t сценарий перекомпилирует все функции в sci-файлах. По умолчанию установлена %f.
// Эта возможность используется, когда скомпилированная версия функции не соответствует коду sci-файла, например, когда вы его немного модернизировали.
// verb — логическая константа; в случае %t процесс сборки будет сопровождать вывод информации о состоянии в командное окно.
// Names — вектор из строк, которые являются именами функций в библиотеки. По умолчанию будут просмотрен весь каталог dir_name,
// и имена sci-файлов будут приняты за имена функций.

После сборки библиотеки в каталоге появятся новые файлы:

  • bin-файлы, которые являются скомпилированными версиями функций в sci-файлах. Из этих файлов и подгружаются функции, поэтому sci-файлы, которые будут находиться рядом с ними, будут фактически уже не нужны. Тем не менее, не следует удалять sci-файлы, потому что они могут вам понадобится в будущем для отладки или для оптимизации, или просто для того, чтобы освежить в памяти применяемые алгоритмы.
  • файл names — служебный файл, который содержит имена функций библиотеки. Используется во время подгрузки специальной функцией.
  • файл lib — служебный файл, который используется сценарием подгрузки и фактически является головой библиотеки.

Перед непосредственной сборкой следует придерживаться следующих правил:

  1. Каждая законченная функция должна хранится в своем sci-файле, и имя этой функции должно в точности совпадать с именем файла. Например, если функция имеет имя function123, то и файл должен быть function123.sci.
  2. Внутри sci-файла может быть несколько функций, но только та функция, которая находится в голове файла будет доступна для вызова в среде. Другие функции считаются вспомогательными и могут взаимодействовать только с головной в пределах данного файла.
  3. Наличие расширения sci желательно всегда, так как это облегчает работу функции genlib().

Для загрузки готовой библиотеки в среду используется функция lib(). Ее вызов достаточно прост

namelib = lib('lib_dir')

// lib_dir — каталог, в котором хранится собранная библиотека

Функция lib() ищет файлы lib и names и с помощью них подгружает библиотеку. В случае успеха функция вернет имя загруженной библиотеки. Для выгрузки используется знакомая вам функция clear.

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

Каталог со sci-файлами, если рассуждать не строго, тоже можно назвать библиотекой. Выгрузить такую библиотеку можно с помощью функции getd(), которая работает примерно как и lib(), только проходится непосредственно по sci-файлам. Однако, такой подход имеет ряд недостатков:

  • нескомпилированные функции работают медленнее;
  • выгрузить функции все сразу уже невозможно, и вам придется применять clear для каждой функции в отдельности.

Всегда следует стремится объединять функции в библиотеки из-за удобства такой организации. Файлы sci рекомендуется применять на начальных этапах, когда реализуемый вами алгоритм еще толком не завершен.

Приемы работы с командным окном при программировании[править]

Создание пользовательских модулей[править]

Большие и сложные проекты, созданные в среде Scilab, обычно состоят из большого количества файлов с большим количеством библиотек функций. Такие проекты могут существовать в более организованном виде, называемом модулем (англ. Module или ATOMS Toolbox) или иногда его еще называют расширением. Тут следует отметить, что практически все функции, которыми мы до этого пользовались, также являются частью какого-либо модуля, подключенного к ядру Scilab. Модули гораздо удобны в обращении, так как их легко подключать и отключать от основного сценария Scilab.

Некоторые модули написаны разработчиками, а некоторые были ими добавлены в пакет ввиду их полезности. Кроме того, пользователи Scilab могут самостоятельно создать модуль и затем официально распространять его через Интернет.

Модули различаются по их целевому назначению на:

  • Обычные — взаимодействие с модулем происходит главным образом из командного окна;
  • Xcos модули — взаимодействие с модулем происходит из встроенного моделировщика Xcos.

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

Подключение официальных модулей[править]

Для управления официальными модулями служит подсистема ATOMS. С данной подсистемой можно работать из командного окна, но для непосредственной загрузки удобнее всего пользоваться ее графическим окном. Чтобы его открыть сделайте активным Командное окно, а затем в меню Инструменты выберите пункт Управление модулями Atoms. Чтобы окно открылось, вы должны быть подключены к Интернет.

После запуска ATOMS, он подключится к сервису Scilab.org и обновит список официальных модулей, хранящихся на нем. Все они отсортированы по категориям в виде дерева. Открыв категорию, перед вами появится список модулей, и, выбирая из списка некоторый модуль, в правой части окна будет представлена такая информация, как назначение модуля, его автор(ы), версия, дата создания, размер.

Упражнение

В качестве примера перейдите в категорию Образование (англ. Education) и выделите модуль Sudoku (Авторы: Stefan Bleeck, Cleve Moler, Michael Baudin). По названию модуля несложно понять, что в его задачи входит генерирование таблиц настольной игры Судоку. Теперь попробуем установить модуль, для чего нажмите на кнопку Установить после всего текста внизу. После загрузки модуля и его установки, он выделится зеленым цветом, кроме того, внизу окна вы можете заметить галочку на параметре Автозагрузка. Это означает, что при загрузке Scilab, этот модуль будет подгружаться главным сценарием автоматически.

Однако, после загрузки модуль еще не активен. Чтобы он загрузился, перейдите на Командное окно и введите команду

-->atomsAutoload

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

Так как, вообще говоря, вы видите этот модуль впервые, то вам просто необходимо знать какие функции входят в модуль и что они делают. К счастью, каждый правильно собранный модуль содержит справку, которая интегрируется в общую справочную систему Scilab. Нажмите <F1> или, если окно справки было открыто, закройте и откройте его снова. В самом низу вы обязательно увидите, что в дереве появилась новая категория с именем Sudoku. Конечно, дальнейшее удобство работы с модулем зависит от того, насколько на доступном языке написана справка по модулю и здесь стандарта никакого нет, тем не менее, грамотный разработчик всегда должен ставить себя на место пользователя и старается предугадать степень образованности своей потенциальной аудитории, чтобы подбирать понятные слова.

В качестве примера, давайте попробуем функцию данного модуля, которая генерирует таблицу Судоку вместе с ее решением.

-->[A,B]=sudoku_create()    // Внимание: генерация таблицы может занять довольно длительное время на слабом процессоре
 B  =
 
    8.    2.    6.    4.    5.    7.    3.    1.    9.  
    7.    3.    9.    1.    8.    6.    2.    5.    4.  
    1.    4.    5.    3.    9.    2.    8.    7.    6.  
    6.    9.    7.    8.    4.    5.    1.    3.    2.  
    3.    8.    2.    9.    7.    1.    4.    6.    5.  
    5.    1.    4.    6.    2.    3.    7.    9.    8.  
    2.    5.    3.    7.    6.    4.    9.    8.    1.  
    4.    7.    8.    5.    1.    9.    6.    2.    3.  
    9.    6.    1.    2.    3.    8.    5.    4.    7.  
 A  =
 
    0.    2.    0.    0.    5.    0.    3.    0.    9.  
    0.    0.    0.    1.    8.    0.    0.    0.    4.  
    0.    0.    0.    3.    0.    0.    8.    0.    0.  
    0.    9.    7.    0.    0.    0.    0.    0.    2.  
    0.    8.    0.    0.    0.    0.    0.    6.    0.  
    5.    0.    0.    0.    0.    0.    7.    9.    8.  
    0.    0.    3.    0.    0.    4.    0.    0.    0.  
    4.    0.    0.    5.    1.    9.    0.    0.    0.  
    9.    0.    1.    0.    0.    0.    0.    4.    7.

Сейчас в матрице A записана сама таблица Судоку, а в матрице B — ее решение. Почитав справку, вы можете найти функцию, которая группирует таблицу Судоку в более привычный блочный вариант

-->sudoku_print(A)
0 2 0   0 5 0   3 0 9 
0 0 0   1 8 0   0 0 4 
0 0 0   3 0 0   8 0 0 
..
0 9 7   0 0 0   0 0 2 
0 8 0   0 0 0   0 6 0 
5 0 0   0 0 0   7 9 8 
..
0 0 3   0 0 4   0 0 0 
4 0 0   5 1 9   0 0 0 
9 0 1   0 0 0   0 4 7

На этом простом примере, вы теперь должны себе представлять, что такое модуль.

Управление модулями[править]

Подсистема ATOMS сама является модулем, который идет в стандартной поставке Scilab. Подсистема ATOMS по сути является менеджером модулей, по функциональности чем-то похожий на менеджер пакетов dpkg системы Debian. В его конфигурацию входит способ подключения к Интернет и список репозиториев, из которых возможна загрузка модулей. Для управления репозиториями служат следующие функции:

  • atomsRepositoryAdd() — служит для добавления репозитория;
  • atomsRepositoryDel — служит для удаления репозитория из списка;
  • atomsRepositoryList — служит для просмотра текущего списка репозиториев;
  • atomsCategoryList() — позволяет просмотреть все категории модулей репозиториев. Эта информация помогает искать нужные модули;
  • atomsSearch() — помогает найти модуль. Работает как поисковая программа. В качестве аргумента вы должны ввести поисковый запрос в виде строки, и функция попытается найти совпадения в описании модулей.

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

-->atomsRepositoryList
 ans  =
!http://atoms.scilab.org/5.4  official  !

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

-->atomsGetInstalled
 ans  =
 
!sudoku  0.1.1-1  user  SCIHOME\atoms\x64\sudoku\0.1.1-1  I  !

В предыдущем упражнении мы установили модуль Sudoku версии 0.1.1-1. Пометка user указывает на то, что модуль установлен только для текущего пользователя, затем идет каталог в котором он расположен и наконец флаг I (от англ. Intentionaly) — пользователем. Еще есть флаг A (от англ. Automatically) — автоматически. Флаг A,как правило, присваивается модулям, установившимся независимо от пользователя, чьи функции используются целевыми модулями. Для того, чтобы посмотреть зависимости, следует вызывать функцию atomsDepTreeShow(), которая выведет их в виде дерева.

Для управления модулями служат следующие функции:

  • atomsShow() — позволяет получить полную информацию о желаемом модуле, который есть в списке ATOMS. Для правильного вызова нужно передать вектор с двумя строками: именем модуля и его версии. Например, для модуля Судоку вызов должен быть таким atomsShow(['sudoku' '0.1.1-1']).
  • atomsLoad() — позволяет подгрузить один или несколько модулей, если они не загружены на данный момент.
  • atomsIsInstalled() — позволяет проверить установлен ли модуль на компьютере по его имени.
  • atomsIsLoaded() — позволяет проверить подгружен ли модуль по его имени.
  • atomsRemove() — позволяет удалить один или несколько модулей.
  • atomsInstall() — позволяет установить один или несколько модулей. Этот метод установки неудобен, потому что вы должны знать имена модулей репозитория. Более удобным вариантом является использование графического окна.
  • atomsGetLoadedPath() — позволяет получить путь к каталогу, в который установлен модуль.
  • atomsGetLoaded — позволяет получить список подгруженных модулей.
  • atomsTest() — запускает сценарии-тесты для проверки производительности функций модуля. Используется главным образом для проверки возможностей аппаратной части компьютера в плане решения конкретных задач.
  • atomsUpdate() — позволяет обновить указанный модуль до последней версии, хранящейся в репозитории.

Во время сеанса подгруженные модули выгрузить нельзя (по крайней мере, в библиотеке ATOMS нет для этого отдельной функции), однако вы можете управлять списком автозагрузки с помощью следующих функций:

  • atomsAutoloadAdd() — позволяет добавить несколько модулей в список автозагрузки.
  • atomsAutoloadDel() — позволяет удалить несколько модулей из списка автозагрузки.
  • atomsAutoloadList() — позволяет просмотреть текущий список автозагрузки.
  • atomsAutoload() — позволяет подгрузить сразу все модули с пометкой автозагрузка. В начале каждого сеанса эта функция вызывается главным сценарием автоматически. Модули со снятом флагом автоматической загрузки следует подгружать функцией atomsLoad().

Наконец остались функции по конфигурированию подсистемы ATOMS. Некоторые параметры конфигурации доступны в глобальных настройках среды.

  • atomsGetConfig() — позволяет получить текущую конфигурацию ATOMS: способ подключения и способ загрузки с репозитория.
  • atomsSetConfig() — позволяет поменять конфигурацию.
  • atomsSaveConfig() — позволяет сделать бэкап-файл для восстановления конфигурации.
  • atomsRestoreConfig() — позволяет восстановить конфигурацию и ключевые файлы подсистемы ATOMS.

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

Самостоятельно

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

  1. Отыщите в списке категорий официального репозитория категорию Теория чисел.
  2. С помощью командного окна установите модуль «Number». Обратите внимание, что устанавливать модуль нужно по имени, которое возвращает atomsList().
  3. Ответьте на вопрос «сколько модулей было установлено после вызова пункта 2?».
  4. Исключите модуль «Number» из автозагрузки.
  5. Подгрузите модуль в текущий сеанс.
  6. Запустите все сценарии теста производительности для модуля «Number».
  7. Выведите в командное окно полную информацию о модуле.
  8. Удалите модуль «Number».

Структура модуля[править]

Теперь рассмотрим модуль изнутри. Для начала зайдите в каталог программы Scilab. Далее перейдите в каталог contrib/toolbox_skeleton. В этом каталоге приведена стандартная структура каталогов для любого модуля Scilab.

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

  • macros — набор макросов (.sci) или функций, написанных на языке Scilab. В этот каталог также входят типовые сценарии работы с макросами, которые строятся по единому шаблону. Любой модуль должен иметь по меньшей мере один sci-файл, иначе идея модуля становится бесполезной.
  • src — если в ваш модуль будут входить функции, написанные на стороннем языке (например, С или С++), то файлы с исходными кодами должны помещаться сюда. В этом каталоге файлы с исходными кодами дополнительно должны быть разложены по отдельным каталогам (fortran для кодов на языке Фортран). В этот каталог также входит сценарий компиляции функций.
  • sci_gateway — если каталог src имеет коды на стороннем языке, то в этот каталог помещается сценарий компоновки и загрузки функций в Scilab.
  • jar — сюда помещаются скомпилированные пакеты на языке Java.
  • help — сюда помещаются файлы формата xml со справочной информацией модуля вместе со сценарием сборки. В каталоге справочная информация на разных языках должна быть разложена по соответствующим каталогам.
  • etc — сценарии первичной инициализации модуля.
  • tests — наборы тестов для проверки работоспособности модуля.
  • demos — необязательный набор сценариев с примерами использования модуля.
  • includes — заголовочные h-файлы. Этот каталог используется, если вы планируете распространять свой модуль.

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

  • readme.txt — текстовый файл, в котором обычно приводится информация по установке модуля.
  • DESCRIPTION — типовой файл, в который записывается такая информация как имя модуля, версия, категория, условия распространения и др.
  • builder.sce — главный сборочный сценарий.
  • loader.sce — главный сценарий, загружающий модуль. Генерируется автоматически builder.sce.
  • license.txt — лицензионный файл. Является не обязательным, если вы используете какую-либо стандартную лицензию, так как она описывается в DESCRIPTION.

Сложный пример — сборка модуля[править]

  1. Пример взят из пособия Боден, М. Программирование в Scilab. — 152 с. — С. 38.