Основные принципы программирования функциональное программирование

Оглавление (нажмите, чтобы открыть):

Основные принципы программирования

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

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

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

· частотный принцип основан на выделении в алгоритмах и в обрабатываемых структурах действий и данных по частоте использования. Для действий, которые часто встречаются при работе программного обеспечения, обеспечиваются условия их быстрого выполнения. К данным, к которым происходит частое обращение, обеспечивается наиболее быстрый доступ. «Частые» операции стараются делать более короткими;

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

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

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

· принцип функциональной избыточности учитывает возможность проведения одной и той же работы (функции) различными средствами. Особенно важен учет этого принципа при разработке пользовательского интерфейса для выдачи данных из-за психологических различий в восприятии информации;

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

· общесистемные принципы рекомендуется применять следующие общесистемные принципы:

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

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

· принцип развития предусматривает в программном обеспечении возможность его наращивания и совершенствования компонентов и связей между ними;

· принцип комплексности заключается в том, что программное обеспечение обеспечивает связность обработки информации, как отдельных элементов, так и для всего объема данных в целом на всех стадиях обработки;

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

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

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

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

Основные принципы программирования: функциональное программирование. Элементы функционального программирования

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

Языки программирования, о которых не каждый знает

Я начал программировать еще в детстве, и годам к двадцати пяти мне казалось, что я все знаю и понимаю. Объектно ориентированное программирование стало частью моего мозга, все мыслимые книги о промышленном программировании были прочитаны. Но у меня оставалось такое ощущение, будто я что-то упустил, что-то очень тонкое и необыкновенно важное. Дело в том, что, как и многих в девяностые годы, в школе меня учили программировать на Pascal (о да, слава Turbo Pascal 5.5! — Прим. ред.), потом был C и C++. В университете Fortran и потом Java, как основной инструмент на работе. Я знал Python и еще несколько языков, но все это было не то. А серьезного образования в области Computer Science у меня не было. Однажды во время перелета через Атлантику я не мог заснуть, и мне захотелось что-то почитать. Каким-то волшебным образом у меня под рукой оказалась книга про язык программирования Haskell. Мне кажется, именно тогда я понял истинный смысл выражения «красота требует жертв».

Теперь, когда меня спрашивают, как я выучил Haskell, я так и говорю: в самолете. Этот эпизод изменил мое отношение к программированию вообще. Конечно, после первого знакомства многие вещи казались мне не вполне понятными. Пришлось напрячься и изучить вопрос более тщательно. И знаешь, прошло десять лет, многие функциональные элементы стали частью промышленных языков, лямбда-функции уже есть даже в Java, вывод типов — в С++, сопоставление с образцом — в Scala. Многие думают, что это какой-то прорыв. И в этой серии статей я расскажу тебе про приемы функционального программирования, используя разные языки и их особенности.

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

Поэтому если и есть язык, с которого нужно начинать изучение функционального программирования, так это Lisp. Вообще, это целое семейство языков, куда входит довольно популярный сейчас язык для JVM под названием Clojure . Но в качестве первого функционального языка он не особо подходит. Для этого лучше использовать язык Scheme , который был разработан в MIT и до середины двухтысячных годов служил основным языком для обучения программированию. Хотя сейчас вводный курс с тем же названием, что упомянутая книга, был заменен на курс по Python, она все еще не потеряла своей актуальности.

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

Синтаксис за две минуты

Синтаксис в языке Lisp, хм, слегка спорный. Дело в том, что идея, лежащая в основе синтаксиса, крайне проста и построена на основе так называемых S-выражений . Это префиксная запись, в которой привычное тебе выражение 2 + 3 записывается как (+ 2 3) . Это может показаться странным, но на практике дает некоторые дополнительные возможности. Кстати, (+ 2 10 (* 3.14 2)) тоже работает:). Таким образом, вся программа — это набор списков, в которых используется префиксная нотация. В случае языка Lisp сама программа и абстрактное синтаксическое дерево — «если вы понимаете, о чем я» �� — по сути, ничем не отличаются. Такая запись делает синтаксический анализ программ на Lisp очень простым.
Раз уж мы говорим о языке программирования, то следует сказать о том, как определять функции в этом языке.

Тут нужно сделать небольшое отступление. Существует одна тонкость, значимость которой в современной литературе недооценена. Нужно все-таки разделять функцию в математическом смысле и функцию, как мы ее понимаем в функциональном программировании. Дело в том, что в математике функции являются декларативными объектами, а в программировании они используются для организации процесса вычислений, то есть в каком-то смысле, скорее, представляют собой императивное знание, знание, отвечающее на вопрос «как?». Именно поэтому Абельсон и Сассман в своей книге это очень тщательно разделяют и называют функции в программировании процедурами. В современной литературе по функциональному программированию это не принято. Но я все же настоятельно рекомендую разделять эти два смысла слова «функция» хотя бы у себя в голове.

Самый простой способ определить функцию — это написать следующий код. Начнем с неприлично простого:

(define (sq-roots a b c) (let ((D (- (* b b) (* 4 a c)))) (if ( (add 4 3) => (add 5 2) => (add 6 1) => (add 7 0) => 7

Во втором случае мы будем иметь примерно следующее:

(add-1 3 4) => (succ (add-1 3 3)) => (succ (succ (add-1 3 2))) => (succ (succ (succ (add-1 3 1)))) => (succ (succ (succ (succ (add-1 3 0))))) => (succ (succ (succ (succ 3)))) => (succ (succ (succ 4))) => (succ (succ 5)) => (succ 6) => 7

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

Списки

Один из важнейших элементов функционального программирования, наряду с рекурсией, — списки . Они обеспечивают основу для сложных структур данных. Как и в других функциональных языках, списки являются односвязными по принципу голова — хвост. Для создания списка используется функция cons , а для доступа к голове и хвосту списка — функции car и cdr соответственно. Так, список (list 1 2 3) — это не что иное, как (cons 1 (cons 2 (cons 3 «()))) . Здесь «() — пустой список. Таким образом, типичная функция обработки списка выглядит так:

(define (sum lst) (if (null? lst) 0 (+ (car lst) (sum (cdr lst)))))

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

Функции высших порядков

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

(define (map f lst) (if (null? lst) lst (cons (f (car lst)) (map f (cdr lst)))))

Функция map применяет функцию f к каждому элементу списка. Как бы это странно ни выглядело, но теперь мы можем выразить функцию вычисления длины списка length через sum и map:

(define (length lst) (sum (map (lambda (x) 1) lst)))

Если ты вдруг сейчас решил, что все это как-то слишком просто, то давай подумаем вот над чем: как сделать реализацию списков, используя функции высших порядков?

То есть нужно реализовать функции cons , car и cdr так, чтобы они удовлетворяли следующему соотношению: для любого списка lst верно, что значение (cons (car lst) (cdr lst)) совпадает с lst . Это можно сделать следующим образом:

(define (cons x xs) (lambda (pick) (if (eq? pick 1) x xs))) (define (car f) (f 1)) (define (cdr f) (f 2))

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

Использование quote и метапрограммирование

Одна приятная особенность языка Lisp делает его необыкновенно удобным для написания программ, которые занимаются преобразованием других программ. Дело в том, что программа состоит из списков, а список — это основная структура данных в языке. Существует способ просто «закавычить» текст программы, чтобы она воспринималась как список атомов.

Атомы — это просто символьные выражения, к примеру («hello «world) , что то же самое, что и «(hello world) , или в полной форме (quote (hello world)) . Несмотря на то что в большинстве диалектов Lisp есть строки, иногда можно обходиться quote . Что более важно, с помощью такого подхода можно упростить кодогенерацию и обработку программ.

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

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

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

(define (deriv exp var) (cond ((number? exp) 0) ((variable? exp) (if (same-variable? exp var) 1 0)) ((sum? exp) (make-sum (deriv (addend exp) var) (deriv (augend exp) var))) ((product? exp) (make-sum (make-product (multiplier exp) (deriv (multiplicand exp) var)) (make-product (deriv (multiplier exp) var) (multiplicand exp)))) (else (error «unknown expression type — DERIV» exp))))

Здесь функция deriv представляет собой реализацию алгоритма дифференцирования так, как его проходят в школе. Данная функция требует реализации функций number? , variable? и так далее, которые позволяют понять, какую природу имеет тот или иной элемент выражения. Также нужно реализовать дополнительные функции make-product и make-sum . Здесь используется пока неизвестная нам конструкция cond — это аналог оператора switch в таких языках программирования, как C и Java.

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

(define (variable? x) (symbol? x)) (define (same-variable? v1 v2) (and (variable? v1) (variable? v2) (eq? v1 v2))) (define (make-sum a1 a2) (list «+ a1 a2)) (define (make-product m1 m2) (list «* m1 m2)) (define (sum? x) (and (pair? x) (eq? (car x) «+))) (define (addend s) (cadr s)) (define (augend s) (caddr s)) (define (product? x) (and (pair? x) (eq? (car x) «*))) (define (multiplier p) (cadr p)) (define (multiplicand p) (caddr p))

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

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

(deriv «(+ x 3) «x) => (+ 1 0) (deriv «(* (* x y) (+ x 3)) «x) => (+ (* (* x y) (+ 1 0)) (* (+ (* x 0) (* 1 y)) (+ x 3)))

Для тривиальных случаев (например, умножение на 0) задача упрощения решается довольно легко. Этот вопрос остается читателю. Большинство примеров в этой статье взяты из книги SICP, поэтому в случае возникновения трудностей можно просто обратиться к источнику (книга находится в открытом доступе).

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

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

(define (desugar-define def) (let ((fn-args (cadr def)) (body (caddr def))) (let ((name (car fn-args)) (args (cdr fn-args))) (list «define name (list «lambda args body)))))

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

(desugar-define «(define (succ x) (+ x 1))) => (define succ (lambda (x) (+ x 1)))

Однако это не работает для обычных определений, таких как (define x 5) .
Если мы хотим удалить синтаксический сахар в большой программе, содержащей множество различных определений, то мы должны реализовать дополнительную проверку:

(define (sugared? def) (and (eq? (car def) «define) (list? (cadr def))))

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

(map desugar-define prog)

Заключение

В данной статье я не ставил себе задачу рассказать про Scheme сколь-нибудь подробно. Мне прежде всего хотелось показать несколько интересных особенностей языка и привлечь читателя к изучению функционального программирования. Этот чудесный язык при всей его простоте имеет свое очарование и особенности, которые делают программирование на нем очень увлекательным. Что касается инструмента для работы со Scheme, то сильные духом могут замахнуться на MIT-Scheme , а остальные — пользуйтесь прекрасной учебной средой Dr. Racket . В одной из следующих статей я обязательно расскажу, как написать собственный интерпретатор Scheme.

Рассказываем о принципах функционального программирования: какие у него минусы, и какие языки относятся к функциональным.

Основные концепции

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

Чистые функции

Чистая функция максимально проста. Она должна всегда возвращать один и тот же результат. Посмотрите на эту JavaScript-функцию:

var z = 10; function add(x, y)

function add (x , y ) <

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

Изменяемые данные и побочные эффекты

Вернемся к примеру кода. Если мы добавим в качестве аргумента функции add() , переменную z , которая объявлена выше, наша функция перестанет быть чистой и предсказуемой. Почему? Потому что z объявлена как обычная переменная: она доступна для изменения из любого места программы.

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

Корректный код чистой функции с z должен выглядеть так:

const x = 10; const z = 10; add (x, z); // вернет 20

add (x , z ) ; // вернет 20

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

Еще один пример не функционального кода – классические циклы. Вспомним, как выглядит типичный цикл for в JavaScript:

var acc = 0; for (var i = 1; i end) < return acc; >else < return sumRange(start + 1, end, acc + start); >> console.log(sumRange(1, 10, 0)); // выведет 55

function sumRange (start , end , acc ) <

return sumRange (start + 1 , end , acc + start ) ;

console . log (sumRange (1 , 10 , 0 ) ) ; // выведет 55

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

Композиция функций

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

function addOne(x) < return x + 1; >function timesTwo(x) < return x * 2; >console.log(addOne(timesTwo(3))); // выведет 7 console.log(timesTwo(addOne(3))); // выведет 8

function addOne (x ) <

function timesTwo (x ) <

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

Польза функционального программирования

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

Недостатки функционального программирования

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

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

Функциональное программирование в языках

Так как функциональное программирование – это прежде всего подход к написанию кода, использовать его принципы можно в любом языке. Однако существуют языки, специально заточенные под функциональный подход. Первый и самый известный из них – Lisp. Он появился еще в 1958 году. Его автор – Джон Маккарти, информатик и автор термина «искусственный интеллект». Lisp по сей день популярен в среде проектировщиков ИИ.

Более современные функциональные языки, такие как Elm и Elixir, по данным GitHub и Stack Overflow постепенно и уверенно набирают популярность. Рост популярности JavaScript также привел к повышенному интересу к концепциям функционального программирования для применения в этом языке.

— ООП не сможет больше спасать нас от «Облачных монстров».

Примечание переводчика: Есть два понятия — параллельность (выполнение одновременно, независимо) и конкурентность (выполнение по шагам, поочерёдно, но одновременно несколько задач) и как всегда, мне пришлось поломать голову подобрая правильные термины.

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

Возможно вы уже слышали такое выражение, вроде: “Clojure”, “Scala”, “Erlang” или даже “Java теперь имеет лямбды”. И вы имеете хоть и отдалённое представление о «Функциональном программировании». Если вы участник какого-либа программисткого сообщества, тогда эта тема могла уже вами обсуждаться.

Если вы поищите в Google по словосочетанию «Функциональное программирование», вы не увидите что-то нового. Второй язык из созданных ранее уже охватывает эту тему, он был создан в 50-ых и называется Lisp. Тогда, какого чёрта, эта тема стала популярна только сейчас? Всего то 60 лет спустя?

В начале, компьютеры были очень медленными

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

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

Подобные языки к функциональным, использующим менее строгое понятие. Функция в математике не может изменить вызывающее её окружение и запомнить результаты своей работы, а только предоставляет результат вычисления функции. Программирование с использованием математического понятия функции вызывает некоторые трудности, поэтому функциональные языки, в той или иной степени предоставляют и императивные возможности, что ухудшает дизайн программы (например возможность безболезненных дальнейших изменений). Дополнительное отличие от императивных языков программирования заключается в декларативности описаний функций. Тексты программ на функциональных языках программирования описывают «как решить задачу», но не предписывают последовательность действий для решения. Первым, спроектированным функциональным языком стал Лисп . Вариант данного языка широко используется в системе автоматизированного проектирования AutoLISP

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

  • краткость и простота;
  • строгая типизация;
  • модульность;
  • функции — объекты вычисления;
  • отложенные (ленивые) вычисления.

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

Ссылки

  • https://roman-dushkin.narod.ru/fp.html — Курс лекций по функциональному программированию , читаемый в МИФИ с 2001 года.

Wikimedia Foundation . 2010 .

Смотреть что такое «Функциональный язык программирования» в других словарях:

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

функциональный язык — Язык программирования, в котором действия над данными выражаются в виде обращений к функциональным процедурам. [ГОСТ 19781 90] Тематики обеспеч. систем обраб. информ. программное EN functional language … Справочник технического переводчика

Ruby Семантика: мультипарадигмальный Тип исполнения: интерпретатор Появился в: 1995 г. Автор(ы): Юкихиро Мацумото Последняя версия: 1.9.1 … Википедия

Функциональный язык — 37. Функциональный язык Functional language Язык программирования, в котором действия над данными выражаются в виде обращений к функциональным процедурам Источник: ГОСТ 19781 90: Обеспечение систем обработки информации программное. Термины и… … Словарь-справочник терминов нормативно-технической документации

Erlang Файл:Erlang logo.png Семантика: мультипарадигмальный: конкурентное, функциональное программирование Появился в: 1987 г. Автор(ы): Типизация данных: строгая, динамическая Основные реализации: E … Википедия

Scheme Семантика: функциональный Тип исполнения: интерпретатор или компилятор Появился в: 1970 г. Автор(ы): Гай Стил и Джеральд Сассмен Типизация данных … Википедия

У этого термина существуют и другие значения, см. Миранда. Miranda функциональный язык программирования, созданный в 1985 году Дэвидом Тёрнером в качестве стандартного функционального языка. Имеет строгую полиморфную систему типов,… … Википедия

Hope функциональный язык программирования, разработанный в начале 1980 х годов; является предшественником языков Miranda и Haskell. В журнале Byte за август 1985 впервые опубликовано руководство по языку Hope. Пример программы вычисления… … Википедия

У этого термина существуют и другие значения, см. SASL. SASL полностью функциональный язык программирования, разработанный Дэвидом Тёрнером в Сент Эндрюсском университете в 1972 году, на базе аппликативного подмножества ISWIM. В 1976 году… … Википедия

У этого термина существуют и другие значения, см. Scala. Scala Класс языка: Мультипарадигмальный: функ … Википедия

Книги

  • Программирование в Clojure. Практика применения Lisp в мире Java , Эмерик Ч., Карпер Б., Гранд К.. Почему многие выбирают Clojure? Это — функциональный язык программирования, не только позволяющий пользоваться Java-библиотеками, службами и другими ресурсами JVM, но и соперничающий с…

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

Энциклопедичный YouTube

Что такое функциональное программирование

Математика и константы / Введение в программирование, урок 4 (JavaScript ES6)

Реактивное программирование и современные веб-интерфейсы

Александр Чирцов о математике в физике

Анна Андреева. Решение олимпиадных задач по математике

Субтитры

Языки функционального программирования

Ещё не полностью функциональные изначальные версии и Лиспа , и APL внесли особый вклад в создание и развитие функционального программирования. Более поздние версии Lisp, такие как Scheme , а также различные варианты APL поддерживали все свойства и концепции функционального языка .

Как правило, интерес к функциональным языкам программирования, особенно чисто функциональным, был скорее научный, нежели коммерческий. Однако, такие примечательные языки как Erlang , OCaml , Haskell , Scheme (после 1986) а также специфические (статистика), Wolfram (символьная математика), и (финансовый анализ), и XSLT (XML) находили применение в индустрии коммерческого программирования. Такие широко распространенные декларативные языки как SQL и Lex /Yacc содержат некоторые элементы функционального программирования, например, они остерегаются использовать переменные. Языки работы с электронными таблицами также можно рассматривать как функциональные, потому что в ячейках электронных таблиц задаётся массив функций, как правило зависящих лишь от других ячеек, а при желании смоделировать переменные приходится прибегать к возможностям императивного языка макросов.

История

Первым функциональным языком был Лисп , созданный Джоном Маккарти в период его работы в в конце пятидесятых и реализованный, первоначально, для IBM 700/7000 (англ.) русск. . В Лиспе впервые введено множество понятий функционального языка, хотя при этом в языке применяется не только парадигма функционального программирования . Дальнейшим развитием Лиспа стали такие языки как Scheme и Dylan .

Концепции

Некоторые концепции и парадигмы специфичны для функционального программирования и в основном чужды императивному программированию (включая объектно-ориентированное программирование). Тем не менее, языки программирования обычно представляют собой гибрид нескольких парадигм программирования, поэтому «большей частью императивные» языки программирования могут использовать какие-либо из этих концепций. [ ]

Функции высших порядков

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

Функции высших порядков позволяют использовать карринг — преобразование функции от пары аргументов в функцию, берущую свои аргументы по одному. Это преобразование получило своё название в честь Х. Карри .

Чистые функции

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

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

Хотя большинство компиляторов императивных языков программирования распознают чистые функции и удаляют общие подвыражения для вызовов чистых функций, они не могут делать это всегда для предварительно скомпилированных библиотек, которые, как правило, не предоставляют эту информацию. Некоторые компиляторы, такие как gcc , в целях оптимизации предоставляют программисту ключевые слова для обозначения чистых функций . Fortran 95 позволяет обозначать функции как «pure» (чистые) .

Рекурсия

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

Подход к вычислению аргументов

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

print (len ([ 2 + 1 , 3 * 2 , 1 / 0 , 5 — 4 ]))

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

Как правило, нестрогий подход реализуется в виде редукции графа. Нестрогое вычисление используется по умолчанию в нескольких чисто функциональных языках, в том числе Miranda , Clean и Haskell . [ ]

ФП в нефункциональных языках

Принципиально нет препятствий для написания программ в функциональном стиле на языках, которые традиционно не считаются функциональными, точно так же, как программы в объектно-ориентированном стиле можно писать на структурных языках. Некоторые императивные языки поддерживают типичные для функциональных языков конструкции, такие как функции высшего порядка и списковые включения (list comprehensions), что облегчает использование функционального стиля в этих языках. Примером может быть функциональное программирование на Python. Другим примером является язык Ruby , который имеет возможность создания как lambda-объектов, так и возможность организации анонимных функций высшего порядка через блок с помощью конструкции yield.

Стили программирования

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

# императивный стиль target = # создать пустой список for item in source_list : # для каждого элемента исходного списка trans1 = G (item ) # применить функцию G() trans2 = F (trans1 ) # применить функцию F() target . append (trans2 ) # добавить преобразованный элемент в список

Функциональная версия выглядит по-другому:

# функциональный стиль # языки ФП часто имеют встроенную функцию compose() compose2 = lambda A , B : lambda x : A (B (x )) target = map (compose2 (F , G ), source_list )

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

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

  • Рефал (для этой категории, представленной единственным языком, нет общепринятого названия);
  • Аппликативные (Лисп , , Tcl , Rebol);
  • Комбинаторные (APL / / , FP / FL );
  • Бесточечные (чистые конкатенативные) (Joy , Cat , Factor , подмножество PostScript).

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

Особенности

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

Сильные стороны

Повышение надёжности кода

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

Удобство организации модульного тестирования

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

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

Возможности оптимизации при компиляции

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

Возможности параллелизма

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

Недостатки

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

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

Функциональное программирование на PHP

Скачать исходные файлы

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

Мастер Йода рекомендует:  Что такое линкбилдинг

Scala, Haskell и другие функциональные языки программирования процветают, а другие, более консервативные языки, такие как Java, начали перенимать некоторые парадигмы функционального программирования (смотрите замыкания в версии Java7 и отложенные вычисления для списков в Java8).

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

Парадигмы программирования

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

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

Каждая парадигма программирования отнимает часть нашей свободы:

  • Модульное программирование лишает нас неограниченного размера программы;
  • Структурное и процедурное программирование лишает оператора « go-to » и ограничивает программиста путем использования последовательного исполнения, ветвлений и циклов;
  • Объектно-ориентированное программирование отнимает указатели на функции;
  • Функциональное программирование лишает нас возможности использовать присвоение и изменяемые состояния.

Принципы функционального программирования

В функциональном программировании у вас нет данных, представленных с помощью переменных.

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

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

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

Данные не присваиваются переменным. Некоторые величины могут быть определены и присвоены, однако в большинстве случаев они являются функциями, присвоенными « переменным ». Я поместил « переменные » в кавычки, потому что в функциональном программировании они не изменяются.

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

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

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

И, наконец, так как мы не присваиваем значения переменным, циклы while и for нетипичны для функционального программирования и заменяются на рекурсию.

Покажите мне код!

Хватит разговоров и философии для одного урока. Давайте программировать!

Создайте новый PHP проект в своей любимой среде разработки или редакторе кода. Создайте в нем папку « Tests ». Затем создайте два файла: в папке проекта – файл FunSets.php и в папке « Tests» – FunSetsTest.php . Мы напишем приложение, с тестами, которое будет представлять концепцию множеств.

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

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

Наши ограничения в программировании

Итак, давайте начнем! Хотя нет, подождите. Зачем? Для того чтобы проявить уважение к концептам функционального программирования мы должны применить следующие ограничения к нашему коду:

  • Никаких присвоений. Нам не разрешается присваивать значения переменным. Однако можно присваивать функции переменным;
  • Никаких изменяемых состояний. Нам не разрешается, в случае присвоения, изменять значение того, что присвоено. Также не разрешается изменять значение любой переменной, значение которой установлено как параметр для текущей функции. Таким образом, нельзя изменять параметры;
  • Нельзя использовать циклы while и for . Нам не разрешено пользоваться PHP командами « while » и « for ». Зато мы можем определить наш собственный метод, чтобы организовать цикл по элементам множества и назвать его foreach , или for , или while .

Ограничения не применяются к тестам. Учитывая природу PHPUnit , мы будем использовать для тестирования объектно-ориентированный код на PHP. Кроме того, чтобы лучше разместить наши тесты, мы упакуем весь рабочий код в отдельный класс.

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

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

Определяющая множество функция – это метод contains .

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

Теперь идея немножко проясняется. Функция contains имеет два параметра:

  • $set – представляет множество, определенное как функция;
  • $elem – представляет собой элемент, определенный как значение.

В данном контексте все, что метод contains должен делать – это вызвать функцию $set с параметром $elem .

Давайте упакуем все в тест.

И поместим наш рабочий код в класс в файле FunSets.php :

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

Множество с единственным элементом

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

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

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

Вот это да! Просто очуметь.

Итак, функция singletonSet принимает в качестве параметра элемент $elem . Затем она возвращает другую функцию, которая имеет входной параметр $otherElem и сравнивает $elem с $otherElem .

Как это все работает? Во-первых, вот эта строка:

Затем происходит вызов функции contains($singleton, 1) . Которая, в свою очередь, вызывает $singleton . Таким образом, код сводится к:

Который фактически выполняет код со значением $otherElem равным единице.

Что, конечно же, возвращает значение true, и наш тест успешно пройден.

Вы уже улыбаетесь? Вы чувствуете, как ваш мозг начинает закипать? Я точно это чувствовал, когда сначала написал этот пример на языке Scala, и почувствовал снова, когда сделал это на PHP. Я думаю, это нечто необычное.

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

У нас нет переменных, содержащих единицу или имеющих состояние единицы. Нет состояний, нет присвоений, нет изменчивости, нет циклов. Тут мы на верном пути.

Объединение множеств

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

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

Нам нужна функция с именем union , которая принимает два параметра, оба являются множеством. Помните, что множества это просто функции для нас, так что наша функция union будет принимать в качестве параметров две функции.

Затем мы хотим иметь возможность проверять при помощи функции contains содержит ли объединение элемент или нет. Таким образом, наша функция union должна возвращать другую функцию, которую можно использовать для вызова contains .

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

Пересечение и разница

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

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

Фильтр на множестве

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

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

Мы создаем множество с тремя элементами: 1, 2, 3. И помещаем его в переменную $u123 , чтобы легче распознавать его в наших тестах. Затем мы определяем функцию, к которой хотим применить тест, и помещаем ее в $condition .

Наконец, мы вызываем функцию фильтрации для нашего множества $u123 вместе с $condition и помещаем получившееся множество в $filteredSet . Затем мы запускаем проверку при помощи contains , чтобы убедиться, действительно ли множество выглядит так, как мы хотели.

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

Пожалуйста, готово! Мы реализовали фильтрацию с помощью всего трех строк кода. Точнее, если условие выполняется для рассматриваемого элемента, то мы запускаем функцию contains на множестве для этого элемента. Если не выполняется – просто возвращаем false . Вот и все.

Цикл по элементам

Следующий шаг – это создание различных функций циклов. Самая первая – forall() – будет принимать в качестве параметров $set и $condition и возвращать значение true , если $condition применимо ко всем элементам $set . Это приводит к следующему тесту:

Мы выделяем создание $u123 из прошлого теста в частный метод. Затем мы определяем три разных условия: больше, чем ноль; больше, чем один; больше чем два. Так как наше множество содержит числа один, два и три, то только условие « больше, чем ноль » должно вернуть значение true , а остальные – false . В действительности, мы можем выполнить тест при помощи другого рекурсивного метода, используемого для перебора всех элементов.

Начнем с определения некоторых ограничений для нашего множества. Значения элементов должны быть в диапазоне от -1000 до +1000. Это разумное ограничение накладывается для того, чтобы сохранить пример достаточно простым. Функция forall вызывает private -метод forallItertor с нужными параметрами, чтобы рекурсивно решить, удовлетворяют ли все элементы условию.

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

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

Единственная разница в том, что мы возвращаем значение false , если вышли за границы, и используем логическое « Или » вместо « И » во втором операторе if .

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

Отображение обозначает, что мы применяем действие ко всем элементам множества. Для отображения нам не нужна помощь итератора, мы можем повторно воспользоваться exists() и вернуть те элементы exists() , которые удовлетворяют результату $action , примененного к $element . Это, возможно, не совсем очевидно на первый взгляд, поэтому давайте посмотрим, как все происходит.

  • Мы передаем множество <1, 2>и действие $element * 2 (удвоение) в функцию map;
  • Она вернет функцию, что очевидно, которая имеет в качестве параметра элемент и использует множество и действие с уровня выше;
  • Эта функция вызовет функцию exists со множеством < 1, 2 >и условной функцией $currentElement, равной $elem * 2;
  • Функция exists() переберет все элементы от -1000 до +1000 (границы нашего множества). Когда она найдет элемент, равный удвоенному значению от того, что пришло из функции contains (значение $currentElement), то функция вернет true;
  • Другими словами, последнее сравнение вернет значение true для вызова функции contains со значением два, если текущее значение, умноженное на два, равно двум.

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

Практический пример

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

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

Сейчас этот код может показаться вполне нормальным, но в нем есть огромная проблема. 80% метода authenticate() пользуется информацией из AdminModules. А это создает очень сильную зависимость.

Было бы намного разумнее взять эти три вызова и создать отдельный метод с использованием AdminModules.

Таким образом, переместив генерирование в AdminModules, нам удалось свести три зависимости всего к одной. Public-интерфейс AdminModules также был упрощен с трех до одного метода. Однако мы еще не у цели – плагин AuthPlugin до сих пор напрямую зависит от класса AdminModules.

Объектно-ориентированный подход

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

AuthPlugin получил конструктор. Конструктор принимает параметр типа ApplicationModule, интерфейс, и вызывает функцию getPermissions() для этого внедренного объекта.

Интерфейс ApplicationModule определяет единственный public-метод getPermissions() , с именем пользователя в качестве параметра.

И наконец, в классе AdminModules необходимо реализовать интерфейс ApplicationModule.

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

Функциональный подход

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

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

Интерфейс Authentication просто определяет два метода.

И наконец, AuthPlugin реализует Authentication и определяет входную функцию в private атрибуте класса. Теперь функция authentication() стала туповатой. Она просто вызывает функцию и затем устанавливает возвращаемое значение. Она полностью изолирована от входных объектов.

Если мы посмотрим на схему, то увидим два важных изменения:

  • Вместо AdminModule, AuthPlugin реализует интерфейс;
  • AuthPlugin передает с помощью обратного вызова модуль AdminModule или любой другой модуль, переданный в функцию permissionsFunction.

Какой подход использовать?

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

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

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

Данная публикация представляет собой перевод статьи « Functional Programming in PHP » , подготовленной дружной командой проекта Интернет-технологии.ру

Функциональное программирование: структура и интерпретация. Часть I

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

Языки программирования, о которых не каждый знает

Я начал программировать еще в детстве, и годам к двадцати пяти мне казалось, что я все знаю и понимаю. Объектно ориентированное программирование стало частью моего мозга, все мыслимые книги о промышленном программировании были прочитаны. Но у меня оставалось такое ощущение, будто я что-то упустил, что-то очень тонкое и необыкновенно важное. Дело в том, что, как и многих в девяностые годы, в школе меня учили программировать на Pascal (о да, слава Turbo Pascal 5.5! — Прим. ред.), потом был C и C++. В университете Fortran и потом Java, как основной инструмент на работе. Я знал Python и еще несколько языков, но все это было не то. А серьезного образования в области Computer Science у меня не было. Однажды во время перелета через Атлантику я не мог заснуть, и мне захотелось что-то почитать. Каким-то волшебным образом у меня под рукой оказалась книга про язык программирования Haskell. Мне кажется, именно тогда я понял истинный смысл выражения «красота требует жертв».

Теперь, когда меня спрашивают, как я выучил Haskell, я так и говорю: в самолете. Этот эпизод изменил мое отношение к программированию вообще. Конечно, после первого знакомства многие вещи казались мне не вполне понятными. Пришлось напрячься и изучить вопрос более тщательно. И знаешь, прошло десять лет, многие функциональные элементы стали частью промышленных языков, лямбда-функции уже есть даже в Java, вывод типов — в С++, сопоставление с образцом — в Scala. Многие думают, что это какой-то прорыв. И в этой серии статей я расскажу тебе про приемы функционального программирования, используя разные языки и их особенности.

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

Поэтому если и есть язык, с которого нужно начинать изучение функционального программирования, так это Lisp. Вообще, это целое семейство языков, куда входит довольно популярный сейчас язык для JVM под названием Clojure. Но в качестве первого функционального языка он не особо подходит. Для этого лучше использовать язык Scheme, который был разработан в MIT и до середины двухтысячных годов служил основным языком для обучения программированию. Хотя сейчас вводный курс с тем же названием, что упомянутая книга, был заменен на курс по Python, она все еще не потеряла своей актуальности.

Обложка знаменитой книги «Структура и интерпретация компьютерных программ»

Хакер #196. Все о Docker

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

Синтаксис за две минуты

Синтаксис в языке Lisp, хм, слегка спорный. Дело в том, что идея, лежащая в основе синтаксиса, крайне проста и построена на основе так называемых S-выражений. Это префиксная запись, в которой привычное тебе выражение 2 + 3 записывается как (+ 2 3) . Это может показаться странным, но на практике дает некоторые дополнительные возможности. Кстати, (+ 2 10 (* 3.14 2)) тоже работает :). Таким образом, вся программа — это набор списков, в которых используется префиксная нотация. В случае языка Lisp сама программа и абстрактное синтаксическое дерево — «если вы понимаете, о чем я» �� — по сути, ничем не отличаются. Такая запись делает синтаксический анализ программ на Lisp очень простым.
Раз уж мы говорим о языке программирования, то следует сказать о том, как определять функции в этом языке.

Тут нужно сделать небольшое отступление. Существует одна тонкость, значимость которой в современной литературе недооценена. Нужно все-таки разделять функцию в математическом смысле и функцию, как мы ее понимаем в функциональном программировании. Дело в том, что в математике функции являются декларативными объектами, а в программировании они используются для организации процесса вычислений, то есть в каком-то смысле, скорее, представляют собой императивное знание, знание, отвечающее на вопрос «как?». Именно поэтому Абельсон и Сассман в своей книге это очень тщательно разделяют и называют функции в программировании процедурами. В современной литературе по функциональному программированию это не принято. Но я все же настоятельно рекомендую разделять эти два смысла слова «функция» хотя бы у себя в голове.

Самый простой способ определить функцию — это написать следующий код. Начнем с неприлично простого:

Да, это именно то, что ты подумал, — решение квадратного уравнения на Scheme. Но этого более чем достаточно, чтобы разглядеть все особенности синтаксиса. Здесь sq-roots — это название функции от трех формальных параметров.

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

Теперь о выражениях. В нашей функции sq-roots мы использовали конструкцию if . Вот здесь-то и начинается функциональное программирование.

Дело в том, что в отличие от императивных языков, таких как C, в функциональных языках if — это выражение, а не оператор. На практике это означает, что у него не может отсутствовать ветка else. Потому что выражение всегда должно иметь значение.

Нельзя рассказать про синтаксис, не поговорив о синтаксическом сахаре. В языках программирования синтаксическим сахаром называют конструкции, которые не являются необходимыми, а лишь облегчают чтение и переиспользование кода. Для начала приведем классический пример из языка C. Многие знают, что массивы не обязательное средство выражения, так как есть указатели. Да, действительно, массивы реализованы через указатели, и a[i] для языка C — это то же самое, что и *(a + i) . Данный пример вообще довольно необычный, с ним связан забавный эффект: так как операция сложения остается коммутативной в случае указателей, то последнее выражение — это то же самое, что и *(i + a) , а это может быть получено при удалении синтаксического сахара из выражения i[a] ! Операция удаления синтаксического сахара в английском языке называется специальным словом desugaring.

Возвращаясь к языку Scheme, следует привести важный пример синтаксического сахара. Для определения переменных, как и в случае функций, используется ключевое слово (в Lisp и Scheme это называется специальной формой) define . К примеру, (define pi 3.14159) определяет переменную pi . Вообще говоря, точно так же можно и определять функции:

это то же самое, что и

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

Если посмотреть на let с точки зрения лямбда-выражения, то легко заметить следующее соответствие:

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

Функциональное программирование

Функциональные языки бывают чистыми и нечистыми. Чистые функциональные языки сравнительно редки, к ним относятся в первую очередь Haskell и Clean. В чистых языках нет побочных эффектов. На практике это означает отсутствие присваивания и ввода-вывода в том виде, к которому мы привыкли. Это создает ряд трудностей, хотя в уже упомянутых языках это решено довольно хитроумно, и на этих языках пишут код с большим количеством ввода-вывода. Языки типа Lisp, OCaml или Scala допускают функции с побочными эффектами, и в этом смысле данные языки зачастую более практичны.

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

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

Пускай у нас есть две функции — succ и prev . Первая возвращает число, на 1 большее, чем аргумент, а вторая — на 1 меньшее. Теперь попробуем определить операцию сложения, причем двумя способами:

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

Во втором случае мы будем иметь примерно следующее:

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

Списки

Один из важнейших элементов функционального программирования, наряду с рекурсией, — списки. Они обеспечивают основу для сложных структур данных. Как и в других функциональных языках, списки являются односвязными по принципу голова — хвост. Для создания списка используется функция cons , а для доступа к голове и хвосту списка — функции car и cdr соответственно. Так, список (list 1 2 3) — это не что иное, как (cons 1 (cons 2 (cons 3 ‘()))) . Здесь ‘() — пустой список. Таким образом, типичная функция обработки списка выглядит так:

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

Функции высших порядков

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

Функция map применяет функцию f к каждому элементу списка. Как бы это странно ни выглядело, но теперь мы можем выразить функцию вычисления длины списка length через sum и map :

Если ты вдруг сейчас решил, что все это как-то слишком просто, то давай подумаем вот над чем: как сделать реализацию списков, используя функции высших порядков?

То есть нужно реализовать функции cons , car и cdr так, чтобы они удовлетворяли следующему соотношению: для любого списка lst верно, что значение (cons (car lst) (cdr lst)) совпадает с lst . Это можно сделать следующим образом:

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

Использование quote и метапрограммирование

Одна приятная особенность языка Lisp делает его необыкновенно удобным для написания программ, которые занимаются преобразованием других программ. Дело в том, что программа состоит из списков, а список — это основная структура данных в языке. Существует способ просто «закавычить» текст программы, чтобы она воспринималась как список атомов.

Атомы — это просто символьные выражения, к примеру (‘hello ‘world) , что то же самое, что и ‘(hello world) , или в полной форме (quote (hello world)) . Несмотря на то что в большинстве диалектов Lisp есть строки, иногда можно обходиться quote . Что более важно, с помощью такого подхода можно упростить кодогенерацию и обработку программ.

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

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

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

Здесь функция deriv представляет собой реализацию алгоритма дифференцирования так, как его проходят в школе. Данная функция требует реализации функций number? , variable? и так далее, которые позволяют понять, какую природу имеет тот или иной элемент выражения. Также нужно реализовать дополнительные функции make-product и make-sum . Здесь используется пока неизвестная нам конструкция cond — это аналог оператора switch в таких языках программирования, как C и Java.

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

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

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

Для тривиальных случаев (например, умножение на 0) задача упрощения решается довольно легко. Этот вопрос остается читателю. Большинство примеров в этой статье взяты из книги SICP, поэтому в случае возникновения трудностей можно просто обратиться к источнику (книга находится в открытом доступе).

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

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

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

Однако это не работает для обычных определений, таких как (define x 5) .
Если мы хотим удалить синтаксический сахар в большой программе, содержащей множество различных определений, то мы должны реализовать дополнительную проверку:

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

Заключение

В данной статье я не ставил себе задачу рассказать про Scheme сколь-нибудь подробно. Мне прежде всего хотелось показать несколько интересных особенностей языка и привлечь читателя к изучению функционального программирования. Этот чудесный язык при всей его простоте имеет свое очарование и особенности, которые делают программирование на нем очень увлекательным. Что касается инструмента для работы со Scheme, то сильные духом могут замахнуться на MIT-Scheme, а остальные — пользуйтесь прекрасной учебной средой Dr. Racket. В одной из следующих статей я обязательно расскажу, как написать собственный интерпретатор Scheme.

Итак, вы хотите научиться функциональному программированию (Часть 1)

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

Обучение вождению

Когда мы только учились водить машину, мы старались изо всех сил. Конечно, это выглядело легко, когда мы смотрели, как водят другие люди. Но на деле всё оказывалось сложнее.

Мы упражнялись на машине родителей и не выезжали на автострады, пока в совершенстве не осваивали улицы родного района.

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

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

Вспомните, на что был похож первый раз за рулём другой машины? Было ли это похоже на самый первый раз за рулём машины вообще? Даже не близко. В первый раз всё было таким незнакомым. Конечно, мы сидели до этого в машине, но только в роли пассажира. На этот раз мы оказывались в сидении водителя. Один на один со всеми рычагами, кнопками и педалями.

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

После всего этого мы вели свою машину как по маслу. Но почему в этот раз всё было так просто по сравнению с первым разом?

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

Может, несколько вещей были реализованы как-то иначе и, может быть, они имели какие-то дополнительные функциональные возможности, но мы и так не использовали их во всём нашем водительском опыте. Рано или поздно мы изучали всё новые примочки. Как минимум, те, что нам реально требовались.

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

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

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

Ваш первый космический корабль

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

Если вы собираетесь летать на таком аппарате, то вряд ли будете надеяться, что навыки вождения на дороге как-то особенно вам помогут. Вы начнёте всё с нуля ( Мы же программисты, чёрт возьми. Мы начинаем считать с нуля.).

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

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

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

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

Забудьте всё, что вы знаете

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

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

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

Вспомните, как вы, чтобы выехать с проезжей части, давали задний ход на машине. Но на космическом корабле нет механизма реверса. Теперь вы должны подумать: “ЧТО? НЕТ ЗАДНЕГО ХОДА? КАК Я ДОЛЖЕН ВОДИТЬ БЕЗ ЗАДНЕГО ХОДА?”.

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

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

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

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

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

Самое главное — это то, чтобы вы поняли.

Чистота

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

Чистые функции — очень простые. Они всего лишь производят операция над входными данными.

Вот пример чистой функции:

Заметьте, что функция add не прикасается к переменной z . Она не читает её значения и ничего не пишет в неё. Функция читает только x и y , свои входные данные, и возвращает результат их суммы.

Это и есть чистая функция. Если функция add имеет доступ к переменной z , она больше не может быть чистой.

Это пример другой чистой функции:

Если функция justTen чистая, она может возвращать только значение-константу. Почему?

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

Пока функции, не принимающие параметров, не работают, они не очень полезны. Было бы лучше объявить justTen просто как константу.

Более полезные чистые функции принимают хотя бы один параметр.

Взгляните на этот пример:

Посмотрите, эта функция ничего не возвращает. Она складывает x и y , записывает результат в переменную z , но не возвращает её.

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

Все полезные чистые функции должны возвращать что-нибудь.

Давайте рассмотрим пример с первой функцией add ещё раз:

Обратите внимание, что add(1, 2) в результате всегда даёт 3 . Конечно, сюрприз не большой, но это потому что функция чистая. Если бы функция add брала значение откуда-то снаружи, вы бы никогда не могли наверняка предсказать её поведение.

Чистая функция всегда возвращает одинаковые значения для одинаковых входных данных .

Поскольку чистые функции не могут изменять внешние переменные, все эти функции являются нечистыми:

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

Чистые функции не имеют побочных эффектов.

В таких языках императивного программирования как JavaScript, Java и C# побочные эффекты везде. Это делает отладку проблематичной, потому что в коде Вашей программы переменная может быть изменена где угодно. В общем, если у Вас баг из-за переменой, принявшей неверное значение в неподходящее время, где Вы будете искать ошибку? Везде? Так дело не пойдёт.

На этом месте, вы, вероятно, думаете: “КАК, ЧЁРТ ПОБЕРИ, Я СДЕЛАЮ ХОТЬ ЧТО-НИБУДЬ ОДНИМИ ТОЛЬКО ЧИСТЫМИ ФУНКЦИЯМИ?”.

В функциональном программировании вы не пишите только чистые функции.

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

Неизменяемость

Вы помните, когда впервые увидели следующий код:

И тот, кто учил вас программированию, говорил забыть изученное на уроках математики. Ведь в математике x никогда не мог равняться x + 1 .

Но в императивном программировании данный код означает «взять текущее значение x , прибавить к нему 1 , положить результат обратно в x ».

Что ж, в функциональном программировании выражение x = x + 1 недопустимо. Так что Вам надо вспомнить то, что вы забыли из математики. Если так можно выразиться.

В функциональном программировании нет переменных.

Сохранённые значения всё ещё называются переменными по историческим причинам, но они являются константами, то есть x, однажды приняв какое-либо значение, сохраняет его на всю жизнь.

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

Вот пример переменной-константы в Elm — чистом языке функционального программирования для web-разработки:

Если вы не знакомы с синтаксисом семейства языков программирования ML, позвольте мне объяснить. addOneToSum – это функция, принимающая 2 параметра: y и z .

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

Внутри блока in вычисления могут включать значения, объявленные в блоке let , а именно: x . Возвращается результат вычисления x + y + z или, в точности, возвращается 1 + y + z , так как x = 1 .

И снова я могу услышать, как вы вопрошаете: “КАК, ЧЕРТ ПОБЕРИ, Я ДОЛЖЕН СДЕЛАТЬ ХОТЬ ЧТО-НИБУДЬ БЕЗ ПЕРЕМЕННЫХ?!”.

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

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

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

Да, кстати, и всё это без циклов.

“СНАЧАЛА БЕЗ ПЕРЕМЕННЫХ, А ТЕПЕРЬ ЕЩЁ И БЕЗ ЦИКЛОВ? Я ТЕБЯ НЕНАВИЖУ. ”

Попридержите коней. Это не значит, что мы не можем использовать циклы, просто здесь нет таких характерных операторов как for , while , do , repeat и так далее.

Функциональное программирование использует рекурсию для выполнения цикла.

Вот два примера реализации цикла в JavaScript.

Обратите внимание, как рекурсия в функциональном подходе осуществляет то же самое, что и оператор цикла for , вызывая саму себя с новым параметром запуска (start + 1) и с новым счетчиком (acc + start) . Она не изменяет старых значений. Вместо этого она использует новые значения, высчитанные из старых.

К сожалению, такие примеры не очевидны в JavaScript (даже если вы потратили некоторое время на их изучение) по двум причинам. Во-первых, синтаксис JavaScript засорён, а во-вторых, вы, вероятно, не привыкли думать рекурсивно.

Пример на языке Elm читать и, следовательно, понимать легче:

Так этот код выполняется:

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

Я не объясняю здесь преимущества использования парадигмы неизменяемости, но вы можете посмотреть параграф под названием Global Mutable State в статье Why Programmers Need Limits, если хотите изучить эту тему.

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

Также, если программа многопоточная, исполнение никакого потока не разрушит ваши планы. Поскольку значение — константа и если поток захочет изменить его, ему придётся создать новое значение из старого.

Ещё в середине девяностых я написал игровой движок для Creator Crunch и самый большой источник ошибок был связан с вопросом многопоточности. Я хотел бы знать про неизменяемость в то время. Но тогда меня больше волновала разница между двух и четырёх скоростными приводами CD-ROM при игре.

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

Мой мозг.

Пока что достаточно.

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

Основы функционального программирования: Учебное пособие

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

Целью курса является изучение методов функционального программирования. В курсе рассматриваются основные идеи и подходы функционального программирования, а также методы реализации функциональных языков. Данная публикация входит в состав «Библиотеки учебных курсов», формирование которой ведется в рамках программы академического сотрудничества MSDN Academic Alliance (MSDN AA).

Заметки о функциональном программировании. Основы.

2015-01-11 00:00:00 +0000

Всем привет. Последнее время функциональное программирование (далее ФП) стремительно входит в жизнь рядового разработчика. Последние версии даже таких императивных языков как C++ и Java включают механизмы работы с анонимными функциями. Умные люди говорят, что теперь на Java можно писать в функциональном стиле. К сожалению практика показывает, что понимание функционального стиля часто сводится к написанию императивного кода, но с анонимными процедурами. В итоге, с таким “функциональным стилем”, есть риск резко опуститься на дно говнокода.

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

Кроме текста будут примеры на Scala. Пока планирую, что статьи будут моим вольным изложением замечательного курса с coursera.org Принципы функционального программирования в Scala, а что получиться на самом деле — покажет время. Надеюсь, будет интересно и полезно. И так, начнем.

Парадигмы программирования

Для начала давайте разберемся что такое парадигма программирования и какие парадигмы бывают. Можно, например, дать такое определение:

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

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

  • Императивное программирование
  • Функциональное программирование
  • Логическое программирование

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

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

Старое доброе императивное программирование

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

  • переменные, значения которых можно получить
  • управляющие структуры типа if-then-else , return , циклы
  • присвоение значений переменным

Короче императивные программы можно рассматривать как упорядоченный набор инструкций для компьютеров фон Неймановской архитектуры, чуть более “навороченных” машин Тьюринга.

Вычисление в императивных программах — это изменение состояния переменных. Работа с переменными, каким бы язык не был, похожа по стилю на работу в самом примитивном императивном языке — ассемблере. Переменные — это ячейки памяти, присвоения и чтения — это сохранение и загрузка значений из памяти, управляющие структуры — переходы (jmp).

В 1978 году Джон Бэкус в своей лекции «Can Programming Be Liberated from the Von Neumann Style?» рассказал миру чем же плох такой подход. Дело оказалось в том, что чистое императивное программирование не позволяет выражать абстрактные понятия. Проектировать, например, интеллект беспилотного автомобиля только в терминах присвоения переменных и условных выражений совсем не просто. В качестве решения этой проблемы Джон Бэкус предложил использовать функциональное программирование. В отличии от других попыток решить проблему, таких как процедуры, структуры и ООП, функциональная парадигма имеет под собой математическое основание и проста логически. Давайте попробуем в этом убедиться.

Функциональная парадигма

В основе ФП лежит лямбда-исчисление А. Черча. Глубоко в теорию лезть не будем, а остановимся на практических следствиях: любое вычисление можно представить как последовательность замен значений аргументов функции на сами аргументы (подстановок). Более формально это выглядит это так:

f(x1, . , xn) — функция от n аргументов,

B — выражение, включающее в себя x1, . , xn ,

[v1/x1, . , vn/xn]B — выражение B в котором все термы x1, . , xn заменены на термы v1, . , vn ,

v1, . , vn — произвольные выражения, в том числе и функции.

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

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

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

Как мы видим ФП проще императивного программирования, но благодаря функциям обладает свойством обобщения (по научному обобщение — это операция лямбда-абстракции). Кроме того теперь понятно, что написание “лямбдочки” в коде на Java 8 не делает код функциональным. Для того, чтобы превратить императивный код в функциональный нужно выразить мысли через совсем другие понятия.

На практике существует мало языков, полностью следующих теории. Одна из причин — практическая сложность применения чисто функциональных языков, например отсутствие возможности сохранить часть вычисления под каким-либо именем (по содержанию это переменная, но не по сути). Но основная проблема заключается в продолжении одного из достоинств ФП — отсутствия у вычислений сайдэффектов. Суть проблемы в том, что обеспечить, например, ввод/вывод только в рамках ФП невозможно. Приходиться либо выносить подобные возможности за рамки языка (как в XSLT), придумывать для ввода/вывода еще одну математическую теорию (Haskell) или забыть про пункт об отсутствии сайдэффектов (Scala, Clojure, Smalltalk, Ruby, JavaScript).

Стратегии вычислений

В предыдущем пункте мы рассмотрели модель подстановки. Но из рассмотрения выпала важная часть — приоритет операций. Предположим у нас есть код:

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

В Scala возможно две стратегии вычислений, в зависимости от приоритета операции взятия функции (в науке лямбда-абстракции) и подстановки (бэта-редукции):

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

Рассмотрим вызов по значению в нашем примере:

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

А теперь не зациклилась, такие дела. Теоретически говнокод, зацикливающийся при вызове по значению может дать результат при вызове по имени. Обратное не верно. Как не удивительно на первый взгляд, в Scala по умолчанию используется вызов по значению. Для вызова по имени в объявление типа параметра нужно добавить => , например def f(x: => Int) . Вызов по значению по умолчанию был сделан по соображениям производительности, т.к. авторы посчитали что на Scala будет часто писаться код подобный этому:

Очевидно, что при вызове по имени x был бы рассчитан 3 раза:

По мнению авторов Scala, вызов по значению производительнее и лучше вызова по имени. Хотя авторы Haskell, например, считают по другому и гордятся своим решением. Я же просто приведу пример кода, в котором вызов по имени быстрее:

Хвостовая рекурсия

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

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

Это алгоритм Евклида для нахождения НОД, который уместился в 2 строчки. Давайте посмотрим как он работает.

Теперь все понятно. Давайте теперь рассмотрим пример нахождения факториала.

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

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

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

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

Что дальше

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

Языки функционального программирования

Отложенные вычисления

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

Если функциональный язык не поддерживает отложенные вычисления, то он называется строгим. На самом деле, в таких языках порядок вычисления строго определен. В качестве примера строгих языков можно привести Scheme, Standard ML и Caml.

Языки, использующие отложенные вычисления, называются нестрогими. Haskell — нестрогий язык, так же как, например, Gofer и Miranda. Нестрогие языки зачастую являются чистыми.

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

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

§ Lisp (List processor). Считается первым функциональным языком программирования. Нетипизирован. Содержит массу императивных свойств, однако в общем поощряет именно функциональный стиль программирования. При вычислениях использует вызов-по-значению. Существует объектно-ориентированный диалект языка — CLOS.

§ ISWIM (If you See What I Mean). Функциональный язык-прототип. Разработан Ландиным в 60-х годах XX века для демонстрации того, каким может быть язык функционального программирования. Вместе с языком Ландин разработал и специальную виртуальную машину для исполнения программ на ISWIM’е. Эта виртуальная машина, основанная на вызове-по-значению, получила название SECD-машины. На синтаксисе языка ISWIM базируется синтаксис многих функциональных языков. На синтаксис ISWIM похож синтаксис ML, особенно Caml.

§ Scheme. Диалект Lisp’а, предназначенный для научных исследований в области computer science. При разработке Scheme был сделан упор на элегантность и простоту языка. Благодаря этому язык получился намного меньше, чем Common Lisp.

§ ML (Meta Language). Семейство строгих языков с развитой полиморфной системой типов и параметризуемыми модулями. ML преподается во многих западных университетах (в некоторых даже как первый язык программирования).

§ Standard ML. Один из первых типизированных языков функционального программирования. Содержит некоторые императивные свойства, такие как ссылки на изменяемые значения и поэтому не является чистым. При вычислениях использует вызов-по-значению. Очень интересная реализация модульности. Мощная полиморфная система типов. Последний стандарт языка — Standard ML-97, для которого существует формальные математические определения синтаксиса, а также статической и динамической семантик языка.

§ Caml Light и Objective Caml. Как и Standard ML принадлежит к семейству ML. Objective Caml отличается от Caml Light в основном поддержкой классического объектно-ориентированного программирования. Также как и Standard ML строгий, но имеет некоторую встроенную поддержку отложенных вычислений.

§ Miranda. Разработан Дэвидом Тернером, в качестве стандартного функционального языка, использовавшего отложенные вычисления. Имеет строгую полиморфную систему типов. Как и ML преподаётся во многих университетах. Оказал большое влияние на разработчиков языка Haskell.

§ Haskell. Один из самых распространённых нестрогих языков. Имеет очень развитую систему типизации. Несколько хуже разработана система модулей. Последний стандарт языка — Haskell-98.

§ Gofer (GOod For Equational Reasoning). Упрощённый диалект Haskell’а. Предназначен для обучения функциональному программированию.

§ Clean. Специально предназначен для параллельного и распределённого программирования. По синтаксису напоминает Haskell. Чистый. Использует отложенные вычисления. С компилятором поставляется набор библиотек (I/O libraries), позволяющих программировать графический пользовательский интерфейс под Win32 или MacOS.

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

Функциональный подход породил целое семейство языков, родоначальником которых, как уже отмечалось, стал язык программирования LISP. Позднее, в 70-х годах, был разработан первоначальный вариант языка ML, который впоследствии развился, в частности, в SML, а также ряд других языков. Из них, пожалуй, самым «молодым» является созданный уже совсем недавно, в 90-х годах, язык Haskell.

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

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

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

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

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

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

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

Заметим, что под термином «функция» в математической формализации и программной реализации имеются в виду различные понятия.

Так, математической функцией f с областью определения A и областью значений B называется множество упорядоченных пар

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

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

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

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

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

Рассмотрим эволюцию языков программирования, развивающихся в рамках функционального подхода.

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

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

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

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

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

Семейство языков функционального программирования довольно многочисленно. Об этом свидетельствует не столько значительный список языков, сколько тот факт, что многие языки дали начало целым направлениям в программировании. Напомним, что LISP дал начало целому семейству языков: Scheme, InterLisp, COMMON Lisp и др.

Не стал исключением и язык программирования SML, который был создан в форме языка ML Р. Милнером (Robin Milner) в MIT (Massachusetts Institute of Technology) и первоначально предназначен для логических выводов, в частности, доказательства теорем. Язык отличается строгой типизацией, в нем отсутствует параметрический полиморфизм.

Развитием «классического» ML стали сразу три современных языка с практически одинаковыми возможностями (параметрический полиморфизм, сопоставление с образцом, «ленивые» вычисления). Это язык SML, разработанный в Великобритании и США, CaML, созданный группой французских ученых института INRIA, SML/NJ – диалект SML из New Jersey, а также российская разработка – mosml («московский» диалект ML).

Близость к математической формализации и изначальная функциональная ориентированность послужили причиной следующих преимуществ функционального подхода:

1. простота тестирования и верификации программного кода на основе возможности построения строгого математического доказательства корректности программ;

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

3. безопасная типизация: недопустимые операции с данными исключены;

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

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

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

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

1. интеграция различных языков функционального программирования (при этом максимально используются преимущества каждого из языков, в частности, Scheme предоставляет механизм сопоставления с образцом, а SML – возможность вычисления по мере необходимости);

2. интеграция различных подходов к программированию на основе межъязыковой инфраструктуры Common Language Infrastructure, или CLI (в частности, возможно использование C# для обеспечения преимуществ объектно-ориентированного подхода и SML – функционального, как в настоящем курсе);

3. общая унифицированная система типизации Common Type System, CTS (единообразное и безопасное управление типами данных в программе);

4. многоступенчатая, гибкая система обеспечения безопасности программного кода (в частности, на основе механизма сборок).

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

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

Многие современные языки функционального программирования являются строго типизированными языками (строгая типизация). Строгая типизация обеспечивает большую безопасность. Многие ошибки могут быть исправлены на стадии компиляции, поэтому стадия отладки и общее время разработки программ сокращаются. Строгая типизация позволяет компилятору генерировать более эффективный код и тем самым ускорять выполнение программ. Наряду с этим, существуют функциональные языки с динамической типизацией. Тип данных в таких языках определяется во время выполнения программы (гл. 3). Иногда их называют «безтиповыми». К их, достоинствам следует отнести то, что программы, написанные на этих языках, обладают большей общностью. Недостатком можно считать отнесение многих ошибок на стадию выполнения программы и связанную с этим необходимость применения функций проверки типов и соответствующее сокращение общности программы. Типизированные языки способствуют генерации более «надежного» кода, а типизированные более «общего».

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

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

Common Lisp. Версия Лиспа, которая с 1970 г. может считаться стандартом языка, благодаря поддержке со стороны лаборатории искусственного интеллекта Массачусетского технологического института, безтиповый, энергичный, с большим набором императивных включений, допускающих присваивание, разрушение структур. Практический. Достаточно сказать, что на Лиспе был написан векторный графический редактор Автокад.

Scheme. Диалект Лиспа, предназначенный для научных исследований в области компьютерной науки и обучения функциональному программированию. Благодаря отсутствию императивных включений язык получился намного меньше, чем Common Lisp. Восходит к языку, разработанному Дж. Маккарти в 1962 г. Академический, безтиповый, энергичный, чистый.

Refal. Семейство языков, разработанных В. Ф. Турчиным. Старейший член этого семейства впервые реализован в 1968 году в России. Широко используется и поныне в академических кругах. Содержит элементы логического программирования (сопоставление с образцом). Поэтому язык Refal предлагается в данном учебном пособии в качестве языка для самостоятельного изучения.

Miranda. Строго типизированный, поддерживает типы данных пользователя и полиморфизм. Разработан Тернером на основе более ранних языков SALS и KRC. Имеет ленивую семантику. Без императивных включений.

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

Clean. Диалект Haskell, приспособленный к нуждам практического программирования. Как и Haskell, является ленивым чисто функциональным языком, содержит классы типов. Но Clean также содержит интересные особенности, которые не имеют эквивалента в Haskell. Например, императивные возможности в Clean основаны на уникальных типах, идея которых заимствована из линейной логики (linear logic). Clean содержит механизмы, которые позволяют значительно улучшить эффективность программ. Сред этих механизмов явное подавление отложенных вычислений. Реализация Clean является коммерческим продуктом, но свободная версия доступна для исследовательских и образовательных целей.

ML(Meta Language). Разработан группой программистов во главе с Робертом Милиером в середине 70-х гг. в Эдинбурге (Edinburgh Logic for Computable Functions). Идея языка состояла в создании механизма для построения формальных доказательств в системе логики для вычислимых функций. В 1983 язык был пересмотрен дополнен такими концепциями, как модули. Стал называться стандартный ML. ML – это сильно типизированный язык со статическим контролем типов и аппликативным выполнением программ. Он завоевал большую популярность в исследовательских кругах и в области компьютерного образования.

Не нашли то, что искали? Воспользуйтесь поиском:

Сравнение функционального и Императивное программирование (C#) Functional Programming vs. Imperative Programming (C#)

В этом разделе сравнивается и противопоставляется функциональное программирование с традиционным императивным (процедурным) программированием. This topic compares and contrasts functional programming with more traditional imperative (procedural) programming.

Сравнение функционального и Командное программирование Functional Programming vs. Imperative Programming

Принципы функционального программирования формулировались специально для поддержки чисто функционального подхода к решению проблем. The functional programming paradigm was explicitly created to support a pure functional approach to problem solving. Функциональное программирование является одной из форм декларативного программирования. Functional programming is a form of declarative programming. В отличие от этого, большинство традиционных языков, в том числе такие языки объектно-ориентированного программирования (OOP), как C#, Visual Basic, C++ и Java, разрабатывались в основном для императивного (процедурного) программирования. In contrast, most mainstream languages, including object-oriented programming (OOP) languages such as C#, Visual Basic, C++, and Java, were designed to primarily support imperative (procedural) programming.

При императивном подходе разработчик пишет код, подробно определяющий шаги, которые должен выполнить компьютер для достижения цели. With an imperative approach, a developer writes code that describes in exacting detail the steps that the computer must take to accomplish the goal. Такое программирование иногда называют алгоритмическим. This is sometimes referred to as algorithmic programming. В отличие от него, функциональный подход сводится к составлению решения задачи в виде набора функций, которые должны быть выполнены. In contrast, a functional approach involves composing the problem as a set of functions to be executed. Разработчик подробно определяет вход каждой функции и возвращаемые ею результаты. You define carefully the input to each function, and what each function returns. В следующей таблице описаны некоторые важные различия между этими двумя подходами. The following table describes some of the general differences between these two approaches.

Характеристика Characteristic Императивный подход Imperative approach Функциональный подход Functional approach
Основная направленность усилий программиста Programmer focus Способы выполнения (алгоритмы) задач и отслеживания изменений в их состоянии. How to perform tasks (algorithms) and how to track changes in state. Требуемые данные и преобразования. What information is desired and what transformations are required.
Изменения состояния State changes Важно! Important. Не существует. Non-existent.
Порядок выполнения Order of execution Важно! Important. Низкая значимость. Low importance.
Управление основным потоком данных Primary flow control Циклы, условия и вызовы функций (методов). Loops, conditionals, and function (method) calls. Вызовы функций, включая рекурсивные. Function calls, including recursion.
Основная единица обработки Primary manipulation unit Экземпляры структур или классов. Instances of structures or classes. Функции как полноценные объекты и коллекции данных. Functions as first-class objects and data collections.

Безусловно, большинство языков программирования было разработано в целях поддержки определенных подходов к программированию, но многие языки общего назначения являются достаточно гибкими, чтобы поддерживать несколько подходов. Although most languages were designed to support a specific programming paradigm, many general languages are flexible enough to support multiple paradigms. Например, большинство языков, содержащих указатели на функции, могут использоваться для надежной поддержки функционального программирования. For example, most languages that contain function pointers can be used to credibly support functional programming. Более того, в C# включены языковые расширения, предназначенные для поддержки функционального программирования, в том числе лямбда-выражения и определения типов. Furthermore, C# includes explicit language extensions to support functional programming, including lambda expressions and type inference. Одной из форм декларативного, функционального программирования является технология LINQ. LINQ technology is a form of declarative, functional programming.

Функциональное программирование с помощью XSLT Functional Programming Using XSLT

Многие разработчики XSLT знакомы с чисто функциональным подходом. Many XSLT developers are familiar with the pure functional approach. Наиболее эффективный способ разработки таблицы стилей XSLT состоит в том, что каждый шаблон рассматривается как изолированное, составное преобразование. The most effective way to develop an XSLT style sheet is to treat each template as an isolated, composable transformation. При этом совершенно не приходится задумываться над тем, в каком порядке должны проводиться вычисления. The order of execution is completely de-emphasized. XSLT исключает побочные эффекты (если не считать того, что применение механизмов экранирования, предназначенных для выполнения процедурного кода, может приводить к получению результатов функций, не соответствующих определениям этих функций). XSLT does not allow side effects (with the exception that escaping mechanisms for executing procedural code can introduce side effects that result in functional impurity). Таким образом, XSLT является эффективным инструментом, но некоторые его характеристики не оптимальны. However, although XSLT is an effective tool, some of its characteristics are not optimal. Например, программные конструкции приходится представлять на языке XML, в связи с чем объем кода становится довольно большим, поэтому его сопровождение затрудняется. For example, expressing programming constructs in XML makes code relatively verbose, and therefore difficult to maintain. Кроме того, в управлении потоком широко применяется рекурсия, что может привести к созданию трудночитаемого кода. Also, the heavy reliance on recursion for flow control can result in code that is hard to read. Дополнительные сведения об XSLT см. в разделе Преобразования XSLT. For more information about XSLT, see XSLT Transformations.

Тем не менее XSLT доказал свою полезность при чисто функциональном подходе для преобразования XML из одного вида в другой. However, XSLT has proved the value of using a pure functional approach for transforming XML from one shape to another. Чисто функциональное программирование с помощью LINQ to XML во многом похоже на XSLT. Pure functional programming with LINQ to XML is similar in many ways to XSLT. Но программные конструкции, вводимые LINQ to XML и в C#, позволяют писать чисто функциональные преобразования, более удобочитаемые и легко обслуживаемые, чем XSLT. However, the programming constructs introduced by LINQ to XML and C# allow you to write pure functional transformations that are more readable and maintainable than XSLT.

Преимущества чистых функций Advantages of Pure Functions

Основная причина реализации функциональных преобразований в виде чистых функций заключается в том, что чистые функции являются компонуемыми, т. е. самодостаточными без сохранения состояния. The primary reason to implement functional transformations as pure functions is that pure functions are composable: that is, self-contained and stateless. Это дает ряд преимуществ, включая следующие. These characteristics bring a number of benefits, including the following:

Повышенная удобочитаемость и обслуживаемость. Increased readability and maintainability. Это объясняется тем, что каждая функция разрабатывается для выполнения конкретных задач, определяемых аргументами. This is because each function is designed to accomplish a specific task given its arguments. Функция не зависит от какого-либо внешнего состояния. The function does not rely on any external state.

Упрощается разработка, основанная на ранее созданном коде. Easier reiterative development. Код становится более приемлемым для оптимизации кода, поэтому легче реализовать изменения в проекте. Because the code is easier to refactor, changes to design are often easier to implement. Например, предположим, что в процессе написания сложного преобразования выясняется, что какой-то код повторяется несколько раз. For example, suppose you write a complicated transformation, and then realize that some code is repeated several times in the transformation. Если оптимизация кода предусматривает преобразование в чистый метод, то полученный чистый метод можно вызывать в любое время, не беспокоясь о побочных эффектах. If you refactor through a pure method, you can call your pure method at will without worrying about side effects.

Упрощаются тестирование и отладка. Easier testing and debugging. Чистые функции проще тестировать отдельно от основной части кода, поэтому можно написать проверочный код, в котором чистая функция вызывается с типичными значениями, допустимыми краевыми значениями и недопустимыми краевыми значениями. Because pure functions can more easily be tested in isolation, you can write test code that calls the pure function with typical values, valid edge cases, and invalid edge cases.

Освоение нового подхода разработчиками объектно-ориентированных приложений Transitioning for OOP Developers

Традиционное объектно-ориентированное программирование (OOP) является таковым, что большинство разработчиков привыкает писать код в императивном (процедурном) стиле. In traditional object-oriented programming (OOP), most developers are accustomed to programming in the imperative/procedural style. Переходя к разработке в чисто функциональном стиле, они должны изменить свое мышление и подход к разработке. To switch to developing in a pure functional style, they have to make a transition in their thinking and their approach to development.

Чтобы решить задачу, разработчики объектно-ориентированных приложений проектируют иерархии классов, добиваются правильной инкапсуляции и мыслят в терминах контрактов между классами. To solve problems, OOP developers design class hierarchies, focus on proper encapsulation, and think in terms of class contracts. Для них первостепенное значение имеет обеспечение правильного поведения и состояния типов объектов, а для достижения этого используются такие возможности языка, как классы, интерфейсы, наследование и полиморфизм. The behavior and state of object types are paramount, and language features, such as classes, interfaces, inheritance, and polymorphism, are provided to address these concerns.

В отличие от этого, в функциональном программировании применяется подход к вычислительным проблемам как к определению чисто функциональных преобразований коллекции данных. In contrast, functional programming approaches computational problems as an exercise in the evaluation of pure functional transformations of data collections. В функциональном программировании приходится отказываться от применения состояний и изменяющихся данных, а вместо этого сосредоточиваться на применении функции. Functional programming avoids state and mutable data, and instead emphasizes the application of functions.

К счастью, C# не требует резкого перехода на функциональное программирование, поскольку поддерживает как императивный, так и функциональный подход. Fortunately, C# doesn’t require the full leap to functional programming, because it supports both imperative and functional programming approaches. Разработчик может сам выбирать нужный подход в зависимости от конкретного сценария. A developer can choose which approach is most appropriate for a particular scenario. В действительности в программах часто сочетаются оба стиля. In fact, programs often combine both approaches.

Основные принципы программирования: функциональное программирование. Язык функционального программирования

— ООП не сможет больше спасать нас от «Облачных монстров».

Примечание переводчика: Есть два понятия — параллельность (выполнение одновременно, независимо) и конкурентность (выполнение по шагам, поочерёдно, но одновременно несколько задач) и как всегда, мне пришлось поломать голову подобрая правильные термины.

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

Возможно вы уже слышали такое выражение, вроде: “Clojure”, “Scala”, “Erlang” или даже “Java теперь имеет лямбды”. И вы имеете хоть и отдалённое представление о «Функциональном программировании». Если вы участник какого-либа программисткого сообщества, тогда эта тема могла уже вами обсуждаться.

Если вы поищите в Google по словосочетанию «Функциональное программирование», вы не увидите что-то нового. Второй язык из созданных ранее уже охватывает эту тему, он был создан в 50-ых и называется Lisp. Тогда, какого чёрта, эта тема стала популярна только сейчас? Всего то 60 лет спустя?

В начале, компьютеры были очень медленными

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

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

String reverse(String arg) < if(arg.length == 0) < return arg; >else < return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); >>
Эта функция довольно медленная, потому что она повторно вызывает сама себя . Здесь возможна утечка памяти, так как множество раз создаются временные объекты. Но это функциональный стиль. Вам может показать странным, как люди могут так программировать. Ну, я как раз собирался вам рассказать.

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

Unit тестирование

Вот она, голубая мечта unit-тестеров. Можно протестировать каждую функцию в программе используя только нужные аргументы. Нет необходимости вызывать функции в правильном порядке или воссоздавать правильное внешнее состояние. Всё что вам нужно, это передать аргументы, которые соответствуют граничным случаям. Если все функции в вашей программе проходят Unit-тесты, то вы можете быть намного более уверены в качестве вашего ПО, чем в случае императивных языков программирования. В Java или C++ проверки возвращаемого значения не достаточно — функция может поменять внешнее состояние, которое тоже подлежит проверке. В ФП такой проблемы нет.

Отладка

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

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

Многопоточность

Если дела обстоят подобным образом, то почему так редко функциональные языки программирования используются в многопоточных приложениях? На самом деле чаще, чем вы думаете. Компания Ericsson разработала функциональный язык под названием Erlang для использования на отказоустойчивых и масштабируемых телекоммуникационных коммутаторах. Многие отметили преимущества Erlang-а и стали его использовать . Мы говорим о телекоммуникациях и системах контроля трафика, которые далеко не так просто масштабируются, как типичные системы, разработанные на Wall Street. Вообще-то, системы написанные на Erlang, не такие масштабируемые и надёжные, как Java системы. Erlang системы просто сверхнадёжные.

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

Компилятор функционального языка может проанализировать код, классифицировать функции, которые создают строки s1 и s2 , как функции потребляющие много времени, и запустить их параллельно. Это невозможно сделать в императивном языке, потому что каждая функция может изменять внешнее состояние и код, идущий непосредственно после вызова, может зависеть от неё. В ФП автоматический анализ функций и поиск подходящих кандидатов для распараллеливания — это тривиальнейшая задача, как автоматический inline ! В этом смысле функциональный стиль программирования соответствует требованиям завтрашнего дня. Разработчики железа уже не могут заставить CPU работать быстрее. Вместо этого они наращивают количество ядер и заявляют о четырёхкратном увеличении скорости многопоточных вычислений. Конечно они очень вовремя забывают сказать, что ваш новый процессор покажет прирост только в программах, разработанных с учётом распараллеливания. Среди императивного ПО таких очень мало. Зато 100% функциональных программ готовы к многопоточности из коробки.

Развёртывание по горячему

В идеале нужно обновить все нужные участки кода не останавливая систему в принципе. В императивном мире это невозможно [пер. в Smalltalk-е очень даже возможно]. Представьте себе выгрузку Java класса на лету и перезагрузка новой версии. Если бы мы так сделали, то все экземпляры класса стали бы нерабочими, потому что потерялось бы состояние, которое они хранили. Нам пришлось бы писать хитрый код, для контроля версий. Пришлось бы серриализовать все созданные экземпляры класса, потом уничтожить их, создать экземпляры нового класса, попытаться загрузить серриализованные данные в надежде, что миграция пройдёт нормально и новые экземпляры будут валидными. И кроме того, миграционный код необходимо писать каждый раз вручную. И ещё миграционный код должен сохранять ссылки между объектами. В теории ещё куда ни шло, но на практике это никогда не заработает.

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

Доказательные вычисления и оптимизация (Machine Assisted Proofs and Optimizations)

Дополнительно вы можете использовать математический аппарат, чтобы доказать корректность участков ваших программ. При желании можно написать инструменты, которые анализируют код и автоматически создают Unit-тесты для граничных случаев! Такая функциональность бесценна для сверхнадёжных систем (rock solid systems). При разработке систем контроля кардиостимуляторов или управления воздушным трафиком такие инструменты просто необходимы. Если же ваши разработки не находятся в сфере критически важных приложений, то инструменты автоматической проверки всё равно дадут вам гигантское преимущество перед вашими конкурентами.

Функции высшего порядка

В ФП функция — это не тоже самое, что функция в Java или C. Это надмножество — они могут тоже самое, что Java функции и даже больше. Пусть у нас есть функция на C:

Int add(int i, int j) < return i + j; >
В ФП это не тоже самое, что обычная C функция. Давайте расширим наш Java компилятор, чтобы он поддерживал такую запись. Компилятор должен превратить объявление функции в следующий Java код (не забывайте, что везде присутствует неявный final):

> Символ add не совсем функция. Это маленький класс с одним методом. Теперь мы можем передавать add в качестве аргумента в другие функции. Мы можем записать его в другой символ. Мы можем создавать экземпляры add_function_t в runtime и они будут уничтожены сборщиком мусора, если станут ненужными. Функции становятся базовыми объектами, как числа и строки. Функции, которые оперируют функциями (принимают их в качестве аргументов) называются функциями высшего порядка. Пусть это вас не пугает. Понятие функций высшего порядка почти не отличается от понятия Java классов, которые оперируют друг другом (мы можем передавать классы в другие классы). Мы можем называть их «классы высшего порядка», но никто этим не заморачивается, потому что за Java не стоит строгое академическое сообщество.

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

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

Void handleMessage(Message msg) < // . msg.setClientCode("ABCD_123"); // . sendMessage(msg); >// . >
Теперь представьте себе, что система поменялась, и теперь нужно распределять сообщения между двумя серверами вместо одного. Всё остаётся неизменным, кроме кода клиента — второй сервер хочет получать этот код в другом формате. Как нам справиться с этой ситуацией? Мы можем проверять, куда должно попасть сообщение, и в зависимости от этого устанавливать правильный код клиента. Например так:

Class MessageHandler < void handleMessage(Message msg) < // . if(msg.getDestination().equals("server1") < msg.setClientCode("ABCD_123"); >else < msg.setClientCode("123_ABC"); >// . sendMessage(msg); > // . >
Но такой подход плохо масштабируется. При добавлении новых серверов функция будет расти линейно, и внесение изменений превратится в кошмар. Объектно ориентированный подход заключается в выделении общего суперкласса MessageHandler и вынесение логики определения кода клиента в подклассы:

Abstract class MessageHandler < void handleMessage(Message msg) < // . msg.setClientCode(getClientCode()); // . sendMessage(msg); >abstract String getClientCode(); // . > class MessageHandlerOne extends MessageHandler < String getClientCode() < return "ABCD_123"; >> class MessageHandlerTwo extends MessageHandler < String getClientCode() < return "123_ABCD"; >>
Теперь для каждого сервера мы можем создать экземпляр соответствующего класса. Добавление новых сервером становится более удобным. Но для такого небольшого изменения многовато текста. Пришлось создать два новых типа чтобы просто добавить поддержку различного кода клиента! Теперь сделаем тоже самое в нашем языке с поддержкой функций высшего порядка:

; > MessageHandler handler = new MessageHandler(); handler.handleMessage(someMsg, getClientCodeOne);
Мы не создавали новых типов и не усложняли иерархию классов. Мы просто передали функцию в качестве параметра. Мы достигли того же эффекта, как и в объектно-ориентированном аналоге, только с некоторыми преимуществами. Мы не привязывали себя к какой-либо иерархии классов: мы можем передавать любые другие функции в runtime и менять их в любой момент, сохраняя при этом высокий уровень модульности меньшим количеством кода. По сути компилятор создал объектно-ориентированный «клей» вместо нас! При этом сохраняются все остальные преимущества ФП. Конечно абстракции, предлагаемые функциональными языками на этом не заканчиваются. Функции высшего порядка это только начало

Каррирование

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

Паттерн Адаптер наиболее часто применяется к «стандартной» единице абстракции в Java — классу. В функциональных языках паттерн применяется к функциям. Паттерн берёт интерфейс и преобразует его в другой интерфейс, согласно определённым требованиям. Вот пример паттерна Адаптер:

Int pow(int i, int j); int square(int i) < return pow(i, 2); >
Этот код адаптирует интерфейс функции, возводящей число в произвольную степень, к интерфейсу функции, которая возводит число в квадрат. В аккадемических кругах этот простейший приём называется каррирование (в честь специалиста по логике Хаскелла Карри (Haskell Curry), который провёл ряд математических трюков, чтобы всё это формализовать). Так как в ФП функции используются повсеместно в качестве аргументов, каррирование используется очень часто, чтобы привести функции к интерфейсу, необходимому в том или ином месте. Так как интерфейс функции — это её аргументы, то каррирование используется для уменьшения количества аргументов (как в примере выше).

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

Square = int pow(int i, 2);
Этой строкой мы автоматически создаём функцию возведения в квадрат с одним аргументом. Новая функция будет вызывать функцию pow , подставляя 2 в качестве второго аргумента. С точки зрения Java, это будет выглядеть следующим образом:

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

Ленивые вычисления

String s1 = somewhatLongOperation1(); String s2 = somewhatLongOperation2(); String s3 = concatenate(s1, s2);
В императивных языках программирования очерёдность вычисления не вызывает никаких вопросов. Поскольку каждая функция может повлиять или зависеть от внешнего состояния, то необходимо соблюдать чёткую очерёдность вызовов: сначала somewhatLongOperation1 , затем somewhatLongOperation2 , и concatenate в конце. Но не всё так просто в функциональных языках.

Как мы уже видели ранее somewhatLongOperation1 и somewhatLongOperation2 могут быть запущены одновременно, потому что функции гарантированно не влияют и не зависят от глобального состояния. Но что, если мы не хотим выполнять их одновременно, нужно ли вызывать их последовательно? Ответ — нет. Эти вычисления должны быть запущены, только если какая-либо другая функция зависит от s1 и s2 . Нам даже не нужно выполнять их до тех пор, пока они понадобятся внутри concatenate . Если вместо concatenate мы подставим функцию, которая в зависимости от условия использует один аргумент из двух, то второй аргумент можно даже не вычислять! Haskell — это пример языка с отложенными вычислениями. В Haskell отсутствует гарантия какой-либо очередности вызовов (вообще!), потому что Haskell выполняет код по мере необходимости.

Ленивые вычисления обладают рядом достоинств как и некоторыми недостатками. В следующем разделе мы обсудим достоинства и я объясню как уживаться с недостатками.

Оптимизация

Абстрагирование структур управления

Unless(stock.isEuropean()) < sendToSEC(stock); >
Мы хотим, чтобы функция sendToSEC выполнялась только если фонд (stock) не европейский. Как можно реализовать unless ? Без ленивый вычислений нам бы понадобилась система макросов, но в языках, подобных Haskell, это не обязательно. Мы можем объявить unless в виде функции!

Void unless(boolean condition, List code) < if(!condition) code; >
Заметьте, что code не будет выполняться, если condition == true . В строгих языках такое поведение невозможно повторить, так как аргументы будут вычислены прежде, чем unless будет вызвана.

Бесконечные структуры данных

Недостатки

В ленивом языке никто не гарантирует, что первая строка выполнится раньше второй! Это означает, что мы не можем делать ввод-вывод, не можем нормально использовать нативные функции (ведь их нужно вызывать в определённом порядке, чтобы учитывать их побочные эффекты), и не можем взаимодействовать с внешним миром! Если мы введём механизм для упорядочивания выполнения кода, то потеряем преимущество математической строгости кода (а следом потеряем все плюшки функционального программирования). К счастью ещё не всё потеряно. Математики взялись за работу и придумали несколько приёмов для того, чтобы убедится в правильном порядке выполняемых инструкций не потеряв функционального духа. Мы получили лучшее от двух миров! Такие приёмы включают в себя продолжения (continuation), монады (monads) и однозначная типизация (uniqueness typing). В данной статье мы поработаем с продолжениями, а монады и однозначную типизацию отложим до следующего раза. Занятно, что продолжения очень полезная штука, которая используется не только для задания строгого порядка вычислений. Об этом мы тоже поговорим.

Продолжения

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

Int i = add(5, 10); int j = square(i);
Функция add возвращает число 15, которое записывается в i , в том месте, где функция и была вызвана. Затем значение i используется при вызове square . Заметьте, что ленивый компилятор не может поменять очередность вычислений, ведь вторая строка зависит от результата первой. Мы можем переписать этот код с использованием Стиль Передачи Продолжения (Continuation Passing Style или CPS), когда add возвращает значение в функцию square .

Int j = add(5, 10, square);
В таком случае add получает дополнительный аргумент — функцию, которая будет вызвана после того, как add закончит работать. В обоих примерах j будет равен 225.

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

System.out.println(«Please enter your name: «); System.in.readLine();
Эти две строки не зависят друг от друга, и компилятор волен поменять их порядок по своему хотению. Но если мы перепишем в CPS, то тем самым добавим нужную зависимость, и компилятору придётся проводить вычисления одно за другим!

System.out.println(«Please enter your name: «, System.in.readLine);
В таком случае println должен будет вызвать readLine , передав ему свой результат, и вернуть результат readLine в конце. В таком виде мы можем быть уверены, что эти функции будут вызваны по очереди, и что readLine вообще вызовется (ведь компилятор ожидает получить результат последней операции). В случае Java println возвращает void . Но если бы возвращалось какое-либо абстрактное значение (которое может служить аргументом readLine), то это решило бы нашу проблему! Конечно выстраивание таких цепочек функций сильно ухудшает читаемость кода, но с этим можно бороться. Мы можем добавить в наш язык синтаксических плюшек, которые позволят нам писать выражения как обычно, а компилятор автоматически выстраивал бы вычисления в цепочки. Теперь мы можем проводить вычисления в любом порядке, не потеряв при этом достоинств ФП (включая возможность исследовать программу математическими методами)! Если вас это сбивает с толку, то помните, что функции — это всего лишь экземпляры класса с единственным членом. Перепишите наш пример так, чтобы println и readLine были экземплярами классов, так вам станет понятней.

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

Как только мы переведём программу к CPS виду, становится ясно, что у каждой инструкции есть продолжение, функция в которую будет передаваться результат, что в обычной программе было бы точкой вызова. Возьмём любую инструкцию из последнего примера, например add(5,10) . В программе, написанной в CPS виде, понятно что будет являться продолжением — это функция, которую add вызовет по окончанию работы. Но что будет продолжением в случае не-CPS программы? Мы, конечно, можем конвертировать программу в CPS, но нужно ли это?

Оказывается, что в этом нет необходимости. Посмотрите внимательно на наше CPS преобразование. Если вы начнёте писать компилятор для него, то обнаружите, что для CPS версии не нужен стек! Функции никогда ничего не возвращают, в традиционном понимании слова «return», они просто вызывают другую функцию, подставляя результат вычислений. Отпадает необходимость проталкивать (push) аргументы в стек перед каждым вызовом, а потом извлекать (pop) их обратно. Мы можем просто хранить аргументы в каком-либо фиксированном участке памяти и использовать jump вместо обычного вызова. Нам нет нужны хранить первоначальные аргументы, ведь они больше никогда не понадобятся, ведь функции ничего не возвращают!

Таким образом, программы в CPS стиле не нуждаются в стеке, но содержат дополнительный аргумент, в виде функции, которую нужно вызвать. Программы в не-CPS стиле лишены дополнительного аргумента, но используют стек. Что же хранится в стеке? Просто аргументы и указатель на участок памяти, куда должна вернуться функция. Ну как, вы уже догадались? В стеке храниться информация о продолжениях! Указатель на точку возврата в стеке — это то же самое, что и функция, которую нужно вызвать, в CPS программах! Чтобы выяснить, какое продолжение у add(5,10) , достаточно взять из стека точку возврата.

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

Хорошо, теперь мы уяснили, что же такое текущее продолжение. Что это значит? Если мы возьмём текущее продолжение и сохраним его где-нибудь, мы тем самым сохраним текущее состояние программы — заморозим её. Это похоже на режим гибернации ОС. В объекте продолжения хранится информация, необходимая для возобновления выполнения программы с той точки, когда был запрошен объект продолжения. Операционная система постоянно так делает с вашими программами, когда переключает контекст между потоками. Разница лишь в том, что всё находится под контролем ОС. Если вы запросите объект продолжения (в Scheme это делается вызовом функции call-with-current-continuation), то вы получите объект с текущим продолжением — стеком (или в случае CPS — функцией следующего вызова). Вы можете сохранить этот объект в переменную (или даже на диск). Если вы решите «перезапустить» программу с этим продолжением, то состояние вашей программы «преобразуется» к состоянию на момент взятия объекта продолжения. Это то же самое, как переключение к приостановленному потоку, или пробуждение ОС после гибернации. С тем исключением, что вы можете проделывать это много раз подряд. После пробуждения ОС информация о гибернации уничтожается. Если этого не делать, то можно было бы восстанавливать состояние ОС с одной и той же точки. Это почти как путешествие по времени. С продолжениями вы можете себе такое позволить!

В каких ситуациях продолжения будут полезны? Обычно если вы пытаетесь эмулировать состояние в системах лишенных такового по сути. Отличное применение продолжения нашли в Web-приложениях (например во фреймворке Seaside для языка Smalltalk). ASP.NET от Microsoft прикладывает огромные усилия, чтобы сохранять состояние между запросами, и облегчить вам жизнь. Если бы C# поддерживал продолжения, то сложность ASP.NET можно было бы уменьшить в два раза — достаточно было бы сохранять продолжение и восстанавливать его при следующем запросе. С точки зрения Web-программиста не было бы ни единого разрыва — программа продолжала бы свою работу со следующей строки! Продолжения — невероятно полезная абстракция для решения некоторых проблем. Учитывая то, что всё больше и больше традиционных толстых клиентов перемещаются в Web, важность продолжений будет со временем только расти.

Сопоставление с образцом (Pattern matching)

Давайте начнём наше знакомство с Pattern matching следующим примером. Вот функция вычисления чисел Фибоначи на Java:

Int fib(int n) < if(n == 0) return 1; if(n == 1) return 1; return fib(n - 2) + fib(n - 1); >
А вот пример на Java-подобном языке с поддержкой Pattern matching-а

Int fib(0) < return 1; >int fib(1) < return 1; >int fib(int n) < return fib(n - 2) + fib(n - 1); >
В чём разница? Компилятор реализует ветвление за нас.

Подумаешь, велика важность! Действительно важность не велика. Было подмечено, что большое количество функций содержат сложные switch конструкции (это отчасти верно для функциональных программ), и было принято решение выделить этот момент. Определение функции разбивается на несколько вариантов, и устанавливается паттерн на месте аргументов функции (это напоминает перегрузку методов). Когда происходит вызов функции, компилятор на лету сравнивает аргументы со всеми определениями и выбирает наиболее подходящий. Обычно выбор падает на самое специализированное определение функции. Например int fib(int n) может быть вызвана при n равном 1, но не будет, ведь int fib(1) — более специализированное определение.

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

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

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

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

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

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

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

Классификация функциональных языков

В качестве примера чистого функционального языка можно привести Haskell . Однако большинство функциональных языков являются гибридными и содержат свойства как функциональных, так и императивных языков. Яркие примеры — языки Scala и Nemerle. В них органично сочетаются характеристики как объектно-ориентированных языков, так и функциональных. Реализована хвостовая рекурсия и её оптимизация, функция является полноправным объектом, то есть может быть сохранена в переменной, передана в качестве аргумента в другую функцию или возвращена из функции.

Также функциональные языки делят на строгие и нестрогие . К нестрогим языкам относят те, которые поддерживают отложенные вычисления (F#), то есть аргументы функции вычисляются только тогда, когда они действительно понадобятся при вычислении функции. Ярким примером нестрогого языка является Haskell. В качестве примера строгого языка можно привести Standard ML .

Некоторые функциональные языки реализованы поверх платформообразующих виртуальных машин (JVM, .NET), то есть приложения на этих языках могут работать в среде времени исполнения (JRE, CLR) и использовать встроенные классы. К ним относятся Scala, Clojure (JVM), F#, Nemerle, SML.NET (.NET).

Ссылки

  • https://fprog.ru/ — Журнал «Практика функционального программирования»
  • https://www.intuit.ru/department/pl/funcpl/1/ — Основы функционального программирования. Л. В. Городняя
  • https://roman-dushkin.narod.ru/fp.html — Курс лекций по функциональному программированию , читаемый в МИФИ с 2001 года;
  • https://alexott.net/ru/fp/books/ — Обзор литературы о функциональном программировании . Рассматриваются книги как на русском, так и на английском языке.

Wikimedia Foundation . 2010 .

Смотреть что такое «Язык функционального программирования» в других словарях:

язык прграммирования Лисп — Язык функционального программирования. Тематики информационные технологии в целом EN Lisp … Справочник технического переводчика

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

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

У этого термина существуют и другие значения, см. Scala. Scala Класс языка: Мультипарадигмальный: функ … Википедия

Oz Семантика: функциональный, процедурный, декларативный, объектно ориентированный, вычисления с ограничениями, Н модели, параллельные вычисления Тип исполнения: компилируемый Появился в: 1991 Автор(ы): Gert Smolka his students Релиз … Википедия

AWL (Alternative Web Language) Класс языка: мультипарадигмальный: функциональный, процедурный, объектно ориентированный Тип исполнения: интерпретируемый Появился в: 2005 г. Типизация данных: динамическая … Википедия

У этого термина существуют и другие значения, см. Леда (значения). Леда (Leda) мультипарадигмальный язык программирования, спроектированный Тимоти Баддом. Язык Leda исходно создавался с целью совмещения императивного программирования, объектно… … Википедия

Erlang Файл:Erlang logo.png Семантика: мультипарадигмальный: конкурентное, функциональное программирование Появился в: 1987 г. Автор(ы): Типизация данных: строгая, динамическая Основные реализации: E … Википедия

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

Python был задуман в 1980 х годах, а его создание началось в декабре 1989 года Гвидо ван Россумом в составе центра математики и информатики в Нидерландах. Язык Python был задуман как потомок языка программирования ABC, способный к обработке… … Википедия

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

Энциклопедичный YouTube

Что такое функциональное программирование

Математика и константы / Введение в программирование, урок 4 (JavaScript ES6)

Реактивное программирование и современные веб-интерфейсы

Александр Чирцов о математике в физике

Анна Андреева. Решение олимпиадных задач по математике

Субтитры

Языки функционального программирования

Ещё не полностью функциональные изначальные версии и Лиспа , и APL внесли особый вклад в создание и развитие функционального программирования. Более поздние версии Lisp, такие как Scheme , а также различные варианты APL поддерживали все свойства и концепции функционального языка .

Как правило, интерес к функциональным языкам программирования, особенно чисто функциональным, был скорее научный, нежели коммерческий. Однако, такие примечательные языки как Erlang , OCaml , Haskell , Scheme (после 1986) а также специфические (статистика), Wolfram (символьная математика), и (финансовый анализ), и XSLT (XML) находили применение в индустрии коммерческого программирования. Такие широко распространенные декларативные языки как SQL и Lex /Yacc содержат некоторые элементы функционального программирования, например, они остерегаются использовать переменные. Языки работы с электронными таблицами также можно рассматривать как функциональные, потому что в ячейках электронных таблиц задаётся массив функций, как правило зависящих лишь от других ячеек, а при желании смоделировать переменные приходится прибегать к возможностям императивного языка макросов.

История

Первым функциональным языком был Лисп , созданный Джоном Маккарти в период его работы в в конце пятидесятых и реализованный, первоначально, для IBM 700/7000 (англ.) русск. . В Лиспе впервые введено множество понятий функционального языка, хотя при этом в языке применяется не только парадигма функционального программирования . Дальнейшим развитием Лиспа стали такие языки как Scheme и Dylan .

Концепции

Некоторые концепции и парадигмы специфичны для функционального программирования и в основном чужды императивному программированию (включая объектно-ориентированное программирование). Тем не менее, языки программирования обычно представляют собой гибрид нескольких парадигм программирования, поэтому «большей частью императивные» языки программирования могут использовать какие-либо из этих концепций. [ ]

Функции высших порядков

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

Функции высших порядков позволяют использовать карринг — преобразование функции от пары аргументов в функцию, берущую свои аргументы по одному. Это преобразование получило своё название в честь Х. Карри .

Чистые функции

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

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

Хотя большинство компиляторов императивных языков программирования распознают чистые функции и удаляют общие подвыражения для вызовов чистых функций, они не могут делать это всегда для предварительно скомпилированных библиотек, которые, как правило, не предоставляют эту информацию. Некоторые компиляторы, такие как gcc , в целях оптимизации предоставляют программисту ключевые слова для обозначения чистых функций . Fortran 95 позволяет обозначать функции как «pure» (чистые) .

Рекурсия

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

Подход к вычислению аргументов

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

print (len ([ 2 + 1 , 3 * 2 , 1 / 0 , 5 — 4 ]))

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

Как правило, нестрогий подход реализуется в виде редукции графа. Нестрогое вычисление используется по умолчанию в нескольких чисто функциональных языках, в том числе Miranda , Clean и Haskell . [ ]

ФП в нефункциональных языках

Принципиально нет препятствий для написания программ в функциональном стиле на языках, которые традиционно не считаются функциональными, точно так же, как программы в объектно-ориентированном стиле можно писать на структурных языках. Некоторые императивные языки поддерживают типичные для функциональных языков конструкции, такие как функции высшего порядка и списковые включения (list comprehensions), что облегчает использование функционального стиля в этих языках. Примером может быть функциональное программирование на Python. Другим примером является язык Ruby , который имеет возможность создания как lambda-объектов, так и возможность организации анонимных функций высшего порядка через блок с помощью конструкции yield.

Стили программирования

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

# императивный стиль target = # создать пустой список for item in source_list : # для каждого элемента исходного списка trans1 = G (item ) # применить функцию G() trans2 = F (trans1 ) # применить функцию F() target . append (trans2 ) # добавить преобразованный элемент в список

Функциональная версия выглядит по-другому:

# функциональный стиль # языки ФП часто имеют встроенную функцию compose() compose2 = lambda A , B : lambda x : A (B (x )) target = map (compose2 (F , G ), source_list )

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

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

  • Рефал (для этой категории, представленной единственным языком, нет общепринятого названия);
  • Аппликативные (Лисп , , Tcl , Rebol);
  • Комбинаторные (APL / / , FP / FL );
  • Бесточечные (чистые конкатенативные) (Joy , Cat , Factor , подмножество PostScript).

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

Особенности

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

Сильные стороны

Повышение надёжности кода

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

Удобство организации модульного тестирования

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

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

Возможности оптимизации при компиляции

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

Возможности параллелизма

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

Недостатки

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

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

Добавить комментарий