Декларативный подход к программированию реферат

Обновлено: 06.07.2024

© 2003 И.А. Дехтяренко

2. История.

Козьма Прутков "Плоды раздумий"

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

Фактически современную математическую логику основал Готлоб Фреге. В 1899 г. он придумал Begriffsschrift ("исчисление понятий" или "система обозначения понятий") [Frege]. Это был самый значительный шаг в логике со времён Аристотеля. Формальна система Фреге позволила логикам разработать строгое определение доказательства. Кроме того, Фреге был первым, кто серьёзно отнёсся к идее применения функций к сущностям отличным от чисел (и в частности к другим функциям).

С начала XX века математическая логика - объект пристального внимания математиков. Для информатики большое значение имеет работа Жака Эрбрана [Herbrand], в которой он предложил несколько версий процедуры доказательства теорем в логике первого порядка.

Собственно история начиняется в 1958 году, когда Джон Маккарти изобрел язык Лисп [McCarthy] (LISP - LISt Processing) первый в мире функциональный язык программирования. В каком-то смысле Лисп - функциональный эквивалент Фортрана. В течение некоторого времени Лисп, и Фортран были единственными широко доступными языками программирования (кроме ассемблерных языков). Как "первый блин" Лисп обладал рядом недостатков. Самый заметный из них - громоздкий синтаксис.
Например, определение факториала:

Это обилие скобок дало повод некоторым острословам утверждать, что LISP в действительности означает "Lots of Infuriating, Stupid Parenthesis". Изначально такую запись предполагалось использовать только для данных и как промежуточное представление, а для программ применять другую нотацию, что-то вроде:

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

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

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

Несколько отвлекаясь от темы. С Лиспом связаны многие нововведения, ставшие ныне привычными: полноэкранные редакторы, и многооконный графический интерфейс, символические отладчики и инспекторы данных, инкрементные компиляторы и диалоговые системы программирования не говоря уж об автоматическом управлении памятью и "уборке мусора". Common Lisp - первый стандартизированный язык объектно-ориентированного программирования. Что же касается декларативного (и в частности функционального) программирования, Лисп по мере развития удалялся от него, обзаведясь полным набором императивных инструментов вплоть до таких маргинальных, как go.

В 1964 г. Питер Ландин продемонстрировал связь языков программирования, в том числе Лиспа и Алгола с лямбда-исчислением, придумал абстрактную машину для вычисления лямбда-выражений "SECD-машину" [Landin64]. С тех пор варианты этой машины часто использовались для реализации функциональных языков.

В 1966 г. Ландин предложил язык ISWIM (If you See What I Mean) [Landin66], который сыграл роль Алгола в функциональном программировании. Он не использовался как промышленный язык, но послужил прототипом для всех "современных" функциональных языков. ISWIM создавался как лямбда-исчисление с более привычным синтаксисом (привычным, по крайней мере, для тех, кто читает математические тексты). Именно Ландин ввёл в обиход термин "синтаксический сахар". Среди нововведений - инфиксные операции, локальные определения (let- и where- выражения) использование отступов для определения структуры программы. Факториал на ISWIM выглядит вполне современно.

В 1963 г. Джон Алан Робинсон [Robinson] реализовал метод автоматического доказательства теорем, получивший название "принцип резолюции". Идея этого метода принадлежит Эрбрану, но поскольку тот не думал о применении своего метода на компьютерах (что не удивительно в 1930 г.), он оказался не очень подходящим с вычислительной точки зрения. Робинсон сделал его пригодным для реального применения и разработал эффективный алгоритм унификации, являющийся основой метода.

В 1971 г Ален Колмероэ положил резолюцию в основу Пролога (PROLOG - PROgrammation en LOGique) [Colmerauer] - первого языка логического программирования. Чтобы добиться приемлемой эффективности и избежать "комбинаторных взрывов" на язык были наложены существенные ограничения. Роберт Ковальский показал [Kowalski], что эти ограничения эквивалентны использованию известного подмножества логики предикатов, так называемых хорновских дизъюнктов [Horn] и таким образом подвёл теоретический фундамент под Пролог. Именно процедурная интерпретация логических предложений, предложенная Ковальским, даёт основания говорить о Прологе как о первом логическом языке, несмотря на то, что ранее уже существовали системы, основанные на логике, такие как Absys Майкла Фостера и Теда Элкока [Elcock, Foster] и ПРИЗ Энна Тыугу [Тыугу]. Интересно что, работая над Лиспом, Маккарти также пытался создать логический язык, но отсутствие эффективных алгоритмов вывода не позволило ему сделать это.

В 1976 г. Девидом Уорреном создана реализация Пролога, известная как Edinburgh Prolog [Warren]. Компилятор Уоррена продемонстрировал эффективность не хуже, чем лучшие существующие в то время реализации Лиспа и вызвал интерес к Прологу как к реальному языку программирования. Программы на Прологе компилировались в команды специально разработанной виртуальной машины, получившей название WAM (Warren's Abstract Machine). В этой реализации Пролог обрёл свой привычный вид. На долгое время Edinburgh Prolog стал стандартом де-факто для Пролога.

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

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

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

В 1976 г. Петер Хендерсон и Джеймс Моррис реализовали диалект Лиспа Lispkit [Henderson], в котором впервые использовались ленивые вычисления - важная концепция современного функционального программирования. Собственно ленивые вычисления были введены ещё в Алголе-60 как следствие передачи параметров по имени, но там они воспринимались как ошибка создателей языка и никогда не реализовывались в полном объеме. Действительно, такая схема вычислений противоречит принципу императивного программирования - явному описанию последовательности вычислений.

Значительное влияние на популяризацию функционального программирования оказала лекция Джона Бекуса и его статья [Backus]. В 1977 г. Бекус получает премию Тьюринга, прежде всего за участие в создании Фортрана и Алгола. В своей лекции он обрушивается с критикой на императивные языки, за то, что они вынуждают программиста сосредоточится на операциях с памятью компьютера, а не на проблеме, которую надо решить и за то, что они не обладают полезными математическими свойствами для рассуждений о программах. Достаётся и лямбда-исчислению. Бекус сравнивает его с использованием произвольных передач управления в императивных языках. Взамен он предлагает системы программирования, основанные на "функциональных формах" (то есть функциях высшего порядка или комбинаторах), которые выражают общие образцы вычислений. Функции высшего порядка - важна особенность современных функциональных языков программирования, но другие предложения Бекуса широкого признания не получили. Программирование без переменных - не лёгкое занятие и требуются некоторые усилия, чтобы понять определение факториала на FP.

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

ML (Meta Language, изначальное предназначение- метаязык для системы доказательств) [Gordon] очень похож на ISWIM, но имел важную особенность: полиморфную систему типов данных разработанную Робином Милнером [Milner]. Подобная система была раньше предложена Роджером Хиндли [Hindley] и сейчас часто называется системой типов Хиндли-Милнера. Наличие механизма вывода типов позволило избавить программиста от необходимости явно описывать типы функций и в то же время производить строгий контроль типов.
ML не чисто функциональный язык, он включает и императивные инструкции. С тех пор ML развивался и включил в себя многие особенности других функциональных языков. Появилось несколько диалектов, наиболее известные из которых Standard ML [Harper], CAML [Cousineau] и чисто функциональный Lazy ML [Augustsson].

Факториал на ML:

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

В 1985 г. Дэвид Тёрнер объединил в одном языке Miranda [Turner] многие важные особенности функциональных языков (функции высшего порядка, ленивые вычисления, алгебраические типы данных, параметрический полиморфизм, сопоставление с образцом). Введены удобные конструкторы списков (ZF-выражения), позаимствованные из другого языка Тёрнера KRC.
Факториал на Миранде можно выразить, например, так: или, используя сопоставления, так: и даже вот так (с использованием ZF-выражений):

Тёрнер запатентовал свой язык (Miranda TM - торговая марка Research Software, Ltd), что замедлило его развитие и распространение. На основе Миранды было создано много языков, в том числе и популярные ныне Haskell [!] и Clean.

В 1981 г. стартует японский проект "вычислительных систем следующего поколения", который вызвал взрыв интереса к логическому программированию. Пролог входит в число самых популярных языков. Появляются реализации для всех распространенных компьютеров. Начинаются исследования по параллельным логическим языкам, таким как Parlog, Concurent Prolog, GHC.

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

Часто можно слышать о крахе японского проекта. На самом деле технические задачи были выполнены - созданы мощные параллельные компьютеры и соответствующие им языки программирования. Но оказалось, что потребность в них невелика. Та область исследований ("искусственный интеллект"), с которой обычно связывалась с применением Лиспа и Пролога попала в очередной кризис. Сказался и общий спад в экономике. Примерно в это же время обанкротились и американские компании, выпускавшие Лисп-машины по $70000.

Положение начало изменятся в последнее время. Производительность компьютеров давно перестала быть проблемой, сложность решаемых задач возрастает, а эйфория ООП постепенно утихает. Кроме того, значительные успехи были достигнуты в технике реализации. Успело сложиться мнение, что декларативные языки "принципиально неэффективны". Но, применяя методы глобального анализа программ, создатели систем Aquarius Prolog [VanRoy] и Parma [Taylor] смогли вплотную приблизился к лучшим компиляторам императивных языков. Маленькой сенсацией стал компилятор для функционального языка Sisal [Feo, Cann], ориентированного на численные расчёты, созданный в американском ядерном исследовательском центре (Lawrence Livermore National Laboratory). Он превзошёл не только Си, но и Фортран, бывший в этой области вне конкуренции.

  • Повышение "чистоты" языков, то есть устранение из них недекларативных средств. Поскольку нельзя просто выбросить эти средства, необходимо найти приемлемые декларативные альтернативы.
  • Создание специализированных средств для эффективного решения определённых классов задач. Это, прежде всего, языки "программирования в ограничениях". Эти языки содержат встроенные "решатели уравнений" специфических видов, например, линейные уравнения/неравенства и уравнения принадлежности в конечных областях. Такие средства позволяют практически решать многие задачи, которые нереально решить общими методами.
  • Синтез различных стилей программирования. В частности объединение функционального и логического программирования и даже объединение их с объектно-ориентированным программированием. Важный принцип этих исследований - найти, простую теоретическую основу для такого синтеза.
    Frege G. Begriffsschrift, eine der arithmetischen nachgebildete Formelsprache des reinen Denkens. 1879 Schonfinkel M. Ueber die Bausteine der mathematischen Logik. Mathematische Annalen, 1924. Herbrand J. Une methode de demonstration. Thesis, 1931 Horn A. On sentences which are true of direct unions of algebras. Journal of Symbolic Logic, 1951. McCarthy J. Recursive functions of symbolic expressions and their computation by machine. CACM 3(4), 1960 Landin P. The Mechanical evaluation of Expressions. Computer Journal v.6, 1964 Landin P. The next 700 programming languages. CACM 9(3) 1966. ( 1,2MB pdf ) Robinson A. J. A machine oriented logic based on the resolution principle. Journal of the ACM vol 12 1965. [перевод: Робинсон Дж. Машинно-ориентированная логика, основанная на принципе резолюции. Кибернетический сборник вып.7, 1970.] Hindley R. The principle type scheme of an object in combinatory logic. Transactions AMS 146, 1969 Elcock, E.W. Descriptions. In Machine Intelligence. vol. 3, Edinburgh, 1968. Foster, J.M. Assertions: Programs written without specifying unnecessary order. ibid. Тыугу Э. Х. Решение задач на вычислительных моделях. - Ж. вычис. матем. и матем. физизики. 1970 Colmerauer A., Kanoui H., Pasero R., Roussel P. Un Systeme de Comunication Homme-machine en Francais. Universite d'Aix Marseille, 1973 Kowalski R. Predicate Logic as Programming Language. IFIP Congress, 1974 Warren D. Implementing Prolog - compiling logic program. Edinburgh, 1977. Henderson P., Morris J.H. A Lazy Evaluator. Conference Record of the Third ACM symposium on Principles of Programming Languages 1976 Backus J. Can Programming be Liberated from the von Neumann style? A functional style and its Algebra of Programs. CACM 21(8), 1978 ( 3MB pdf) Milner R. A theory of type polymorphism in programming. Journal of Computer and System Science 17(3), 1978. Gordon M. J., Milner R., Wadsworth C.P. A metalanguage for interactive proof in LCF. Conference Record of the 5th Annual ACM Symposium on Principles of Programming Languages.1978 Burstall R., MacQueen D., Sannella D. Hope: an experimental applicative language. Proceedings First LISP Conference, 1980. Augustsson L. A compiler for Lazy ML. Proceedings 1984 ACM Conference on LISP and Functional Programming, 1984 Harper R., MacQueen D., Milner R. Standard ML. Technical Report, University of Edinburgh, 1986. Cousineau G. Huet G. The CAML primer. Technical Report, INRIA,1990 Turner D. Miranda : A non-strict functional language with polymorphic types Proceedings, Functional Programming Languages and Computer Architecture, 1985 Feo J. T et al. A Report on the SISAL Language Project. 1990. Cann D. Retire FORTRAN? A debate rekindled. CACM 35(8), 1992. Taylor A. LIPS on a MIPS: Results from a Prolog Compiler for a RISC. In ICLP, 1990. Van Roy P. Can Logic Programming Execute as Fast as Imperative Programming?

[!] Для тех, кого ещё не достали факториалы имеется замечательная коллекция на Haskell.

Вы наверняка слышали о таких понятиях, как императивное и декларативное программирование, и скорее всего гуглили определения. И поэтому вы наверняка видели что-то подобное: «Императивное программирование — это описание того, как ты делаешь что-то, а декларативное — того, что ты делаешь. Это объяснение отлично подходит тем, кто уже разобрался в этом вопросе — но не новичкам.

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

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

Императивный подход (как): Я вижу, что тот угловой столик свободен. Мы пойдём туда и сядем там.

Декларативный подход (что): Столик для двоих, пожалуйста.

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

Я задам вам вопрос и хочу, чтобы вы придумали и императивный, и декларативный подход.

Мой адрес: Энск, улица Победы, дом 134.

Неважно, как я попаду к твоему дому, важно, на какой машине я приеду. У неё будет или императивная механическая КПП, или декларативная автоматическая КПП. Достаточно метафор?

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

Итак, я повторюсь: многие (если не все) декларативные подходы имеют слой неких императивных абстракций.

Теперь мы перейдём от приятных метафор к реальному коду. Сперва посмотрим, какие языки являются декларативными, а какие — императивными:

Вот типичные примеры на SQL и HTML:

Достаточно взглянуть на них, чтобы понять, что происходит. Они декларативны, заявляя, что должно быть сделано, а не как. Вы описываете желаемый результат, не углубляясь в инструкции. Неважно, как будут выбраны пользователи из Мексики. Неважно, как браузер распарсит ваш article . Важно, что вы получите мексиканских пользователей и новый header и paragraph на сайте.

Пока неплохо. Давайте рассмотрим примеры на JavaScript.

Представьте, что вы на собеседовании. Откройте консоль и ответьте на следующие вопросы.

  1. Напишите функцию, называющуюся double , которая принимает массив чисел и возвращает новый массив, каждый элемент которого в два раза больше входного: double([1,2,3]) -> [2,4,6] .
  2. Напишите функцию, называющуюся add , которая принимает массив и возвращает сумму всех его элементов: add([1,2,3]) -> 6 .
  3. Используя jQuery (или чистый JavaScript), добавьте обработчик события click к элементу с id , равным btn . По нажатию переключите класс highlight и смените текст на Add Highlight или Remove Highlight , в зависимости от текущего состояния элемента.

Давайте взглянем на самые распространённые подходы к решению этих задач, которые являются императивными.

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

  1. Очевидно, что все они описывают, как решить проблему: мы явно указываем все шаги.
  2. Это уже не так очевидно для тех, кто не привык думать декларативно, или даже функционально. В каждом примере происходит изменение какого-либо состояния. В первых двух примерах происходило изменение переменной results , а в третьей состояние было в самой DOM — и его мы тоже изменяли.
  3. Это уже субъективно, но я считаю, что код выше нечитаем. Я не могу с первого взгляда понять, что происходит — вместо этого мне приходится читать код построчно.

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

Заметьте, что в первых двух примерах я использовал встроенные методы JavaScript, map и reduce . Как видите, декларативные решения вновь оказались абстракциями над императивными реализациями. Но нас не интересует, как реализованы эти методы. Мы также не изменяем состояния, да и читается этот код лучше.

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

Декларативный подход к программированию что такое

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

Что такое декларативное программирование:

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

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

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

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

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

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

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

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

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

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

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

Эти языки, как правило, неполные тюрингские . Примерами являются SQL для управления данными в базе данных, регулярные выражения или XSL для управления данными XML.

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

Вывод:

В этой статье вы прочитали что такое декларативный подход к программированию и всё остальное с этим связанное.

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

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

Но сначала, небольшое введение.

Императивность vs Декларативность

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

Другое подобное разделение заключается в императивном програмировании против декларативного.

Без погружения вглубь этого, императивное программирование — это стиль программирования, в котором программисты говорят компьютеру, что нужно сделать, объясняя ему, как это нужно сделать. Императивное программирование даёт множество конструкций, которые мы используем каждый день: управление потоком ( if - then - else синтаксис и циклы), арифметические операторы ( + , - , * , / ), операторы сравнения ( === , > , , и т.д.), и логические операторы ( && , || , ! ).

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

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

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

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

Декларативные заменители

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

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

Арифметика

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


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

Рамда предоставляет функции add, subtract, multiply и divide для использования вместо стандартных арифметических операций. Так что мы можем использовать рамдовскую multiply там, где мы использовали самописную функцию, мы можем взять преимущество каррированной функции add для замены нашей addOne , и мы также можем написать square с помощью multiply .


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

Мы можем использовать add(1) и subtract(1) для увеличения и уменьшения, но так как эти две операции такие распространённые, Ramda предоставляет inc и dec вместо них.

Так что мы можем ещё немного упростить наш конвеер:


subtract является заменой бинарного оператора - , но у нас ещё имеется унарный оператор - для отрицания значения. Мы также можем использовать multiply(-1) , но Ramda предоставляет функцию negate для выполнения этой задачи.

Сравнение

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


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

Давайте преобразуем наш код на использование equals вместо === и gte вместо >= .


Ramda также предоставляет gt для > , lt для и lte для

В дополнение к equals есть ещё identical для определения, являются ли два значения ссылками на то же пространство в памяти.

Существует набор случаев основных применений для === : проверка, что строка или массив являются пустыми ( str === '' или arr.length === 0 ) и проверка, является ли переменная равной null или undefined . Ramda предоставляет удобные функции для обоих случаев: isEmpty и isNil.

Логика

Во второй части (и чуть выше), мы использовали функции both и either вместо операторов && и || . Мы также говорили о complement для мест с ! .

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

Но иногда нам нужно применить && , || и ! к различным значениям. Для подбоных случаев Ramda предоставляет нам функции and, or и not. Я думаю следующим образом: and , or и not работают со значениями, в то время как both , either и complement работают с функциями.

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

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


defaultTo проверяет второй аргумент на isNil . Если проверка провалилась он вернёт полученное значение, в ином случае вернёт первый аргумент, переданный ей.

Условия

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

ifElse

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


Обратите внимание, что наше условие ( age >= 21 ) и вторая ветвь ( age + 1 ) могут быть обе написаны как функции age . Мы можем переписать первую ветвь ( 21 ) как функцию-константу ( () => 21 ). Теперь у нас будет три функции, которые принимают (или игнорируют) age .

Теперь мы на позиции, когда мы можем использовать функцию ifElse из Ramda, которая является эквивалентом структуры if. then..else или её более короткого кузена, тернарного оператора ( ?: ).


Как мы упомянули выше, функции сравнения не работают подобно функциям объединения, так что здесь нам нужно начать использовать заполнитель ( __ ). Мы также можем применить lte вместо этого:

Константы

Функции-константы весьма полезны в ситуациях, подобных этой. Как вы можете предположить, Ramda предоставляет нам сокращение. В данном случае, сокращение называется always.


Ramda также предоставляет T и F в качестве дальнейших сокращений для always(true) и always(false)

Тождественность

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

Как вы уже можете ожидать, Ramda предоставляет нам функцию identity:


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

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

Если, как в нашем случае, вторая ветвь является тождественностью, мы можем использовать when вместо ifElse :


Если первая ветвь условия является тождественностью, мы можем использовать unless. Если мы перевернём наше условие на использование gte(__, 16) , мы можем использовать unless .

Ramda также предоставляет функцию cond, которая может заменить выражение switch или цепочку выражений if. then. else .


Мне не понадобилось использовать cond в моём коде с Ramda, но я писал подобный код на Lisp много лет назад, так что cond чувствуется старым другом.

Заключение

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

Далее

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

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

Декларативное программирование

Направлен на относительно узкий круг задач искусственного интеллекта.

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

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

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

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

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

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

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

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

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

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

Читайте также: