Компьютеры с современный мир

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

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

Побочный эффект

Функции и процедуры

В некоторых языках программирования (например, в Паскале) функции и процедуры (подпрограммы, не возвращающие значения) чётко разграничены синтаксисом языка . В других - например, в языке Си , - процедуры являются частным случаем (подмножеством) функций, возвращающими значение типа (псевдотипа) void - пустое значение.

Аргументы и параметры

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

Функция без аргументов

Такая функция не требует никаких аргументов .

См. также

Ссылки

  • Функции PHP. Синтаксис и примеры использования функций PHP

Wikimedia Foundation . 2010 .

Смотреть что такое "Функция (программирование)" в других словарях:

    В Викисловаре есть статья «функция» Функция многозначный термин, который означает такое отношение между элементами, в котором изменение в одном влечет измен … Википедия

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

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

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

    В области компьютеризации понятие программирования сетевых задач или иначе называемого сетевого программирования (англ. network programming), довольно сильно схожего с понятиями программирование сокетов и клиент серверное программирование,… … Википедия

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

    ПРОГРАММИРОВАНИЕ МАТЕМАТИЧЕСКОЕ - комплекс математич. моделей и методов решения задач отыскания экстремума (максимума или минимума) функций многих переменных при ограничениях в виде неравенств. Имеется в виду, что переменные характеризуют какие либо аспекты механизма… … Российская социологическая энциклопедия

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

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

    ПРОГРАММИРОВАНИЕ, МАТЕМАТИЧЕСКОЕ - раздел прикладной математики, применяющийся в качестве метода в экономических исследованиях. Разрабатывает теорию и методы решения условных экстремальных задач, является основной частью формального аппарата анализа разнообразных задач управления … Большой экономический словарь

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

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

#include // подключение заголовка с функцией rand rand() int randomNumber = rand(); // стандартная функция генерации случайных чисел

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

Рассмотрим общий формат для прототипа функций:

ReturnedDataType functionName (dataType par1, ..., dataType parN);

где, returnedDataType — тип данных, возвращаемого функцией, значения;
functionName — имя функции
dataType — тип данных параметра функции, это тот же самый тип данных, что и при объявлении переменной
par1 ... parN — параметры функции.

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

Int mult (int x, int y);

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

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

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

#include int multiplication(int num1, int num2); //прототип функции int main() { int num1; int num2; printf("Введите два числа для умножения: "); scanf("%d", &num1); scanf("%d", &num2); printf("Результат умножения %d\n", multiplication(num1, num2)); // вызов функции getchar(); return 0; } int multiplication(int num1, int num2) // определение функции { return num1 * num2; }

Эта программа начинается с включения единственного заголовочного файла, в строке 1. Следующей строкой является прототип функции умножения. Обратите внимание, что в конце объявления прототипа есть точка с запятой! Функция main возвращает целое число, в строке 16. Чтобы соответствовать стандарту функция main всегда должна возвращать некоторое значение. У вас не должно возникнуть проблем с пониманием ввода и вывода значений в функциях, если вы внимательно изучили предыдущие уроки.

Обратите внимание на то как на самом деле функция multiplication() принимает значение. Что же происходит на самом деле? А на самом деле это работает так: функция multiplication принимает два целых значения, умножает их и возвращает произведение. Результат работы этой программы будет точно таким же, как если бы мы сделали так:

Printf("Результат умножения %d\n", num1 * num2);

Функция multiplication() на самом деле определяется ниже функции main . А так как прототип этой функции объявлен выше главной функции, то при вызове функции multiplication() внутри main() компилятор не выдаст ошибку. Пока прототип присутствует, функция может использоваться даже если нет её фактического определения. Тем не менее, вызов функции не может быть осуществлен ранее, чем будет определена эта функция.

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

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

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

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

P.S.: если вам нужен хороший сервер, то вы можете воспользоваться арендой серверов в Москве . Также вы можете воспользоваться другими услугами, предоставленными на сайте it-express.ru: развертывание отказоустойчивых серверов, системы хранения данных и др.

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

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

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

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

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

Функция определяет собственную (локальную) область видимости, куда входят входные параметры, а также те переменные, которые объявляются непосредственно в теле самой функции.

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

  • 1 Примеры функций
    • 1.1 JavaScript
    • 1.2 ActionScript
    • 1.3 С++
    • 1.4 C#
    • 1.5 Pascal
    • 1.6 PHP
    • 1.7 Stadard ML
    • 1.8 Visual Basic
    • 1.9 PureBasic
  • 2 См. также
  • 3 Ссылки

Примеры функций

JavaScript

function name(text, element) { document.getElementById(element).innerHTML = text; }

ActionScript

public function name(text: string) { var textfield: TextField = new TextField(); textfield.text = text; }

С++

void name(std::string text) { std::cout << text; }

C#

public void name(string text) { System.Console.WriteLine(text); }

Pascal

procedure name(var text: string) begin write(text); end;

PHP

function name($text) { echo $text; }

Stadard ML

fun name t = print t

или, что то же самое (см. карринг):

Fun name = print

Visual Basic

Sub Name(text) Console.WriteLine(text) End Sub

PureBasic

Procedure.l Name(text.s) PrintN(text) EndProcedure

См. также

  • Анонимная функция
  • Функция (математика)

Ссылки

Функция (программирование) Информацию О

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

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

Существует множество встроенных в язык программирования функций. С некоторыми такими в Python мы уже сталкивались. Это print(), input(), int(), float(), str(), type(). Код их тела нам не виден, он где-то "спрятан внутри языка". Нам же предоставляется только интерфейс – имя функции.

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

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

i = 0 while i < 3 : a = int (input () ) b = int (input () ) print (a+b) i += 1

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

print () a = int (input () ) b = int (input () ) print ("Всего" , a+b, "шт." ) print () a = int (input () ) b = int (input () ) print ("Всего" , a+b, "шт." )

Пример исполнения программы:

Сколько бананов и ананасов для обезьян? 15 5 Всего 20 шт. Сколько жуков и червей для ежей? 50 12 Всего 62 шт. Сколько рыб и моллюсков для выдр? 16 8 Всего 24 шт.

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

Определение функции. Оператор def

В языке программирования Python функции определяются с помощью оператора def. Рассмотрим код:

def countFood() : a = int (input () ) b = int (input () ) print ("Всего" , a+b, "шт." )

Это пример определения функции. Как и другие сложные инструкции вроде условного оператора и циклов функция состоит из заголовка и тела. Заголовок оканчивается двоеточием и переходом на новую строку. Тело имеет отступ.

Ключевое слово def сообщает интерпретатору, что перед ним определение функции. За def следует имя функции. Оно может быть любым, также как и всякий идентификатор, например, переменная. В программировании весьма желательно давать всему осмысленные имена. Так в данном случае функция названа "посчитатьЕду" в переводе на русский.

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

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

Вызов функции

Рассмотрим полную версию программы с функцией:

def countFood() : a = int (input () ) b = int (input () ) print ("Всего" , a+b, "шт." ) print ("Сколько бананов и ананасов для обезьян?" ) countFood() print ("Сколько жуков и червей для ежей?" ) countFood() print ("Сколько рыб и моллюсков для выдр?" ) countFood()

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

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

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

print ("Сколько бананов и ананасов для обезьян?" ) countFood() print ("Сколько жуков и червей для ежей?" ) countFood() print ("Сколько рыб и моллюсков для выдр?" ) countFood() def countFood() : a = int (input () ) b = int (input () ) print ("Всего" , a+b, "шт." )

Результат:

Сколько бананов и ананасов для обезьян? Traceback (most recent call last ) : File "test.py" , line 2 , in < module> countFood() NameError: name "countFood" is not defined

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

Функции придают программе структуру

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

Пусть надо написать программу, вычисляющую площади разных фигур. Пользователь указывает, площадь какой фигуры он хочет вычислить. После этого вводит исходные данные. Например, длину и ширину в случае прямоугольника. Чтобы разделить поток выполнения на несколько ветвей, следует использовать оператор if-elif-else:

figure = input () if figure == "1" : a = float (input ("Ширина: " ) ) b = float (input ("Высота: " ) ) print ("Площадь: %.2f" % (a*b) ) elif figure == "2" : a = float (input ("Основание: " ) ) h = float (input ("Высота: " ) ) print ("Площадь: %.2f" % (0.5 * a * h) ) elif figure == "3" : r = float (input ("Радиус: " ) ) print ("Площадь: %.2f" % (3.14 * r**2 ) ) else : print ("Ошибка ввода" )

Здесь нет никаких функций, и все прекрасно. Но напишем вариант с функциями:

def rectangle() : a = float (input ("Ширина: " ) ) b = float (input ("Высота: " ) ) print ("Площадь: %.2f" % (a*b) ) def triangle() : a = float (input ("Основание: " ) ) h = float (input ("Высота: " ) ) print ("Площадь: %.2f" % (0.5 * a * h) ) def circle() : r = float (input ("Радиус: " ) ) print ("Площадь: %.2f" % (3.14 * r**2 ) ) figure = input ("1-прямоугольник, 2-треугольник, 3-круг: " ) if figure == "1" : rectangle() elif figure == "2" : triangle() elif figure == "3" : circle() else : print ("Ошибка ввода" )

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

Если нам когда-нибудь захочется вычислять площадь треугольника по формуле Герона, а не через высоту, то не придется искать код во всей программе (представьте, что она состоит из тысяч строк кода как реальные программы). Мы пойдем к месту определения функций и изменим тело одной из них.

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

Практическая работа

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

Основная ветка программы, не считая заголовков функций, состоит из одной строки кода. Это вызов функции test(). В ней запрашивается на ввод целое число. Если оно положительное, то вызывается функция positive(), тело которой содержит команду вывода на экран слова "Положительное". Если число отрицательное, то вызывается функция negative(), ее тело содержит выражение вывода на экран слова "Отрицательное".

Локальные и глобальные переменные

Это третья статья в цикле «Теория категорий для программистов».

Кому нужны типы?

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

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

Итак, вопрос в том, хотим ли мы, чтобы обезьяны были счастливы, или создавать корректные программы?
(прим. переводчика: не стоит оскорбляться, автор просто любит менее скучные метафоры, чем ГСЧ и «случайные последовательности байт», а не называет программистов обезьянами).

Обычно цель мысленного эксперимента с печатающими обезьянами - создание полного собрания сочинений Шекспира (прим. переводчика: или Война и Мир Толстого) . Проверка орфографии и грамматики в цикле резко увеличит шансы на успех. Аналог проверки типов пойдет еще дальше: после того, как Ромео объявлен человеком, проверка типов убедится, что на нем не растут листья и что он не ловит фотоны своим мощным гравитационным полем.

Типы нужны для компонуемости

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

Единственный серьезный аргумент, который я слышу против строгой статической типизации: она может отвергнуть некоторые программы, которые семантически верны. На практике это случается крайне редко (прим. переводчика: во избежания срача замечу, что тут автор не учел, или несогласен, что есть много стилей, и привычный программсистом на скриптовых языках duck-typing тоже имеет право на жизнь. С другой стороны, duck-typing возможен и в строгой системе типов через templates, traits, type classes, interfaces, много есть технологий, так что мнение автора нельзя считать строго неверным.) и, в любом случае, каждый язык содержит какой-то черный ход, чтобы обойти систему типов, когда это действительно необходимо. Даже Haskell имеет unsafeCoerce. Но такие конструкции должны использоваться разумно. Персонаж Франца Кафки, Грегор Замза, нарушает систему типов, когда он превращается в гигантского жука, и мы все знаем, как это кончилось (прим. переводчика: плохо:) .

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

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

Строгая статическая типизация часто используется в качестве предлога для нетестирования кода. Иногда вы можете услышать, как Haskell-программисты говорят: «Если код собирается, он правильный.» Конечно, нет никакой гарантии, что программа, корректная с точки зрения типов, коректна в смысле правильного результата. В результате такого отношения в ряде исследований Haskell не стал сильно опережать остальные языки по качеству кода, как можно было бы ожидать. Кажется, что в коммерческих условиях необходимость чинить баги существует только до определенного уровня качества, что в основном связано с экономикой разработки программного обеспечения и толерантности конечного пользователя, и очень слабо связано с языком программирования или методологией разработки. Лучшим критерием было бы измерить, сколько проектов отстает от графика или поставляется с сильно сниженным функционалом.

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

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

Что такое типы?

Простейшее описание типов: они представляют собой множества значений. Типу Bool (помните, конкретные типы начинаются с заглавной буквы в Haskell) соответствует множество из двух элементов: True и False. Тип Char - множество всех символов Unicode, например "a" или "ą".

Множества могут быть конечными или бесконечными. Тип String, который, по сути, синонимом списка Char, - пример бесконечного множества.

Когда мы обьявляем x, как Integer:
x:: Integer
мы говорим, что это элемент множества целых чисел. Integer в Haskell - бесконечное множество, и может быть использовано для арифметики любой точности. Есть и конечное множество Int, которое соответствует машинному типу, как int в C++.

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

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

В идеальном мире мы могли бы просто сказать, что типы в Haskell - множества, а функции в Haskell - математические функции между ними. Существует только одна маленькая проблема: математическая функция не выполняет какой-либо код - она знает только ответ. Функция в Haskell должна ответ вычислять. Это не проблема, если ответ может быть получен за конечное число шагов, каким бы большим оно ни было. Но есть некоторые вычисления, которые включают рекурсию, и те могут никогда не завершиться. Мы не можем просто запретить незавершающиется функции в Haskell потому, что различить, завершается функция, или нет - знаменитая проблема остановки - неразрешима. Вот почему ученые-компьютерщики придумали гениальную идею, или грязный хак, в зависимости от вашей точки зрения, - расширить каждый тип специальным значением, называнным bottom (прим. переводчика: этот термин (bottom) слышится как-то по-дурацки на русском, если кто знает хороший вариант, пожалуйста, предлагайте.) , которое обозначается _|_ или в Unicode ⊥. Это «значение» соответствует незавершающемуся вычислению. Так функция, объявленная как:
f:: Bool -> Bool
может вернуть True, False, или _|_; последнее значит, что функция никогда не завершается.

Интересно, что, как только вы принимаете bottom в систему типов, удобно рассматривать каждую ошибку времени исполнения за bottom, и даже позволить функции возвращать bottom явно. Последнее, как правило, осуществляется с помощью выражения undefined:
f:: Bool -> Bool f x = undefined
Это определение проходит проверку типов потому, что undefined вычисляется в bottom, которое включено во все типы, в том числе и Bool. Можно даже написать:
f:: Bool -> Bool f = undefined
(без x) потому, что bottom еще и член типа Bool -> Bool.

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

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

Зачем нам математическая модель?

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

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

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

Не важно, что программисты никогда формально не доказывают корректность. Мы всегда «думаем», что мы пишем правильные программы. Никто не сидит за клавиатурой, говоря: «О, я просто напишу несколько строк кода и посмотрю, что происходит." (прим. переводчика: ах, если бы...) Мы считаем, что код, который мы пишем, будет выполнять определенные действия, которые произведут желаемые результаты. Мы, как правило, очень удивлены, если это не так. Это означает, что мы действительно думаем о программах, которые мы пишем, и мы, как правило, делаем это, запуская интерпретатор в наших головах. Просто, очень трудно уследить за всеми переменными. Компьютеры хороши для исполнения программ, люди - нет! Если бы мы были, нам бы не понадобились компьютеры.

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

Рассмотрим определение функции факториала в Haskell, языке, легко поддающемуся денотационной семантике:
fact n = product
Выражение - это список целых чисел от 1 до n. Функция product умножает все элементы списка. Точно так, как определение факториала, взятое из учебника. Сравните это с C:
int fact(int n) { int i; int result = 1; for (i = 2; i <= n; ++i) result *= i; return result; }
Нужно ли продолжать? (прим. переводчика: автор слегка схитрил, взяв библиотечную функцию в Haskell. На самом деле, хитрить было не нужно, честное описание по определению не сложнее) :
fact 0 = 1 fact n = n * fact (n - 1)
Хорошо, я сразу признаю, что это был дешевый прием! Факториал имеет очевидное математическое определение. Проницательный читатель может спросить: Какова математическая модель для чтения символа с клавиатуры, или отправки пакета по сети? Долгое время это был бы неловкий вопрос, ведущий к довольно запутанным объяснениям. Казалось, денотационная семантика не подходит для значительного числа важных задач, которые необходимы для написания полезных программ, и которые могут быть легко решаемы операционной семантикой. Прорыв произошел из теории категорий. Еугенио Моджи обнаружил, что вычислительные эффекты могут быть преобразованы в монады. Это оказалось важным наблюдением, которое не только дало денотационной семантике новую жизнь и сделало чисто функциональные программы более удобными, но и дало новую информацию о традиционном программировании. Я буду говорить о монадах позже, когда мы разработаем больше категорийных инструментов.

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

Чистые и Грязные функции

То, что мы называем функциями в C++ или любом другом императивном языке, не то же самое, что математики называют функциями. Математическая функция - просто отображение значений в значения.

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

Кроме того, вычисление квадрата числа не должно иметь побочного эффекта, вроде выдачи вкусного ништячка вашей собаке. «Функция», которая это делает, не может быть легко смоделирована математической функцей.

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

Примеры типов

Как только вы решите, что типы - это множества, вы можете придумать некоторые весьма экзотические примеры. Например, какой тип соответствует пустому множеству? Нет, это не void в C++, хотя этот тип называется Void в Haskell. Это тип, который не наполнен ни одним значением. Вы можете определить функцию, которая принимает Void, но вы никогда не сможете ее вызвать. Чтобы ее вызвать, вам придется обеспечить значение типа Void, а его там просто нет. Что касается того, что эта функция может вернуть - не существует никаких ограничений. Она может возвращать любой тип (хотя этого никогда не случится, потому что она не может быть вызвана). Другими словами, это функция, которая полиморфна по возвращаемому типу. Хаскеллеры назвали ее:
absurd:: Void -> a
(прим. переводчика: на С++ такую функцию определить невозможно: в С++ у каждого типа есть хотя бы одно значение.)

(Помните, что a - это переменная типа, которая может быть любым типом.) Это имя не случайно. Существует более глубокая интерпретация типов и функций с точки зрения логики под названием изоморфизм Карри-Говарда. Тип Void представляет неправдивость, а функция absurd - утверждение, что из ложности следует что-нибудь, как в латинской фразе «ex falso sequitur quodlibet.» (прим. переводчика: из ложности следует что угодно.)

Далее идет тип, соответствующий одноэлементному множеству. Это тип, который имеет только одно возможное значение. Это значение просто «есть». Вы могли сразу его не признать, но это void в C++. Подумайте о функциях от и в этот тип. Функция из void всегда может быть вызвана. Если это чистая функция, она всегда будет возвращать один и тот же результат. Вот пример такой функции:
int f44() { return 44; }
Вы можете подумать что эта функция принимает «ничего», но, как мы только что видели, функция, которая принимает «ничего» не может быть вызвана, потому что нет никакого значения, представляющего тип «ничего». Итак, что же эта функция принимает? Концептуально, она принимает фиктивное значение, у которого есть только единственный экземпляр, так что мы можем явно его не указывать в коде. В Haskell, однако, есть символ этого значения: пустая пара скобок (). Таким образом, из за забавного совпадения (или не совпадения?), вызов функции от void выглядит одинаково и в C++ и в Haskell. Кроме того, из-за любви Хаскеля к лаконичности, тот же символ () используется и для типа, конструктора и единственного значения, соответствующего одноэлементному множеству. Вот эта функция в Haskell:
f44:: () -> Integer f44 () = 44
Первая строка обьявляет, что f44 преобразует тип (), названный «единица», в тип Integer. Вторая строка определяет, что f44 с помощью паттерн-матчинга преобразует единственный конструктор для единицы, а именно () в число 44. Вы вызываете эту функцию, предоставляя значение ():
f44 ()
Обратите внимание, что каждая функция от единицы эквивалентна выбору одного элемента из целевого типа (здесь, выбирается Integer 44). На самом деле, вы можете думать о f44, как ином представлении числа 44. Это пример того, как мы можем заменить прямое упоминание элементов множества на функцию (стрелку). Функции из единицы в некий тип А находятся во взаимно-однозначном соответствии с элементами множества A.

А как насчет функций, возвращающих void, или, в Haskell, возвращающих единицу? В C++ такие функции используются для побочных эффектов, но мы знаем, что такие функции - не настоящие, в математическом смысле этого слова. Чистая функция, которая возвращает единицу, ничего не делает: она отбрасывает свой аргумент.

Математически, функция из множества А в одноэлементное множество отображает каждый элемент в единственный элемент этого множества. Для каждого А есть ровно одна такая функция. Вот она для Integer:
fInt:: Integer -> () fInt x = ()
Вы даете ей любое целое число, и она возвращает единицу. Следуя духу лаконичности, Haskell позволяет использовать символ подчеркивания в качестве аргумента, который отбрасывается. Таким образом, не нужно придумывать для него название. Код выше можно переписать в виде:
fInt:: Integer -> () fInt _ = ()
Обратите внимание, что выполнение этой функции не только не зависит от значения, ей переданного, но и от типа аргумента.

Функции, которые могут быть определены одной и той же формулой для любого типа называются параметрически полиморфными. Вы можете реализовать целое семейство таких функций одним уравнением, используя параметр вместо конкретного типа. Как назвать полиморфную функцию из любого типа в единицу? Конечно, мы назовем ее unit:
unit:: a -> () unit _ = ()
В C++ вы бы реализовали ее так:
template void unit(T) {}
(прим. переводчика: дабы помочь компилятору оптимизировать ее в noop, лучше так):
template void unit(T&&) {}
Далее в «типологии типов» набор из двух элементов. В C++ он называется bool, а в Haskell, что не удивительно, Bool. Разница в том, что в C++ bool является встроенным типом, в то время как в Haskell он может быть определен следующим образом:
data Bool = True | False
(Читать это определение стоит так: Bool может быть или True или False.) В принципе, можно было бы описать этот тип и в C++:
enum bool { true, false };
Но C++ перечисление на самом деле целое число. Можно было бы использовать C++11 «class enum», но тогда пришлось бы уточнять значение именем класса: bool::true или bool::false, не говоря уже о необходимости включать соответствующий заголовок в каждом файле, который его использует.

Чистые функции из Bool просто выбирают два значения из целевого типа, одно, соответствующее True и другое - False.

Функции в Bool называются предикатами. Например, библиотека Data.Char в Haskell содержит много предикатов, например IsAlpha или isDigit. В C++ есть похожая библиотека , которая обьявляет, помимо прочего, функции isalpha и isdigit, но они возвращают int, а не булевое значение. Настоящие предикаты определены в и называются ctype::is(alpha, c) и ctype::is(digit, c).

Похожие публикации