Многие люди представляют функциональное программирование как нечто очень сложное и «наукоемкое», а представителей ФП-сообщества – эстетствующими философами, живущими в башне из слоновой кости.
До недавнего времени такой взгляд на вещи действительно был недалек от истины: говорим ФП, подразумеваем Хаскель и теорию категорий. В последнее время ситуация изменилась и функциональная парадигма набирает обороты в web-разработке, не без помощи F#, Scala и React. Попробуем взглянуть на «паттерны» функционального программирования, полезные для решения повседневных задач с точки зрения ООП – парадигмы.
ООП широко распространено в разработке прикладного ПО не одно десятилетие. Все мы знакомы с SOLID и GOF. Что будет их функциональным эквивалентом?.. Функции! Функциональное программирование просто «другое» и предлагает другие решения.
Основные принципы функционального проектирования (дизайна)
Функции как объекты первого класса
В отличие от «классического» ООП (первые версии C++, C#, Java) функции в ФП представляют собой самостоятельные объекты и не должны принадлежать какому-либо классу. Удобно представлять функцию как волшебный железнодорожный тоннель: подаете на вход яблоки, а на выходе получаете бананы
(apple -> banana)
.Синтаксис F# подчеркивает, что функции и значения равны в правах:
let z = 1
let add = x + y // int -> int ->int
Композиция как основной «строительный материал»
Если у нас есть две функции, одна преобразующая яблоки в бананы
(apple -> banana)
, а другая бананы в вишни (banana -> cherry)
, объединив их мы получим функции преобразования яблок в вишни (apple -> cherry)
. С точки зрения программиста нет разницы получена эта функция с помощью композиции или написана вручную, главное – ее сигнатура.Композиция применима как на уровне совсем небольших функций, так и на уровне целого приложения. Вы можете представить бизнес-процесс, как цепочку вариантов использования (use case) и скомпоновать их в функцию
httpRequest -> httpResponse
. Конечно это возможно только для синхронных операций, но для асинхронных есть реактивное функциональное программирование, позволяющее сделать тоже самое.Можно представлять себе композицию функций как фрактал. Определение фрактала в строгом смысле не совпадает с определением композиции. Представляя фрактал вы можете визуализировать как ваш control flow состоит из скомпонованных функций, состоящих из скомпонованных функций, состоящих из…
Шаблон компоновщик (Composite) в ООП тоже можно представлять себе «фракталом», но компоновщик работает со структурами данных, а не преобразованиями.
Типы != классы
У системы типов в ФП больше общего с теорией множеств, чем с классами из ООП.
int
– это тип. Но тип не обязательно должен быть примитивом. Customer
– это тоже тип. Функции могут принимать на вход и возвращать функции. int -> int
– тоже тип. Так что «тип» — это название для некоторого множества.Типы тоже можно компоновать. Большая часть функциональных ЯП работает с алгебраической системой типов, отличающейся от системы классов в ООП.
Перемножение (логическое «и», record type в F#)
На первый взгляд это может показаться странным, однако в этом есть смысл. Если взять множество людей и множество дат, «перемножив» их мы получим множество дней рождений.type Birthday = Person * Date
Сложение (логическое «или», discriminated union type в F#)
type PaymentMethod =
| Cash
| Cheque of ChequeNumber
| Card of CardType * CardNumber
Discriminated union – сложное название. Проще представлять себе этот тип как выбор. Например, вы можете на выбор оплатить товар наличными, банковским переводом или с помощью кредитной карты. Между этими вариантами нет ничего общего, кроме того, все они являются способом оплаты.
Однажды нам пригодились «объединения» для моделирования предметной модели.
Entity Framework умеет работать с такими типами из коробки, нужно лишь добавить id.
Стремление к «полноте»
Давайте рассмотрим функцию «разделить 12 на». Ее сигнатура
int -> int
и это ложь! Если мы подадим на вход 0, функция выбросит исключение. Вместо этого мы можем заменить сигнатуру на NonZeroInteger -> int
или на int -> int option
.ФП подталкивает вас к более строгому и полному описанию сигнатур функций. Если функции не выбрасывают исключений вы можете использовать сигнатуру и систему типов в качестве документации. Вы также можете использовать систему типов для создания предметной модели (Domain Model) и описания бизнес-правил (Business Rules). Таким образом можно гарантировать, что операции не допустимые в реальном мире не будут компилироваться в приложении, что дает более надежную защиту, чем модульные тесты. Подробнее об этом подходе вы можете прочитать в отдельной статье.
Функции в качестве аргументов
Хардкодить данные считается дурным тоном в программирование, вместо этого мы передаем их в качестве параметров (аргументов методов). В ФП мы идем дальше. Почему бы не параметризировать и поведение?
Вместо функции с одним аргументом опишем функцию с двумя. Теперь не важно, что это за список и куда мы выводим данные (на консоль или в лог).
let printList anAction aList =
for i in aList do
anAction i
Пойдем дальше. Рассмотрим императивный пример на C#. Очевидно, что в данном коде присутствует дублирование (одинаковые циклы). Для того чтобы устранить дублирование нужно выделить общее и выделить общее в функцию:
public static int Product(int n)
{
int product = 1; // инициализация
for (int i = 1; i <= n; i++) // цикл
{
product *= i; // действие
}
return product; // возвращаемое значение
}
public static int Sum(int n)
{
int sum = 0; // инициализация
for (int i = 1; i <= n; i++) // цикл
{
sum += i;
}
return sum; // возвращаемое значение
}
В F# для работы с последовательностями уже есть функция fold:
let product n =
let initialValue = 1
let action productSoFar x = productSoFar * x
[1..n] |> List.fold action initialValue
let sum n =
let initialValue = 0
let action sumSoFar x = sumSoFar+x
[1..n] |> List.fold action initialValue
Но, позвольте, в C# есть
Aggregate
, который делает тоже самое! Поздравляю, LINQ написан в функциональном стиле :)Рекомендую цикл статей Эрика Липперта о монадах в C#. С десятой части начинается объяснение «монадической» природы SelectMany
Функции в качестве интерфейсов
Допустим у нас есть интерфейс.
interface IBunchOfStuff
{
int DoSomething(int x);
string DoSomethingElse(int x); // один интерфейс - одно дело
void DoAThirdThing(string x); // нужно разделить
}
Если взять SRP и ISP и возвести их в абсолют все интерфейсы будут содержать только одну функцию.
interface IBunchOfStuff
{
int DoSomething(int x);
}
Тогда это просто функция
int -> int
. В F# не нужно объявлять интерфейс, чтобы сделать функции взаимозаменяемыми, они взаимозаменяемы «из коробки» просто по своей сигнатуре. Таким образом паттерн «стратегия» реализуется простой передачей функции в качестве аргумента другой функции:let DoSomethingWithStuff strategy x =
strategy x
Паттерн «декоратор» реализуется с помощью композиции функций
let isEvenWithLogging = log >> isEven >> log // int -> bool
Здесь автор для простоты изложения опускает вопросы семантики. При моделировании реальных предметных моделей одной сигнатуры функции не всегда достаточно.
Каррирование и частичное применение
Итак, использую одну только композицию мы можем проектировать целые приложения. Плохие новости: композиция работает только с функциями от одного параметра. Хорошие новости: в ФП все функции являются функциями от одного параметра.
Обратите внимание, сигнатура
int -> int -> int
не содержит скобок не случайно. Можно воспринимать сложение, как функцию от двух аргументов типа int
, возвращающую значение типа int
или как функцию от одного аргумента, возвращающую функциональный тип int -> int
. Возвращаемая функция будет называться сумматор по основанию n, где n — число переданное аргументом в первую функцию. Повторив эту операцию рекурсивно можно функцию от любого числа аргументов преобразовать в функции от одного аргумента.Такие преобразования возможны не только для компилируемых функций в программировании, но и для математических функций. Возможность такого преобразования впервые отмечена в трудах Готтлоба Фреге, систематически изучена Моисеем Шейнфинкелем в 1920-е годы, а наименование получило по имени Хаскелла Карри — разработчика комбинаторной логики, в которой сведение к функциям одного аргумента носит основополагающий характер.
Возможность преобразования функций от многих аргументов к функции от одного аргумента естественна для функциональных ЯП, поэтому компилятор не будет против, если вы передадите только одно значения для вычисления суммы.
let three = 1 + 2
let three = (+) 1 2
let three = ((+) 1) 2
let add1 = (+) 1
let three = add1 2
Это называется частичным применением. В функциональных ЯП частичное применение заменяет принцип инъекции зависимостей (Dependency Injection)
// эта функция требует зависимость
let getCustomerFromDatabase connection (customerId:CustomerId) =
from connection
select customer
where customerId = customerId
// а эта уже нет
let getCustomer1 = getCustomerFromDatabase myConnection
Продолжения (continuations)
Зачастую решения, закладываемые в реализацию, оказываются не достаточно гибкими. Вернемся к примеру с делением. Кто сказал, что я хочу, чтобы функция выбрасывала исключения? Может быть мне лучше подойдет «особый случай»
int Divide(int top, int bottom)
{
if (bottom == 0)
{
// кто решил, что нужно выбросить исключение?
throw new InvalidOperationException("div by 0");
}
else
{
return top/bottom;
}
}
Вместо того, чтобы решать за пользователя, мы можем предоставить решение ему:
void Divide(int top, int bottom, Action ifZero, Action<int> ifSuccess)
{
if (bottom == 0)
{
ifZero();
}
else
{
ifSuccess( top/bottom );
}
}
Если вы когда-нибудь писали асинхронный код, то наверняка знакомы с «пирамидой погибели» (Pyramid Of Doom)
Продолжения позволяют исправить этот код и избавиться от уровней вложенности. Для этого необходимо инкапуслировать условный переход в функцию:
let ifSomeDo f opt =
if opt.IsSome then
f opt.Value
else
None
И переписать код, используя продолжения
let example input =
doSomething input
|> ifSomeDo doSomethingElse
|> ifSomeDo doAThirdThing
|> ifSomeDo (fun z -> Some z)
Монады
Монады – это одно из «страшных» слов ФП. В первую очередь, из-за того, что обычно объяснения начинаются с теории категорий. Во вторую — из-за того что «монада» — это очень абстрактное понятие, не имеющее прямой аналогии с объектами реального мира. Я большой сторонник подхода «от частного к общему». Поняв практическую пользу на конкретном примере проще двигаться дальше к более полному и абстрактному определению.
Зная о «продолжениях», вернемся к аналогии с рельсами и тоннелем. Функцию, в которую передаются аргумент и два «продолжения» можно представить как развилку.
Но такие функции не компонуются :(
На помощь приходит функция bind
let bind nextFunction optionInput =
match optionInput with
// передаем результат выполнения предыдущей функции в случае успеха
| Some s -> nextFunction s
// или просто пробрасываем значение None дальше
| None -> None
Код пирамиды погибели может быть переписан с помощью
bind
// было
let example input =
let x = doSomething input
if x.IsSome then
let y = doSomethingElse (x.Value)
if y.IsSome then
let z = doAThirdThing (y.Value)
if z.IsSome then
let result = z.Value
Some result
else
None
else
None
else
None
// стало
let bind f opt =
match opt with
| Some v -> f v
| None -> None
let example input =
doSomething input
|> bind doSomethingElse
|> bind doAThirdThing
|> bind (fun z -> Some z)
Кстати, это называется «monadic bind». Скажите своим друзьям, любителям хаскеля, что вы знаете, что такое «monadic bind» и вас примут в тайное общество:)
Bind можно использовать для сцепления асинхронных операций (промисы в JS устроены именно так)
Bind для обработки ошибок
Если у вас появилось смутное ощущение, что дальше идет описание монады Either
, так оно и есть
Рассмотрим код на C#. Он выглядит достаточно хорошо: все кратко и понятно. Однако в нем отсутствует обработка ошибок. Действительно, что может пойти не так?
string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
validateRequest(request);
canonicalizeEmail(request);
db.updateDbFromRequest(request);
smtpServer.sendEmail(request.Email)
return "OK";
}
Мы все знаем, что обрабатывать ошибки нужно. Добавим обработку.
string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
var isValidated = validateRequest(request);
if (!isValidated)
{
return "Request is not valid"
}
canonicalizeEmail(request);
try
{
var result = db.updateDbFromRequest(request);
if (!result)
{
return "Customer record not found"
}
}
catch
{
return "DB error: Customer record not updated"
}
if (!smtpServer.sendEmail(request.Email))
{
log.Error "Customer email not sent"
}
return "OK";
}
Вместо шести понятных теперь 18 не понятных строчек. Это 200% дополнительных строчек кода. Кроме того, линейная логика метода теперь зашумлена ветвлениями и ранними выходами.
С помощью
bind
можно абстрагировать логику обработки ошибок. Вот так будет выглядеть метод без обработки ошибок, если его переписать на F#:А вот этот код но уже с обработкой ошибок:
Более подробно эта тема раскрыта в отдельном докладе.
Функторы
Мне не очень понравилось описание функторов у Скотта. Прочитайте лучше статью «Функторы, аппликативные функторы и монады в картинках»
Моноиды
К сожалению, для объяснения моноидов не подходят простые аналогии. Приготовьтесь к математике.
Я предупредил, итак, математика
- 1 + 2 = 3
- 1 + (2 + 3) = (1 + 2) + 3
- 1 + 0 = 1
0 + 1 = 1
И еще немного
- 2 * 3 = 6
- 2 * (3 * 4) = (2 * 3) * 4
- 1 * 2 = 2
2 * 1 = 2
Что общего между этими примерами?
- Есть некоторые объекты, в данном случае числа, и способ их взаимодействия. Причем результат взаимодействия — это тоже число (замкнутость).
- Порядок взаимодействия не важен (ассоциативность).
- Кроме того, есть некоторый специальный элемент, взаимодействие с которым не меняет исходный объект (нейтральный элемент).
За более строгим определением обратитесь к википедии. В рамках статьи обсуждается лишь несколько примеров применения моноидов на практике.
Замкнутость
Дает возможность перейти от попарных операций к операциям на списках
1 * 2 * 3 * 4
[ 1; 2; 3; 4 ] |> List.reduce (*)
Ассоциативность
Применение принципа «разделяй и властвуй», «халявная» параллелизация. Если у нашего процессора 2 ядра и нам нужно рассчитать значение
1 + 2 + 3 + 4
. Мы можем вычислить 1 + 2
на первом ядре, а 3 + 4
— на втором, а результат сложить. Больше последовательных вычислений — больше ядер.Нейтральный элемент
С
reduce
есть несколько проблем: что делать с пустыми списками? Что делать, если у нас нечетное количество элементов? Правильно, добавить в список нейтральный элемент.Кстати, в математике часто встречается определение моноида как полугруппы с нейтральным элементом. Если нейтральный элемент отсутствует, то можно попробовать его доопределить, чтобы воспользоваться преимуществами моноида.
Map / Reduce
Если ваши объекты — не моноиды, попробуйте преобразовать их. Знаменитая модель распределенных вычислений Google — не более чем эксплуатация моноидов.
Эндоморфизмы
Функции с одинаковым типом входного и выходного значения являются моноидами и имеют специальное название — «эндоморфизмы» (название заимствовано из теории категорий). Что более важно, функции, содержащие эндоморфизмы могут быть преобразованы к эндоморфизмам с помощью частичного применения.
Грег Янг открыто заявляет, что Event Sourcing — это просто функциональный код. Flux и unidirectional data flow, кстати тоже.
Монады VS моноиды
Монады являются моноидами, ведь как известно, монада — это всего лишь моноид в категории эндофункторов, а монадические законы — не более чем определение моноида в контексте продолжений.
Кстати, бастион ООП — GOF тоже содержит монады. Паттерн «интерпретатор» — это так называемая свободная монада.
Комментарии (279)
TheShock
18.09.2017 03:04+26Ваше сравнение паттернов с ФП — манипулятивная картинка для глуповатых программистов. Чистой воды демагогия. Специально для хипстоты, которая любит кричать: «ааа, ооп — такое глупое, не то, что фп», но при этом не очень любит думать.
Точно такой же бред можно сочинить и в обратную сторону:
— Функции высшего порядка / Класс
— Каррирование / Класс
— Композиция функций / Класс
— Предикат / Класс
— Функтор / Класс
— Лифт / Класс
— Моноид / Класс
— Монада / Класс
И так далее из списка "жаргона ФП"
Паттерны — это просто примеры некоторых общепринятых подходов, при помощи которых можно решать задачи. И таких не существует разве что в тех парадигмах, которыми никто не пользуется.
Особо смешно, что сперва автор приводит эту картинку, которая показывает, как все просто в ФП решается просто функциями, а потом зачем-то пишет статью с кучей практик (их можно назвать даже паттернами, лол). Странно, что у него мозг не взрывается от такого диссонанса. Шапку в мороз носите и выбирайте теплую, а не красивую.
Ну вот даже это что — «просто функция»? Или, все-таки, «паттерн»?
что делать с пустыми списками? Что делать, если у нас нечетное количество элементов? Правильно, добавить в список нейтральный элемент
И да, на счет КДПВ. Я, конечно, могу ошибаться, но, насколько я знаю, в ФП, Лисп — это просто баловство вроде JS в сравнении с Haskell, потому мне непонятно, почему он улетел так высоко.spmbt
18.09.2017 04:43+1Ношение красивой, но не тёплой шапки в мороз повышает вероятность дополнительного финансирования, на утеплитель. Это знают все теоретики красивых стилей, но не говорят о конечной цели: ).
AndreyRubankov
18.09.2017 10:17+5Добавлю еще про манипуляции в статье:
А вот этот код но уже с обработкой ошибок:
На самом деле, данный код совсем не соответствует семантике приведенного в пример «ужасного ООП с проверкой ошибок».
В ООП, каждая ошибка выдает какой-то свой результат, в ФП варианте – все ошибки просто игнорируются. Если добавить проверку ошибок как в ООП, то будет не на много лучше чем в ООП, на который автор жаловался.
ФП вариант с проверкой ошибок соответствует ООП варианту без проверки, где каждый метод может кинуть RuntimeException в случае ошибки.marshinov Автор
18.09.2017 10:43-1ФП вариант с проверкой ошибок соответствует ООП варианту без проверки, где каждый метод может кинуть RuntimeException в случае ошибки.
Здесь я с вами согласен, но только для однопоточного кода. С TPL вариант с exception'ами уже не такой хороший. Кроме этого, нужно гарантировать, что в программе только один catch, иначе высок риск «проглатывания» ошибок.AndreyRubankov
18.09.2017 11:24-1TPL
Это уже смесь фп и ооп подходов и является нетрадиционным. Потому этот кейс не стоит брать во внимание – там другие правила.
Кроме этого, нужно гарантировать, что в программе только один catch, иначе высок риск «проглатывания» ошибок.
Не обязательно.
Эксепшены должны ловиться теми, кто их должен обрабатывать. Под обработкой так же подразумевается возможность обернуть в другой эксепшн и кинуть дальше.
Это все тот же ".catch()" в промисах, но в промисах он менее гибкий.marshinov Автор
18.09.2017 11:32+1Потому этот кейс не стоит брать во внимание – там другие правила
Если вас кейс на данный момент не слишком волнует, это не значит, что его не стоит брать во внимание. Вот был у вас однопоточный код, стал он выполняться двое суток. Надо параллелить, а у вас внутри вездеthrow
. Весь код на помойку, переписываем на TPL с нуля? Весело, но дорого.AndreyRubankov
18.09.2017 12:10-11. Мои предыдущие комменты в этом треде относятся конкретно к данной статье, а не к всевозможным особенностям реализации каких-то фреймворков.
2. Теперь конкретно ваш пример:Вот был у вас однопоточный код, стал он выполняться двое суток. Надо параллелить, а у вас внутри везде throw.
Это самое плохое решение в данном случае.
Сегодня Вы распараллелили код и получили профит, но проблема так и осталась – вы ее просто замаскировали и Вы с нею столкнетесь через время в еще больших масштабах.
Такие проблемы не появляются на ровном месте, а если вы с нею столкнулись, то у вас проблемы гораздо большего масштаба, чем «у нас в коде везде throw».marshinov Автор
18.09.2017 12:24+1Такие проблемы не появляются на ровном месте, а если вы с нею столкнулись, то у вас проблемы гораздо большего масштаба, чем «у нас в коде везде throw».
Случай из жизни. Жили были программисты. Совместили они логику валидации и прасинга документов. И выбрасывали они исключения для валидации. И нормально все было, пока не стали присылать сводные xls-файлы на несколько десятков миллионов строк. И, нет-нет, нельзя клиенту объяснить, что обмен xls-файлами такого размера не самое оптимальное решение.
Ждать сутки для разбора этого файла последовательно — не вариант, тем более, что железо на проде позволяет и не все строки файлов связаны.Map / Reduce
— прям то, что нужно. В итоге не контролируемые побочные эффекты (функции валидации, дергающие БД и выбрасывающие исключения) значительно затруднили мне тот самыйMap / Reduce
.
УEither
есть сильные и слабые стороны. Вы продолжаете настаивать на том, что есть один православный способ на все случаи жизни, а все остальное к дело не относится и вообще другой случай. У автора пример тоже максимально простой, чтобы не пугать монадами и прочими эндофункторами. В ASP.NET MVC с обработкой ошибок действительно хорошо — можно обработать все декларативно. Но вы точно уверены, что у вас все исключения должны возвращать код 500? 401, 412, 422, не? Посмотрите доклад про rop целиком, прежде чем судить. Там есть много здравых мыслей.raveclassic
18.09.2017 12:30+3А еще вместо Either можно взять аналог Validation
0xd34df00d
18.09.2017 18:53Эм, а разве
AccValidation
обеспечивает семантику early return?raveclassic
18.09.2017 20:52Ммм, а должна? Это ж аппликатив, аккумулирующий все ошибки в полугруппе "слева"?
0xd34df00d
18.09.2017 20:54А зачем тогда оно вообще надо? Завернуть в MonadWriter, и всё.
В Either и ей подобных-то у вас основная прелесть в том, что в последующих функциях вам не нужно думать о том, что предыдущие могли завершиться с ошибкой — вы рассматриваете только безошибочную ветку.
raveclassic
18.09.2017 23:38Так ведь Writer просто собирает эффекты в моноид слева, по-прежнему предоставляя доступ к значению.
Validation же — своего рода Either, но со сбором всех ошибок в полугруппу (причем удобно в NonEmptyArray) без доступа к значению при провале (так как его, значения, нет). Поэтому, например, в scala cats Validated описывается как способ параллельной валидации, а Either — последовательной.
Если вы про early return для map функтора, то там да, все ок — failure-ветка "закорачивается".
PS. Вы можете меня поправить, я пока разбираюсь со всем этим добром :)
0xd34df00d
19.09.2017 00:01Без примеров кода сложно.
Validation же — своего рода Either, но со сбором всех ошибок в полугруппу (причем удобно в NonEmptyArray) без доступа к значению при провале (так как его, значения, нет).
А это как?
Вот у меня есть
foo :: a -> AccValidation err1 b
иbar :: b -> AccValidation err2 c
. Как я могу вызватьbar
, еслиfoo
мне вернула ошибку, и никакогоb
нет?
А, ну, собственно,
AccValidation
не является монадой, её там в списке инстансов нет. Ожидаемо.raveclassic
19.09.2017 00:34Без примеров кода сложно.
Примеры там на гитхабе есть
Как я могу вызвать bar, если foo мне вернула ошибку, и никакого b нет?
Никак, так как при попытке определить
bind/flatMap
ломаетсяap
и перестает аггрегировать ошибки. Так что только черезap
и с одинаковым типом значения. Но, вроде, можно считерить и ненадолго перегнать в Either. В cats даже есть хелпер withEither
mayorovp
18.09.2017 12:44+1В итоге не контролируемые побочные эффекты (функции валидации, дергающие БД и выбрасывающие исключения) значительно затруднили мне тот самый Map / Reduce.
Что-то мне кажется, что дерганье БД было намного большей проблемой чем исключения.
nikolayv81
20.09.2017 08:36ИМХО так и было, из личного опыта буквально на днях, с учётом ограничений xls на 1е6 строк в листе, не оптимизированный код грузит полный лист в БД как есть примерно за минуту(а вот если построчно вставлять теже самые пол миллиона, то выходит только на вставку в одной транзакции около получаса), дальнейшая обработка инструментами Oracle это никак не часы, видать просто при разработке использовались тестовые наборы по 100 строк на файл, а потом поставили на prod и внедрили.
AndreyRubankov
18.09.2017 12:48+2Да, знакомая ситуация. Но тут ведь другая проблема:
Спроектированная система была не предназначена для такого – это архитектурная проблема.
Решение архитектурных проблем – это всегда больно. Даже если бы у Вас не было бы throw, но была бы логика хождения в базу и изменения какого-то общего состояния. Вы бы мучались не меньше. Но в этом случае вы бы проклинали «Stateful». Но вы упускаете тот момент, что Вы переделывали Архитектуру уже работающего приложения.
Вы переделывали самолет в вертолет во время полета, а жалуетесь на то, что заклепки хуже болтов.marshinov Автор
18.09.2017 13:40+1В данном случае проблема вообще в нарушении SRP изначально как мне кажется. Исключения и доступ к БД уже сверху наложились.
Если бы валидация была чистой функцией и если бы ее потрудились выделить в отдельный класс, а необходимые данные загружались бы из бд на основе спецификаций через QueryObject, инжектируемого через интерфейсную ссылку с помощью IOC-контейнера, то мне бы осталось только немного перекомпоновать код и распараллелить. Но этого не случилось.
В данном случае есть «правильный» вариант (болты). Нужно было их сразу ставить. Была бы дополнительная сложность, кривая обучения, но я заметил, что по примеру джуниоры копипастят код разного качества примерно с одной эффективностью. Лучше учить копипастить сразу хороший:)
Я строго на стороне дяди Боба: ФП и ООП — инструменты, не заменяющие, а дополняющие друг-друга. Полезно знать и применять при необходимости приемы и той и другой.
На мой взгляд ФП поднимает ряд вопросов, традиционно игнорируемых ООП-сообществом: формальное доказательство корректности программ (вместо решения задачи численным методом: юнит-тестами), точное определение возможных случаев и реакций на них. С помощьюEither
можно перенести проверку на этап компиляции с рантайма. Как известно, чем раньше найдена ошибка, тем дешевле ее поправить.
Практическая применимость и tool-support таких концепций — отдельный вопрос. Я не считаю, что IO — супер-элегантное решение проблемы ввода-вывода в хаскеле: «посмотрите это выглядит как императивный код, но это не он»:)
mayorovp
18.09.2017 12:41+1А что не так в TPL с исключениями?
marshinov Автор
18.09.2017 12:52Вы правы, что сам по себе TPL не при чем. Просто в многопоточном / асинхронном коде проблемы обработки исключений (exception handling) становятся более очевидными, особенно то, что касается отслеживания exit points.
Вообще монады vs исключения vs коды возврата тема, мягко говоря, спорная:)mayorovp
18.09.2017 12:54+2Хорошо, а в чем особенность отслеживания exit points в многопоточном коде?
Druu
18.09.2017 12:58+2> В ООП, каждая ошибка выдает какой-то свой результат, в ФП варианте – все ошибки просто игнорируются.
Это в maybe. Если в either, то не будут игнорироваться.AndreyRubankov
18.09.2017 13:09Там конкретный код взятый из статьи, который игнорирует ошибки. Никаких «если».
ps: можете привести пример правильного кода, который будет обрабатывать ошибки точно так же, как это делается в ООП варианте? Будет интересно посмотреть, на сколько он будет лучше смотреться.Druu
18.09.2017 13:14+2> там конкретный код взятый из статьи
Там ведь код на картинке будет тот же самый. Просто в другой монаде (она там, вообще говоря, не указана, просто мы из контекста знаем, что автор говорил о Maybe).
AndreyRubankov
18.09.2017 13:27+2Отлично, я в целом про это и говорил. Автор спекулирует кодом.
В ООП версии но ставил некрасивые обработчики, чтобы показать всю уродливость, хотя так никто не делает.
А в ФП версии кода, он даже не добавил никаких обработчиков, мол «вот смотрите как красиво!».
Если бы автор привел пример, где каждая ветка, которая может пойти не так выдавала бы свое сообщение об ошибке, это выглядело бы уже не так красочно, как он показывал ;-)marshinov Автор
18.09.2017 13:50Ошибки там не игнорируются. Возвращается первая ошибка, дальше выполнение не идет. Druu верно отметил: есть разница между
Maybe
иEither
. Кстати, Скотт не кисло глумится над тем, как обычно объясняют монады апологеты ФП и какое впечатление это производит.AndreyRubankov
18.09.2017 14:04+2К сожалению в репозитории я не нашел указанного метода.
Под «игнорируются» я имел ввиду, что в данном коде нету явных обработчиков ошибок. Они будут где-то снаружи (или внутри).
После ошибки в первой функции мы «перейдем на красные пути» и выйдем из функции с результатом, который говорит про ошибку – я прав?
И тут вопрос, который я изначально подымал: Кто обрабатывает эти ошибки?
Судя по коду есть 2 варианта:
1. ошибки обрабатываются внутри каждой функции, которую вызывает updateCustomerWithErrorHandling;
2. ошибка обрабатывается снаружи updateCustomerWithErrorHandling.
В обоих случаях – мы получаем несоответствие между двумя примерами кода.0xd34df00d
18.09.2017 18:55Ошибки и в ООП-стиле не обрабатываются. Всё, что там проверяется — что функция не кинула исключение или не вернула код ошибки, и если она его вернула, то оно просто передаётся вызывающему коду. Никакого кода для попыток восстановления от ошибки там нет.
В случае Either это всё красиво и аккуратно спрятано под капотом bind для Either.
marshinov Автор
18.09.2017 14:20-1Вы по ссылке пройдите все-таки. Там ещё один полуторачасовой доклад, посвященны этой теме. С примерами и доскональным разбором как же эта магия работает. Продолжительность доклада «паттерны фп» — ещё полтора часа. Если тема будет востребована, я могу попробовать собраться с силами и перевести и его:)
vbif
18.09.2017 16:31Всё очень просто, вот вам полуторочасовой про то, как просто этим пользоваться, а потом ещё один доклад— ещё полтора часа.
0xd34df00d
18.09.2017 18:55+1А вы на ООП-языках за 10 минут все паттерны познали?
Druu
18.09.2017 15:54+2> Если бы автор привел пример, где каждая ветка, которая может пойти не так выдавала бы свое сообщение об ошибке
Так эти ветки внутри функций за |>
Функции выдают ошибку. В коде с монадами нам не надо при этом создавать ветки в вызывающем коде, а в коде с ООП — нужно было бы.mayorovp
18.09.2017 16:00Так, может быть, тогда и [кажущаяся] разница в красоте ООП- и ФП-решений связана исключительно с тем, в каком месте размещается проверка и возврат сообщения об ошибке?
Druu
18.09.2017 16:05+1Ну да, исключения по control-flow изоморфны maybe/either/etc или что там еще. Однако, вариант с монадами лучше исключений тем, что, ну, там нет исключений :)
Плюс — всегда можно упороться трансформерами и засовывать туда на call site дополнительную логику, не меняя сам код. Но я лично не ярый сторонник подобного.mayorovp
18.09.2017 16:17+4Эдак можно и про вариант с исключениями сказать что он лучше монад тем что в нем нет монад :-)
Druu
18.09.2017 16:31Дык монады это не сущность, это просто то, как мы называем определенную конструкцию. А так там обычные ф-и — к которым монады полностью сводятся. И именно в том, что у монад под капотом обычные функции, которые ведут себя как обычные функции — и есть преимущество «монадического» решения. Не требуются специальные костыли рантайма, информация о наличии ошибок — содержится в типе, при наличии статической типизации — компилятор гарантирует что ошибка будет обработана, ну и поскольку это все — first-class citizen, то оно может быть допилено под конкретные нужды. С исключениями такое уже не прокатит.
mayorovp
18.09.2017 16:48Монада — это тип данных, то есть нечто существующее в рантайме. Чем же в таком случае отличаются монады и костыли рантайма? Почему раскрутка стека — костыль рантайма, а целый лишний тип данных — нет?
По поводу типизации — вон в Java есть checked exceptions. Все как вы написали — информация содержится в типе, компилятор гарантирует что исключение будет поймано. И даже под свои нужны исключения прекрасно допиливаются.
Druu
18.09.2017 16:52+3> Монада — это тип данных, то есть нечто существующее в рантайме. Чем же в таком случае отличаются монады и костыли рантайма?
Монады — это то, как мы используем некий набор функций. Способ. Мы можем этот способ инкапсулировать в тип данных, но это совсем не обязательно. И этот способ мы можем реализовать сами на основе базовых объектов, функций, и поправить его как угодно. Никакой поддержки со стороны компилятора или рантайма при этом не требуется. Исключения, в противовес, это особый объект, который дан нам разработчиком языка, вещь в себе.
> По поводу типизации — вон в Java есть checked exceptions.
И они по определенным причинам не взлетели.mayorovp
18.09.2017 16:58+3И они по определенным причинам не взлетели.
И эти причины заключаются в том, что на самом деле никому не нужна столь строгая типизация. Тем не менее, если конкретно вам она нужна — возможность ее использовать есть.
Druu
18.09.2017 17:06> Тем не менее, если конкретно вам она нужна — возможность ее использовать есть.
Я могу написать ф-ю, которая принимает лямбду с заданными checked exceptions? Нет.
Проблема у checked exceptions не с тем, что они, с-но, checked exceptions, а с тем, что их хреново реализовали в джаве. Можно реализовать по-человечески — но штука в том, что в этом случае оно будет только названием отличаться от встроенной в язык монады Either. Точнее — это _и будет_ монада Either!mayorovp
18.09.2017 17:14+2Я могу написать ф-ю, которая принимает лямбду с заданными checked exceptions? Нет.
raveclassic
18.09.2017 17:16-2Так это ж не лямбда, а чит чистой воды
TheShock
18.09.2017 17:18-1Почему?
raveclassic
18.09.2017 17:26-1Может, потому-что это не лямбда, а объект с методом
call
?mayorovp
18.09.2017 17:31И в чем же принципиальная разница?
Java довольно многословна, это факт. Но чем кроме 4 букв и 1 точки этот объект отличается от лямбды?
raveclassic
18.09.2017 17:38-1Странно, что вы тогда не
apply
использовали. Судя по докам, так оно и реализуется.mayorovp
18.09.2017 17:39Э, вы вообще о чем?
raveclassic
18.09.2017 17:43Ну, запись
x -> x + 2
это же тоже самое, что
new Function<Number, Number>() { @Override public Number apply(x: Number) { return x + 2; } }
Не? Я не пишу на джаве
mayorovp
18.09.2017 18:54Это зависит от требуемого интерфейса. У Function метод называется apply, а у Callable — call. В своем интерфейсе можно объявить любой метод с любой сигнатурой.
Druu
18.09.2017 17:31+1Какой смысл в лямбдах, если надо для каждой велосипедить интерфейс? :)
О том речь и шла — в джаве checked exceptions реализованы крайне неудобно. Потому не взлетели. А если сделать так, чтобы взлетели, то оно будет как монада :)TheShock
18.09.2017 17:35Следуя вашей логике, Хаскель не взлетел, потому что неудобно, а если сделать так, чтобы взлетел, то будет как Джава.
raveclassic
18.09.2017 17:38Ну, ADT есть не только в хаскеле
TheShock
18.09.2017 17:43-1Та не, я более абстрактно шутил. Хаскель менее популярный, чем Джава, а следуя той логике, чтобы сделать его популярным — нужно превратить в Джаву.
Druu
18.09.2017 17:45+1> Следуя вашей логике, Хаскель не взлетел, потому что неудобно
Нет, это не по моей логике. По чьей-то чужой, возможно.
mayorovp
18.09.2017 17:38+1Напомню: исходно обсуждался ООП-подход против функционального. Исключения относятся к первому, лямбды — ко второму. ООП возможно вовсе без лямбд.
Да, "классическое" ООП многословно, это его недостаток, который никто не скрывает — и именно потому в такие классические ООП-языки как C++, Java и C# активно добавляют элементы ФП (те же лямбды). Но к исключениям против монад это отношения не имеет.
Druu
18.09.2017 17:46-1> функционального. Исключения относятся к первому, лямбды — ко второму. ООП возможно вовсе без лямбд.
Да это просто пример. У checked exceptions много разных проблем из-за врожденной кривизны.mayorovp
18.09.2017 18:57-1Да это просто пример. У checked exceptions много разных проблем из-за врожденной кривизны.
Аргументируйте, пожалуйста.
0xd34df00d
18.09.2017 19:14Я могу написать функцию, которая принимает функцию, бросающую произвольный список исключений, и возвращает функцию, которая бросает тот список исключений плюс (или минус) ещё пара моих типов?
mayorovp
18.09.2017 20:51Я не спорю, в рамках ФП все является либо функцией, либо значением, либо и тем и другим — и исключения тут никаким боком не нужны.
Но я спрашивал про недостатки checked exceptions в рамках подхода ООП. Где нет никаких функций, возвращающих функции.
Druu
19.09.2017 04:36-1Проблема состоит во взаимодействии двух вещей:
1. В случае ООП мы работаем со стейтом, вместо того, чтобы прокидывать аргументы явно
2. Проверяемые исключения не являются first class citizen
В итоге на практике сильно ограничивается возможность композиции методов с проверяемыми исключениями. В случае монад мы можем писать как угодно методы без исключений, а потом на самом верху лифтануть в функтор по нужному аргументу. При этом весь внутренний код о наличии дополнительной обработки сверху не в курсе. В случае с исключениями они по стеку протекают от внутреннего метода до внешних — везде «заражая» сигнатуры, которые при изменениях постоянно надо поправлять или перебрасывать исключения. В итоге на практике вместо того, чтобы инструментом пользоваться, его стремятся сломать и обойти, потмоу что чересчур много геморроя. Если бы ООП код писался в ФП стиле, с явной передачей, то этой проблемы бы не было, т.к. логику можно было бы точно так же выделить, а потом пробрасывать аргументы сверху.0xd34df00d
19.09.2017 06:28Люто не хватает анонимных типов-сумм, кстати. По крайней мере, мне сходу неочевидно, как взять две функции с возвращаемым типом
Either a' a
иEither b' b
и построить их монадическую композицию в типEither (a'|b') b
.
0xd34df00d
18.09.2017 18:57Проверки в том коде, который приведён как ООП-вариант, ничего не делают сами по себе, а лишь дублируют проверки из вызываемых функций. Обратите внимание, всё, что эти проверки просто пробрасывают сообщение об ошибке дальше.
mayorovp
18.09.2017 18:58Пожалуйста, прочитайте ветку обсуждения сначала, а не с середины. Тут уже давно обсуждается совсем другой код.
0xd34df00d
18.09.2017 19:15Я запутался. Можете для оголтелых ФПистов ссылкой ткнуть?
mayorovp
18.09.2017 20:53Автор исходного сравнения соорудил соломенное чучело и разбил его в пух и прах. Ветка началась с сообщения о том, что можно в рамках подхода ООП написать код без разбросанных по коду проверок (с помощью исключений).
Дальше начался холивар "исключения — это плохо, но мы никому не расскажем почему именно".
0xd34df00d
18.09.2017 20:57Почему, рассказали же — по взгляду на функцию нельзя понять, что она возвращает, а исключения — это на самом деле часть возвращаемого значения функции (с теоретической точки зрения, по крайней мере). Я вот лично вообще в каждом треде о методах обработки ошибок говорю про монадическую обработку с упором именно на это.
И таки да, мне самому интересно, почему checked exceptions в джаве не взлетели.
mayorovp
18.09.2017 21:02Ну, я упростил пересказ. Полная ветка выглядит пока что именно так: исключения — плохо, потому что их нет в сигнатуре, а checked exceptions — еще хуже, но почему мы не расскажем.
TheShock
18.09.2017 21:10-1по взгляду на функцию нельзя понять, что она возвращает
Вон ниже про ФП такой диалог:
— Вот только эксепшены не объявлены в сигнатуре функции, и о наличии «красных путей» можно только догадываться.
— Это было бы аргументом если сторонники ФП брали за правило всегда объявлять сигнатуры своих функций. Вот только…
— В статически-типизированных все хорошо с сигнатурами, просто их можно опускать, потому что вывод типов работает лучше
Так в чем разница? В том же C# точно так же на этапе компиляции можно узнать обо всех потенциальных ошибках.lair
18.09.2017 21:44+3В том же C# точно так же на этапе компиляции можно узнать обо всех потенциальных ошибках.
Только для известных реализаций, не для интерфейсов. К сожалению. И не для переданных делегатов.
(Я, правда, не говорю, что это обязательно нужно. Просто указываю ряд ограничений.)
0xd34df00d
19.09.2017 00:03Так в чем разница?
Разница между отсутствием и присутствием при необязательном объявлении?
В том же C# точно так же на этапе компиляции можно узнать обо всех потенциальных ошибках.
Я последний раз C# тыкал очень давно. Как там это можно узнать?
Кроме того, есть разница между «можно узнать» и «тайпчекер заставит по умолчанию».
AndreyRubankov
18.09.2017 16:36+1В коде с монадами нам не надо при этом создавать ветки в вызывающем коде, а в коде с ООП — нужно было бы.
Вообще-то нет, не обязательно.
Метод, который выполняет какое-то действие кидает эксепшн (переходит на «красные пути»), который прерывает дальнейшее выполнение кода.
Функция validateRequest явно не занимается формированием ответа на, допустим, http запрос, значит она отдает ответ, который просто переводит исполнение на «красные пути», и где-то тааам, в конце кто-то примет этот ответ и сделает из него ответ на запрос.
в случае с эксепшеном:… и кто-то перехватит эксепшн и сделает из него ответ на запрос.
– суть та же, но способ реализации другой.
Вопрос: почему автор не использовал эксепшены и намеренно изуродовал ООП код? =)raveclassic
18.09.2017 16:48Метод, который выполняет какое-то действие кидает эксепшн (переходит на «красные пути»), который прерывает дальнейшее выполнение кода.
Вот только эксепшены не объявлены в сигнатуре функции, и о наличии "красных путей" можно только догадываться.
mayorovp
18.09.2017 16:57Это было бы аргументом если сторонники ФП брали за правило всегда объявлять сигнатуры своих функций. Вот только ...
… ни одна функция из определенных в обсуждаемом тексте в примерах на F# не имеет явной сигнатуры! Тут не то что о наличии "красных путей", тут в принципе о типах аргументов и возвращаемых значений надо догадываться!
И самое интересное — это таки работает. А значит, "нужно догадываться" еще не означает "трудно разобраться". И с исключениями можно поступить так же.
Тем более, для сложных случаев есть документация.
А еще в Java есть checked exceptions.
marshinov Автор
18.09.2017 16:59… ни одна функция из определенных в обсуждаемом тексте не имеет явной сигнатуры! Тут не то что о наличии «красных путей», тут в принципе о типах аргументов и возвращаемых значений надо догадываться!
В статически-типизированных все хорошо с сигнатурами, просто их можно опускать, потому что вывод типов работает лучше. Но можете и объявлять везде.mayorovp
18.09.2017 17:01+1Почему для аргументов и возвращаемых значений считается нормальным использовать "вывод типов", а для исключений всем достань и полож сигнатуру?
raveclassic
18.09.2017 17:05+1А как вы (окей, без checked exceptions) поймете, свалится ли у вас функция в рантайме, если в ее типе нет на это указания?
mayorovp
18.09.2017 17:18Обычно мне этого знать просто не нужно: я пишу код, который будет корректно работать при любом развитии событий, и это совсем не сложно.
Но если мне нужно узнать про конкретную функцию — а посмотрю ее исходный код (если она моя) или документацию (если она сторонняя).
raveclassic
18.09.2017 17:24+1Обычно мне этого знать просто не нужно: я пишу код, который будет корректно работать при любом развитии событий, и это совсем не сложно.
Сложно, несложно, это уже человеческий фактор.
Но если мне нужно узнать про конкретную функцию — а посмотрю ее исходный код (если она моя) или документацию (если она сторонняя).
Ну а можно этого не делать и положиться на компилятор.
mayorovp
18.09.2017 21:09Идея исключений — в удешевлении общего случая жертвуя частным.
Так вот: в общем случае мне не нужно смотреть какие именно исключения кидает код.
0xd34df00d
18.09.2017 19:11+1Но если мне нужно узнать про конкретную функцию — а посмотрю ее исходный код (если она моя) или документацию (если она сторонняя).
А можно просто спросить компилятор о типе этой функции и не лазить в её исходники или документацию (где типы ошибок ещё не факт что описаны).
Druu
18.09.2017 17:00> ни одна функция из определенных в обсуждаемом тексте в примерах на F# не имеет явной сигнатуры!
Явной не имеет, но по факту сигнатура есть. Вы увидите ее, если наведете мышкой на ф-ю в ИДЕ, ну и компилятор вам в любом случае напомнит, если вы вдруг не обработаете ошибку.
А явная сигнатура кому нужна-то, по факту?0xd34df00d
18.09.2017 19:13Явная сигнатура полезна, чтобы ограничить количество вариантов в Хиндли-Милнере и получить более читаемые сообщения об ошибках от тайпчекера. В хаскеле поэтому, например, писать сигнатуры топ-левел-
функцийбайндингов — правило хорошего тона.
Ну и, кроме того, не всегда компилятор может вывести тип, если использовать всякие наркоманистые фундепы, семейства типов и прочие полиморфизмы высшего ранга.
Druu
18.09.2017 19:21+1> Ну и, кроме того, не всегда компилятор может вывести тип
В этом случае и проблем с отсутствием сигнатуры не будет, очевидно :)
raveclassic
18.09.2017 17:04+1А сигнатура потому-что выводится. И если у вас у значения вывелся тип
Option<number>
, вам компилятор не даст работать с ним как с number.
Ну и не все на джаве пишут.
0xd34df00d
18.09.2017 18:46+2Про лисп у меня было ровно то же замечание, да.
Да и вообще, безусловно, что монады, что CPS какой-нибудь, что вообще вся иерархия тайпклассов из
Control.*
(arrow, arrowplus, monad, monadplus, monadzero, alternative, вот это всё), это всё тоже вполне себе паттерны. Просто математически чуть более обоснованные, чуть более гранулярные и потому чуть более композабельные.
AlexPublic
18.09.2017 05:07+5Было бы действительно интересно почитать статью на тему паттернов проектирование в мире функционального программирования. Но к сожалению в данной статье ничего подобного не видно. Здесь имеется только обзор всем известных основ ФП и всё. Вот например какие паттерны мне надо применить при проектирование GUI библиотеки? Человек из ООП мира ответит сразу (ну или может пойдёт в начале полистает всем известную книжку и ответит потом). А что скажет сторонник ФП? «Использовать монады и функции»? Ну так это аналогично ответу «использовать классы» и ничего не говорит об архитектуре. В отличие от таких понятий как например «паттерн Посетитель» и т.п.
В общем я сильно порадовался названию статьи и полностью разочарован её содержимым. Буду ждать следующую попытку от адептов ФП…Druu
18.09.2017 07:04-2> А что скажет сторонник ФП?
В ФП нет паттернов, так что вопрос смысла не имеет.alkozko
18.09.2017 08:05+4Паттерны — это не только то, что написано в книжке GoF, а просто набор общепринятых подходов к решению некоторого класса задач.
Так почему паттернов ФП нет? там все задачи уникальные? Или может в ФП не принято решать одну и туже задачу как все и надо обязательно сделать свой велосипед (сомневаюсь, но вдруг)? Или какие-то другие причины?
Мне правда интересноBlessMaster
18.09.2017 08:22-1Раздел про монады — это ж самые, что ни на есть, паттерны. Далеко не все, конечно, статья бедновата даже по меркам введения.
Druu
18.09.2017 08:47> Так почему паттернов ФП нет? там все задачи уникальные?
Нет, потому что решения тривиальны (например, паттерн «стратегия» — просто ф-я, и паттерн «фабрика» — тоже просто функция, и эти функции ничем друг от друга не отличаются, глядя на них вы даже не сможете сказать, какой там паттерн). Основная причина того, что паттерны в ООП существуют — это низкая гибкость применяемых в ООП методов. Ну трудно выражать какие-то вещи через обвязку из наследования, интерфейсов, вот этого вот всего, потому и появляются всякие хитрые схемы, предназначенные для того, чтобы каким-то единым образом описывать вещи, которые сложно описываются. В ФП же гибкость, наоборот, высокая (возможно даже, что слишком высокая), там нет смысла описывать реализацию паттернов в силу их очевидности. На паттерны могут тянуть монады/стрелки/етц., да рекурсивных схемы и их обвязка — но эти вещи слишком абстрактны и не имеют конкретного назначения (в то время как паттерны, вроде, должны иметь).AndreyRubankov
18.09.2017 11:44+3Паттерны, как уже говорилось, это не про GoF. Паттерны – это про подходы к написание и структурированию кода.
Если какой-то подход имеет строго выраженную идею, структуру и применяется для достижения конкретных целей – это паттерн.
Каррирование, Промисы, Коллбеки – это все паттерны.Druu
18.09.2017 13:04-4> Если какой-то подход имеет строго выраженную идею, структуру и применяется для достижения конкретных целей – это паттерн.
Я и говорю, в этом понимании в ФП нету паттернов.
> Каррирование, Промисы, Коллбеки – это все паттерны.
Нет, каррирование и коллбеки — не паттерны. Так как у них нет назначения. То самое упомянутое вами «применяется для достижения конкретных целей». Не существует никаких конкретных целей для применения каррирования в общем. Так же как нету их для применения, например, монад в общем. Правильнее называть это каким-то другим словам, например — «концепция».
TheShock
18.09.2017 15:20-3Я и говорю, в этом понимании в ФП нету паттернов.
О! А вот и целевая аудитория статьи, я выше о них смеялся.
Если в какой-нибудь парадигме нету паттернов, то это значит что она настолько ненужна в практической разработке, что под нее даже не пишут никакую теорию. Вы считаете, что это об ФП?Druu
18.09.2017 15:58+1> то это значит что она настолько ненужна в практической разработке, что под нее даже не пишут никакую теорию
Под ФП есть огромное количество теории. Просто паттернов в этой теории нету. Там есть монадные трансформеры, стрелки, а также зигохистоморфные препроморфизмы с пределами и копределами. А паттернов — вот нет, не завезли :)
И, да, эту теорию можно применять на практике. Например, благодаря этой теории я знаю, что могу использовать синтаксис генераторов в js с любой монадой, в которой fmap применяет свой аргумент не более раза :)TheShock
18.09.2017 16:04-1То есть паттерны есть, но их нету? И вот пример паттерна, но это не паттерн, потому что это ФП, а не богомерзкое ООП, а если хипстеры узнают, что я признал это паттерном — засмеют на афтерпати.
Druu
18.09.2017 16:07> То есть паттерны есть, но их нету?
Давайте определимся с терминологией, чтобы друг друга понимать. Я под «паттерном» понимаю нечто, что, в частности, имеет четкое назначение. Это соответствует общепринятому смыслу, вкладываемому в термин «паттерн». В этом смысле паттернов в ФП нету, потому как все конструкции слишком универсальны. Если же под «паттерном» подразумеваются какие-то «классические» приемы, которые туда-сюда ходят и применяется в том или ином контексте — то такое, безусловно, есть.TheShock
18.09.2017 16:30+1Давайте определимся с терминологией
Давайте. По вашему, паттерны ФП внезапно перестают быть паттернами в какой-то момент универсальности? А насколько именно они должны быть «универсальными», чтобы перестать быть «паттерном»? «зигохистоморфный препроморфизм» — уже не такой и универсальный, он уже стал паттерном? А паттерн «Интерфейс» внезапно перестает быть паттерном? И все просто потому вы хотите, чтобы в священном ФП не было того же, что есть в отврательном ООП?Druu
18.09.2017 16:38-1> По вашему, паттерны ФП внезапно перестают быть паттернами в какой-то момент универсальности?
По моему (и это, повторюсь, не только по моему, это общепринятый взгляд на вещи) сущность перестает быть паттерном, если у нее нет конкретного, четко формулируемого назначения.
> А паттерн «Интерфейс» внезапно перестает быть паттерном?
Там же у вас в первых же строчках на вики написано конкретное назначение паттерна — обеспечить программисту простой или более программно-специфический способ доступа к другим классам. Сможете сходным образом описать назначение монад?
> И все просто потому вы хотите
Я ничего не хочу. Мне лично без разницы, как и что вы будете называть. Кажется, я достаточно ясно раскрыл свою точку зрения, а какими конкретно словами она выражается — для меня совершенно несущественно. Если фп-конструкции удовлетворяют вашему пониманию термина «паттерн» — ну так пусть удовлетворяют, я не против. Сущность объекта не зависит от того, как этот объект называют, пусть хоть «тирьямпампация» :)vbif
18.09.2017 16:42+1То есть у монады нет назначения? У объекта первого класса нет назначения?
Druu
18.09.2017 16:44-2> То есть у монады нет назначения?
Именно так. У _конкретной_ монады назначение есть. У монады в общем (какой-то, неизвестно какой) — назначения нет. Это слишком общий объект, сказать про что-то «оно монада» — практически, ничего полезного не сказать. Что с этим чем-то будут делать — вывести уж точно нельзя.
> У объекта первого класса
Монада как бы не особо объект первого класса да и вообще не программный объект. Ее программным объектом, конечно, можно сделать (запаковать нужные ф-и в класс, например), но этого не требуется. Слово «конструкция» на мой взгляд наиболее точно отражает положение вещей. Есть некая конструкция (которая может быть реализована как угодно), и если она обладает определенными особенностями, то это — монада.vbif
18.09.2017 16:51Организация последовательных вычислений — чем не назначение?
Druu
18.09.2017 16:54+2Тем, что этим занимается только некоторый класс монад, но далеко не все. Условно говоря, у нас есть несколько широких классов монад: монады контейнерные (типа List), монады, протаскивающие контекст (State), монады, управляющие потоком управления (вроде Maybe) ну и эзотерические, имя им легион (Cont, например)
TheShock
18.09.2017 16:59+2Ну ладно. Тогда давайте попробуем так.
Монада Maybe, монада Option — это паттерны? Они четко соответствуют вашему определению: «нечто, что, в частности, имеет четкое назначение»Druu
18.09.2017 17:01+1> Монада Maybe, монада Option — это паттерны?
Да, конкретные монады (из классических, то есть у которых есть какая-то общепринятая семантика, как у той же Maybe), наверное, можно считать паттернами.
Однако, почему тогда не считать паттерном в ООП какой-то _конкретный_ класс? Так ведь не поступают, по каким-то причинам.TheShock
18.09.2017 17:20-1Видите ли, интересует не только конктретный класс, но и паттерны его использования.
Ingas
18.09.2017 19:10+1Джентльмены, откройте глаза!
Эта серия лекция популяризировала паттерн Railway [Oriented] Programming.
Гугль выдает:
Результатов: примерно 15 600 000 (0,53 сек.)
myrslok
19.09.2017 21:51Не все 15 миллионов по делу. Большая часть результатов относится к каким-то железнодорожным делам. Если искать то же в кавычках, то результатов становится примерно в тысячу раз меньше. Думаю, вы преувеличиваете известность этого словосочетания.
marshinov Автор
18.09.2017 08:17Вот например какие паттерны мне надо применить при проектирование GUI библиотеки?
Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.
Druu
18.09.2017 08:41+3«react+redux» не имеет никакого отношения к ФП.
marshinov Автор
18.09.2017 08:50-3Да? Абрамов несколько иного мнения 29:00.
Druu
18.09.2017 09:12+3Он может быть какого угодно мнения, реальности это не меняет. Весь визг про ФП в рамках реакта/редакса — не более чем маркетинговый прием. Недобросовестная реклама.
marshinov Автор
18.09.2017 10:41+3Позвольте. Редюсеры должны быть чистыми функциями, состояние не должно изменяться, вместо этого мы возвращаем новое состояние. Кажется это принципы функционального программирования. Пока вы не привели никакой аргументации, кроме прямых обвинений в недобросовестной рекламе.
RidgeA
18.09.2017 11:09+3Разве чистые функции — это единственная концепция ФП?
raveclassic
18.09.2017 12:31+2Зачем же тогда писать "никакого"?
«react+redux» не имеет никакого отношения к ФП.
Druu
18.09.2017 13:08> Позвольте. Редюсеры должны быть чистыми функциями, состояние не должно изменяться, вместо этого мы возвращаем новое состояние. Кажется это принципы функционального программирования.
Это были бы принципы функционального программирования, если бы на этом подходе строился поток управления в целом. Но 90% логики остается в грязных функциях с сайд-эффектами, а в редьюсерах — мелочь. Редакс ведь не про редьюсеры, редьюсеры можно исопльзовать и без редакса. Он про стор (который работает грязно) и про action creators и их middleware (которые работают грязно). В редаксе используются чистые функции? Используются, с этим не поспоришь. Но они в той же мере абсолютно везде используются. Важно — как используются. Если подход основан на использовании чистых ф-й — можно говорить об ФП. Если подход основан на использовании грязных функций (как в случае редакса) и там где-то применяются чистые функции — то тут об ФП говорить рановато.
TheShock
18.09.2017 15:37+1Абрамов несколько иного мнения 29:00.
о! Группи Абрамова! Если Дан что-то сказал — надо отключить свой мозг и принять за истину! Думать не нужно, нужно помнить заветы Абрамова.
У Редакса есть элементы ФП. Код там процедурный. Диспатч — процедурный. В языке С sqrt — тоже чистая функция, которая не изменяет свой аргумент, но от этого весь язык не становится функциональнымmarshinov Автор
18.09.2017 16:27Вы вырываете из контекста. Я сослался на мейнтейнера. У меня к redux много вопросов, особенно к процедурному стилю. Приходится писать очень много boilerplate или не идиоматичный redux'у код (проблема с обучением новых сотрудников).
«react+redux» не имеет никакого отношения к ФП.
Вы и другие участники ветки уже признали, что отношение есть: react / redux используют элементы ФП. Очевидно, что в них много побочных эффектов, потому что в web-ориентированном UI по-другому быть вообще не может.
В JS идеи начали просачиваться в том числе из-за проблем с data-flow и callback hell. Оказалось, что есть продолжения и с ними все попроще. А вот монады в JS пока никому не нужны, потому что никто целей чистоты, как в хаскеле не ставит.
Есть проблема — асинхронный код, который есть в 90% приложений и необходимость его как-то поддерживать и развивать. Есть решение — промисы. Копнули чуть дальше, оказывается еще и чистые функции есть. Подход прагматичный и инструментальный.TheShock
18.09.2017 16:32Ну а вы признаете, что были неправы тут?
Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.
Это не «функциональное программирование», пусть и с элементами.
Я сослался на мейнтейнера
Который отличный маркетолог, эвангелист, но плохой источник истины.marshinov Автор
18.09.2017 17:01-2По-моему, вы просто переходите на личности. Ну не нравится он вам и все. Остальное уже не важно, главное похейтить. Вот вам не нравится его pr. Вы делаете pr на хейте Абрамова. У него хотя-бы не вторично.
TheShock
18.09.2017 17:10Погодите, а где я перешел на личность Абрамова? Тем, что я не считаю кого-то источником истины — это «переход на личности»? Что-то вроде «Ты сказал, что бог не всемогущ. Ты — богохульник и еретик!». Вы прям истинный религиозный фанатик.
Я никогда не высмеивал Абрамова. Я высмеиваю только его фанаток-программистов, которые заменяют свой мозг его речами.
И, кстати, так проигнорировали главный вопрос. Потому повторюсь:
вы признаете, что были неправы?
marshinov Автор
18.09.2017 18:51-1«react+redux» не имеет никакого отношения к ФП
и
У Редакса есть элементы ФП. Код там процедурный. Диспатч — процедурный. В языке С sqrt — тоже чистая функция, которая не изменяет свой аргумент, но от этого весь язык не становится функциональным
Спорите со мной уже вы про Абрамова. А оригинальный вопрос был:
Вот например какие паттерны мне надо применить при проектирование GUI библиотеки?
React и Redux — это примеры применения элементов ФП в UI-библиотеках. Это максимально точный ответ. В комментариях мне пишут, что «react и redux не имеют отношения» а потом «содержат элементы».
Содержать элементы уже != иметь отношение?
Очевидно, что react и redux — не хаскель. Это никто утверждать не будет. Кстати, если перейти на ссылку с видео то там он утверждает ровно тоже самое «redux использует идеи функционального программирования. Они не новые».
Суть вопроса «какие есть примеры ФП в UI» уже утратила актуальность. В ветке мы обсуждаем недобросовестный маркетинг и девочек.
вы признаете, что были неправы?
В чем конкретно я не прав? Может и признаю, пока не понял.TheShock
18.09.2017 19:03Процитирую вас:
Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.
Но это, как мы уже выяснили, не функциональное, а так, моментами использует функциональное.
Вопрос был про «паттерны создания UI на ФП», а вы в ответ привели процедурную либу с некоторыми функциональными элементами и еще ошибочно назвали ее функциональной.
Другой момент — был задан вопрос «какие есть практики создания UI на ФП». Все, что прилетело в ответ — «вот используйте библиотеку» (это просто фейспалм). Данное грустное обстоятельство указывает на слабую теоретическую базу.
TheShock
18.09.2017 17:18Вы делаете pr на хейте Абрамова
И да, это лол. Я не выступаю на конференциях, где кого-то хейтю вместо выступления по теме, не рисую глуповатые сравнительнительные таблицы с хейтом, не пишу статьи, которые начинаю с хейта. А комментарии на хабре слишком быстро забываются, чтобы считаться пиаром.
AlexPublic
18.09.2017 20:04А можно увидеть именно сами паттерны (словесное описание и т.п.), а не какую-то библиотечку из дикого JS мира? )
KoCMoHaBT61
18.09.2017 11:18-2Да не то, что паттерны!
Как вообще можно проектировать на ФП, вот в чём вопрос.0xd34df00d
18.09.2017 19:36-1Пишете сигнатуры функций, добиваетесь, чтобы тайпчекалось — всё, спроектировали.
TheShock
18.09.2017 19:38-2А через три месяца выбрасываете неподдерживаемый лапшекод и возвращаетесь в Джаву.
0xd34df00d
18.09.2017 19:44-2Где через три месяца выбрасываете неподдерживаемый императивный лапшекод и возвращаетесь в хаскель.
TheShock
18.09.2017 19:50Где через три месяца снова выбрасываете весь код и идете работать грузчиком.
Тут спрашивают: «как писать поддерживаемые программы на фп», на что получают от вас ответ из разряда: «ну пишите как-то, чтобы оно скомпилировалось».
Не удивительно, что с такими советами никто не пишет на ФП ничего сложного и выходящего за простую обработку данных. Ну, например, игры, редакторы музыки, изображений и видео. Все годами пишется на императивной технике и никто не выбрасывает и не возвращается в Хаскель, лол.
Возможно, потому что в императивной технике люди хотя бы думают, как сделать результат поддерживаемым, а в фп единственный совет — «Пишете сигнатуры функций, добиваетесь, чтобы тайпчекалось — всё, спроектировали». Супер!0xd34df00d
18.09.2017 20:04+2Спрашивали в контексте паттернов. Никогда не приходилось поддерживать неподдерживаемый код, где синглтон на визиторе и фабрикой с инверсией зависимостей погоняет?
Ну и да, ваше утверждение, что никто не пишет, некорректно хотя бы потому, что существует по крайней мере один человек, так пишущий. И, конечно, реальность чуть сложнее, чем «просто чтоб тайпчекалось» — пишете сначала модуль, у него продумываете, что он должен отдавать наружу, какие функции внешнему миру от него нужны, пишете сигнатуры этих функций, ставите undefined в реализациях, пишете вызывающий код, который этот модуль использует, например, убеждаетесь, что оно имеет смысл, а дальше пишете реализации потихонечку. Получается более чем поддерживаемо, тестируемо, отлаживаемо и дружелюбно к изменениям.
<зануда>Ну и да, вопрос был не о том, как писать поддерживаемый код, а «как вообще можно проектировать на ФП».
TheShock
18.09.2017 20:15«как вообще можно проектировать на ФП».
Ну смотрите — на вопрос «как вообще можно управлять самолетом?» можно просто ответить: «берете штурвал и двигаете им, пока не прилетите на место», а можно дать ссылку на пару талмудов об управлении самолетом.
Никогда не приходилось поддерживать неподдерживаемый код, где синглтон на визиторе и фабрикой с инверсией зависимостей погоняет?
Конечно стыкался с таким. Со всяким стыкался. Но все-равно, у тебя есть куча хорошей теории о проектировании крупных систем, откуда можно почерпнуть множество разномастного опыта. Да, некоторые люди будут иметь культ карго, некоторые вообще все неправильно поймут. Да и без какого-либо опыта сложно понять всю эту теорию. Но с ФП — тебя просто садят в горящий самолет и кричат: «лети».
пишете сначала модуль ...
Вы же понимаете, что скорее описываете «как писать», а не «как проектировать»?0xd34df00d
18.09.2017 23:56Ну смотрите — на вопрос «как вообще можно управлять самолетом?» можно просто ответить: «берете штурвал и двигаете им, пока не прилетите на место», а можно дать ссылку на пару талмудов об управлении самолетом.
Учитывая коннотации, вопрос скорее был «вот в автомобиле штурвал есть, а в этих ваших самолётах как управлять ими?».
Но все-равно, у тебя есть куча хорошей теории о проектировании крупных систем, откуда можно почерпнуть множество разномастного опыта.
Людей, которые проектируют большие и сложные системы эффективно и без неявных зависимостей между разными кусками (а в императивном коде их понасадить очень легко), я встречал, ну, очень мало.
Но с ФП — тебя просто садят в горящий самолет и кричат: «лети».
Почему? На крупном масштабе тот же подход, хоть ООП, хоть ФП.
Вы же понимаете, что скорее описываете «как писать», а не «как проектировать»?
Нет. Бьёте на модули, как обычно, а на уровне модулей уже вот как я описываю.
KoCMoHaBT61
19.09.2017 06:24-4пишете сначала модуль, у него продумываете...
Уже не функциональный подход.
Функции в ФП декларируются как стейтлесс, поэтому их можно звать из любого места в любое время. Смысла делить проект на модули, кроме как для оптимизации загрузки программы, нету.0xd34df00d
19.09.2017 06:30+3Вы, конечно, извините, но это не имеет смысла вообще.
Функциональный подход в смысле чистоты функций вообще никак не связан с выделением изолированных интерфейсов. Вы пишете модуль, предоставляющий, не знаю, хешмапу не потому, что хотите что-то там оптимизировать, а потому, что это чистый, изолированный и реюзабельный модуль. Ну, как класс, только модуль — экспортируемый им тип данных плюс набор функций для работы с ним.
Кроме того, программы на каком-нибудь там хаскеле чаще компилируются, чем интерпретируются, поэтому модули там только добавляют компилятору геморроя с точки зрения кроссмодульной оптимизации.
KoCMoHaBT61
19.09.2017 09:23-5То есть — у тебя есть анализ в виде модульности, но нет синтеза вообще. И эту, не знаю, «хэшмапу» (которая, на самом деле, не хэшмапа, а «контейнер очень важных данных») ты зовёшь не напрямую, а через макаронины идиотских экспортов.
Собственно-говоря — так оно и выглядит в реале, но хотелось верить, что есть кто-то, кто знает как проектировать функционально.0xd34df00d
19.09.2017 19:52То есть — у тебя есть анализ в виде модульности, но нет синтеза вообще.
Синтеза чего?
И эту, не знаю, «хэшмапу» (которая, на самом деле, не хэшмапа, а «контейнер очень важных данных») ты зовёшь не напрямую, а через макаронины идиотских экспортов.
Эта фраза тоже не имеет особого смысла. Когда мне нужна хешмапа, я делаю
import qualified Data.HashMap.Strict as M
(ну или Lazy, зависит от ситуации). Когда мне нужно её создать, я делаюM.fromList
, илиM.singleton
, илиmempty
, или вроде того. Ну и так далее.
Собственно-говоря — так оно и выглядит в реале, но хотелось верить, что есть кто-то, кто знает как проектировать функционально.
У меня ощущение, что у вас уже есть ответ, а всё, что вам говорится, вы под этот ответ подгоняете.
KoCMoHaBT61
20.09.2017 11:26-1Анализ и Синтез это термины логики. Анализ — это разложение чего-либо на составные элементы, Синтез это сборка чего-то из элементов, которые получены в результате Анализа.
Что-же касается Хэшмапы.
Тебе не нужна Хэшмапа, но ты этого не понимаешь!
Тебе нужен контейнер для неких данных.
Возвращаясь к вопросу проектирования — для каких данных тебе нужен контейнер, как эти данные связаны с другими данными, какое место занимают эти данные во множестве других данных. На эти вопросы ответа нет, потому, что данные ты не формализовал и не обобщил. А из этого ответа, через долгое (или недолгое, как повезёт) исходит решение, какой конкретно контейнер ты должен применить в данном случае.
Модули — это не ответ. Модульность даёт некоторое смысловое разбиение предметной области на группы. Ещё большее и удачное разбиение (Анализ) даёт ОО. Но мы говорим о ФП, причём в теоретическом смысле, не вдаваясь в подробности.0xd34df00d
20.09.2017 18:49+1Анализ — это разложение чего-либо на составные элементы, Синтез это сборка чего-то из элементов, которые получены в результате Анализа.
Анализ в ФП в ваших терминах тоже есть.
Тебе не нужна Хэшмапа, но ты этого не понимаешь! Тебе нужен контейнер для неких данных.
На каком-то уровне мне и контейнер не нужен, а нужно решить бизнес-задачу.
Возвращаясь к вопросу проектирования — для каких данных тебе нужен контейнер, как эти данные связаны с другими данными, какое место занимают эти данные во множестве других данных. На эти вопросы ответа нет, потому, что данные ты не формализовал и не обобщил.
Трудно формализовать и обобщать сферические задачи в вакууме. Когда есть реальная задача, то там можно применять те же соображения о контейнерах, что в ООП.
Модули — это не ответ. Модульность даёт некоторое смысловое разбиение предметной области на группы. Ещё большее и удачное разбиение (Анализ) даёт ОО.
Почему?
mayorovp
19.09.2017 06:34+2Модуль — это прежде всего способ организации кода. Модули не имеют отношения к используемой парадигме.
lair
19.09.2017 11:31+1Функции в ФП декларируются как стейтлесс, поэтому их можно звать из любого места в любое время.
Ээээ, что? Сокрытие информации никто не отменял, оно не только к состоянию относится.
KoCMoHaBT61
19.09.2017 14:04-1А нету информации, нечего скрывать.
Есть функция, которая принимает нечто и нечто другое выдаёт.lair
19.09.2017 14:57+1Функция — это тоже информация.
KoCMoHaBT61
19.09.2017 18:47Не может человеческий язык состоять из одних глаголов.
lair
19.09.2017 18:54+2Не может, и что? К тому, что функция — это информация, и иногда нуждается в сокрытии, это отношения не имеет.
Yuuri
20.09.2017 11:53Из одних существительных – тоже.
KoCMoHaBT61
20.09.2017 12:43-1Отличная статья, кстати, если-бы там кругом не упоминалась богомерзкая Java.
Более забавная статья есть на хабре:
habrahabr.ru/post/161885
Только она больше вопросов порождает, чем отвечает на вопрос «как проектировать на ФЯ».Yuuri
20.09.2017 13:59Лично я не понимаю этот вопрос, он слишком абстрактный. А как проектировать на не-ФЯ?
0xd34df00d
19.09.2017 19:56+1Когда я делаю
M.fromList [...]
, я получаюM.HashMap K V
. И вот кишки этой хешмапы имеет смысл скрывать, например. Равно как и имеет смысл скрывать вспомогательные функции, если вы пишете библиотеку и гарантировать сохранность этих функций с точки зрения внешнего пользовательского API вы не хотите.KoCMoHaBT61
20.09.2017 11:32-2Удивительное пристрастие к хэшмапу, оно похоже на стэндфордовский курс функционального программирования, только там они заботились о задаче «как мне быстренько посчитать атомную бомбу».
…
Реальные задачи немного другие.
Вот например, постановка задачи про «Грабить Корованы» — до хэшмапы там как до эльфов лесом.
Если проектировать «Корованы» с Функциональным подходом, то мы быстренько наталкиваемся на такую проблему, что нам тяжело описать «Корован» и очень легко описать операцию «Грабить».Druu
20.09.2017 12:56> что нам тяжело описать «Корован»
Почему же тяжело? Прикидываем данные, которые должны описывать караван, и пишем соответствующий тип.KoCMoHaBT61
20.09.2017 13:03-3В ФП у тебя минимум два «Корована»!
Первый это Неограбленный_Корован, а второй это Ограбленный_Корован.
И нет никакого жёстко заданного «типа».Druu
20.09.2017 13:19> И нет никакого жёстко заданного «типа».
Как это нет? Есть, конечно. Как же компилятор типы проверяет, если их нет? :)
> В ФП у тебя минимум два «Корована»!
Берете Clean (это такой чисто функциональный ЯП), объявляется тип вашего каравана uniqueness и будет только один караван! Чудо! :)0xd34df00d
20.09.2017 18:52Скоро можно даже будет не брать Clean, линейные типы почти завезли в хаскель.
raveclassic
20.09.2017 13:19+3type Caravan = { gold: number }; const rob = (caravan: Caravan): Caravan => ({ gold: 0 });
Парам парам пам.
Caravan
— жестко заданный тип.
0xd34df00d
20.09.2017 18:52+1В ФП у тебя минимум два «Корована»!
Первый это Неограбленный_Корован, а второй это Ограбленный_Корован.Если старый корован вам не нужен, его GC подберёт. Если нужен — ну, так он вам и так нужен. Персистентность — это зачастую преимущество.
И нет никакого жёстко заданного «типа».
В дополнение к коду на другом языке рядом:
data Caravan = Caravan { name :: String, camelsCount :: Int, gold :: Double } robCaravan :: Caravan -> Caravan robCaravan c = c { gold = 0 }
KoCMoHaBT61
20.09.2017 19:00Персистентность ведёт к потере стейтлесс преимущества ФП.
0xd34df00d
20.09.2017 19:17Но как? Если что.
KoCMoHaBT61
20.09.2017 22:39Хоть коммент удаляй — масло-масляное.
Ладно, напишу свой вариант:
rob({caravan,_Name,_CamelsCount,_Gold}) -> {caravan}; rob(_) -> ok.
Druu
18.09.2017 07:07+4> Обратите внимание, сигнатура int -> int -> int не содержит скобок не случайно.
Она не содержит скобок из-за того, что скобки там ничего не значат, в силу правоассоциативности "->".myrslok
19.09.2017 11:56Скобки не "ничего не значат", а подразумеваются, причем определенным образом. В
(int -> int) -> int
их нельзя опустить.
Alex_T666
18.09.2017 07:08+5В конечном итоге, как всегда, выясняется, что настоящее искусство программирования, это умение разумно выбрать и использовать оптимальный инструмент из всего зоопарка.
Поэтому скажем большое спасибо создателям зоопарка, но заклеймим религиозных фанатиков, сбивающих с толку юных программистов.
znsoft
18.09.2017 08:18+2мне, незнающему с какой стороны посмотреть на этот новомодный ФП статья дала понимание того, что коллбэки со времен asm x86 переросли в отдельную парадигму программирования
BlessMaster
18.09.2017 08:25-2Что лишний раз подтверждает, что статья, хоть и с картинками и благими намерениями, но в общем — так себе статья.
DmitryKoterov
18.09.2017 08:33Cherry — это на практике черешня, а не вишня. (Так же как dinner — это на практике ужин, а не обед.)
marshinov Автор
18.09.2017 08:53А черешня — не вишня? Это самая важная деталь, из-за которой в переводе весь смысл метафоры потерян?:)
DmitryKoterov
18.09.2017 12:55Ну это все троллинг, конечно — извините. Мне просто подумалось, что если cherry — это вишня, то композиция функций — это фрактал, почему бы и нет. (Да, я понимаю, что это перевод. Но.)
Если говорить про метафоры, то вот еще проскальзывает в статье избитая тема: «функция в ФП- это объект первого порядка». Ну на практике (такой же, как с вишней-черешней) это не совсем тот «первый порядок», который имелся в виду, когда Чёрч придумывал лямбда-исчисление. Вот что, например, имеется в виду под «настоящим первым порядком» с практической точки зрения: habrahabr.ru/post/322052 —
True = t => f => t
False = t => f => f
И дальше все остальное через это выводится.
А то, что коллбэки можно передавать в параметрах или что функции комбинировать, так это как черешня-вишня: вроде бы и то же, а вроде бы и вообще нет.marshinov Автор
18.09.2017 13:53Я целенаправленно скачал лекции по лябда-исчислению и даже посмотрел первые три. Кому как, конечно, но мне с вишенками и яблочками «заходит» лучше. Для математики такие аналогии смехотворны. Но мы же программы пишем, а не теоремы доказываем.
injecto
18.09.2017 08:53+4Вместо этого популизма лучше почитать что-то более адекватное.
mibori
18.09.2017 18:41+1Забавно, что сам Евгений, по прошествию стольких лет, сейчас кодит гораздо больше на Java, чем на Haskell )
KoCMoHaBT61
18.09.2017 10:52-3С самого начала отличный пример:
(apple -> banana) (banana -> cherry)
apple, banana, cherry — это разные сущности. У них абсолютно разные аттрибуты, параметры и инварианты, но — функциональщики с лёгкостью необыкновенной преобразовывают одно в другое. При этом они даже не могут объяснить (или не удосуживаются), что это за преобразование такое (apple->banana), что за этим стоит из предметной области.
getCustomerFromDatabase — в функциональщине вообще невалидная функция (по названию). Потому, что нету ни Кастомера ни Датабазы.Throwable
18.09.2017 12:38+4Потому что основа функционального программирования — это stateless-операции над аргументами функции. Когда мы добавляем внешний изменяемый state в качестве, например, базы данных, то теряется идемпотентность функций, и результат композиции становится непредсказуемым. Чтобы жестко зафиксировать последовательный порядок применений функций, требуется использовать специальную монаду IO, с которой программирование фактически становится императивным. Кроме того, результат вычисления функции — это уже не просто Response, а еще и измененный State. То есть красивая идеальная схема HttpResponse = WebApplication(HttpRequest) уже не работает, и проблема здесь будет не в getCustomerFromDatabase(), а в updateCustomerInDatabase().
Поэтому для data-driven приложений функциональное программирование не лучший выбор.marshinov Автор
18.09.2017 12:42Чтобы жестко зафиксировать последовательный порядок применений функций, требуется использовать специальную монаду IO, с которой программирование фактически становится императивным.
Все-таки точнее сказать: «код выглядит как императивный». IO позволяет отделить вычисления от исполнения.
KoCMoHaBT61
18.09.2017 14:55Про монады я ничего не знаю, писал на Эрланге.
Так вот, этот самый Эрланг изначально противоречит «основе функционального программирования» — а именно в модульной структуре. Если наши функции такие стейтлесс, то нахрена ограничивать доступ к функциям в модуле с помощью -export([blablafun/1]).
Ну ладно… Ты не замечаешь, что в ответе ты применил Структуру Данных, которой быть не должно? То есть идеальная схема — это:
{http_response, _bla, _bla ,_bla, _bla} = handle({http_request,_bla,_bla,_bla,_bla}).
… и эти оба тупла — это совсем не структуры данных…
А до кастомеров в датабазах и о последовательности исполнения мы ещё и близко не добрались.
0xd34df00d
18.09.2017 19:42+1Можно смотреть на IO не как на внешний мир, а как на токены, взаимодействующие с внешним миром, а ваша программа — чистая композиция этих (пусть и нечистых) токенов.
Кроме того, в сколь угодно нетривиальной программе чистый функциональный кусок вполне себе составляет немалую часть, и посему, по крайней мере, в моём опыте даже программы, перекладывающие данные из двух БД в третью, выигрывают от функциональщины.
igrishaev
18.09.2017 10:53+2Это тоже слишком сложно. Просто используйте неизменяемые коллекции, функции без побочных эффектов и ориентируйтесь на данные, а не паттерны.
marshinov Автор
18.09.2017 11:29+1Просто используйте неизменяемые коллекции
Всегда и везде и память ваша закончится:) Кроме шуток, R# перестал работать в VS2015 как раз из-за неизменяемых коллекций в Roslyn, которые выжрали всю оперативку. Анализаторы Roslyn выключить нельзя. Пришлось JetBrains запилить Rider.
igrishaev
18.09.2017 13:53Судя по картинке, Лисп вообще вне конкуренции. Почему же речь идет идет за F#, который двумя этажами ниже? И как лисперы живут без типов, монад?
alexeykuzmin0
18.09.2017 18:47+6монада — это всего лишь моноид в категории эндофункторов
Не один год не мог понять, что такое монада, а после этой фразы резко понял.
Большое спасибо!
Gebb
18.09.2017 18:52+1Функции с одинаковым типом входного и выходного значения являются моноидами
Странное утверждение. Функция из А в А — это бинарное отношение на множестве А, то есть, подмножество декартова произведения (АхА). Моноид — полугруппа с единицей. Может, имелось в виду, что множетво всех таких фнкций образует моноид относительно операции композиции?
marshinov Автор
18.09.2017 18:55Стиль изложения материала у автора намеренно упрощен, его даже обвиняют в популизме. При переводе я не стал корректировать, потому что тогда получилось бы уж совсем вольное изложение по мотивам, а это и так рерайт.
Ingas
18.09.2017 19:04+1Стиль изложения автора — «открывай редактор, набивай код, смотри что получается, думай, изобретай».
Т.е абсолютно адекватный материалу.
Рекомендую!
И далеко не все у него просто. Когда он объясняет различие аппликатива и монад, если не seasoned haskeler/MLer — мозг приходится на повышенную тактовую частоту переключать.
Ingas
18.09.2017 18:56+1Господа, я удивлен, что серия лекций, которая популяризировала выражение «Railway oriented programming» кого-то в 2017 году поражает «новизной».
Кстати, «Railway oriented programming» (который является ни чем иным как паттерном(!)) — имеет реализации (или подражания) почти для каждого значимого языка.
Я полагал, что если даже первоисточник кто-то не читал, то уже с последствиями его — обязательно должен был столкнуться.
Ну, а если вам так нравится удивляться тому что уже вошло в повсеместную практику — то может быть вам в новинку будет лекция Норвига 98-го года norvig.com/design-patterns/design-patterns.pdf
Рекомендую!
mSnus
Это все звучит очень круто. Но вам не кажется, что за стремлением к высокому искусству несколько теряется смысл программирования?
Когда-то изобрели процедурный подход. Жить стало круче, стали плодить процедуры из всего, чего можно, где можно было обойтись прямым кодом: вместо a = a + 2 стали писать Add(a, 2). Что улучшает читаемость и поддерживаемость кода.
Потом стали работать с ООП, и все завертелось ещё круче — все должно быть объектами! Теперь мы можем писать a.Add(2), что ещё лучше улучшает читаемость и поддерживаемость!
Но это мало, далее пошла мода на интерфейсы. Стоит ведь предусмотреть, что 2 — это не совсем два, а добавить — это не всегда сложить. Реализуем интерфейс IAdd! Что улучшит читаемость и поддерживаемость кода, само собой, а также сделает его ну ОЧЕНЬ гибким.
Тут уже наворотили столько, что чтобы не изобретать велосипед, улучшить читаемость и поддерживаемость, надо воспользоваться соответствующим паттерном — желательно с древнеримским названием типа MVXXML. Каждому интерфейсу по контроллеру, каждому контроллеру по интерфейсу! Теперь код мало того, что необходимо поддерживать — это ещё должна делать толпа джуниоров, пара сениоров и главный архитектор.
Но это мало. Это, знаете ли, не Bleeding Edge! Заверните, пожалуйста, в функциональное программирование, нарежьте монадами по 100гр кусочек и подайте под мелко нашинкованными лямбдами. Выглядит вкусно? Что, добавить 2 к переменной a? Это прошлый век! Как вы можете оскорблять высокую кухню самой постановкой такой задачи!
Идите к этим, как их, низкоуровневым! Пусть выдадут вам
add ax, 2! А у нас — искусство!
symbix
ООП не о том, что мы прибавляем 2, а о том, зачем мы это делаем. (Поэтому никакого .add там не будет).
musuk
Там будет CQRS и Add Action
vlreshet
Там будет какой-нибудь MathOperationsFactory, который вернёт AddingService, который примет два числа, а вернёт объект типа MathOperationResult. Или же вообще, MathOperationsBuilder, которому надо будет на вход передать два числа, класс отвечающий за сложение, а потом класс-конфиг, который укажет в каком виде это обработать и отдать. Как-то так ?\_(?)_/?
symbix
Ну вот вы шутите, а многие за чистую монету такое принимают. И даже так пишут :-)
Для математики ООП вообще не нужно. ООП удобно для моделирования предметной области на языке этой самой предметной области. ФП удобно для реализации алгоритмов и всяких цепочек взаимодействий-преобразований. Никто не запрещает совмещать :-)
Или еще можно сказать, что инструментами ООП удобно описывать "что надо сделать", а инструментами ФП — "как мы это делаем".
mayorovp
Вообще-то строго наоборот: алгоритмы на чистых функциональных языках описываются плохо, а потому средствами ФП удобнее описывать что надо сделать.
А вот "как" описывается, в основном, структурным программированием, которое не относится ни к ООП, ни к ФП.
mayorovp
Да, поясню по поводу ООП. ООП может использоваться для написания как императивного кода — в таком случае его скрещивают со структурным программированием, так и для написания декларативного — в таком случае к нему часто добавляют элементы функционального. Проще всего показать это на примере языка C#.
Императивный подход:
Декларативный подход:
Эквивалентный код:
В первом случае используется структурное программирование, во втором случае угадывается монада List. Но оба подхода активно используют ООП и его паттерны (первый код неявно использует паттерн "итератор", второй к нему добавляет "цепочку ответственности").
symbix
А, ну это смотря что считать ООП.
Я вот не считаю, что если я написал
class ImmutableCollection<T>
с методами map и filter, то это ООП. :-)mayorovp
Если это простой класс не привязанный ни к какой иерархии — то тут конечно же от ООП будет только обертка. А вот если ваш ImmutableCollection реализует хотя бы интерфейс IEnumerable (C#) или Iterable (Java) — то это уже типичный механизм ООП.
Druu
С чего бы это наличие интерфейса свидетельствовало об ООП? Интерфейс — это просто описание набора операций, доступных для данного типа.
mayorovp
С того, что тут задействуются два кита ООП — наследование и полиморфизм (ну, второе еще не задействуется — но я все же предполагаю что написанный код кто-то использует).
Кроме того, тут используется паттерн из мира ООП ("итератор"), который запрещен в мире функционального программирования, поскольку построен вокруг изменяемого состояния — так что как только вы его реализовали, ваш код перестает быть чистым функциональным.
Druu
> Кроме того, тут используется паттерн из мира ООП («итератор»)
Это как вы определили, что там используется итератор? Из кода это никак не следует.
> С того, что тут задействуются два кита ООП — наследование и полиморфизм
Интерфейсы не наследуются, а реализуются. И если интерфейс в стиле тайпклассов хаскеля, не предполагает полиморфизма подтипов (по крайней мере вне костылей из existential types)? Будет уже не ООП?
mayorovp
Следует. Интерфейсы IEnumerable и Iterable — это стандартные реализации паттерна "итератор".
Это всего лишь особенность конкретного языка.
Мы все еще говорим о C# и Java или обсуждаем какой-то неопределенный язык программирования?
Разумеется, в том же Хаскеле классы не являются признаком ООП по построению. Но Хаскель — чистый функциональный язык, а я говорю про мультипарадигменный C#.
Druu
> Следует. Интерфейсы IEnumerable и Iterable — это стандартные реализации паттерна «итератор».
То есть если я поменяю IEnumerable на List, код чудесным образом перестанет быть ООП?
> Это всего лишь особенность конкретного языка.
Это особенность интерфейсов. Интерфейсы нельзя наследовать, потому что интерфейс — это контракт. Что значит «наследовать контракт»?
> Мы все еще говорим о C# и Java или обсуждаем какой-то неопределенный язык программирования?
Какая разница, на каком? У вас что, _один и тот же_ код то функциональный, то ООП в зависимости от того, какой язык?
0xd34df00d
Классы в хаскеле и классы в сишарпе — это две большие разницы.
Druu
> Классы в хаскеле и классы в сишарпе — это две большие разницы.
В хаскеле нету классов
0xd34df00d
Есть. Просто типов :)
Druu
То, что там в названии одинаковое слово, не означает, что между ними есть что-то общее. Это совершенно другая сущность.
mayorovp
Таки есть, хотя к классам из ООП они имеют довольно слабое отношение.
A Gentle Introduction to Haskell: Classes
mayorovp
Когда придумывались классические "три кита" ООП, интерфейсов/контрактов не было, отсюда и некоторое несоответствие в определениях. Технически интерфейс мало отличается от абстрактного класса без полей.
Применительно к интерфейсам в C#, при реализации интерфейса в некотором смысле наследуются все методы-расширения, определенные для этого интерфейса. Например, любой класс, реализующий
IEnumerable<T>
, автоматически получает методы-расширения Where, Select, SelectMany, Join и прочие.Мои ответы надо рассматривать в контексте тех комментариев, которые я писал ранее и тех вопросов, на которые я отвечал.
Напомню, началась эта ветка с моего ответа на вот этот комментарий:
Если вы считаете что слово
class
автоматически означает ООП — то лучше расскажите об этом symbix, а не мне. Если вы так не считаете — то я не понимаю какой тезис вы пытаетесь отстоять в этом споре.Druu
> Применительно к интерфейсам в C#, при реализации интерфейса в некотором смысле наследуются все методы-расширения, определенные для этого интерфейса. Например, любой класс, реализующий IEnumerable, автоматически получает методы-расширения Where, Select, SelectMany, Join и прочие.
И? Это вы к чему? Как это меняет тот факт, что интерфейс — это контракт?
> Если вы так не считаете — то я не понимаю какой тезис вы пытаетесь отстоять в этом споре.
Мой тезис — интерфейсы не имеют никакого отношения к ООП. Они вообще не привязаны к какой-либо парадигме и существуют хоть в ООП, хоть в ФП, хоть в процедурном программировании (интерфейс модуля, например).
mayorovp
Вы опять путаете общее понятие и элемент языка.
Интерфейс как элемент языка очень похож на абстрактный класс без полей.
Druu
> Интерфейс как элемент языка очень похож на абстрактный класс без полей.
Давайте по порядку. Есть интерфейсы, интерфейсы — это контракты (абстрактные классы, конечно же, тоже). Идея накладывать контракты на программные сущности — не является чем-то специфичным для ООП. Это моя точка зрения. Можете свою полностью сформулировать, потому что мне совершенно непонятно, к чему вы ведете.
mayorovp
Моя точка зрения в том, что реализация интерфейса классом в языке C# с точки зрения парадигмы ООП является частным случаем наследования как общего понятия, а потому код на C#, который использует реализацию нетривиального интерфейса классом, можно считать написанным в парадигме ООП (не исключая возможность присутствия и других парадигм).
Druu
Мне кажется, что в рамках подобной логики абсолютно любой код на языке, поддерживающем ООП, будет считаться написанным в ООП-стиле. По крайней мере, мне не удалось сейчас придумать какой-то контр-пример.
mayorovp
«Подобной логики» — это какой?
Я видел программы на C#, в которых все методы были статические и находились в классе Program, а остальные классы были без конструкторов и с публичными полями. Это было в чистом виде структурное программирование на C#.
Но если не брать такие крайние случаи — то да, почти любая программа на C# использует парадигму ООП. В этом нет ничего удивительного — все же это основная парадигма языка.
Druu
> Я видел программы на C#, в которых все методы были статические и находились в классе Program
Но ведь _класс_, да еще и статический — это со всей очевидностью ООП-шная конструкция. Как и методы. Ровно в той же степени, как интерфейсы или абстрактные классы. Разве из этого не следует сразу сделать вывод, что это код в ООП-стиле? И не-ООП код, получается, на c# вообще не написать?
mayorovp
Пожалуйста, перечитайте мои комментарии еще раз. Там есть ответ на ваш вопрос. А спорить ради спора я не собираюсь.
DistortNeo
Не понимаю, зачем тут спорить. Всё зависит от использования интерфейса.
Когда мы приводим объект к типу `IEnumerable`, например, `void Foo(IEnumerable obj)`, то интерфейс неотличим от абстрактного класса.
Когда же мы накладываем ограничение на тип, например `void Foo(T obj) where T: IEnumerable`, интерфейс является контрактом.
Всё остальное типа особенности множественного наследования здесь не имеет значения.
ApeCoder
Можно сделать более специфичный контракт который унаследует поддерживаемые операции у другого контракта и добавит свои.
Кстати, каждому классу концептуально соотвествует свой наиболее специфичный контракт. Типа "Поток-в-памяти это такой поток вообще, который обязуется еще и хранить то, что в него запихали в ОЗУ и предоставлять потом по требованию"
0xd34df00d
Что вы называете подтипами?
И почему existentials сразу костыли? У них есть куча своих весьма изящных применений.
Druu
> И почему existentials сразу костыли?
Костыли — в качестве эмуляции подтипирования. Потому что подтипирование в хаскеле считается ненужным.
> Что вы называете подтипами?
Не совсем понимаю вопроса. То же, что и все? Есть системы типов с подтипированием (например, lambda<:), у них есть семантика.
0xd34df00d
Отлично, тогда мы на одном языке говорим на тему подтипов.
Но как на хаскеле existentials помогают эмулировать подтипы? Я то ли ни разу с таким не сталкивался, то ли не распознал это, когда таки сталкивался.
Druu
> Но как на хаскеле existentials помогают эмулировать подтипы?
data IShow = forall a. (Show a) => IShow a
тогда любой инстанс IShow ведет себя так же, как в ООП ведет себя класс, реализующий интерфейс IShow. Отличие только в наличии обертки, но на самом деле чисто с формальной точки зрения запись data IShow = forall a. (Show a) => a тоже валидна, просто в хаскеле так нельзя (вроде бы нельзя, по крайней мере, без каких-то хитрых расширений).
Druu
> тогда любой инстанс IShow
Любой инстанс Show, конечно же.
0xd34df00d
Я бы не назвал это сабтайпингом. Обычный type erasure — есть у вас некоторая коробка, в которой лежит объект, про который вы только знаете, что вы его можете отображать, и всё.
Druu
> объект, про который вы только знаете, что вы его можете отображать, и всё.
Но ведь сабтайпинг именно так и работает: «перед нами объект, про который мы можем сказать, что мы с ним можем делать все, что и с объектом, подтипом которого он является». В данном случае любой инстанс Show ведет себя как подтип IShow (если без боксинга).
0xd34df00d
Но вы с ним не можете делать ничего другого.
Да и чем тогда это отличается от
a
в функцииfoo :: Show a => a -> Smth
?Druu
> Да и чем тогда это отличается от a в функции foo :: Show a => a -> Smth?
Тем, что foo можно применять только к одному конкретному типу a, а foo :: IShow => smth к любому «подтипу» IShow (т.к. а там нет).
> Но вы с ним не можете делать ничего другого.
Как и в случае подтипирования — если вы написали ф-ю, которая работает с данным интерфейсом, вы не можете делать ничего, кроме операций, определенных в данном интерфейсе.
0xd34df00d
Итератор в ФП — это скорее обычный список (по крайней мере, в ленивых хаскелеподобных языках). Собственно, если относиться к списку не как к структуре данных, а к методу управления потоком исполнения, то всё становится сильно проще и понятнее.
0xd34df00d
Это, кстати, к разговору о паттернах в ФП. Использование ленивого списка как итератора и управляющей конструкции — вполне себе паттерн.
mayorovp
Ну уж нет. Итератор в ООП — это общий паттерн для организации обхода произвольных коллекций, а head/tail-список — это конкретная реализация.
Аналогом паттерна "Итератор" в Хаскеле можно назвать классы Traversable и Foldable, но никак не список.
0xd34df00d
У
Foldable
-то не зря есть методtoList
— то бишь, любой Foldable не зря имеет морфизм в списки (чаще всего забывающий, но для целей обхода это неважно).mayorovp
Но первична-то именно операция foldr, а toList выражается через нее.
0xd34df00d
Ну, естественно, это ж не прямой аналог.
Думать в терминах
foldMap
, на мой взгляд, удобнее, кстати.lair
… вот мы и пришли к вопросу "что же такое ООП".
TheShock
«ООП — это миф, или как на обманывают тыжпрограмисты», смотрите на РЕНТВ сегодня в 26.00.
lair
Я считаю, что опечатку "как на обманывают" надо расшифровывать "как, нах, обманывают".
symbix
Ну, от того, что реализован интерфейс, тоже пока еще ничего не случится. ООП появится там, где будет
someMethod(Iterable foo)
. :-)0xd34df00d
Позволю себе не согласиться. Выражать всякие алгоритмы на деревьях, или, не знаю, преобразования графов, или, не знаю, симуляцию регулярок — одно удовольствие на ФП. Но да, делается это не так, как в императивном стиле.
mayorovp
Это не алгоритмы. Алгоритм — это по определению последовательность действий, что для функциональных языков едва ли не запретное слово :-)
Функциональное выражение алгоритма на деревьях — это скорее схема такого алгоритма чем сам алгоритм.
0xd34df00d
С алгоритмами вообще всё очень сложно. Вот вы когда сказали, например, «последовательность», сразу отсекли класс параллельных алгоритмов, где важна не вся последовательность (сиречь полный порядок), а лишь частичный порядок между некоторыми из действий. Но это так.
Ну, в ленивых языках, которые не выполняются, а редуцируют графы, действительно, с последовательностью действий всё очень сложно (и очень сложно с оценкой их производительности и потребления памяти, и это куда сложнее всех этих трихомонадочек, кстати).
Фиг с ними с деревьями. Чем
take 5 . sortBy (comparing fst)
хуже такого кода?mayorovp
Ничем не хуже. Просто
take 5 . sortBy (comparing fst)
— декларативное выражение того что мы хотим получить в результате, а приведенный вами фрагмент кода на C++ — императивный алгоритм получения.0xd34df00d
Но есть очевидный и в известном смысле гладкий морфизм. На плюсах я тоже могу написать немного библиотечного кода, чтобы потом делать
take(5) | sortBy(comparing(fst))
, но это ж не сделает плюсы функциональным языком? Да и в какой момент код перестанет быть императивным и станет функциональным?mayorovp
Код перестает быть императивным когда из него пропадают явные указания на промежуточные состояния процесса решения задачи.
Так, в вашем примере после исполнения
std::sort
контейнерvec
оказывается в состоянии "отсортирован, но может содержать более 5 элементов". Это промежуточное состояние явно требуется кодом — но оно не требуется постановкой задачи и никоим образом из нее не следует!В то же время в варианте
take 5 . sortBy (comparing fst)
состояние "отсортирован, но может содержать более 5 элементов" имеет не входной список и не выходной — а некоторый промежуточный, не имеющий даже имени. Он полностью скрыт реализацией.Тем не менее, если сделать библиотеку на плюсах и написать что-то типа
x = take(5) | sortBy(comparing(fst)) | x
илиx.transform(take(5) | sortBy(comparing(fst)))
— то эта команда, хоть и написана в декларативном стиле, скорее всего будет являться частью какого-то императивного алгоритма."Функциональным" же код делает применение паттернов и подходов функционального программирования, сам по себе функциональный код может быть как декларативным, так и императивным.
0xd34df00d
То есть, если я напишу
или, одним выражением и с сохранением типа всего терма,
то это станет императивным кодом?
Если это спрятано внутри отдельной функции, то это точно так же является деталью реализации.
Мой поинт, если что, в том, что на таком уровне грань тонка и размыта.
Druu
> то это станет императивным кодом?
Не станет, потому что у вас никакого промежуточного состояния нету. f x where x = y — это просто (x => f(x))(y)
mayorovp
PS Последовательность — это не полный порядок. К примеру, на множестве вещественных чисел полный порядок задан — но последовательности они не образуют.
А параллельный алгоритм обычно можно рассматривать как несколько таких последовательностей.
myrslok
Вы, наверное, имеете в виду линейный порядок.
mayorovp
Да, точно. Наверное, 0xd34df00d тоже имел в виду именно его.
0xd34df00d
Да, его.
symbix
Ну, да, наверное, скорее "выражение алгоритма". Я несколько неудачно выразился, а 0xd34df00d правильно понял, что я имел ввиду.
DistortNeo
Я, кстати, так уже даже делал для задачи генерации кода обработки изображений в рантайме.
nevoroman
Позвольте не согласиться. Мне кажется, что как раз этот доклад — максимально про практичность, а не про «высокое искусство» и иже с ним. Потому что на каждую красивую конструкцию здесь приводится вполне конкретная и понятная проблема, которую эта конструкция отлично решает.
Ну и, разумеется, стоит понимать, что разнообразные функции «add» — это всего лишь максимально простой пример для демонстрации паттерна, а не реальный юзкейс. Разумный программист и без того понимает, что забивать гвозди микроскопом — не лучшая идея. А неразумный найдет способ сделать чушь и без функционального программирования.
mayorovp
Ага, особенно "практичен" вот этот кусок кода:
Был обычный алгоритм, который делал вполне конкретную вещь. Из него вынесли наружу всю конкретику, оставив тривиальный код. Но ведь
[1..10]
иprintLn
на самом деле никуда не делись! Они просто перешли к вызывающему коду, теперь каждый кто вызываетprintList
должен указывать еще и эти[1..10]
иprintLn
.В итоге уровень абстракции от такого преобразования на самом деле уменьшился, а не увеличился.
Напомнило https://xkcd.com/1790/
Что самое веселое, даже после такого преобразования код все еще не абстрагирован от всего. К примеру, для асинхронных операций код будет работать некорректно — запустит их параллельно вместо последовательного выполнения. О ужас! Нам срочно нужна операция
foldM
!PS в целом пост мне понравился, но конкретно этот пример вызвал возмущение своей бессмысленностью.
DistortNeo
В целом согласен: мне тоже неприятно, когда основы ФП объясняют на какой-то тривиальщине типа вычисления факториалов, когда выигрыша от абстрагирования не видно.
Кстати, называть функцию `printList` после такого преобразования уже некорректно, правильнее `visitList`. А `printList` — результат частичного применения `visitList` с параметром `printLn`.
ApeCoder
Вероятно, потому, что функция теперь называется неправильно. Она не печаетает а применяет действие к списку поэтому ее надо назвать по другому
Функция скрывает, как именно происходит перебор — например значение i уже наружи не видно и не надо о нем заботиться
Асинхронные операции это синхронные операции по созданию тасков. Так что для них код скорее всего даже не скомпилируется — будет ожидать функцию не возвращающую значения а на вход подадут нечто, возвращающее таск
mayorovp
Но раньше-то она скрывала список и действия!
Хорошо, что ошибочный код не скомпилируется. Но плохо что он при этом не будет работать.
ApeCoder
Это уже не она. Никто не мешает оставить старую, просто абстрагировать перебор оттуда
Если надо последовательно объединить асинхронные функции просто воспользуйтесь другой функцией
mayorovp
Автору статьи тоже не помешало бы так сделать :-)
Но зачем в таком случае абстракция над перебором списка, если она не позволяет нам повторно использовать код?
ApeCoder
Она нам позволяет повторно использовать код. Но не весь. Какого поведения вы хотите от нее? Чтобы выполняла параллельно? Чтобы ждала выполнения каждого шага? Это и надо специфицировать. То есть передавать ей функцию, которая не возвращает task, а что-то делает.
0xd34df00d
Кстати, для этого достаточно произвести композицию функции
map
с соответствующим примитивом, написав вместотакое:
Ну или просто
0xd34df00d
Чтобы прибавить к одному числу другое, не нужны никакие монады. И даже jquery не нужно.