Comments 23
Не надо переусложнять.
Функциональное программирование – это строго математичная вещь, там почти всё расписано до аксиом формальным символьным выводом. А паттерны проектирования - некие практические наблюдения, советы, обобщающие практику. Не пытайтесь объяснить первое через второе, это значительный шаг в сторону удаления от ясности ума.
Ну и замыкания в ФП не "замораживают" значения, они только сохраняют лексический контекст. Захват значений в замыканиях – это сиплюсплюсовская объектно-ориентированная примочка.
В статье вы используете частично вычисленные функции, это не замыкания. И никаких экземпляров у них тоже нет, это получение одной функции в результате каррирования другой функции. Являются ли функции умножения на 0.2 и умножения на 0.3 двумя экземплярами функции умножения? Нет, это три разные функции. А то вы так дорассуждаетесь до того, что все программы являются экземплярами стандартной библиотеки.
Функция - синглтон в любых языках. Функции создаются в момент интерпретации кода программы (в том числе при eval). У функции есть явные и неявные параметры. К неявным относятся: лексический контекст и объектный контекст (ссылка на объект). Последний в некоторых языках всё же явный. Замыкание - экземпляр функции с хотя бы одним заданным неявным параметром. Это буквально рантайм структура из 2-3 ссылок. JIT компилятор может оптимизировать такие замыкания, на лету создавая новые функции, где неявные аргументы подставлены в код и оптимизированы.
Я говорю только о том, что не надо предельно математически ясное в ФП понятие функции, которое имеет формальное аксиоматическое определение семантики (можно посмотреть, например, на 66 странице R7RS) объяснять мутным словом "синглтон".
Хотя, конечно, функцию можно привести как пример синглтона. Но условный и размытый, как всё в паттернах проектирования. А если я порождаю несколько копий кода функции и дальше, например, занимаюсь модификацией этого кода (в том языке, который это позволяет)? Это тогда уже и не синглтон будет. Вопрос практики применения.
Я в своей статье рассматривал "синглтон" в контексте lifestyle - сколько экземпляров одной функции порождается в процессе выполнения программы. Вот и по мнению коллеги @nin-jin функции в программах создаются в одном экземпляре (если я правильно понял его коммент выше). Паттерн ОО-проектирования "синглтон" к моим рассуждениям отношения не имеет. Но очень легко запутаться, когда два разных понятия имеют одно и то же имя.
замыкания в ФП не "замораживают" значения, они только сохраняют лексический контекст
Но при этом разные лямбды, созданные в одном месте в исходнике, могут под одним именем ссылаться на разные объекты - так что это никак не сиглтон.
Не очень понял вашу мысль. Разные лямбды представляют собой разные сущности, каждая из которых в отдельности может являться синглтоном.
Ну банально
* (defun mycar (p) (lambda () (car p)))
MYCAR
* (defvar p1 (cons 1 1))
P1
* (defvar p2 (cons 2 2))
P2
* (defvar c1 (mycar p1))
C1
* (defvar c2 (mycar p2))
C2
* (funcall c1)
1
* (funcall c2)
2
Лексически у нас одна лямбда в первой строке, но дальше создаются разные инстансы с разным поведением.
каждая из которых в отдельности может являться синглтоном.
Всё таки синглтон подразумевает, что при конструировании объекта мы каждый раз получаем одну и ту же сущность. А так то любой объект можно считать синглтоном самого себя )
Я бы сказал, что это скорее ловушка неполного символьного обозначения, чем сущностная проблема. Если записывать значения лямбд в каком-нибудь гипотетическом языке, где семантика лиспа будет явно отражена в синтаксисе, то мы должны написать что-то вроде:
c1 = (lambda () (car p)) {в контексте p=p1}
c2 = (lambda () (car p)) {в контексте p=p2}
и это две отчётливо разные лямбды.
Просто так исторически повелось, что контекст имени в синтаксисе опускается. Но в семантике-то он присутствует.
Из статьи складывается ощущение, что ФП - следующий этап развития после ООП, а ПП - вообще какой-то мезозой. Однако, стоит отметить, что ФП и объектная декомпозиция - вещи ортогональные. Объекты, точно так же могут быть неизменяемыми и содержать лишь чистые методы. И наоборот, грязные методы могут описывать идемпотентные инварианты между атрибутами объектов. Вот вам пример на ОРП на подумать:
// чистый объект
class TaxCalculator extends PureObject {
amount() { return 0 }
vat() { return this.amount() * 0.2 }
sales() { return this.amount() * 0.07 }
total() { return this.vat() + this.sales() }
}
class MyBasket extends ReactiveObject {
// чистая функция, но грязный метод
@mem cost( next ) { return next ?? 100 }
// это вообще процедура в объекте
@mem change( diff ) {
this.cost( this.cost() + diff )
}
// а тут у нас ленивая фабрика с реактивным связыванием
@mem taxes() {
return new TaxCalculator({
amount: ()=> this.cost()
})
}
// вызывается каждый раз, когда налоги реально меняются
@mem loging() {
console.log( 'taxes: ', this.taxes().total() )
}
}
Мне сложно "с листа" понять, о чём этот код - мне не хватает бэкграунда. Например, PureObject
, ReactiveObject
- я не знаю, что это за классы. @mem
- это похоже на декоратор, я тоже не знаю, что он делает. Вы показываете "верхушку айсберга" и спрашиваете, что "под водой". Я не знаю. Реактивность не находится в фокусе моих интересов, чтобы я этим заинтересовался. Я даже "чистый объект" не осилил (так и не понял, чем он отличается от "грязного"?). Почему тут наследование от PureObject
, а в примере - от $mol_object
? В общем, это для меня слишком сложно. Но спасибо, что предложили подумать.
PureObject
, ReactiveObject
вставлены для простоты понимания кода, можно и без них. $mol_object даёт фабрику make, через которую удобно переопределяются методы при создании. @mem мемоизирует последнее возвращённое значение.
Пример в ООП части, я бы не согласился что это ООП, по-моему это процедура calculate обернутая в неймспейс TaxCalculator.
По-моему ООП не работает без контекста юзкейсов для которых делаются классы.
Нужно создать класс, который представит значение размера налога в зависимости от цены и от налога - это и есть юзкейс.
Я бы такой код написал в ООП.
class TaxOfPrice {
constructor(price, tax) {
this.price = price;
this.tax = tax;
}
valueOf() {
return (this.price * this.tax).toFixed(2);
}
toString() {
return this.valueOf();
}
}
class Tax {
constructor(tax) {
this.tax = tax;
}
valueOf() {
return this.tax;
}
}
const vat = new Tax(0.2);
const sales = new Tax(0.07);
console.log(`of price 100 vats = ${new TaxOfPrice(100, vat)}`); // of price 100 vats = 20.00
console.log(`of price 100 sales = ${new TaxOfPrice(100, sales)}`); // of price 100 sales = 7.00
console.log(`of price 100 vats and sales = ${new TaxOfPrice(100, vat + sales)}`); // of price 100 vats and sales = 27.00
И это не синглтоны =). Как по мне в ООП синглтоны не очень нужны именно для ООП. Они скорее нужны иногда для какой-то оптимизации на уровне компьютера. а на уровне ООП по-моему не нужны - чисто логически.
Напоминает дискуссии средневековых схоластиков на тему "сколько ангелов уместится на острие иглы" )
А чисто по практическим соображениям - допустим, у вас есть чудный код, который вот прямо сейчас надо поправить, а разработчика уже нет.
Посадите за компьютер хоть кого-то, кто js последний раз в школе учил - и он разберется, что делает функция:
function vat(amount) {
return calculateTax(0.2, amount); // 20% НДС
}
Но теперь вам нужно еще добавить еще расчет налогов сотрудника:
- взять ФОТ, из него вычесть, сколько там, 18% соц. фонд, из него же 3% медстрах, из ОСТАВШЕГОСЯ 13% НДФЛ, а остаток выдать в руки сотруднику (цифры точно не помню, не суть важно)
Ваш "новый программист" берет за основу старый код, практически копипасту, и пишет:
function to_ss(amount){
return calculateTax(0.18, amount); // 18% соц.
}
function to_ms(amount){
return calculateTax(0.03, amount); // 3% мед.
}
function to_ndfl(amount){
return calculateTax(0.13, amount); // 13% НДФЛ.
}
const ss = to_ss(1000);
const ms = to_ms(1000);
const ndfl = to_ndfl(1000 - ss - ms);
const pay = 1000 - ss - ms - ndfl;
Этого будет достаточно чтобы не попасть ни под санкции со стороны налоговой, ни под санкции со стороны какой-нибудь комиссии по труду.
Всё просто и наглядно, и на вопрос сотрудника "Э, где моя тыща?" - ответ виден сразу.
А теперь допустим что у вас был код вот такой:
const calculateTax = (taxRate) => (amount) => amount * taxRate;
const vat = calculateTax(0.2); // 20% НДС
const salesTax = calculateTax(0.07); // 7% налог с продаж
Ну-ка, сходу поменяйте его под новую задачу, и не ошибитесь с порядком вычитания налогов...
Да, это тоже несложно, но уже не наглядно. Уже не говоря про классы...
Это к тому, что налицо усложнение, у этого усложнения должна быть практическая выгода. В чем она тут? Быстрее обрабатывается? Потребляет меньше памяти? Сложнее заменить опытных программистов?
Вот мне тут Игорь Иванович подсказывает:
const calculateTax = (taxRate) => (amount) => amount * taxRate;
const vat = calculateTax(0.2); // 20% НДС
const toSS = calculateTax(0.18); // 18% соц.
const toMS = calculateTax(0.03); // 3% мед.
const toNDFL = calculateTax(0.13); // 13% НДФЛ.
const ss = toSS(1000);
const ms = toMS(1000);
const ndfl = toNDFL(1000 - ss - ms);
const pay = 1000 - ss - ms - ndfl;
В общем, не сильно сложнее вашего варианта. Всё зависит от "заточки" мозгов разраба. Я, например, чистый JS'ник. "Висну" на первом же ts-декораторе - мозги не приучены читать такой код. После Java долго привыкал к PHP, после PHP долго привыкал к JS. Но отвыкаю быстро - полгода и нужно плющить мозг, чтобы понять о чём код. К стрелочным функциям в JS тоже не быстро приспособился, несколько месяцев ушло.
Гипотетически, можно заставить исполняться нечто находящееся в стеке — так gcc в рамках расширения компилятора поступает в C для вложенных функций, вроде как. В таком случае функция уже не будет синглтоном, но во-первых это плюс уязвимость, а во-вторых техника эта вроде как тонкая, низкоуровневая и неприятная.
Если этот пример считать, то всё же функции можно сделать не синглтоном, просто это антипаттерн, который или для странного api, или для какой-то jit-компиляции (не совсем это) и применять.
Похоже на то, что стОит добавлять в заголовок технологию/язык программирования. Мне, как дотнетчику было вообще неясно о чем речь, пока не дошел до кода. Хотя замыкания в сишарпе есть, большинство даже не осознают их наличие, ибо они как неуловимый Джо. Их даже нет смысла классифицировать и как-то обозначать. При этом синглтоны разумеется есть и используются иногда, но они вообще не похожи на ЭТО, как и весь подход к ооп. "Функция это синглтон" воспринимается вообще как метафора, ибо это физически невозможно. Все написанное казалось абракадаброй, пока не обнаружил что это про жс. Предупреждать же надо.
Как-то однажды знаменитый учитель Кх Ан вышел на прогулку с учеником Антоном. Надеясь разговорить учителя, Антон спросил: "Учитель, слыхал я, что объекты - очень хорошая штука - правда ли это?" Кх Ан посмотрел на ученика с жалостью в глазах и ответил: "Глупый ученик! Объекты - всего лишь замыкания для бедных."
Пристыженный Антон простился с учителем и вернулся в свою комнату, горя желанием как можно скорее изучить замыкания. Он внимательно прочитал все статьи из серии "Lambda: The Ultimate", и родственные им статьи, и написал небольшой интерпретатор Scheme с объектно-ориентированной системой, основанной на замыканиях. Он многому научился, и с нетерпением ждал случая сообщить учителю о своих успехах.
Во время следующей прогулки с Кх Аном, Антон, пытаясь произвести хорошее впечатление, сказал: "Учитель, я прилежно изучил этот вопрос, и понимаю теперь, что объекты - воистину замыкания для бедных." Кх Ан в ответ ударил Антона палкой и воскликнул: "Когда же ты чему-то научишься? Замыкания - это объекты для бедных!" В эту секунду Антон обрел просветление.
Путаясь в замыканиях