Ввод-вывод, оператор присваивания, арифметические операции
Язык программирования Паскаль. Знакомство со средой программирования Турбо Паскаль. Основные понятия. Первая программа
Паскаль — язык профессионального программирования, который назван в честь французского математика и философа Блеза Паскаля (1623-1662) и разработан в 1968-1971 гг. Никлаусом Виртом. Первоначально был разработан для обучения, но вскоре стал использоваться для разработки программных средств в профессиональном программировании.
Паскаль популярен среди программистов по следующим причинам:
- Прост для обучения.
- Отражает фундаментальные идеи алгоритмов в легко воспринимаемой форме, что предоставляет программисту средства, помогающие проектировать программы.
- Позволяет четко реализовать идеи структурного программирования и структурной организации данных.
- Использование простых и гибких структур управления: ветвлений, циклов.
- Надежность разрабатываемых программ.
Турбо Паскаль — это система программирования, созданная для повышения качества и скорости разработки программ (80-е гг.). Слово Турбо в названии системы программирования — это отражение торговой марки фирмы-разработчика Borland International (США).
Систему программирования Турбо Паскаль называют интегрированной (integration — объединение отдельных элементов в единое целое) средой программирования, т.к. она включает в себя редактор, компилятор, отладчик, имеет сервисные возможности.
Основные файлы Турбо Паскаля:
Turbo.exe — исполняемый файл интегрированной среды программирования;
Turbo.hlp — файл, содержащий данные для помощи;
Turbo.tp — файл конфигурации системы;
Turbo.tpl — библиотека стандартных модулей, в которых содержатся встроенные процедуры и функции (SYSTEM, CRT, DOS, PRINTER, GRAPH, TURBO3, GRAPH3).
Запуск интегрированной среды программирования
Для запуска интегрированной среды программирования нужно установить текущим каталог с Турбо Паскалем (TP7\BIN) и ввести команду: turbo.exe.
Задание. Запустите среду программирования и рассмотрите экран. Перед вами полоса меню, область окна и строка статуса. Нажмите клавишу F10 — теперь вам доступны все опции меню. Используя клавиши управления курсором, рассмотрите меню. С командами меню мы будем знакомиться постепенно. Нажмите клавишу Esc (вы вышли из меню). Перемещая курсор в окне, следите за строкой статуса. Какая информация отражается в этой строке?
Почти все, что вы видите и делаете в среде Турбо Паскаль, происходит в окнах.
Окно — это область экрана, которую можно перемещать, изменять в размере, перекрывать, закрывать и открывать.
Интегрированная среда программирования Турбо Паскаль позволяет иметь любое количество открытых окон, но в любой момент времени активным может быть только одно.
Активное окно – это окно, с которым вы в настоящий момент работаете.
Общие горячие клавиши:
F2 — сохраняет файл активного окна;
F3 — появление диалогового окна и возможность открыть файл;
F4 — запускает программу до строки, на которой стоит курсор;
F5 — масштабирует диалоговое окно;
F6 — переходит к следующему открытому окну;
F7 — запускает программу в режиме отладки с заходом внутрь процедур;
F8 — запускает программу в режиме отладки, минуя вызов процедур;
F9 — компилирование программы в текущем окне;
F10 — возвращение в меню.
Мы начнем изучение меню с наиболее важных и необходимых режимов.
Как войти в меню? Всего есть три возможности:
С помощью клавиш управления курсором подсветите слово FILE и нажмите клавишу «Enter». Что вы видите?
Появилась вертикальная таблица со списком команд, называемая выпадающим меню. Познакомимся с ним.
Open-F3 — открыть существующий файл (при активизации этой опции появляется окно со списком файлов, где можно выбрать необходимый),
New — создать новый файл (очищает память редактора и переводит в режим создания нового файла, которому присваивается имя Noname.pas; имя можно изменить при записи файла на диск),
Save-F2 — сохранить файл (переписывает файл из памяти редактора на диск),
Save as — сохранить с новым именем,
Save all — сохранить все в окнах (записывает содержимое всех окон редактора в соответствующие файлы),
Change dir — смена каталога (позволяет изменить установленный по умолчанию диск или каталог),
Print — печать файла,
Get info — выдача информации о текущем состоянии программы и используемой памяти,
DOS Shell — выход в DOS без выгрузки из памяти (для возврата ввести команду exit),
Exit — выход и выгрузка из памяти.
Программы на языке Паскаль имеют блочную структуру:
1. Блок типа PROGRAM — имеет имя, состоящее только из латинских букв и цифр. Его присутствие не обязательно, но рекомендуется записывать для быстрого распознавания нужной программы среди других листингов.
2. Программный блок, состоящий в общем случае из 7 разделов:
-
раздел описания модулей (uses);
Общая структура программы на языке Паскаль следующая:
Начнем знакомство с Паскалем с программы, которая складывает два числа и выводит сумму на экран.
Откройте файл, в который Вы запишите эту программу. Для этого нажмите клавишу F10, чтобы выйти в главное меню, затем клавишами перемещения курсора выберите опцию File, а в выпавшем меню команду New.
Примечание. Обратите внимание на оформление текста программы.
-
Найдите в этой программе заголовок, раздел описания переменных, признак начала программы, признак конца программы, тело программы, комментарий.
А теперь подведем итог вашим размышлениям.
Имя этой программы Summa2. Заметим, что требования к имени выполняются: оно отражает содержание программы, а также не содержит недопустимых символов.
Далее идет специально выделенный комментарий, в котором вы должны записать подробно условие задачи и указать, кто написал эту программу и когда.
Из разделов описаний имеется лишь один — раздел переменных. Он начинается со служебного слова Var. Мы описали три переменные: number1, number2, result. Все они переменные целого типа. Поэтому мы перечислили их через запятую, поставили двоеточие и указали тип переменных. Подобные объявления разделяются между собой точкой с запятой.
После описательной части идет раздел операторов, начинающийся со служебного слова Begin, после которого идут операторы языка.
Недостатком этой программы является то, что значения переменных постоянны. А нам нужно научиться писать такие программы, которые решают поставленные задачи в общем виде, т. е. для любых значений переменных. Для этого мы научимся запрашивать значения у пользователя, анализировать их и выдавать соответствующий результат.
Принятие решений в вашем коде — условные конструкции
Во многих языках программирования код должен иметь возможность принимать решения на основе введённых пользователем данных. Например, в игре, если у пользователя осталось 0 жизней, то игра завершается. В приложении о погоде утром отображается восход солнца, а вечером звезды и луна. В этой статье мы рассмотрим как в JavaScript работают так называемые «условия».
| Необходимое условие: | Базовая компьютерная грамотность, базовое понимание HTML и CSS, JavaScript first steps. |
|---|---|
| Цель: | Понять принципы использования операторов условий в JavaScript. |
Выбор одного условия!
Люди (и животные) принимают какие-либо решения всю жизнь, от малозначимых («стоит ли мне съесть одну печеньку или две?») до жизнеопределяющих («стоит ли мне остаться дома и работать на ферме отца или переехать в другую страну и изучать астрофизику?»)
Операторы условия в JavaScript позволяют нам указать разного рода действия в зависимости от выбранного пользователем или системой ответа (например одна печенька или две) и связать его с действием (результатом), например, результатом «съесть одну печеньку» будет «все ещё буду чувствовать себя голодным», а результатом «съесть две печеньки» будет «буду чувствовать себя сытым, но мама меня поругает за то, что я съел все сладости».

Оператор if . else
Давайте глянем на наиболее распространённый тип условного оператора, который вы будете использовать в JavaScript — if . else оператор.
Базовый if . else синтаксис
Базовый if. else синтаксис выглядит как pseudocode:
if (condition) < code to run if condition is true >else
- Ключевое слово if расположено перед круглыми скобками.
- Условие для проверки (condition), расположено внутри круглых скобок (например «это значение больше другого значения?», или «это значение существует?»). Это условие использует операторы сравнения (comparison operators), которые мы изучим позже, и возвратит нам true или false .
- Внутри скобок < >расположен код, который будет выполняться только в том случае, если условие (condition) верно ( true) .
- Ключевое слово else (иначе) .
- Ещё скобки < >, код внутри которых выполнится, только если условие не верно (не true) .
Этот код довольно читабелен — он говорит «if (если) condition (условие) возвращает true (истина) , запусти код A, else (иначе) запусти B»
Стоит заметить, что else и второй блок скобок < >не обязателен — следующий код так же будет работать:
if (condition) < код, который должен выполнить, если условие истина >какой-то другой код
Тем не менее, следует быть осторожным — в случае, если код внутри вторых скобок < >не контролируется условием, то этот код будет выполняться всегда. Это не плохо, просто вы должны помнить об этом, чаще вы хотите запустить один кусок кода или другой, но не оба.
И, наконец, иногда вы можете встретить код if. else без фигурных скобок в сокращённой форме:
if (condition) code to run if condition is true else run some other code instead
Это абсолютно рабочий код, но он менее читаем, лучше использовать фигурные скобки, новые строки и отступы.
Реальный пример
Чтобы лучше понять синтаксис, давайте рассмотрим реальный пример. Представьте, что мать или отец попросили помочь с работой по дому своего ребёнка. Родитель может сказать: «Если ты поможешь мне с покупками, то я дам тебе дополнительные деньги на карманные расходы, которые ты сможешь потратить на игрушку, какую захочешь». В JavaScript, мы можем представить это так:
var shoppingDone = false; if (shoppingDone === true) var childsAllowance = 10; > else var childsAllowance = 5; >
В этом коде, как показано, всегда будет shoppingDone равный false , что означает разочарование для нашего бедного ребёнка. Мы должны предоставить механизм для родителя, чтобы установить для переменной shoppingDone значение true , если ребёнок помог с покупками.
Примечание: вы можете увидеть больше в полной версии этого примера на GitHub (также посмотреть как он работает вживую.)
else if
В предыдущем примере предоставлено два выбора, или результата — но что, если мы хотим больше, чем два?
Существует способ привязать дополнительные варианты/результаты к вашему if. else — использовать else if . Для каждого дополнительного выбора требуется дополнительный блок, который нужно расположить между if() < . >и else < . >— проверьте следующий более сложный пример, который может быть частью простого приложения прогноза погоды:
label for="weather">Выберите тип погоды сегодня: label> select id="weather"> option value="">--Сделайте выбор--option> option value="sunny">Солнечноoption> option value="rainy">Дождливоoption> option value="snowing">Снежноoption> option value="overcast">Облачноoption> select> p>p>
var select = document.querySelector("select"); var para = document.querySelector("p"); select.addEventListener("change", setWeather); function setWeather() var choice = select.value; if (choice === "sunny") para.textContent = "Сегодня хорошо и солнечно. Носите шорты! Идите на пляж, или в парк, и купите мороженое."; > else if (choice === "rainy") para.textContent = "Дождь падает за окном; возьмите плащ и зонт, и не находитесь слишком долго на улице."; > else if (choice === "snowing") para.textContent = "Снег падает - морозно! Лучше всего посидеть с чашкой горячего шоколада или слепить снеговика."; > else if (choice === "overcast") para.textContent = "Дождя нет, но небо серое и мрачное; он все может измениться в любую минуту, поэтому на всякий случай возьмите дождевик."; > else para.textContent = ""; > >
- Здесь у нас есть элемент HTML который позволяет нам выбирать разные варианты погоды и простой абзац.
- В JavaScript мы создаём ссылки на элементы и и добавляем обработчик события элемента , чтобы при его изменении значения запускалась функция setWeather() .
- Когда функция будет запущена, первоначально мы определим значение переменной choice , которая равна выбранному значению в элементе . Затем мы используем условный оператор для отображения текста внутри абзаца в зависимости от того, какое значение у переменной choice . Обратите внимание, как все условия проверяются в else if() блоках, за исключением первого, который использует if() блок.
- Последний выбор, внутри else блока, в основном является «последним средством» — код внутри него будет запущен, если ни одно из условий не будет true . В этом случае он служит для удаления текста из абзаца, если ничего не выбрано, например, если пользователь решает повторно выбрать опцию «—Сделайте выбор—» которая указана в начале.
Примечание об операторах сравнения
Операторы сравнения используют для проверки условий внутри наших условных операторов. Сначала мы посмотрели на операторы сравнения в нашей статье Базовая математика в JavaScript — цифры и операторы . Наш выбор это:
- === и !== — проверяет одно значение идентично или не идентично другому.
- < и >— проверяет одно значение меньше или больше, чем другое.
- = — проверяет одно значение меньше или равно, либо больше или равно другому.
Примечание: Просмотрите материал по предыдущей ссылке, если вы хотите освежить свою память.
Мы хотели бы особо обратить внимание на проверку булевых значений ( true / false ), и общий шаблон, который вы будете встречать снова и снова. Любое значение, которое не есть false , undefined , null , 0 , NaN , или пустая строка ( » ) фактически возвращает true при тестировании как условного оператора. Поэтому вы можете просто использовать имя собственной переменной, чтобы проверить, равна ли она true , или существует (т. е. переменная не равна undefined). Например:
var cheese = "Cheddar"; if (cheese) console.log("Ура! Есть сыр для приготовления бутерброда."); > else console.log("Сегодня нет сыра для бутерброда."); >
И, возвращаясь к нашему предыдущему примеру о ребёнке, выполняющем поручение своего родителя, вы можете это записать так:
var shoppingDone = false; if (shoppingDone) // не нужно явно указывать '=== true' var childsAllowance = 10; > else var childsAllowance = 5; >
Вложенность if . else
Вполне нормально использовать один условный оператор if. else внутри другого — вложить их. Например, мы могли бы обновить наше приложение прогноза погоды, чтобы показать ещё один набор вариантов в зависимости от температуры:
if (choice === "sunny") if (temperature 86) para.textContent = "Сейчас " + temperature + " градусов по фаренгейту — хорошо и солнечно. Идите на пляж, или в парк, и купите мороженое."; > else if (temperature >= 86) para.textContent = "Сейчас " + temperature + " градусов по фаренгейту — Жара! Если вы хотите выйти на улицу, обязательно используйте солнцезащитный крем."; > >
Несмотря на то, что весь код работает вместе, каждый условный оператор if. else работает полностью отдельно от другого.
Логические операторы: И, ИЛИ и НЕ
Если вы хотите проверить несколько условий без записи вложенных if. else условий, логические операторы (en-US) помогут вам. При использовании в условиях, первые два оператора делают следующее:
- && — И; позволяет объединить два или более выражения так, что каждое из них отдельно должно иметь значение true , чтобы в итоге общее выражение имело значение true .
- || — ИЛИ; позволяет объединить два или более выражения так, что одно или несколько из них должно иметь значение true , чтобы в итоге общее выражение имело значение true .
Чтобы дать вам пример оператора И, предыдущий фрагмент кода можно переписать так:
if (choice === "sunny" && temperature 86) para.textContent = "Сейчас " + temperature + " градусов по фаренгейту — хорошо и солнечно. Идите на пляж, или в парк, и купите мороженое."; > else if (choice === "sunny" && temperature >= 86) para.textContent = "Сейчас " + temperature + " градусов по фаренгейту — Жара! Если вы хотите выйти на улицу, обязательно используйте солнцезащитный крем."; >
Так, для примера, первый блок кода выполнится только в том случае, если choice === ‘sunny’ и temperature < 86 вернут значение true .
Давайте посмотрим на быстрый пример оператора ИЛИ:
if (iceCreamVanOutside || houseStatus === "в огне") //если подъехал фургон с мороженым или дом горит console.log("Вы должны быстро покинуть дом."); > else console.log("Вероятно, можно в нем оставаться."); >
Последний тип логического оператора НЕ, выраженный ! оператором, можно использовать для отрицания выражения. Давайте объединим его с ИЛИ в приведённом выше примере:
if (!(iceCreamVanOutside || houseStatus === "on fire")) console.log("Вероятно, можно в нем оставаться."); > else console.log("Вы должны быстро покинуть дом."); >
В этом фрагменте, если условие ИЛИ возвращает true , оператор НЕ будет отрицать это и выражение вернёт false .
Можно сочетать любое количество логических операторов, в любой последовательности и в любой комбинации. В следующем примере код в блоке будет выполняться только в том случае, если оба условия с ИЛИ возвращают true, а следовательно, и оператор И возвращает true:
if ((x === 5 || y > 3 || z 10) && (loggedIn || userName === "Steve")) // код выполняется >
Распространённой ошибкой при использовании логического оператора ИЛИ в условном выражении является указание переменной, значение которой нужно проверить со списком возможных значений этой переменной, разделённых операторами || (ИЛИ). Например.
if (x === 5 || 7 || 10 || 20) // выполнить код >
В данном примере условие в if(. ) всегда будет оцениваться как true, поскольку 7 (или любое другое ненулевое значение) всегда будет оцениваться как true. Фактически, это условие гласит «если х равен 5, или 7 является true». Но нам требуется совсем не это. Чтобы достичь нужной цели, придётся выполнять полноценную проверку после каждого оператора ИЛИ:
if (x === 5 || x === 7 || x === 10 || x === 20) // выполнить код >
Оператор switch
Выражения if. else отлично справляются с добавлением условного кода, однако они не лишены недостатков. Они хорошо подходят для ситуации, когда имеется всего пара вариантов развития событий, каждый из которых имеет блок с приемлемым количеством кода, а также в случаях, когда условие является довольно сложным и включает несколько логических операторов. Если же нам требуется всего лишь задать переменную для определённого выбранного значения или напечатать конкретную фразу при определённом условии, изученный нами синтаксис может оказаться довольно громоздким, особенно если имеется большое количество вариантов выбора.
В этом случае нам поможет оператор switch – он принимает одно единственное выражение или значение, а затем просматривает ряд вариантов, пока не найдут вариант, соответствующий этому значению, после чего выполняет код, назначенный этому варианту. Вот пример использования этого оператора:
switch (выражение) < case choice1: выполнить этот код break; case choice2: выполнить этот код, а не предыдущий break; // вариантов может быть любое количество default: а вообще-то, выполнить только этот код >
- Ключевое слово switch , за которым следует пара круглых скобок.
- В скобках приводится выражение или значение.
- Ключевое слово case , за которым следует вариант выбора (именно он проверяется на соответствие выражению или значению) и двоеточие.
- Код, который будет выполняться, если вариант совпадает с выражением.
- Оператор break , за которым следует точка с запятой. Если вариант совпал с выражением или значением, браузер закончит выполнять блок кода, дойдя до оператора break , и перейдёт к выполнению кода, расположенного после оператора switch.
- Вариантов выбора (пункты 3–5) может быть любое количество.
- Ключевое слово default используется точно также, как любой другой вариант выбора (пункты 3–5) за тем исключением, что после default нет других вариантов выбора, поэтому инструкция break не требуется, никакого кода дальше нет. Это вариант выбора по умолчанию, выбираемый, если ни один из других вариантов не совпал с выражением.
Примечание: Вариант выбора default может быть пропущен, если выражение гарантированно совпадёт с одним из вариантов выбора. В противном случае вариант default необходим.
Пример оператора switch
Давайте рассмотрим реальный пример — перепишем наше приложение прогноза погоды с использованием оператора switch:
label for="weather">Выберите тип погоды сегодня: label> select id="weather"> option value="">--Сделайте выбор--option> option value="sunny">Солнечноoption> option value="rainy">Дождливоoption> option value="snowing">Снежноoption> option value="overcast">Облачноoption> select> p>p>
var select = document.querySelector("select"); var para = document.querySelector("p"); select.addEventListener("change", setWeather); function setWeather() var choice = select.value; switch (choice) case "sunny": para.textContent = "Сегодня хорошо и солнечно. Наденьте шорты! Идите на пляж или в парк, и купите мороженое."; break; case "rainy": para.textContent = "На улице дождь. Возьмите плащ и зонт, и не гуляйте слишком долго"; break; case "snowing": para.textContent = "Идёт снег - морозно! Лучше всего посидеть с чашкой горячего шоколада или слепить снеговика."; break; case "overcast": para.textContent = "Дождя нет, но небо серое и мрачное; он все может измениться в любую минуту, поэтому на всякий случай возьмите дождевик."; break; default: para.textContent = ""; > >
Примечание: вы можете найти этот пример на GitHub (также увидеть как он работает.)
Тернарный оператор
Это последний теоретический раздел данной статьи и мы перейдём к практическим упражнениям. Тернарный или условный оператор имеет простой синтаксис: он проверяет условие и возвращает одно значение или выражение, если условие является true , и другое значение/выражение, если условие является false . Часто это очень удобная альтернатива блоку if. else , позволяющая затрачивать меньшие усилия на написание кода, когда имеется всего лишь два варианта, выбираемых на основе условия true / false . Общая схема оператора:
( условие) ? выполнить этот код : выполнить этот код вместо первого
Приведём простой пример:
var greeting = isBirthday ? "С днём рождения, г-н Кузнецов! Хорошо вам повеселиться!" : "Доброе утро, г-н Кузнецов.";
У нас есть переменная isBirthday , если она true , мы отправляем посетителю поздравление с днём рождения; если нет – выдаём стандартное приветствие.
Пример тернарного оператора
При использовании тернарного оператора не обязательно ограничиваться лишь значениями переменной, можно выполнять функции или строки кода; все, что угодно. В следующем примере показано простое средство выбора темы, задающее внешний вид веб-сайта с помощью тернарного оператора.
label for="theme">Выберите тему: label> select id="theme"> option value="white">Белаяoption> option value="black">Чёрнаяoption> select> h1>Это мой веб-сайтh1>
var select = document.querySelector("select"); var html = document.querySelector("html"); document.body.style.padding = "10px"; function update(bgColor, textColor) html.style.backgroundColor = bgColor; html.style.color = textColor; > select.onchange = function () select.value === "black" ? update("black", "white") : update("white", "black"); >;
Мы используем элемент для выбора темы (чёрная или белая), а также простой (en-US) для отображения заголовка веб-сайта. Кроме того, у нас есть функция update() , принимающая в качестве параметров (входных данных) два цвета. В качестве фона используется первый переданный цвет, а в качестве цвета текста – второй переданный цвет.
Наконец, у нас есть обработчик событий onchange , использующийся для запуска функции, содержащей тернарный оператор. Сначала она проверяет условие — select.value === ‘black’ . Если возвращается true , мы запускаем функцию update() с параметрами чёрного и белого, в результате чего получаем чёрный цвет фона и белый цвет текста. Если возвращается false , мы запускаем функцию update() с параметрами белого и чёрного, в результате чего цвета веб-сайта меняются на противоположные.
Примечание: вы можете найти этот пример на GitHub (также увидеть как он работает.)
Практическое упражнение: простой календарь
В данном примере вы поможете нам закончить простое приложение календаря. Код включает:
- Элемент , позволяющий пользователю выбирать разные месяцы.
- Обработчик событий onchange для обнаружения изменения значения, выбранного в меню .
- Функция createCalendar() , рисующая календарь и отображающая правильный месяц в элементе (en-US).
Вы должны написать условную конструкцию в функции обработчика onchange , сразу после комментария // ДОБАВЬТЕ СЮДА УСЛОВНОЕ ВЫРАЖЕНИЕ . Конструкция должна:
- Проверить выбранный месяц (хранящийся в переменной choice . Это будет значение элемента после изменения значения, например, «Январь».)
- Задать переменную, скажем, days , равную количеству дней в выбранном месяце. Для этого нужно будет проверить количество дней в каждом месяце. Високосный год можно не учитывать.
- Советуем использовать логический оператор OR для группировки нескольких месяцев в рамках одного условия; многие месяцы имеют одинаковое количество дней.
- Подумайте, какое количество дней в месяце встречается чаще всего и используйте его в качестве варианта по умолчанию.
Если допустили ошибку, используйте кнопку «Сброс», чтобы вернуться к исходному виду примера. Если у вас совсем ничего не получается, нажмите «Показать решение».
Примечание: В HTML коде внутри названия месяцев value=»» введены на русском языке. Соответственно ссылки на них из вашего скрипта так же на русском. Не забываем про синтаксис. (прим. — ConstantineZz)
h2>Live outputh2> div class="output" style="height: 500px;overflow: auto;"> label for="month">Выберите месяц: label> select id="month"> option value="Январь">Январьoption> option value="Февраль">Февральoption> option value="Март">Мартoption> option value="Апрель">Апрельoption> option value="Май">Майoption> option value="Июнь">Июньoption> option value="Июль">Июльoption> option value="Август">Августoption> option value="Сентябрь">Сентябрьoption> option value="Октябрь">Октябрьoption> option value="Ноябрь">Ноябрьoption> option value="Декабрь">Декабрьoption> select> h1>h1> ul>ul> div> h2>Editable codeh2> p class="a11y-label"> Press Esc to move focus away from the code area (Tab inserts a tab character). p> textarea id="code" class="playable-code" style="height: 400px;width: 95%"> var select = document.querySelector('select'); var list = document.querySelector('ul'); var h1 = document.querySelector('h1'); select.onchange = function() < var choice = select.value; // ДОБАВЬТЕ СЮДА УСЛОВНОЕ ВЫРАЖЕНИЕ createCalendar(days, choice); >function createCalendar(days, choice) < list.innerHTML = ''; h1.textContent = choice; for (var i = 1; i > createCalendar(31,'Январь'); textarea> div class="playable-buttons"> input id="reset" type="button" value="Сброс" /> input id="solution" type="button" value="Показать решение" /> div>
.output * box-sizing: border-box; > .output ul padding-left: 0; > .output li display: block; float: left; width: 25%; border: 2px solid white; padding: 5px; height: 40px; background-color: #4a2db6; color: white; > html font-family: sans-serif; > h2 font-size: 16px; > .a11y-label margin: 0; text-align: right; font-size: 0.7rem; width: 98%; > body margin: 10px; background: #f5f9fa; >
var textarea = document.getElementById("code"); var reset = document.getElementById("reset"); var solution = document.getElementById("solution"); var code = textarea.value; var userEntry = textarea.value; function updateCode() eval(textarea.value); > reset.addEventListener("click", function () textarea.value = code; userEntry = textarea.value; solutionEntry = jsSolution; solution.value = "Показать решение"; updateCode(); >); solution.addEventListener("click", function () if (solution.value === "Показать решение") textarea.value = solutionEntry; solution.value = "Скрыть решение"; > else textarea.value = userEntry; solution.value = "Показать решение"; > updateCode(); >); var jsSolution = "var select = document.querySelector('select');\nvar list = document.querySelector('ul');\nvar h1 = document.querySelector('h1');\n\nselect.onchange = function() else if(choice === 'Апрель' || choice === 'Июнь' || choice === 'Сентябрь'|| choice === 'Ноябрь') \n\n createCalendar(days, choice);\n>\n\nfunction createCalendar(days, choice) \n >\n\ncreateCalendar(31,'Январь');"; var solutionEntry = jsSolution; textarea.addEventListener("input", updateCode); window.addEventListener("load", updateCode); // stop tab key tabbing out of textarea and // make it write a tab at the caret position instead textarea.onkeydown = function (e) if (e.keyCode === 9) e.preventDefault(); insertAtCaret("\t"); > if (e.keyCode === 27) textarea.blur(); > >; function insertAtCaret(text) var scrollPos = textarea.scrollTop; var caretPos = textarea.selectionStart; var front = textarea.value.substring(0, caretPos); var back = textarea.value.substring( textarea.selectionEnd, textarea.value.length, ); textarea.value = front + text + back; caretPos = caretPos + text.length; textarea.selectionStart = caretPos; textarea.selectionEnd = caretPos; textarea.focus(); textarea.scrollTop = scrollPos; > // Update the saved userCode every time the user updates the text area code textarea.onkeyup = function () // We only want to save the state when the user code is being shown, // not the solution, so that solution is not saved over the user code if (solution.value === "Показать решение") userEntry = textarea.value; > else solutionEntry = textarea.value; > updateCode(); >;
Практическое упражнение: расширяем выбор цветов
В данном примере вы будете использовать пример тернарного оператора, который мы рассматривали ранее, и превратите тернарный оператор в инструкцию switch, что позволит увеличить количество вариантов выбора для простого веб-сайта. Посмотрите на — на этот раз он включает не два, а целых пять вариантов тем. Нужно добавить инструкцию switch сразу под комментарием // ДОБАВЬТЕ ИНСТРУКЦИЮ SWITCH :
- Она должна принимать переменную choice в качестве входного выражения.
- Каждый элемент case должен содержать вариант выбора, соответствующий одному из доступных для выбора значений: белая, чёрная, лиловая, жёлтая или психоделическая тема.
- В блоке каждого элемента case необходимо вызывать функцию update() , которой передаётся два цвета: первый – это цвет фона, а второй – цвет текста. Помните, что значения цветов – это строковые значения, поэтому их нужно заключать в кавычки.
Если допустили ошибку, используйте кнопку «Сброс», чтобы вернуться к исходному виду примера. Если у вас совсем ничего не получается, нажмите «Показать решение».
div class="output" style="height: 300px;"> label for="theme">Выберите тему: label> select id="theme"> option value="white">Белаяoption> option value="black">Чёрнаяoption> option value="purple">Лиловаяoption> option value="yellow">Жёлтаяoption> option value="psychedelic">Психоделическаяoption> select> h1>Это мой веб-сайтh1> div> hr /> textarea id="code" class="playable-code" style="height: 450px;"> var select = document.querySelector('select'); var html = document.querySelector('.output'); select.onchange = function() < var choice = select.value; // ДОБАВЬТЕ ИНСТРУКЦИЮ SWITCH >function update(bgColor, textColor) < html.style.backgroundColor = bgColor; html.style.color = textColor; >textarea > div class="playable-buttons"> input id="reset" type="button" value="Сброс" /> input id="solution" type="button" value="Показать решение" /> div>
var textarea = document.getElementById("code"); var reset = document.getElementById("reset"); var solution = document.getElementById("solution"); var code = textarea.value; function updateCode() eval(textarea.value); > reset.addEventListener("click", function () textarea.value = code; userEntry = textarea.value; solutionEntry = jsSolution; solution.value = "Показать решение"; updateCode(); >); solution.addEventListener("click", function () if (solution.value === "Показать решение") textarea.value = solutionEntry; solution.value = "Скрыть решение"; > else textarea.value = userEntry; solution.value = "Показать решение"; > updateCode(); >); var jsSolution = "var select = document.querySelector('select');\nvar html = document.querySelector('.output');\n\nselect.onchange = function() \n>\n\nfunction update(bgColor, textColor) "; var solutionEntry = jsSolution; textarea.addEventListener("input", updateCode); window.addEventListener("load", updateCode); // stop tab key tabbing out of textarea and // make it write a tab at the caret position instead textarea.onkeydown = function (e) if (e.keyCode === 9) e.preventDefault(); insertAtCaret("\t"); > if (e.keyCode === 27) textarea.blur(); > >; function insertAtCaret(text) var scrollPos = textarea.scrollTop; var caretPos = textarea.selectionStart; var front = textarea.value.substring(0, caretPos); var back = textarea.value.substring( textarea.selectionEnd, textarea.value.length, ); textarea.value = front + text + back; caretPos = caretPos + text.length; textarea.selectionStart = caretPos; textarea.selectionEnd = caretPos; textarea.focus(); textarea.scrollTop = scrollPos; > // Update the saved userCode every time the user updates the text area code textarea.onkeyup = function () // We only want to save the state when the user code is being shown, // not the solution, so that solution is not saved over the user code if (solution.value === "Показать решение") userEntry = textarea.value; > else solutionEntry = textarea.value; > updateCode(); >;
Заключение
Это все, что вам нужно знать на данный момент об условных логических структурах! Уверены, вы хорошо разобрались в теоретическом материале и с лёгкостью справились с предложенными упражнениями. Если же что-то осталось для вас непонятным, перечитайте статью ещё раз или свяжитесь с нами.
Смотрите также
- Comparison operators
- Conditional statements in detail
- if. else reference
- Conditional (ternary) operator reference
- Обзор: Building blocks
- Далее
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 3 авг. 2023 г. by MDN contributors.
Теоретический материал: файловый ввод-вывод (Паскаль)
Файлы. Виды файлов. Основные процедуры для работы с файлами
До сих пор мы рассматривали задачи, в которых во время выполнения программы данные поступают с клавиатуры, а результаты выводятся на экран дисплея. При этом ни исходные данные, ни результаты не сохраняются. Всякий раз при выполнении одной и той же программы, в частности, во время отладки, приходится заново вводить данные. А если их очень много? В языке Паскаль есть возможность записать их на диск. Для этого необходимо оформить исходные данные и результаты в виде файлов, которые хранятся на диске точно так же, как и программы.
Понятие файла — это фундаментальное понятие информатики, вспомним же его определение.
Определение. Файлом называется область памяти на диске, имеющая свое имя.
Вы знаете различные виды физических файлов: системные, графические, текстовые и другие, зачастую созданные той или иной прикладной программой. И любой из этих физических файлов Вы сможете считать, проанализировать, изменить и записать.
Физические файлы можно по-разному представить в программе. Язык Турбо Паскаль предлагает три вида представления файлов:
И Вы, в зависимости от решаемой задачи, вольны выбирать один их трех видов, а, может быть, и несколько. Для того, чтобы сделать правильный выбор, Вы должны хорошо знать не только процедуры и функции, являющиеся общими для всех видов файлов, но и специфичные для каждого вида.
Для работы с конкретным физическим файлом на диске надо представить в программе так называемую файловую переменную и произвести ее логическую связку с этим файлом. Файловые переменные имеют специфическое применение. Над ними нельзя выполнять никаких операций (присваивать значение, сравнивать и др.). Их можно использовать только для выполнения операций с файлами (чтения, записи, удаления файла и т.д.). Кроме того, через файловую переменную можно получить информацию о конкретном файле (тип, параметры, имя файла и т.д.).
По сути, любой физический файл можно представить как последовательность блоков информации некоторого типа. Все компоненты файла имеют общее имя, а каждый имеет еще и свой номер. Начальный элемент имеет нулевой номер.
Количество элементов файла может быть любым: число компонентов файла может изменяться (увеличиваться или уменьшаться), то есть, заранее не фиксируется. В файлах можно хранить достаточно большое количество данных. После каждого элемента файла автоматически ставится признак конца элемента, а в конце файла ставится признак конца файла.
С каждым файлом можно связать понятие «текущий указатель». Это неявно описанная переменная, которая указывает на конкретный элемент файла. Действия с файлами производятся поэлементно, причем в них участвует тот элемент, на который «смотрит» текущий указатель, перемещающийся в результате выполнения действия на следующий элемент.
Главное, чему необходимо научиться при работе с файлами — это записать информацию из программы в файл и считать нужную информацию в выделенную переменную для обработки программой. Общая последовательность действий при этом такова:
— описать переменную файлового типа;
— связать ее с конкретным физическим файлом процедурой assign;
— открыть файл процедурой reset или rewrite;
— выполнить чтение или запись информации;
— по окончании работы с файлом закрыть файл процедурой close.
Формат объявления файловых переменных и особенности работы с различными видами файлов (типизированными, нетипизированными, текстовыми) будут подробно изложены далее.
Процедуры и функции для работы с файлами любого типа
Переменные файлового типа используются в программе только в качестве параметров собственных и стандартных процедур и функций. Все фактические действия с файлами основаны на наборе стандартных процедур языка, входящих в состав модулей System и Dos.
Сначала рассмотрим процедуры модуля System.
Напомним, что он подключен к программам по умолчанию, то есть его не требуется подключать к программе в разделе Uses.
До начала работы с файлами устанавливается связь файловой переменной МуFilе с именем дискового файла. Очень важно различать собственно файл (область памяти на магнитном носителе с некоторой информацией) и переменную файлового типа в Turbo Pascal-программе. Считается, что файловая переменная является представителем некоторого дискового файла в программе. Для того, чтобы реализовать доступ к файлу на магнитном диске, программа должна связать его с файловой переменной. Для этого необходимо установить соответствие между переменной и именем файла. Это имя представляется в виде строки, содержащей имя файла и, может быть, путь к файлу, который формируется по общим правилам MS-DOS.
| assign (МуFilе, ‘с:\МуDirectory\Result.dat’); |
здесь приведено полное (с указанием пути) имя пользовательского файла Result.dat.
Если путь не указан, программа будет искать файл только в своем рабочем каталоге и, как это принято в системе DOS, по указанным в файле аutoехес.bat путям. Кроме указания имени файла на дисковом накопителе может быть указано стандартное имя одного из устройств ввода-вывода: «соn» — консоль, то есть дисплей и клавиатура, «рrn» — или «lpt1» — принтер.
Не разрешается связывать с одним физическим файлом разные файловые переменные программы.
До тех пор, пока файловая переменная не связана с каким-либо дисковым файлом, никакие операции с ней в программе невозможны. Заметим, что можно связать файловую переменную с еще не существующим дисковым файлом. Это делается в случае последующего создания Turbo Pascal-программой файла с данным именем с помощью специальной системной процедуры.
После того, как файловая переменная с помощью процедуры Аssign связана с конкретным дисковым файлом, с ним можно выполнить любую допустимую операцию.
Внимание! Нельзя применять процедуру assign к открытому файлу.
Все файлы, открытые в результате работы программы, должны быть закрыты при завершении программы процедурой
| closе (МуFilе); |
При выполнении этого оператора закрывается физический файл на диске, и фиксируются изменения, связанные с использованием данного файла. Обратите внимание на необходимость закрытия файлов во всех ветвях программы, в том числе в различных аварийных ситуациях. Незакрытые файлы нарушают файловую структуру на диске, что может приводить к серьезным проблемам с настройкой компьютера.
Открытие нового файла производится процедурой, единственный аргумент которой — переменная файлового типа, например:
| rewrite (МуFilе); |
Эта процедура создает на диске файл с именем, связанным с переменной МуFilе процедурой Аssign. Указатель работы с файлом устанавливается на начало файла. После выполнения процедуры rewrite файл доступен как для записи, так и для чтения (в случае текстовых файлов – только для записи).
Внимание! Если файл с таким именем уже существует, он удаляется (его содержимое теряется), и создается новый пустой файл с данным именем.
Открытие существующего файла выполняет процедура
| reset (МуFilе); |
Эта процедура ищет уже существующий файл на диске и открывает его для работы, помещая указатель в начальную позицию. Если файл с установленным в Аssign именем не найден, возникает ошибка ввода/вывода, контроль которой зависит от директивы компилятора (смотрите здесь). После выполнения процедуры reset файл доступен как для записи, так и для чтения (в случае текстовых файлов – только для чтения).
Запись в файл производится процедурой
| write (МуFilе, var1, var2, . varN); |
Первый аргумент этой процедуры — переменная файлового типа, далее следует список записываемых переменных, которые должны соответствовать объявленному типу файла. При выполнении этой операции текущий указатель файла смещается на число позиций, равное числу переменных.
Чтение из файла производится аналогичной процедурой:
| read (МуFilе, var1, var2, . varN); |
Примечание. Особенности выполнения операций записи и чтения для нетипизированных и текстовых файлов описаны в соответствующих разделах.
Положение элементов в файле нумеруется, начиная с номера 0 для первого элемента. После последнего элемента файла автоматически записывается признак конца файла.
Функция FileSize(МуFilе) определяет число элементов в файле. Функция неприменима к текстовым файлам. Файл MyFyle должен быть открыт.
Функция логического типа ЕОF(МуFilе) имеет значение Тruе, если указатель указывает на маркер конца файла (End Of File).
Типизированные файлы
Определение. Типизированный файл — последовательность элементов одного типа. Таким образом, типизированный файл Вы можете представить себе как цепочку однотипных данных. Все компоненты файла имеют общее имя, а каждый – еще и свой номер. Начальный элемент имеет нулевой номер. Длина файла, то есть количество элементов в цепочке — величина произвольная, изменяемая в процессе работы. После последнего элемента автоматически записывается признак конца файла. Описание файлового типа для работы с типизированным файлом имеет синтаксис:
Допустим, мы имеем дело с файлом, в котором записываются переменные типа Word, тогда переменная файлового типа может быть введена двояко – с явным объявлением файлового типа:
| Type WordFile = file of word; Var MyFile : WordFile; |
или без объявления файлового типа:
| Var MyFile : file of word; |
Приведем примеры переменных файлового типа с другими объявлениями:
| Type Student = record Name, SurName : string; YearsOld : byte; Sessia : array [1..10] of byte; end; Var VarFile1 : file of char; VarFile2 : file of Student; VarFile3 : file of string; |
Напомним, что файловые переменные имеют специфическое применение: их можно использовать только для выполнения операций с файлами (чтения, записи, удаления файла и т.д.) и получения информации о конкретном файле (тип, параметры, имя файла и т.д.). Работа с файлами заключается в записи и считывании его компонентов. Для того, чтобы определить, какие данные будут участвовать в операции, используется неявно заданная переменная – указатель на текущий элемент файла. При открытии файла процедурами reset, rewrite указатель устанавливается на его начало. Запись и чтение производятся поэлементно, причем в них участвует тот элемент файла, на который «смотрит» указатель. В результате выполнения действия указатель перемещается к следующему элементу. Пример. Приведем шаблон программы для записи данных в типизированный файл.
| Program Writing; Var FileName : string; FVar : file of byte; Index : byte; Begin write (‘Введите имя файла ‘); readln (FileName); assign (FVar, FileName); rewrite (FVar); for Index := 0 to 99 do write (FVar,Index); close (FVar); End. |
Примечание. В цикле могут быть вычислительные процедуры для получения данных, выводимых в файл. Мы, для простоты, записали в файл счетчик цикла. Внимание! Следует запомнить, что процедура rewrite очистит файл, если файл с таким именем уже есть в рабочем каталоге, поэтому при выборе имен файлов соблюдайте осторожность. Задание 1. Наберите предложенную выше программу и дополните ее выводом на экран элементов файла (воспользуйтесь процедурой считывания из файла read и вывода на экран write). Задание 2. Создайте программу записи и чтения типизированного файла типа string.
Примеры решения задач
Рассмотрите примеры решения задач. Наберите тексты программ, проверьте их. Обратите внимание на комментарий. Выполните задания к задачам. Задача 1. Дан файл, элементами которого являются целые числа. Найти среднее арифметическое элементов файла. В примере выполняется считывание элементов из файла, их суммирование и нахождение среднего арифметического. Предполагается, что типизированный файл уже создан. Задание. Дополните программу созданием типизированного файла file.dat. Добавленные строчки программы прокомментируйте.
| Program Srednee; Uses Crt; Var Kol, Element, Summa :integer; f : file of integer; SrAriph : real; Begin ClrScr; assign(f,’file.dat’); reset(f); Summa :=0; Kol :=0; while not Eof(f) do begin read(f, Element); Inc(Kol); Inc(Summa, Element); end; if Kol > 0 then begin     SrAriph := Summa/Kol;     write(‘Среднее арифметическое элементов файла равно ‘, SrAriph:5:2); end else     write(‘Файл пуст’); close(f); readln; End. |
Задача 2. Наберите на компьютере и проанализируйте текст программы, сформулируйте решаемую в ней задачу, дополните необходимыми операторами и комментарием.
| Program FileString; Uses Crt; Var f, g : file of string; str1, str : string; i : integer; Begin ClrScr; Str1 := »; assign(f,’f’); rewrite(f); assign(g,’g’); rewrite(g); repeat readln(str); write(f,str); for i:=length(str) downto 1 do str1:=str1+str[i]; write(g,str1); str1:=»; until str=»; close(f); close(g); assign(f,’f’); reset(f); assign(g,’g’); reset(g); while not eof(f) do begin read(f,str); writeln(str); end; while not eof(g) do begin read(g,str); writeln(str); end; close(f); close(g); readln; End. |
-
нахождением среднего стажа работы в институте;
| Program TipRecord; Uses Crt; Type Dann=record stag : byte; Surname, WorkName : string; Oklad, Year : integer; End; Var Spisok : file of Dann; Man : Dann; Name : string[12]; Procedure VvodZap (Nomer : integer); Begin with Man do begin writeln(‘Введите данные ‘,Nomer,’-го работника’); write(‘Фамилия: ‘); readln(Surname); write(‘Год рождения: ‘); readln(Year); write(‘Стаж работы: ‘); readln(stag); write(‘Должность: ‘); readln(WorkName); write(‘Оклад: ‘); readln(oklad); write(Spisok, Man); end; End; Procedure FileVvod; Var i, count : Integer; Begin write(‘Введите имя файла данных:’); readln(Name); assign(Spisok, Name); rewrite(Spisok ); write(‘Введите количество работников:’); readln(count); for i:=1 to count do VvodZap(i); close(Spisok); readln; End; Begin ClrScr; FileVvod; readLn; End. |
Процедуры и функции для работы с типизированными файлами
Вспомним, что типизированный файл — это линейная последовательность элементов одного типа. При каждом обращении к файлу (чтении, записи) его текущий указатель перемещается к следующему элементу. Возникает вопрос: можно ли нарушить порядок доступа к элементам файла и, например, не считывая из файла первый и второй элемент, сразу обратиться к третьему? Оказывается, можно.
Для изменения теущего положения указателя используется процедура
| Seek (МуFilе, n); |
где n — требуемое положение указателя.
Внимание! Нумерация элементов типизированного файла начинается с нуля.
Следовательно, для обращения к третьему элементу нужно записать: Seek (МуFilе, 2).
Seek (МуFilе, 0) — устанавливает указатель в начальную позицию (на первый элемент).
Seek (МуFilе, FileSize(МуFilе)) — устанавливает указатель после последнего элемента, то есть на признак конца файла.
Примечание. Функция FileSize(МуFilе) возвращает количество элементов типизированного файла МуFilе.
Текущую позицию указателя дает функция
| FilePos (МуFilе); |
Задача. Составить программу, которая переписывает существующий файл, заменяя все латинские буквы на заглавные.
| Рrogram Writing; Var FileName : string; FVar : file of char; Index : integer; Letter : char; Begin write(‘Enter filename: ‘); readln (FileName); assign (FVar,FileName); reset (FVar); if IOResult <> 0 |
Функция IOResult
Как Вы уже заметили, в предыдущей программе была использована функция IOResult. Рассмотрим, какую роль выполняет эта функция.
Функция IOResult позволяет программисту самостоятельно обработать ошибки, возникающие при работе с файлами, избжав тем самым автоматического завершения программы с выдачей непонятного пользователю аварийного сообщения.
Если контроль за выполнением операций ввода/вывода отключен с помощью директивы компилятора , функция возвращает признак наличия ошибки в процессе выполнения последней операции ввода/вывода. При успешном завершении проверяемой операции возвращаемое функцией IOResult значение равно нулю.
Используя функцию IOResult, нужно помнить о том, что она должна вызываться сразу вслед за проверяемой операцией. Если же Вы хотите провести анализ ошибки позже, Вам придется сохранить возвращаемое функцией значение в некоторой промежуточной переменной.
Просмотрите еще раз фрагмент программы, в которой посредством процедуры reset производится попытка открытия файла. Если эта попытка не увенчалась успехом, то на экран будет выведено сообщение об ошибке.
| reset (F,’C:\TP7\BIN\Text.txt’); if IOResult <> 0 then writeln (‘Ошибка при открытии файла’); |
В своих программах Вы должны применять функцию IOResult.
Замечание. Функция применима к операциям файлового ввода/вывода независимо от вида файла (типизированный, нетипизированный, текстовый).
Решение задач
Задание. Рассмотрите предложенный ниже текст программы. Сформулируйте решаемую задачу.
| Program Files; Uses Crt; Var F, Fnew : file of string; Name, NewName : string[12]; Str : string; Ch : char; Procedure ReadText; Begin repeat write(‘Введите имя файла>’); readln(Name); assign(F, Name); reset(F); if IOresult<>0 then begin writeln(‘Ошибка чтения’); close(F); end; until IOresult=0; writeln(‘Содержание файла ‘, Name,’:’); while not Eof(F) do begin read(F, Str); writeln(Str); end; close(F); End; Procedure EraseFile; Begin erase(F); writeln(‘Файл удален’); End; Procedure ReNameFile; Begin write(‘Введите новое имя файла:’); readln(NewName); rename(F, NewName); writeln(‘Файл ‘, Name,’ переименован в файл ‘, NewName); End; Procedure CopyFile; Begin write(‘Введите имя копии файла ‘, Name,’>’); readln(NewName); reset(F); assign(Fnew, NewName); rewrite(Fnew); while not Eof(f) do begin read(F, Str); write(Fnew, Str); end; close(F); close(Fnew); writeln(‘Файл’, Name,’ скопирован в файл ‘, NewName); End; Begin ClrScr; readTеxt; repeat writeln(‘Удаление файла (D),переименование файла(R)’); writeln(‘Копирование файла (C), выход из программы (Е)’); write(‘Введите символ нужной операции — ‘); readln(ch); case ch of ‘D’,’d’: EraseFile; ‘R’,’r’: ReNameFile; ‘C’,’c’: CopyFile; end; until (ch=’E’) or (ch=’e’); readln; End. |
Тeкстовые файлы, их описание и основные отличия от типизированных файлов
Наряду с типизированными файлами Pascal имеет средства взаимодействия с файлами несколько иной структуры — так называемыми текстовыми файлами. Введение текстовых файлов несколько нарушает стройность языка, однако позволяет использовать Pascal при программировании широкого класса задач, имеющих нечисловой характер и связанных с обработкой текстовой информации. Во многих версиях языка допускается хранение файлов на диске как символьных данных. При считывании файла в оперативную память машины символы файла преобразуются в тот тип данных, который объявлен в программе. Файлы символьных данных называются текстовыми файлами. Структура текстовых файлов отличается от структуры обычных файлов (которые представляют собой линейную последовательность элементов одного типа) тем, что содержимое текстового файла рассматривается как последовательность строк переменной длины, разделённых специальной комбинацией кодов, называемой «конец строки». Как правило, эта комбинация строится из управляющего кода «возврата каретки» (CR, Carriage Return, символ #13), за которым, возможно, следует управляющий код «перевод строки» (LF, Line Feed, символ #10). При вводе c клавиатуры признаком конца строки считается нажатие клавиши Enter. Текстовый файл завершается специальным кодом «конец файла» (символ #26). В большинстве случаев знание конкретной кодировки управляющих символов не обязательно ввиду наличия файловых операций, автоматически учитывающих эти символы. Таким образом, текстовый файл структурно несколько похож на «файл из байтов» (file of byte) с той разницей, что в нем, помимо содержательной информации, встречаются символы специального назначения. Его можно схематически представить в следующем виде: . . . . . . . . . . . . . .#13#10
. . . . . . . . . . . . . . . . . . . .#13#10
. . . . . . . . . . . . . . . . .#13#10
. . . . . . . . . . . .#13#10
. . . . . . . . . . . . . . . . . . . . . . . . . .#13#10
#26 Описанная структура текстовых файлов хорошо согласуется с интуитивно понятным построением текстовой информации и полностью совпадает со стандартной структурой текстов, используемой во многих текстовых редакторах, понимаемой компиляторами с языков программирования и т.д. С каждым файлом на диске в программе должна быть связана некоторая файловая переменная, которая описывается в соответствии с типом файла. Текстовому файлу в Pascal-программе соответствует переменная, которая должна быть описана с указанием стандартного типа text:
| Var TextFile : text; |
Примечание. Слово text не является зарезервированным словом, а считается идентификатором стандартного типа, наряду с идентификаторами integer, real и т.д. После описания переменной типа text ее надо связать с конкретным файлом процедурой assign. Вся последующая работа с файлом будет вестись через файловую переменную. Далее доступ к файлу требуется открыть на чтение или на запись, для этого существуют процедуры reset и rewrite. К примеру, пусть на диске создан текстовый файл text.txt. Для Turbo Pascal описание и связывание файловой переменной f с файлом text.txt, будет выглядеть так :
| Var f: text; Begin assign(f, ‘d:\tp7\bin\text.txt’); reset(f); . . . End. |
Процедура assign( , ) — связывает файл на диске с файловой переменной типа Text. Примечание. Процедура assign не должна использоваться для открытого файла. Если при вызове процедуры assign в качестве имени файла задается пустая строка: assign(f,»), то после обращения к reset(f) переменная f будет связана со стандартным файлом ввода, а после обращения к rewrite(f) – со стандартным файлом вывода. Процедура reset( ) — открывает файл на чтение. Ввод-вывод для текстовых файлов подчиняется тем же правилам, что и для типизированных файлов; однако имеется несколько важных особенностей. Во-первых, для одного текстового файла нельзя одновременно производить операции и ввода, и вывода. Это означает, что после открытия текстового файла процедурой reset возможно только чтение информации из файла, а после процедуры rewrite — только запись в файл. Во-вторых, обмены с текстовыми файлами всегда являются строго последовательными, то есть после чтения из файла элемента с порядковым номером N следующая операция чтения даст элемент с номером N+1. Иными словами, прямой доступ к любому элементу текстового файла невозможен; для текстовых файлов не допускаются вызовы Seek, FilePos, FileSize. Под чтением файла понимают ввод данных из внешнего файла, находящегося на диске, в оперативную память компьютера. Данные файла становятся доступными программе. Внешний файл, из которого читаются данные, часто называют входным файлом. Базовой техникой обменов с текстовыми файлами является посимвольный ввод-вывод. При этом производится чтение или запись всех символов, как информационных, так и специальных. Покажем простую программу, выполняющую чтение некоторого текста. Эта программа выводит на экран последовательность кодов символов, составляющих файл text.txt.
| Program TextFile1; Var f : text; S : char; Begin assign(f, ‘text.txt’); reset(f); while not Eof(f) do begin read(f, S); writeln(S:2, ord(S):4); end; close(f); readln End. |
Задание. Наберите текст программы и запустите программу на выполнение. Просмотрите результат работы программы. Найдите выведенные на экран коды специальных символов. Еще одной особенностью работы с текстовыми файлами является возможность чтения из файла (записи в файл) значений различных базовых типов, тогда как для типизированных файлов тип параметров процедур read и write всегда должен совпадать с базовым типом файла. Например, в текстовый файл можно записать целое или вещественное число, при этом его внутреннее представление будет автоматически преобразовано в строчку символов, образующих изображение этого числа. Рассмотрите простую программу, выполняющую чтение из текстового файла целых чисел и вывод на печать только четных чисел.
| Program TextFile2; Var f : text; Put : string; a : integer; Begin Put := ‘D:\TP7\BIN\Primer2’; assign(f, Put); reset(f); while not Eof(f) do begin readln(f, a); if not odd(a) then writeln(a); end; close(f); readln End. |
- значения целого типа;
- значения вещественного типа;
- значения булевского типа.
Для записи произвольной информации в текстовый файл в языке имеются дополнительные возможности, которые заключаются в задании размера поля записи. Если после записываемой переменной или выражения поместить символ двоеточия, а после него — любое выражение целого типа, то для выводимого значения будет отведено поле, размер которого (число символов) будет равен значению выражения. Следующая тривиальная программа наглядно показывает действие указателя поля:
| Program TextFile3; Var f : text; V : real; i : word; Put : string; Begin Put := ‘D:\TP7\BIN\Primer3’; assign(f, Put); rewrite(f); V := 123.456; write(f, V,#13#10); for i := 10 to 14 do write(f, V:i,#13#10); close(f); End. |
Задание. Наберите программу, дополните ее выводом содержимого текстового файла на экран. Проанализируйте результаты выполнения программы.
Управление размещением значений в текстовых файлах очень удобно использовать при формировании структурированных файлов (списков, таблиц и т.п.). Если количество позиций, занимаемых выводимым значением меньше размера поля, то значение всегда «прижимается» к правому краю поля.
Для вещественных типов имеется дополнительная возможность, позволяющая выводить число в формате с фиксированной точкой, что более наглядно по сравнению с форматом с плавающей точкой, который предусмотрен по умолчанию. Если после указателя размера поля задать через двоеточие еще одно выражение целого типа, то оно будет интерпретироваться как указание числа позиций для дробной части числа.
Способы обмена с текстовыми файлами
На этом занятии мы обобщим уже известные нам сведения о работе с текстовыми файлами и остановимся на особенностях обмена информацией между программой и текстовым файлом.
Операции чтения из файла
reset(f) — открывает существующий файл на чтение. Файловая переменная должна быть связана с внешним файлом с помощью процедуры assign.
Если существующий файл уже открыт, то он закрывается, а затем открывается вновь. Текущая позиция в файле устанавливается на начало файла.
Если f соответствует пустое имя файла (например, после вызова assign(f, »)), то после обращения к процедуре Reset(f) будет использоваться стандартный файл ввода (канал 0).
Текстовый файл становится доступным только на чтение.
При указании директивы компилятора функция IoResult будет возвращать значение 0 в том случае, если операция завершилась успешно, и ненулевой код ошибки в противном случае.
read( [f : text], v1 [, v2, . vn] ) — читает из текстового файла значения одной или более переменных.
Если параметр f не указан, то подразумевается использование стандартной файловой переменной Input. Каждый параметр v является переменной, значение которой должно быть прочитано из файла.
readln( [f : text], [v1, v2, . vn] ) — выполняет процедуру read, затем переходит к следующей строке файла.
Процедура readln является расширением процедуры read и определена для текстовых файлов. Вызов readln(f) с указанием одного параметра-файловой переменной приводит к перемещению текущей позиции файла на начало следующей строки, если она имеется, в противном случае происходит переход к концу файла. Процедура readln без параметров полностью соответствует стандартному вводу.
При указании директивы компилятора функция IoResult будет возвращать значение 0 в том случае, если операция завершилась успешно, и ненулевой код ошибки в противном случае.
Примечание: Процедура работает только для текстовых файлов, включая стандартный ввод. Файл должен быть открыт для ввода.
Процедура readln является очень удобным аналогом read. В случае использования readln после чтения из файла очередной порции символов (и, быть может, преобразования их в значение подходящего типа) текущий указатель файла будет перемещен на начало его следующей строки. Иными словами, остаток строки, расположенный в файле после прочитанного значения, будет пропущен.
Очень часто используется следующий способ чтения из текстового файла. В качестве параметра процедуры readln задается переменная типа string; в этом случае вся очередная строка файла целиком считывается в данную переменную, длина которой автоматически устанавливается равной длине считанной строки. Полученная из файла строка далее может быть обработана так, как это необходимо. Важно отметить, что при таком способе производится чтение только «значащих» символов строки; завершающиеся символы в данном случае играют роль межстрочных разделителей и не считываются в строковую переменную. После чтения строки текущий указатель файла устанавливается на начало следующей строки.
Операции записи в файл
rewrite(f) — cоздаёт и открывает новый файл. Файловая переменная должна быть связана с внешним файлом с помощью процедуры assign.
Если внешний файл уже существует, то он удаляется, и на его месте создаётся новый пустой файл. Если файл уже открыт, то он закрывается, а затем открывается вновь. Текущая позиция в файле устанавливается на начало файла.
Если f соответствует пустое имя файла (например, после вызова assign(f, »)), то после обращения к процедуре rewrite(f) будет использоваться стандартный файл вывода (канал 1).
Текстовый файл становится доступным только на запись.
write( [f : text], v1 [, v2, . vn] ) — записывает значения одной или более переменных в текстовый файл.
Если параметр f не указан, то подразумевается использование стандартной файловой переменной Output. Каждый параметр v является выражением, значение которого должно быть записано в файл. Выводимое выражение должно быть символьного, целого, вещественного, строкового или булевского типа.
Параметр v может иметь вид:
| Var a:real; . . . . . . . write(f, a: 5: 2); |
Такая запись означает, что мы в файл записываем действительное (не целое) число а, размером 5 знаков, 2 знака под дробную часть.
writeln( [f : text], [v1, v2, . vn] ) — выполняет процедуру write, а затем записывает в файл метку конца строки (перевод строки).
При вызове данной процедуры с указанием только параметра-файловой переменной: writeln(f), в файл записывается метка конца строки. Процедура writeln без параметров полностью соответствует стандартному выводу на экран.
Примечание. Процедура работает только для текстовых файлов, включая стандартный вывод. Файл должен быть открыт для вывода.
Логическая функция Eoln
Часто для обработки текстовых файлов используется специфичная для них функция Eoln, позволяющая определить, достигнут ли конец строки. Если достигнут — значение функции равно True, а если нет — False. Таким образом, для анализа конкретных символов строк файла можно применить вложенный цикл типа:
| while not Eof(FileName) do while not Eoln(FileName) do begin end; |
Процедура открытия файла для дополнения
append(f : Тext) — процедура открывает существующий файл для дозаписи. Если файл уже открыт, то он сначала закрывается, а затем открывается заново. Текущая позиция устанавливается на конец файла.
Если в последнем блоке файла размером 128 байтов присутствует символ Сtrl+Z (26 в коде ASCII), то текущая позиция устанавливается в файле таким образом, что при записи первым в блоке будет «затираться» символ Сtrl+Z.
Если переменной f соответствует пустое имя файла (например, после вызова assign(f, »)), то после обращения к процедуре append переменная f будет указывать на стандартный выходной файл.
После обращения к append файл f становится доступным только на запись, и Eof(f) принимает всегда значение True(истина).
Рассмотрите несколько примеров простых программ.
| Program TextFile4;
Var |
| Program TextFile5;
Var |
| Program TextFile6;
Var |
Стандартные текстовые файлы Input и Output. Примеры задач
В Паскале существуют два стандартных текстовых файла: Input и Output. Эти файлы считаются известными в любой Pascal-программе (иными словами, они описаны в стандартном модуле System). Они обозначают (по терминологии MS-DOS), соответственно, стандартный файл ввода и стандартный файл вывода. Обычно эти стандартные файлы связаны с конкретными физическими устройствами компьютера. Так, файловая переменная Input связана с клавиатурой, файловая переменная Output — с экраном дисплея. Эти файлы считаются заранее открытыми, а соответствующие идентификаторы можно использовать в операциях ввода-вывода.
Рассмотрим, например, следующий оператор:
| writeln (Output, ‘Результат равен ‘, (X+Y)*2); |
В соответствии с общими правилами, этот оператор выведет значения двух последних операндов в текущую строку заданного файла, а затем произведет переход к следующей строке. В применении к стандартному файлу Output эти действия будут выглядеть как появление в текущей строчке дисплея литеральных изображений указанных значений, после чего курсор будет перемещен в первую позицию следующей строки.
Аналогично, оператор read (Input, X1, X2); будет выполняться таким образом: система перейдет в состояние ожидания ввода с клавиатуры двух значений. Типы вводимых значений должны совпадать с типами переменных Х1 и Х2. Эти значения при вводе должны отделяться друг от друга одним или несколькими пробелами, а ввод должен быть завершен нажатием клавиши Enter. В процессе ввода значений набираемые на клавиатуре символы отображаются на экране. После нажатия Enter введенные значения будут присвоены переменным Х1 и Х2, и выполнение программы будет продолжено.
Для стандартных файлов Input и Output допускается сокращенная форма записи операций ввода-вывода. Так, если в процедурах read и readln первый параметр опущен, то по умолчанию подразумевается файл Input. Аналогично, отсутствие в процедурах write и writeln первого параметра означает вывод в стандартный файл Output. Вывод в стандартный файл Output используется очень часто — всегда, когда необходимо выдать некоторую информацию из программы на экран.
В соответствии с общими правилами MS-DOS стандартные файлы ввода-вывода могут быть «переназначены», то есть, связаны с другими физическими устройствами или дисковыми файлами. Простейшим способом переназначения является использование для этой цели процедуры assign, например,
| assign (Output,’MyFile.out’); |
После выполнения такого оператора стандартный файл вывода будет переназначен, то есть файловая переменная Output будет связана с дисковым файлом MyFile.out из текущего каталога. Все операции вывода, явно или неявно работающие с файлом Output, будут выводить информацию в указанный дисковый файл.
Задание. Рассмотрите предложенные программы, наберите их на компьютере, выделите необходимые части алгоритма в подпрограммы, сформулируйте решаемые ими задачи, дополните необходимыми операторами и комментарием.
| Program StringCount; Uses Crt; Var i : integer; s : string; f : text; Begin ClrScr; readln(s); assign(f, s); reset(f); i := 0; while not Eof(f) do begin readln(f,s); i:=i+1; end; close(f); writeln(i); readln; End. |
| Program Zamen; Uses Crt; Var s : string; f, d : text; Begin ClrScr; readln(s); assign(f, s); readln(s); assign(d, s); reset(f); rewrite(d); while not Eof(f) do begin readln(f, s); while Pos(‘o’, s)>0 do s[Pos(‘o’, s)] := ‘a’; writeln(d, s); end; close(f); close(d); readln; End. |
| Program MaxInFile; Uses Crt; Var i, j, r, Code : integer; s : string; f : text; Begin ClrScr; readln(s); assign(f, s); reset(f); while not Eof(f) do begin readln(f, s); j:=0; for i:=1 to Length(s) do begin Val(s[i], r, Code); if r>j then j:=r; end; writeln(j); end; close(f); readln; End. |
Нетипизированные файлы. Их особенности. Процедуры blockread и blockwrite
Нетипизированные файлы — это файлы, поддержка которых осуществляется с максимально возможной скоростью. Введение таких файлов в систему Турбо Паскаль было вызвано стремлением повысить эффективность программ, участвующих в интенсивном обмене с внешними наборами данных.
Нетипизированный файл рассматривается в Паскале как совокупность символов или байтов. Выбор char или byte не играет никакой роли, важен лишь объем занимаемых данных.
Такое представление стирает различия между файлами независимо от типа содержащейся в них информации. На практике это приводит к тому, что любой файл, подготовленный как текстовый или типизированный, можно открыть и начать работу с ним, как с нетипизированным набором данных.
Для определения в программе нетипизированного файла служит зарезервированное слово file:
| Var MyFile : file; |
Реализация работы с такими файлами наиболее полно соответствует аппаратной поддержке операций с внешними носителями. За счет этого достигается максимально возможная скорость доступа к наборам данных. Для нетипизированных файлов не нужно терять время на преобразование типов и поиск управляющих последовательностей, достаточно считать содержимое файла в определенную область памяти.
Нетипизированный файл является файлом прямого доступа, что говорит о возможности одновременного использования операций чтения и записи.
Для таких файлов самым важным параметром является длина записи в байтах. Открытие нетипизированного файла с длиной записи в 1 байт можно выполнить следующим образом:
rewrite(MyFile, 1) или reset(MyFile, 1)
Второй параметр, предназначенный только для использования с нетипизированными файлами, задает длину записи файла на сеанс работы.
Особенность аппаратной поддержки заключается в том, что при обращении к внешнему устройству минимально возможным объемом для считывания являются 128 байт. В стремлении добиться наибольшей эффективности файловых операций в Турбо Паскале принято соглашение, по которому длина записи нетипизированного файла по умолчанию составляет 128 байт. Поэтому после открытия файла с помощью вызовов:
rewrite(MyFile) или reset(MyFile)
все процедуры и функции, обслуживающие файлы прямого доступа, работают с записями длиной 128 байт.
Каждый пользователь для своих программ может выбрать наиболее подходящий размер записи. Турбо Паскаль не накладывает каких-либо ограничений на длину записи нетипизированного файла, за исключением требования положительности и ограничения максимальной длины 65535 байтами. При этом следует учитывать два обстоятельства.
Во-первых, для обеспечения максимальной скорости обмена данными следует задавать длину, которая была бы кратна длине физического сектора дискового носителя информации (512 байт).
С другой стороны, нужно помнить, что общий размер файла может не быть кратен выбранному размеру записи (последняя запись может быть неполной). Для того, чтобы гарантированно обеспечить полное чтение всего файла, рекомендуется установить размер записи равным 1.
Более того, фактически пространство на диске выделяется любому файлу порциями — кластерами, которые в зависимости от типа диска могут занимать 2 и более смежных секторов. Как правило, кластер может быть прочитан или записан за один оборот диска, поэтому наивысшую скорость обмена данными можно получить, если указать длину записи, равную длине кластера.
При работе с нетипизированными файлами могут применяться следующие процедуры и функции, применимые к типизированным файлам:
assign (МуFilе, ‘с:\МуDirectory\result.dat’) — процедура связывания логической файловой переменной МуFilе с конкретным физическим файлом на дисковом носителе информации;
closе (МуFilе) — процедура, закрывающая открытый файл;
rewrite (МуFilе) — процедура, создающая новый файл и открывающая его для записи и чтения; при работе с нетипизированными файлами эта процедура имеет дополнительный параметр, который был рассмотрен выше;
reset (МуFilе) — процедура, открывающая существующий файл данных для чтения и записи; при работе с нетипизированными файлами эта процедура имеет дополнительный параметр, который был рассмотрен выше;
eof (МуFilе) — логическая функция, проверяющая, достигнут ли конец файла;
seek (МуFilе, n) — процедура, позволяющая явно изменить значение текущего указателя файла, установив его на элемент с номером n;
filepos (МуFilе) — функция, возвращающая позицию указателя файла; нумерация начинается с нуля;
filesize(МуFilе) — функция, возвращающая количество элементов файла;
rename(МуFilе, FileName) — процедура, позволяющая переименовать существующий файл;
truncate(МуFilе) — процедура, позволяющая удалить часть существующего файла, начиная с текущей позиции и до конца файла;
erase(МуFilе) — процедура, стирающая указанный файл,
Вы должны были заметить, что в списке нет процедур read и write. Для чтения информации из нетипизированного файла и записи информации в него только для данного типа файлов в Турбо Паскаль введены две новые процедуры, поддерживающие операции ввода-вывода с более высокой скоростью.
Процедура BlockRead
| blockread(Var F : file; Var Buf; Kol : word; result : word); |
Процедура считывает из файла F определенное число записей в память, начиная с первого байта переменной Buf.
Параметр Buf представляет любую переменную, которая будет участвовать в обмене данными с дисками. Эту переменную нужно описать в программе так, чтобы ее размер не был меньше размера записи, установленного в параметрах rewrite или reset (как правило, для этих целей используется некоторый массив).
Параметр Kol задает число считываемых записей.
Параметр result является необязательным и содержит после вызова процедуры число действительно считанных записей.
Использование параметра result подсказывает, что число считанных блоков может быть меньше, чем задано параметром Kol. Если result указан при вызове, то ошибки ввода-вывода в такой ситуации не произойдет. Для отслеживания этой и других ошибок чтения можно использовать опции , и функцию IOresult.
Кроме того, что переменная F должна быть описана как нетипизированный файл, она должна быть связана с конкретным физическим диском процедурой assign, и файл должен быть открыт.
Процедура BlockWrite
| blockwrite(Var F : file; Var Buf; Kol : word; result : word); |
Процедура предназначена для быстрой передачи в файл F определенного числа записей из переменной Buf. Все параметры процедуры BlockWrite аналогичны параметрам процедуры BlockRead. Содержимое переменной Buf в количестве Kol записей помещается в файл, начиная с текущего положения указателя файла.
Обе процедуры выполняют операции ввода-вывода блоками. Объем блока в байтах определяется по формуле:
Объем = Kol * recSize,
где recSize — размер записи файла, заданный при его открытии. Суммарный объем разового обмена не должен превышать 64 Кбайт.
Помимо скорости передачи данных преимущество этих процедур заключается в возможности пользователя самостоятельно определять размер буфера для файловых операций. Эта возможность играет значительную роль в тех задачах, где необходимо жесткое планирование ресурсов. Программист должен позаботиться о том, чтобы длина внутреннего представления переменной Buf была достаточной для размещения всех байт при чтении информации с диска. Дело в том, что чтение информации из файла в буфер, равно как и запись из буфера в файл, производится без типового контроля. Поэтому несоблюдение указанного условия может привести к порче соседних с буфером данных или к помещению в файл посторонней информации.
Возможна ситуация, когда при чтении в файле содержится менее Kol записей или в процессе записи на диске не окажется нужного свободного пространства. В этом случае, если параметр result в процедуре BlockRead (BlockWrite) не был задан, то возникнет ошибка ввода-вывода; в противном случае ошибка не будет зафиксирована, а после выполнения процедуры значение параметра result не совпадет со значением параметра Kol и будет равно количеству фактически прочитанных (записанных) записей. Последнее обстоятельство можно проверить, сравнив два указанных значения.
После завершения процедуры указатель смещается на result записей.
Рассмотрите примеры простых задач.
Задача 1. Составить программу, которая создает нетипизированный файл из 100 чисел и выводит на экран k-ый элемент.
| Program Netipiz1; Uses Crt; Type FileType = file; Var f : FileType; P, k : byte; Begin ClrScr; assign(F, ‘MyFile’); rewrite(F,1); Randomize; for k := 1 to 100 do begin P := Random(100); blockwrite(F, P, 1); end; close(F); reset(F,1); for k := 1 to 100 do begin blockread(F, P, 1); write(p,’ ‘); end; write(‘Введите номер нужного элемента ‘); readln(k); Seek(F, k-1); blockread(F, P, 1); writeln(k,’-ий элемент файла равен ‘, P); close(F); End. |
Задача 2. Составить программу, которая создает копию элементов нетипизированного файла f и помещает в файл g.
| Program Netipiz2; Uses Crt; Var f, g : file; s : char; Stroka1, Stroka2 : string; Begin ClrScr; write(‘Введите имя исходного файла: ‘); readln(Stroka1); assign(f, Stroka1); rewrite(f, 1); writeln(‘Введите содержимое файла ‘); readln(s); while s <> #13 do begin blockwrite(f, s, 1); readln(s); end; close(f); reset(f, 1); write(‘Введите имя конечного файла: ‘); read(Stroka2); assign(g, Stroka2); rewrite(g, 1); while not Eof(f) do begin blockread(f, s, 1); blockwrite(g, s, 1); end; close(f); close(g); reset(g, 1); writeln(‘Содержимое конечного файла:’); while not Eof(g) do begin blockread(g, s, 1); write(s); end; close(g); readln; End. |
Задача 3. Составить программу, которая создает массив целых чисел и записывает его в нетипизированный файл, а также вычисляет среднее арифметическое элементов файла.
| Program Netipiz3; Uses Crt; Var f : file; i, k, s : integer; Mas : Array [1..10] of byte; Begin ClrScr; Randomize; for i := 1 to 10 do begin Mas[i] := Random(10); write(Mas[i]:3); end; assign(f, ‘file.dat’); rewrite(f, 1); blockwrite(f, Mas, 10); close(f); reset(f,1); S:=0; i:=0; while not Eof(f) do begin blockread(f, k, 1); s:= s+k; Inc(i); end; close(f); writeln; write(s/i:5:2); readln; End. |
Задание. Наберите приведенные выше тексты программ, убедитесь в их работоспособности.
Использование текстовых файлов в качестве нетипизированных
Задача. Прочитать текстовый файл и заменить находящиеся там символы новыми, отличающимися своими кодами от исходных на определенную величину, изменяющуюся от символа к символу (шифрование методом простой одноалфавитной подстановки). Поместить зашифрованный текст в новый файл, разместив в нем предварительно число перекодированных символов и таблицу смещений кодов.
| Program Kodirovka; Const NofCod = 20; Var FirstFile : text; SecondFile : file; FirstName, SecondName : string; IOres : byte; NofSymb : LongInt; Codes : Array[1..NofCod] of byte; Buffer : Array [1..NofCod] of char; i : word; symbol : char; Procedure WriteAndControl (Var Buf; Amount : word); repeat write(‘Имя исходного файла: ‘); IOres := IOresult; repeat write(‘Имя результирующего файла: ‘); IOres := IOresult; NofSymb := 0; Randomize; i := 0; if i <> 0 NofSymb := FileSize(SecondFile)-NofCod-4; close(SecondFile); |
В этой программе в результирующий файл окончательно будут записаны: общее количество перекодированных символов, таблица смещений кодов символов и перекодированные символы. Выходной файл используется как нетиризированный с размером записи в 1 байт.
Дополнительные процедуры и функции для работы с файлами
Рассмотрим три процедуры из модуля System.
Изменение имени файла производится процедурой
| rename(МуFilе, FileName); |
первый аргумент которой — переменная файлового типа, а второй аргумент — строкового типа — новое имя файла, которое может быть сокращенным или полным (с указанием пути). Действие этой процедуры эквивалентно действию аналогичной процедуры DOS.
Уничтожение части файла от текущего положения указателя до конца файла производится процедурой
| truncate(МуFilе); |
Процедура применима к любым типам файлов, кроме текстовых; файл должен быт предварительно открыт.
Уничтожение всего файла производится процедурой
| erase(МуFilе); |
действие которой эквивалентно удалению файла в операционной системе DOS. Процедура неприменима к открытым файлам.
Рассмотрим некоторые процедуры работы с файлами, входящие в модуль Dos. Напомним, что для его подключения требуется указать
Ряд свойств файла, хранящегося на диске, кодируются так называемыми атрибутами. Атрибуты файла записываются не в сам файл, а в информационный раздел каталога, в котором хранится файл. Для хранения атрибутов отводится один байт, единица в определенном бите которого означает наличие свойства, а нуль — отсутствие. Соответствие битов и свойств показывает схема, приведенная на рисунке.
0 — Только чтение ReadOnly=1,
1 — Скрытый файл Hidden=2,
2 — Системный файл SysFile=4,
3 — Метка тома VolumeID=8,
4 — Подкаталог Directory=16,
5 — Архивный файл Archive=32,
Произвольный файл AnyFile=63.
Каждому атрибуту соответствует определенная константа, равная 2 k , где k — номер бита. Эти константы описаны в модуле Dos, их значения и имена приведены на схеме. В целом байт атрибутов образуется как сумма соответствующих ему констант. Установка атрибута «только чтение» приводит к невозможности изменения содержимого файла или его удаления.
Скрытые файлы игнорируются некоторыми командами операционной системы, в частности, они не показываются по команде Dir. Системные файлы — файлы операционной системы Dos. Атрибут «Архивный» означает, что для этого файла не была создана резервная копия командой BackUp.
Большинство файлов имеют этот атрибут. Определить атрибуты файла можно с помощью процедуры
| GetFAttr(MyFile, Attr); |
которая возвращает переменную Attr типа word, содержащую код атрибутов файла.
Например, проверить свойство «только для чтения» можно следующим образом:
| GetFAttr(MyFile, Attr); if Odd(Attr) then write (‘Только для чтения’) else write(‘Не только для чтения’); |
Установка требуемых атрибутов файла производится процедурой
| setFAttr (MyFile, Attr); |
Процедуры для поиска на диске требуемых файлов используют специальный тип записи SearchRec, определенный в модуле Dos. Запись
| Туре SearchRec= record Fill : аrrау [1..21] оf bytе; Attr : bytе; Time : longint; Size : longint; Name : string[12] end; |
Первое поле записи — массив Fill — используется операционной системой и не должно изменяться в пользовательских программах. Содержание поля Attr — атрибуты файла, рассмотренные выше. Поле Time содержит дату и время записи файла в упакованном виде, для распаковки служит процедура UnpackTime. Size — размер файла в байтах. Name — имя файла, включая разделительную точку и расширение.
| FindFirst (SearchPath, Аttr, SearchResult); |
ищет в каталоге первый файл, удовлетворяющий заданной спецификации. Параметры SearchPath и Аttr содержат исходные данные для поиска. Возвращаемый результат поиска — SearchResult — запись типа SearchRec. SearchPath — строка, содержащая полное имя файла, в том числе каталог, в котором необходимо искать файл, и имя файла. Имя файла (но не путь) может содержать символы звездочки и вопросительного знака, которые, соответственно, заменяют любую последовательность символов или один произвольный символ.
Если путь не приводится, поиск идет в рабочем каталоге. Следовательно, SearchPath = ‘*.dat’ указывает на все файлы с расширением «dat» в текущем каталоге.
Процедура FindNext(SearchResult) употребляется только после процедуры FindFirst и продолжает последовательно поиск файлов с определенным процедурой FindFirst шаблоном.
Функция FSearch(SearchString, DirList) ищет файл, заданный строкой SearchString, в заданном списке каталогов DirList. Список каталогов записывается так же, как в команде DOS Path, то есть различные каталоги разделены точкой с запятой. Результат этой функции — полное имя файла, если он найден по указанным путям.
Функция FExpand(FileName), получив имя файла FileName, расширяет его до полного имени, используя для этого имя текущего каталога. Если в качестве FileName задано имя с полным путем, функция не изменяет его. Если задано только имя файла, то дописывается текущий каталог. Если запись FileName начинается с символа «обратный слэш», то берется текущий диск и к нему дописывается имя FileName. Если запись FileName начинается с символов «..», то берется часть имени текущего каталога на уровень выше.
Процедура FSplit(WholeFileName, Dir, Name, Ext), получив в качестве аргумента полное имя файла WholeFileName, разделяет его на три составные части и возвращает переменные параметры Dir — каталог, Name — имя файла, Ext — расширение. Для каталога, имени файла и расширения в модуле DOS предусмотрены специальные типы — строки ограниченной длины: PathStr, NameStr, ExtStr.
Многие подпрограммы модуля Dos используют переменную DosError для указания на результат выполнения операции. Нулевое значение переменной указывает на отсутствие ошибок, ряд других возможных значений DosError приведен в таблице:
| Код 2 3 5 8 18 |
Значение Файл не найден Путь не найден Нет доступа Недостаточно памяти Больше нет файлов (при поиске) |
Обратим внимание, что типизированные переменные пишутся в файл в том виде, в каком они используются в памяти ЭВМ. Если мы пытаемся прочитать содержимое такого файла обычными средствами DOS, например, нажав F3 в Norton Commander, или непосредственно в среде Паскаль, то каждый байт этих записей воспроизведется как соответствующий символ кодовой таблицы ASCII. Например, если файл имеет байтовый тип, и в него пишутся числа 65, 66, 67, то при его чтении мы увидим АВС. Такая запись данных компактна и удобна, если создаваемые файлы читаются другими программами, для которых эта кодировка естественна. В тех случаях, когда файлы предназначены для просмотра человеком, требуется перевод данных в текстовую форму.
Внешние устройства в качестве файлов
Связь с внешними устройствами в языке Паскаль осуществляется также через файловые переменные.
В Турбо Паскале существует два типа внешних устройств: устройства операционной системы и текстовые устройства.
Устройства операционной системы, с которыми осуществляется обмен информацией, могут быть описаны как файлы со стандартными именами. Эти имена следующие:
CON — консоль (клавиатура, дисплей). С этим устройством стандартно связаны файловые переменные Input и Output.
LPT1, LPT2, LPT3 — печатающие устройства. Если в системе один принтер, то он будет иметь имя LPT1. Если в программе используется стандартный модуль Printer (указан в разделе Uses), можно использовать для вывода на принтер стандартную файловую переменную Lst.
PRN — синоним LPT1.
COM1, COM2 — имена двух последовательных портов.
AUX — синоним COM1.
NUL — фиктивное внешнее устройство.
К текстовым устройствам относятся устройства, не поддерживаемые операционной системой или имеющие другие способы обращения. Например, устройство CRT, поддерживаемое стандартным модулем Сrt. Оно эквивалентно CON в операционной системе, но более быстродействующее и позволяет использовать разные цвета и окна.
С текстовым устройством нельзя связаться процедурой assign. Для связи с ним служит специальная модификация этой процедуры, например, для связи с устройством CRT следует использовать процедуру AssignCrt в модуле Crt.
Функциональное программирование — это не то, что нам рассказывают
Функциональное программирование — это очень забавная парадигма. С одной стороны, про неё все знают, и все любят пользоваться всякими паттерн матчингами и лямбдами, с другой на чистом ФП языке обычно мало кто пишет. Поэтому понимание о том, что же это такое восходит больше к мифам и городским легендам, которые весьма далеко ушли от истины, а у людей складывается мнение, что «ФП подходит для всяких оторванных от жизни программок расчетов фракталов, а для настоящих задач есть зарекомендовавший себя в бою проверенный временем ООП».

Хотя люди обычно признают удобства ФП фич, ведь намного приятнее писать:
int Factorial(int n) < Log.Info($"Computing factorial of "); return Enumerable.Range(1, n).Aggregate((x, y) => x * y); >
чем ужасные императивные программы вроде
int Factorial(int n) < int result = 1; for (int i = 2; i return result; >
Так ведь? С одной стороны да. А с другой именно вторая программа в отличие от первой является функциональной.
Как же так, разве не наоборот? Красивый флюент интерфейс, трансформация данных и лямбды это функционально, а грязные циклы которые мутируют локальные переменные — наследие прошлого? Так вот, оказывается, что нет.
Итак, почему же так получается? Дело в том, что по общепринятому определению, программа считается написанной в функциональном стиле когда она состоит только из чистых функций. Так и запишем:
Функциональная программа — программа, состоящая из чистых функций.
Ок, это мы знали, но что такое чистая функция? Чистая функция — функция, результат вызова которой является ссылочно прозрачным. Или, если формально:
Функция f является чистой если выражение f(x) является ссылочно прозрачным для всех ссылочно прозрачных x
А вот тут начинаются различия с тем, что люди обычно представляют под «чистой функцией». Разве чистая функция — это не та, которая стейт не мутирует? Или там в глобальные переменные не залезает? Да и что это за «ссылочная прозрачность» такая? На самом деле корреляция с этими вещами действительно есть, но сама суть чистоты не в том, чтобы ничего не мутировать, а именно эта самая прозрачность.
Так что же это такое? А вот что:
Ссылочная прозрачность — свойство, при котором замена выражения на вычисленный результат этого выражения не изменяет желаемых свойств программы
Это значит что если у нас где-то написано var x = foo() то мы всегда можем заменить это на var x = result_of_foo и поведение программы не поменяется. Именно это и является главным требованием чистоты. Никаких других требований (вроде неизменяемости) ФП не накладывает. Единственный момент тут — философский, что считать «поведением программы». Его можно определить интуитивно как свойства, которые нам критично важно соблюдать. Например, если исполнение кода выделяет чуть больше или чуть меньше тепла на CPU — то нам скорее всего это пофиг (хотя если нет, то мы можем с этим работать специальным образом). А вот если у нас программа в базу ходить перестала и закэшировала одно старое значение — то это нас очень даже волнует!
Вернемся к нашим примерам. Давайте проверим, выполняется ли наше правило для первой функции? Оказывается, что нет, потому что если мы заменим где-нибудь Factorial(5) на 120 то у нас поменяется поведение программы — в логи перестанет писаться информация которая раньше записывалась (хотя если мы подойдем с позиции «да и хрен ними, с логами» и не будем считать это желаемым поведением, то программу можно будет считать чистой. Но, наверное мы не просто так ту строчку в функции написали, и логи в кибане все же хотели бы увидеть, поэтому сочтем такую точку зрения маловероятной).
А что насчет второго варианта? Во втором случае всё остается как было: можно все вхождения заменить на результат функции и ничего не изменится.
Важно отметить, что это свойство должно работать и в обратную сторону, то есть мы должны иметь возможность поменять все var x = result_of_foo на var x = foo() без изменения поведения программы. Это называется «Equational reasoning», то есть «Рассуждения в терминах эквивалентности». В рамках этой парадигмы что функции, что значения — суть одно и то же, и можно менять одно на другое совершенно безболезненно.
Отсюда важное следствие: программа не обязана работать с неизменяемыми данными чтобы считаться функциональной. Достаточно, чтобы эти изменения не были видны стороннему наблюдателю. Для этого даже придумали специальный механизм называющийся ST , который на уровне типов помогает вам не дать утечь случайно мутабельному состоянию наружу. Типичный пример — пишем инплейс быструю сортировку и забыли скопировать входной массив: ST помогает превратить это в ошибку компиляции. Неизменяемость является важным удобным свойством, но вас никто не заставляет пользоваться только им, при необходимости можно мутировать в хвост и гриву, главное — не нарушить ссылочную прозрачность.
Зачем это нужно
Наверное — самый главный вопрос. Зачем так мучиться? Копировать данные вместо того чтобы изменить напрямую, оборачивать объекты в эти ваши ST чтобы изменения (если они есть) не утекали наружу, и вот это всё… Ответ — для лучшей композиции. В своё время goto очень невзлюбили именно потому, что с ним очень трудно понять как на самом деле программа себя ведет и какой на самом деле поток данных и управления, и переиспользовать функцию написанную с goto было сложно, ведь тогда он умел даже в середину тела функции прыгнуть без каких-либо проблем.
С Equational reasoning всегда просто понять, что происходит: вы можете заменить результат на функцию и всё. Вам не нужно думать, в каком порядке функции вычисляются, не надо переживать насчёт того как оно поведет если поменять пару строчек местами, программа просто передает результаты одних функций в другие.
В качестве примера почему это хорошо могу привести случай из жизни который случился как раз со мной пару месяцев назад. Писал я самый типовой ООП код на C#, и понадобилось мне влезть в старый кусок, где был написан вот такой код (пример упрощён)
var something = function(); DoStuff(this.Field, something);
И понадобилось мне во время выполнения задачи их немного отрефакторить, что я и сделал:
DoStuff(this.Field, function());
Изменения успешно прошли тесты, изменения прошли ревью код замержили, после чего на тестовом стенде начались странные падения. После пары часов отладки обнаружилось, что в кишках function делалось примерно такое:
. что-то считаем this.Field = GetUpdatedVersion(this.Field, localData) // ой! . продолжаем считать и возвращаем результат
Соответственно если раньше с точки зрения компилятора оно выглядело так:
var something = function(); var arg1 = this.Field; // после вызова function - новое значение! var arg2 = something; DoStuff(arg1, arg2);
То после рефакторинга получилось следующее:
var arg1 = this.Field; // до вызова function - остаётся старое значение! var arg2 = function(); DoStuff(arg1, arg2);
Соответственно если раньше функция DoStuff вызывалась с обновленной версией поля, то после рефакторинга начала вызываться со старой.
Какую мораль тут можно вынести? «Нефиг писать функции которые и мутируют, и данные возвращают»? Соглашусь, и отмечу, что ссылочная прозрачность является следующим логичным шагом в этом направлении. В функциональной программе перестановка местами любых двух независимых строчек никогда не приведет к изменению семантики программы.
В общем и целом, ФП направлено на то, чтобы можно было судить о поведении функции наблюдая только её одну. Если вы, как и я, пишете на каком-нибудь C# в обычном императивном стиле, вам кроме этого нужно понимать, как у вас DI работает, что конкретно делает функция function или DoStuff , можно ли эту функцию безопасно из разных потоков вызывать или нет. В ФП вы смотрите на одну функцию, смотрите на её данные, и этой информации вам достаточно чтобы полностью понимать как она работает.
То есть этот стиль направлен на более удобное разделение частей программы друг от друга. Это сильно упрощает понимание кода для людей, которые его не писали. По традиции отмечу, что этим кем-то можете быть вы сами через полгода. Чем больше проект, тем сильнее эффект. Насколько я видел, в достаточно крупных проектах на сотни тысяч строк люди сами в итоге переизобретают все те же принципы, несмотря на то что и язык и платформа обычно достаточно сильно упираются. Потому что просто невозможно отлаживать большие программы, когда всё взаимодействует со всем. Чистота функции, когда её вызов просто возвращает результат, а не пишет вам нескучные рассказы в кибану и не посылает емэйлы на почту, очень в этом помогает. Любой разработчик большого проекта вам скажет, что чётко очерченные контракты и небольшие стейтлесс модули — самые простые и удобные в работе с ними. Функциональный подход всего лишь развивает эту идею до логической точки — все функции должны быть чистыми, и не зависеть от какого-либо состояния.
Как эти принципы отражаются в коде
В качестве сравнения могу предложить вам такой пример, который я взял из Красной книги Scala (совершенно шикарная книга, очень доходчиво и интересно рассказывает о ФП, c крутыми задачками). Правда, для большей понятности я адаптировал текст и код к C#.
Предположим у нас есть кофейная и мы хотим, чтобы люди могли заказывать кофе. Ничего больше не надо, очень простое требование.
ООП вариант
Окей, как нам сказали, так и пишем:
public class Cafe < public Coffee BuyCoffee(CreditCard card) < var cup = new Coffee() card.Charge(cup.Price) return cup >>
Строка card.Charge(cup.Price) является примером побочного эффекта. Оплата кредитной картой предполагает некоторое взаимодействие с внешним миром — например, для этого может потребоваться связаться с компанией-эмитентом кредитной карты через какой-либо веб-сервис, авторизовать транзакцию и всё такое. Побочным эффектом оно называется потому, что все эти действия не имеют отношения к созданию экземпляра Coffee, то есть они как бы находятся «сбоку» от основного результата функции «вернуть стаканчик кофе».
В результате из-за побочного эффекта код трудно тестировать. Любой опытный ООП разработчик скажет «Да сделай ты интерфейс для того чтобы списывать деньги!». Разумное требование, так и поступим:
public class Cafe < public Coffee BuyCoffee(CreditCard card, IPaymentProvider paymentProvider) < var cup = new Coffee() paymentProvider.Charge(card, cup.Price) return cup >>
Несмотря на побочные эффекты у нас появилась возможность тестировать процесс покупки: достаточно в тестах замокать интерфейс IPaymentProvider . Но и тут есть свои недостатки.
- Во-первых нам пришлось ввести IPaymentProvider , хотя если бы не тесты одна конкретная реализация нас бы вполне устроила.
- Во-вторых моком реализующим нужный функционал может быть неудобно пользоваться. Типичный пример — InMemory DB, где мы мокаем Insert/Save/… методы, а потом достаем внутренний стейт (как правило в виде списков) и смотрим, что всё сохранилось куда надо. Надо ли говорить, что инспектировать внутреннее состояние объектов — это нехорошо? И да, можно конечно использовать какой-нибудь фреймворк который сделает за нас большую часть работы, но не всю, да и тащить целый фреймворк просто чтобы протестировать что мы можем купить чашечку кофе выглядит оверкиллом.
- Ну а в-третьих есть проблемы с переиспользованием этой функции. Допустим мы хотим купить N чашечек кофе. В текущих интефрейсах у нас нет простого способа это сделать кроме как написать полностью новую функцию (если мы конечно не хотим заддосить наш платёжный шлюз однотипными запросами):
public class Cafe < public Coffee BuyCoffee(CreditCard card, IPaymentProvider paymentProvider) < var cup = new Coffee() paymentProvider.Charge(card, cup.Price) return cup >public Coffee[] BuyCoffees(int count, CreditCard card, IPaymentProvider paymentProvider) < // нам теперь еще и случай 0 чашек надо обработать, // чтобы не выставить случайно чек на 0 рублей if (count == 0) return Array.Empty(); var cups = Enumerable.Range(0, count).Select(_ => new Coffee()).ToArray(); paymentProvider.Charge(card, cups[0].Price * count) return cups > >
Даже для такого простого случая нам пришлось копипастить код. И если в этом случае это не очень-то принципиально, то в случае сложной развесистой логики это может быть куда больнее.
ФП вариант
Как же нам написать код так, чтобы не столкнуться с этими проблемами? Функциональный подход — вместо фактического списания средств просто выставить счет, а вызывающий код пусть сам решает, что с эти делать. Тогда наша функция будет иметь вид:
public class Cafe < public (Coffee, Charge) BuyCoffee(CreditCard card) < var cup = new Coffee() return (cup, new Charge(card, cup.Price)) >>
Да, вот так просто. Теперь вызывающий код, если это реальное приложение, может произвести транзакцию и списать деньги. А вот если это тест, то он просто может проверить возвращенный объект Charge на все интересующие его свойства. Никаких моков больше не надо: мы разделили события выставления счёта и интерпретацию этого счёта. Charge это простая DTO которая хранит с какой карты сколько надо списать. Легко видеть, что наша функция стала чистой. Она просто возвращает кортеж из двух объектов, которые являются простым описанием данных. Мы можем заменить вызов этой функции на результат, и смысл программы не поменяется. И нам на этом уровне больше не нужен никакой провайдер платежей, ура!
Что насчёт покупки N стаканчиков кофе? Благодаря тому что мы избавились от эффектов, нам не нужно бояться что N вызовов BuyCoffee заспамят наш платежный шлюз, поэтому просто переиспользуем её.
public class Cafe < public (Coffee, Charge) BuyCoffee(CreditCard card) < var cup = new Coffee() return (cup, new Charge(card, cup.Price)) >public (Coffee[], Charge) BuyCoffees(int count, CreditCard card) < var (coffees, charges) = Enumerable.Range(0, count) .Select(_ =>BuyCoffee(card)) .Unzip(); return (coffees, charges.Aggregate((c1, c2) => c1.Сombine(c2)) > >
Ну и дописываем хэлпер-функцию Combine :
public class Charge < public CreditCard Card < get; set; >public double Amount < get; set; >public Charge(CreditCard card, double amount) < Card = card; Amount = amount; >public Charge Combine(Charge other) < if (Card != other.Card) throw new ArgumentException("Can't combine charges to different cards"); return new Charge(Card, Amount + other.Amount); >>
Причем эта хэлпер-функция нам позволяет делать много других крутых штук. Например, теперь мы способны минимизировать количество взаимодействий с платежным шлюзом, комбинируя карты по покупателю:
IEnumerable Coalesce(IEnumerable charges) => charges.GroupBy(x => x.Card).Select(g => g.Aggregate((c1, c2) => c1.Combine(c2))
Это только краткий перечень преимуществ, которые дает чистота функций. И да, заметьте, что язык и там и там используется один и тот же, вся разница только в подходе.
Предвижу, что мне могут возразить, что дескать-то проблема не решена, и теперь код уровнем выше должен делать это списание, только теперь логика немного размазана, и мы просто чуть-чуть упростили тесты конкретно нашего класса Cafe . На самом деле, это не так, потому что код выше тоже может передать решение что делать дальше, а тот код еще дальше, и так до сервиса, который уже реально что-то сделает с этими данными (но и там его можно сделать тестируемым без моков, подробнее об этом в другой статье).
Вторым возражением может быть то, что в ООП варианте мы могли бы настроить IPaymentProvider на то, что он будет заниматься батчингом операций, но и тут возможны сложности: нужно настраивать таймауты, подбирать значения, чтобы батчинг был эффективным и при этом латентность операцией не сильно выросла, плюс вы всё еще будете бояться «плохих» реализаций, которые не будут заниматься батчингом, и так далее. В общем, как ни крути, этот подход получается ощутимо хуже.
Разделение выполнения задачи на создание описателя этой задачи и интерпретацию кажется очень незначительным перекладыванием из пустого в порожнее, однако это очень важная вещь, которую трудно переоценить. Откладывания принятия решения «что нам делать с этими данными» открывает большой простор для действий, и делает многие вещи вроде отмены или повтора операции намного более тривиальными. Концепция на мой взгляд схожа по мощности с RAII: одно простое правило, и очень много далеко идущих хороших последствий.
И это всё?
С точки зрения самой сути ФП — да, это всё. Отсутствие эффектов это единственное требование, которое нужно соблюдать, чтобы программа была функциональной. Но исторически сложилось, что ФП языки обладают более обширным количеством ограничений, а ограничения обычно придумывают не просто так, а чтобы получить от этого преимущества. Ограничение на типы переменных (то что в int переменную нельзя засунуть строку) позволяет писать более надежные программы, ограничения на изменение потока управления (например, запрет goto) ведет к упрощению понимания программ, ограничение на шаблонизацию (Templates vs Generics) позволяет проще писать обобщенный код и иметь более хорошие сообщения об ошибках, и так далее.
Одним из самых крутых преимуществ распространенных ФП языков, на мой взгляд, является ценность сигнатур функций и типов. Дело в том, что в отличие от «грязных» функций, сигнатура чистой обычно дает столько информации, что количество возможных вариантов её реализации снижается до жалких единиц, а в экстремальных случаях компилятор может сгенерировать тело функции по её сигнатуре. Почему это не работает в императивных программах? Потому что там void UpdateOrders() и void UpdateUsers() имеют одну и ту же сигнатуру () -> () , но совсем разное значение. В ФП они будут иметь тип навроде () -> OrdersUpdate и () -> UsersUpdate . Именно потому, что функции разрешено только вычислять значение (а не делать произвольную дичь) мы и можем с уверенностью судить о многих её свойствах, просто глядя на сигнатуру.
Что же нам это дает? Ну, например предположим у нас есть такая функция (пример на Rust)
// принимаем массив объектов, еще какой-то объект, и возвращаем значение того же типа fn foo(a: &[T], b: T) -> T
Я не знаю что внутри этой функции, но по сигнатуре я вижу, что результатом будет один из элементов массива, либо в случае пустого массива — элемент b который я передал. Откуда я это знаю? Оттуда, что функция не делает никаких предположений о типе T . Поэтому она никак не может создать экземпляр самостоятельно. Следовательно, единственный способ получить значение того же типа — взять один из объектов которые мы ей передали.
Соответственно я могу написать такой тест
let a = [1,2,3,4,5]; let b = foo(a, 10); assert!(b == 10 || a.iter().any(|x| x == b))
Этот тест будет выполняться для любой реализации этой функции, если только она не вызывает UB и возвращает хоть какое-то значение (не паникует и не уходит в вечные циклы). Но можно безопасно предположить, что она этого не делает, потому что вряд ли кто-то в здравом уме написал бы функцию которая зачем-то принимает массив любых объектов, но всегда паникует (напомню, что мы ничего не знаем про переданные объекты, поэтому паниковать только в некоторых случаях функция не может).
А теперь давайте уберем второй параметр и посмотрим что произойдет:
fn foo(a: &[T]) -> T
Обратите внимание, что для пустого массива эта функция кинет исключение, панику, войдет в вечный цикл или сделает еще что-то нехорошее. Или, если говорить формально, вернёт Bottom-тип ⊥ . Откуда я это знаю? А потому что функция обязалась вернуть значение T, а мы ей ни одного не передали. То есть её контракт невозможно соблюсти для любого значения аргумента a . Таким образом функция является частично-рекурсивной, и следовательно не определена для пустых массивов. А на неопределенных аргументах функции обычно паникуют.
В общем, глядя на эту сигнатуру сразу видно, что нам стоило бы проверить предварительно массив на пустоту прежде чем вызывать такую функцию.
Почему я взял для примера раст, а не тот же сишарп? А потому что его система типов недостаточно мощная, чтобы гарантировать такое поведение. Пример функции, которая не пройдет тест:
T Foo(List list, T value) => default(T);
Вот хотел бы я в сишарпе положиться на систему типов, да не могу. Нужно идти смотреть реализацию функции. А как только мы пошли смотреть реализацию, то мы потеряли главное преимущество, которое нам даёт программирование на языках высокого уровня — умение инкапсулировать сложность и скрывать её за красивым интерфейсом. Чем меньше информации нам нужно чтобы понять, как работает тот или иной код, тем легче и проще вносить изменения, и тем более надежным получается софт.
А знаете как будет выглядеть в расте функция, которая если массив пустой вернет дефолтное значение T? Вот так:
fn foo(a: &[T]) -> T
Она всё еще может упасть с паникой на пустом массиве, но учитывая что автор явно затребовал возможность создания дефолтного значения этого типа, разумно предположить что именно это и происходит в теле. В конце концов это лишняя писанина, поэтому если автор это написал, то значит как-то скорее всего это использует. А единственное разумное использование такого аргумента — вернуть дефолтное значение когда массив пустой. И мы сразу видим это требование в сигнатуре. Просто превосходно ведь! Напомню, что в сишарпе для этого нужно пойти в тело функции и увидеть там вызов default(T) .
В функциональной парадигме вам в 99% случаев достаточно просто посмотреть на сигнатуру функций чтобы понять, как она работает. Это может показаться неправдоподобным хвастовством, но это так. Haskell коммьюнити довело эту мысль до абсолюта и создало поисковик Hoogle который позволяет искать функции в библиотеках по её сигнатуре. И он отлично работает.
Например (a -> Bool) -> [a] -> [a] (функция, принимающая два аргумента: предикат и список, в качестве результата возвращает список таких же элементов) ожидаемым образом находит функции filter и takeWhile .
Для закрепления предлагаю небольшую загадку. Подумайте, что вот это за функция? Она принимает строку, и возвращает совершенно любой тип.
fn bar(s: String) -> T < . >// раст-вариант bar :: String -> a // хаскель-вариант
Если подумать, то у нас нет никакого способа сделать объект, про тип которого мы ничего не знаем. Потому единственное, что может сделать эта функция — никогда не вернуть результат. То есть вернуть вечный цикл или панику, известный нам ⊥ . Но вспомним, что функция принимает еще и строковую переменную. Для цикла большого смысла её передавать нет, поэтому можно быть практически уверенным в том, что это функция занимается бросанием паники:
fn bar(s: String) -> T
Если вы подумали про рефлексию и создание типа в рантайме — это в принципе тоже возможный исход (хотя в расте и в хаскелле её всё равно нет), но тогда непонятно зачем строковый параметр нужен. Хотя если очень постараться, то можно представить такую функцию. Так что в принципе если это ваш вариант, то смело добавляете себе балл, это тоже возможный вариант для языков, которые это позволяют.
Навык додумать что делает функция по сигнатуре очень выручает, потому что вам не нужно лезть в тело функций чтобы понять, что она может сделать, а что нет. Даже если функция foo из примера выше занимает 1000 строк, она всё равно обязана вернуть либо один из элементов переданного массива, либо второй аргумент. Других вариантов нет. И вам не нужно читать 1000 строк чтобы это понять. Вы просто знаете это глядя на сигнатуру функции.
Разве чисто функциональный язык может сделать что-то полезное?
Этот вопрос меня волновал с тех пор, как я у знал о функциональных языках. «Чёрт», думал я, «Но ведь мне надо в базу сходить, HTTP запрос сделать, в консоль написать в конце концов. Но чистый язык этого не разрешает. Наверное он подходит только чтобы факториалы считать».
Как оказалось, сам ФП язык всё это делать действительно не может, Но тут умные ребята взяли и придумали как это обойти. Они сказали «Окей, программа не может делать грязных действий. Но, а что если мы разделим создание описателя вычисления и его интерпретацию (прямо как в нашем примере с кафе)? А тогда получится, что вся программа чистая, а нечистым является рантайм который выполняет всю грязную работу!».
Как это выглядит? Ну возьмем для примера тип IO , отвечающий за взаимодействие с внешним миром. Это такой же тип, как наш Charge из примера выше, только вместо списания по карте он описывает ввод/вывод. Сам по себе IO ничего не делает, если мы напишем print «Hello world» в хаскелле ничего не произойдет. Но если мы напишем main = print «Hello world» то магическим образом текст попадет на экран. Как же это происходит?
А всё дело в том, что рантайм хаскелля занимается интерпретацией этого IO. То есть все описанные действия происходят за пределами функции main . То есть из всей нашей программы мы собираем гигантскую стейт машину, которую затем рантайм начинает интерпретировать. И этому рантайму разрешено делать «грязные» вещи — ходить в базу, печатать на экран, и делать всё, что угодно. Но с точки зрения кода мы ничего никогда не совершаем.
Если мы хотим в хаскелле сходить в базу, то мы создаем объект СходиВБазу , который сам по себе ничего не делает. Но когда интерпретатор выполняя функцию main столкнется с этим значением, он произведет физическое хождение в базу.
Если использовать аналогию, то хаскель программа это алгоритм записанный на листочке, а рантайм — это робот, который этот алгоритм выполняет. Сам по себе листочек ничего не делает, и просто лежит бездейственно. С точки зрения алгоритма мы не можем ничего «сделать», мы можем только сделать другой листочек с другим набором команд. И пока робот не придет интерпретировать наши записи листочек остается совершенно бездействующим.
Наверное, я вас только запутал этой аналогией, поэтому давайте покажу на примере. Вот программа на Rust:
fn main()
И она выводит «Hello world!». А теперь попробуем написать аналогичную программу на Haskell:
main :: IO () main = do let _ = print "Hello " print "world!"
И она выводит «world!». По сути разница между поведением этих программ и является квинтэссенцией различия чистой и нечистой программы. В случае хаскелля мы создали описатель «выведи Hello «, но никак им не воспользовались. Этот описатель не был проинтерпретирован и надписи на экране не появилось. В качестве результата main мы вернули единственный описатель с world! , который и был выполнен. С другой стороны в случае программы на Rust сам вызов print! уже сам по себе является действием, и мы не можем его никак отменить или преобразовать как-то еще.
Именно возможность работать с эффектами как значениями (выкинуть сам факт того, что мы хотели что-то вывести на экран) очень упрощает жизнь, и делает невозможными баги вроде того что я показал в первом разделе. И когда говорят про «Контроль эффектов в ФП» имеют ввиду именно это. Забегая вперед, можно описывать эффекты функций в стиле «эта функция пишет в базу (причем только вот в ту таблицу), ходит по HTTP (но только через этот прокси, и на вот этот сайт), умеет писать логи и читать конфиги. И всё это будет проверяться во время сборки, и при попытке сходить не на тот сайт или прочитать конфиг не аннотировав такую возможность в сигнатуре будет приводить к ошибке времени компиляции.
Заключение
Как видите, всё противопоставление ООП и ФП совершенно искусственно. Можно писать и в том, и в другом стиле на одном и том же языке, и в принципе даже совмещать. Весь вопрос в том, поощряет ли язык написание в таком стиле или наоборот. Например писать объектно-ориентированно на ANSI C можно, но очень больно. А на джаве просто. С другой стороны писать на джаве в чисто функциональном стиле тяжело, а на Scala или Haskell — просто. Поэтому вопрос скорее заключается в том, что есть два инструмента, один распространен и поддерживается многими языками, другой более интересен по целому спектру свойств, но поддерживается не везде. Ну и дальше ваш выбор как разработчика, какой инструмент вам больше подходит по ситуации.
Лично я для себя вижу очень много преимуществ в функциональной парадигме в плане поддерживаемости кода. Я очень устал от того, что перестановка двух несвязных строчек местами может что-то поломать в совершенно третьем месте. Мне надоело конфигурировать моки и DI. Я не хочу ловить в рантайме ошибки «Метод не был замокан»/»Тип не был зарегистрирован»/». «, в конце концов я не для того выбирал статически типизированный язык.
Конечно, ФП это не серебряная пуля, у него есть свои ограничения, и ему тоже есть куда расти. Но на мой взгляд оно намного интереснее распространенных на текущий момент подходов. «Фишки» ФП языков вроде лямбд, паттер матчингов, АДТ и прочего давно уже не удивляют в мейнстрим языках. Но это всё шелуха, и оно становится реально мощным инструментом только в совокупности с самой главной идеей ФП — идеей ссылочной прозрачности.
- функциональное программирование
- c#
- никто не читает теги
- ссылочная прозрачность
- чистые функции
- Программирование
- Совершенный код
- .NET
- C#
- Функциональное программирование