Процедурное / функциональное программирование ни в коей мере не слабее ООП , даже не вдаваясь в аргументы Тьюринга (мой язык обладает силой Тьюринга и может делать все, что будет делать другой), что мало что значит. Фактически, объектно-ориентированные методы были впервые опробованы на языках, в которых они не были встроены. В этом смысле ОО-программирование - это только особый стиль процедурного программирования . Но это помогает обеспечить соблюдение определенных дисциплин, таких как модульность, абстракция и сокрытие информации , которые необходимы для понимания и сопровождения программы.
Некоторые парадигмы программирования развиваются из теоретического видения вычислений. Такой язык, как Лисп, произошел от лямбда-исчисления и идеи мета-циркулярности языков (аналогично рефлексивности в естественном языке). Роговые выражения породили Пролог и ограничение программирования. Семейство Алгол также обязано лямбда-исчислению, но без встроенной рефлексивности.
Лисп является интересным примером, поскольку он стал испытанием многих инноваций в языке программирования, которые прослеживаются в его двойном генетическом наследии.
Однако языки затем развиваются, часто под новыми именами. Основным фактором эволюции является практика программирования. Пользователи идентифицируют методы программирования, которые улучшают свойства программ, такие как удобочитаемость, ремонтопригодность, доказуемость правильности. Затем они пытаются добавить к языкам функции или ограничения, которые будут поддерживать, а иногда и применять эти методы, чтобы улучшить качество программ.
Это означает, что эти практики уже возможны на более старом языке программирования, но для их использования требуется понимание и дисциплина. Включение их в новые языки в качестве основных понятий со специфическим синтаксисом упрощает использование и понимание этих практик, особенно для менее опытных пользователей (т. Е. Подавляющего большинства). Это также облегчает жизнь искушенным пользователям.
В некотором смысле, именно дизайн языка представляет собой подпрограмму / функцию / процедуру для программы. Как только полезная концепция определена, ей присваивается имя (возможно) и синтаксис, чтобы ее можно было легко использовать во всех программах, разработанных на этом языке. И в случае успеха он будет включен в будущие языки.
Пример: воссоздание ориентации объекта
Сейчас я попытаюсь проиллюстрировать это на примере (который, безусловно, может быть доработан, если дать время). Цель этого примера не в том, чтобы показать, что объектно-ориентированная программа может быть написана в стиле процедурного программирования, возможно, за счет гибкости и удобства обслуживания. Я скорее попытаюсь показать, что некоторые языки без ОО-средств могут фактически использовать функции более высокого порядка и структуру данных, чтобы фактически создать средства для эффективной имитации объектной ориентации , чтобы извлечь выгоду из ее качеств, касающихся организации программы, включая модульность, абстракцию и сокрытие информации. ,
Как я уже сказал, Лисп был испытательным стендом для большой языковой эволюции, включая парадигму ОО (хотя то, что можно было считать первым ОО-языком, было Simula 67, в семействе Алгол). Лисп очень прост, и код для его основного интерпретатора меньше, чем страница. Но вы можете заниматься ОО-программированием на Лиспе. Все, что вам нужно, это функции высшего порядка.
Я не буду использовать эзотерический синтаксис Lisp, а скорее псевдокод, чтобы упростить представление. И я рассмотрю простую существенную проблему: сокрытие информации и модульность . Определение класса объектов при одновременном запрете пользователю доступа к (большей части) реализации.
Предположим, я хочу создать класс с именем vector, представляющий 2-мерные векторы, с методами, включающими: сложение векторов, размер вектора и параллелизм.
function vectorrec () {
function createrec(x,y) { return [x,y] }
function xcoordrec(v) { return v[0] }
function ycoordrec(v) { return v[1] }
function plusrec (u,v) { return [u[0]+v[0], u[1]+v[1]] }
function sizerec(v) { return sqrt(v[0]*v[0]+v[1]*v[1]) }
function parallelrec(u,v) { return u[0]*v[1]==u[1]*v[0]] }
return [createrec, xcoordrec, ycoordrec, plusrec, sizerec, parallelrec]
}
Затем я могу назначить созданный вектор фактическим именам функций, которые будут использоваться.
[vector, xcoord, ycoord, vplus, vsize, vparallel] = vectorclass ()
Зачем быть таким сложным? Потому что я могу определить в функции vectorrec промежуточные конструкции, которые я не хочу видеть видимыми для остальной части программы, чтобы сохранить модульность.
Мы можем сделать еще одну коллекцию в полярных координатах
function vectorpol () {
...
function pluspol (u,v) { ... }
function sizepol (v) { return v[0] }
...
return [createpol, xcoordpol, ycoordpol, pluspol, sizepol, parallelpol]
}
Но я, возможно, захочу безразлично использовать обе реализации. Один из способов сделать это - добавить компонент типа ко всем значениям и определить все вышеупомянутые функции в одной и той же среде: затем я могу определить каждую из возвращаемых функций, чтобы она сначала проверила тип координат, а затем применила конкретную функцию для этого.
function vector () {
...
function plusrec (u,v) { ... }
...
function pluspol (u,v) { ... }
...
function plus (u,v) { if u[2]='rec' and v[2]='rec'
then return plusrec (u,v) ... }
return [ ..., plus, ...]
}
Что я получил: конкретные функции остаются невидимыми (из-за локальной области видимости идентификаторов), а остальная часть программы может использовать только самые абстрактные функции, возвращаемые при вызове vectorclass.
Одно возражение состоит в том, что я мог бы непосредственно определить каждую из абстрактных функций в программе и оставить внутри определение зависимых функций типа координат. Тогда будет скрыт также. Это правда, но тогда код для каждого типа координат будет разрезан на мелкие фрагменты, разбросанные по программе, что будет менее масштабируемым и обслуживаемым.
На самом деле, мне даже не нужно давать им имя, и я мог бы просто сохранить как анонимные функциональные значения в структуре данных, проиндексированной по типу и строке, представляющей имя функции. Эта структура, являющаяся локальной для вектора функции, будет невидима для остальной части программы.
Чтобы упростить использование, вместо того, чтобы возвращать список функций, я могу вернуть единственную функцию с именем apply, принимающую в качестве аргумента явно заданное значение и строку, и применить функцию с правильным типом и именем. Это очень похоже на вызов метода для класса OO.
Я остановлюсь здесь, на этой реконструкции объектно-ориентированного объекта.
Я попытался показать, что не так уж сложно построить пригодную для использования объектную ориентацию на достаточно мощном языке, включая наследование и другие подобные функции. Метациркулярность интерпретатора может помочь, но в основном на синтаксическом уровне, который все еще далек от незначительного.
Первые пользователи объектной ориентации экспериментировали с концепциями именно так. И это в целом верно для многих улучшений языков программирования. Конечно, теоретический анализ также играет определенную роль и помогает понять или уточнить эти концепции.
Но идея о том, что языки, не имеющие ОО-функций, обречены на провал в некоторых проектах, просто необоснованна. В случае необходимости они могут достаточно эффективно имитировать реализацию этих функций. Многие языки обладают синтаксической и семантической способностью достаточно эффективно ориентировать объект, даже если он не встроен. И это больше, чем аргумент Тьюринга.
ООП не учитывает ограничения других языков, но поддерживает или применяет методологии программирования, которые помогают писать лучшую программу, помогая тем самым менее опытным пользователям следовать передовым методам, которые использовали более продвинутые программисты и разрабатывали без этой поддержки.
Я считаю, что хорошей книгой для понимания всего этого может быть Abelson & Sussman: структура и интерпретация компьютерных программ .