В общем случае бесконечную последовательность можно определить через метод
def iterate[A](start: => A)(f: A => A): LazyList[A] =
В метод передается начальный элемент и функция получения следующего элемента. И так можно составить бесконечную последовательность, например, числа Фибоначчи:
val fibs = LazyList.iterate((0, 1))((a, b) => (b, a + b))
fibs.take(10).toList
// List((0,1), (1,1), (1,2), (2,3), (3,5), (5,8),
// (8,13), (13,21), (21,34), (34,55))
Или частные случаи:
// Последовательность единиц
val ones = LazyList.continually(1)
// Натуральные числа
val naturals = LazyList.from(1, 1)
Причем когда мы составляем бесконечную последовательность мы можем добавлять преобразования над этой последовательностью
val fibs = LazyList.iterate((0, 1))((a, b) => (b, a + b))
val algorithm =
fibs
.filter(_._1 % 2 == 0)
.take(10)
algorithm.toList
// List((0,1), (2,3), (8,13), (34,55), (144,233), (610,987),
// (2584,4181), (10946,17711), (46368,75025), (196418,317811))
Т.е. мы разделяем алгоритм от самого вычисления алгоритма. Конечно, тут есть минус, о котором надо помнить: если два раза вызвать algorithm.toList, то два раза посчитается одна и та же последовательность. Проблема легко решается, но надо помнить о ленивых вычислениях.
В ФП обычно используются абстракции while: map, flatMap, filter Под капотом это тот же while, но более читаемо для ФП-разработчиков, потому делается акцент на преобразованиях:
def mult(
matrix1: IndexedSeq[IndexedSeq[Int]],
matrix2: IndexedSeq[IndexedSeq[Int]]
): IndexedSeq[IndexedSeq[Int]] =
matrix1.indices.map: i =>
matrix2.head.indices.map: j =>
val row = matrix1(i)
val col = matrix2.map(r => r(j))
row.indices.foldLeft(0) { case (acc, k) => acc + row(k) * col(k) }
С одной стороны тот же while, с другой - акцент именно на функциях преобразования. Мне кажется, что map и т.д. более безопасны, т.к. уменьшают вероятность опечатки.
Мне кажется, что самый сложный и важный язык программирования, которым должны овладеть все разрабочики без исключения - это блокнот и ручка. Если разработчик не может выразить словами то, что он собирается делать, то это большая беда. Если я вижу, что разработчик сразу бежит реализовывать задачу, то сразу останавливаю. Все остальное - лишь различные способы выразить то, что разработчик написал на бумаге. А дальше зависит от ситуации: иногда приходится писать и не на любимом языке программирования. Например, мне недавно нужно было на typescript написать плагин. Ничего не имею против typescript, но если бы у меня было выбор, то я выбрал бы Scala.
И я бы не назвал Scala хипстерским языком, все-таки создан был в 2003. И возможно постепенное появление новых языков программирования - это как раз очередная попытка научиться наиболее точно и без ошибок выразить то, что написано ручкой на бумаге.
Безусловно! Все так: надо подробно описывать, как все это выглядит в реальном мире и во что превращается. Я начал этот путь в отдельном разделе. С одной стороны рано давать ссылку, с другой - можно получить конструктивную обратную связь. Что касается IO, то это действительно проблема, т.к. часто разработчики пихают IO в те места, где лучше использовать другие концепции, например, Either, Validated, либо Stream. Это тоже отдельная большая тема, которую нужно обсуждать и как-то прояснять, где IO действительно используется, а где - лучше использовать что-то другое.
Да, согласен! Самая большая проблема - высокий порог входа. Я пытаюсь на Scalabook как-то снизить этот порог входа. Время покажет, удастся ли мне это или нет.
Большая проблема состоит в том, что вроде как с Java на Scala перейти не так сложно, но по факту получается, что разработчики продолжают писать на Java, только используя Scala. Как например, тут. Там от Scala только название языка, а так - чистая Java. Я пытался даже контрибьюить в такие репы, но это бесполезно без подробного объяснения на примерах, в чем плюсы и какие минусы в тех или иных подходах.
Да, все верно: в продуктовой разработке и в реальном мире без побочных эффектов работать нельзя!
Есть несколько способов работы с побочными эффектами в Scala:
Работа с внешними эффектами. Подробно концепция работы с внешними эффектами разобрана тут: Внешние эффекты и ввод/вывод. Там можно изучить концепцию, которая применяется в ФП языках. В Scala есть библиотеки для работы с внешними эффектами: Cats Effect, ZIO и другие.
Работа с внешними эффектами - Кратко. Если же кратко, то рекомендуется из функций с внешними эффектами выделять чистые функции. Таким образом внешние эффекты постепенно вытесняются на первоначальный этап запуска сервиса и самый конечный этап выдачи выходных данных клиенту. Внутренняя часть программы - это чистые функции. Из основных плюсов: они более понятные, поддерживаемые и тестируемые. Для функций с побочными эффектами - например, взаимодействие с базой данных - требуется трудоемкое тестирование: надо поднимать testcontainers, прогонять миграцию, накатывать тестовые данные и т.д. Как показывает практика продуктовой разработки в этом случае количество тестов начинает разрастаться в геометрической прогрессии, что в конечном итоге приводит к тому, что разработчики "забивают" на тесты, игнорируют их и проверка кода в лучшем случае переносится на плечи QA, а в худшем случае и чаще всего - на плечи клиента, что крайне негативно влияет на мнение клиента о качестве продукта. В функциональном же подходе, при выносе чистых функций мы разделяем логику от реализации. Мы можем тестировать чистые функции с помощью юнит-тестов - это гораздо быстрее, чем поднимать инфраструктуру. К тому же тесты растут линейно. Если мы выделим N-чистых функций, то нам часто достаточно 2*N юнит-тестов для проверки позитивного и негативного случаев. А для проверки логики мы можем использовать моканную реализацию БД, что позволяет нам отдельно тестировать логику сервиса, что опять же быстрее, чем поднимать контейнеры и прогонять миграцию. В результате на тестирование самого внешнего эффекта чаще всего достаточно только одного позитивного и одного негативного теста с использованием реальной БД и тестовых данных. Причем эти тесты будут простыми и проверять только взаимодействие с БД. Их легко поддерживать, потому что логику и чистые функции мы будем тестить и дорабатывать в рамках юнит-тестов. Соответственно большая часть ошибок и проблем перехватывается на этапе разработки, т.к. разработчикам легче поддерживать код и покрывать его тестами, а не на стороне QA и уж тем более на стороне клиента. Подробнее о подходе тут - Внешние эффекты и ввод/вывод.
Локальные эффекты и изменяемое состояние можно инкапсулировать: подробнее тут - Локальные эффекты и изменяемое состояние. Причем можно переложить на плечи компилятора проверку того, что никто вовне не имеет доступ к изменяемому состоянию, что повышает безопасность сервиса и исключает большинство проблем при распараллеливании, когда из нескольких потоков пытаются обновить один элемент.
Отдельно стоит упомянуть о работе с потоками, когда нам надо, например, обрабатывать файлы. Подход в функциональном программировании инкапсулирует работу с ресурсами, что избавляет от ошибок, когда разработчик забыл закрыть ресурс, или когда надо обработать ресурс с "почти бесконечным" потоком. В Scala используется библиотека для потоковой обработки FS2. Подробнее о принципах работы с потоковой обработкой в функциональном программировании вот тут - Потоковая обработка и инкрементный ввод-вывод
Понятно, что многое зависит от текущей ситуации и текущего клиента, подходов к разработке ПО довольно много, но, уверен, ознакомиться с подходами в функциональном программировании все же стоит. И попробовать, конечно, для общего развития!
Хотел написать кратко, но получилось "как всегда" :)
Спасибо за обратную связь и за ссылку на chisel - добавил себе в закладки!
Сейчас по планам в ближайшие полгода - это добить раздел по ФП https://scalabook.ru/fp/, а затем сконцентрироваться на библиотеках https://typelevel.org/cats-effect/ и https://fs2.io/#/, т.к. они активно используются в нашей компании и нам требуется актуализировать базу знаний по этим библиотекам. А после этих разделов тогда можно будет подумать о chisel
Спасибо за пример!
Можно даже бесконечный файл прочитать. На практике все пользуются уже готовыми решениями, например, fs2.
А в теории, на основе ленивых вычислений есть техника "прыжков на батуте", на основе которой можно потом построить потоковую обработку.
В общем случае бесконечную последовательность можно определить через метод
В метод передается начальный элемент и функция получения следующего элемента. И так можно составить бесконечную последовательность, например, числа Фибоначчи:
Или частные случаи:
Причем когда мы составляем бесконечную последовательность мы можем добавлять преобразования над этой последовательностью
Т.е. мы разделяем алгоритм от самого вычисления алгоритма. Конечно, тут есть минус, о котором надо помнить: если два раза вызвать algorithm.toList, то два раза посчитается одна и та же последовательность. Проблема легко решается, но надо помнить о ленивых вычислениях.
Мне очень нравится книга Functional Programming in Scala, Second Edition, где подробно рассказывается о ФП и ленивых вычислениях в частности. Я переводил книгу вот тут вот - постарался указать все плюсы и минусы.
Спасибо за уточнение! 👍
Под бесконечной последовательностью я имел в виду, например, ленивое рекурсивное определение:
Определяем список простых чисел как:
1) 2 - простое число, добавляем в primes
2) Начиная с каждого следующего числа проверяем, что оно не делится на все primes, меньше квадрата, и если успех - это следующее простое число.
Это бесконечная ленивая рекурсия.
Для того, чтобы что-то получить, надо явно вызвать вычисление, например, преобразовав в связанный список:
По идеологическим точно не стоит, т.к. в Scala активно используются
map- тоже цикл.Я скорее за то, чтобы предложить альтернативу.
Спасибо за важное уточнение!
В ФП обычно используются абстракции
while:map,flatMap,filterПод капотом это тот же
while, но более читаемо для ФП-разработчиков, потому делается акцент на преобразованиях:С одной стороны тот же
while, с другой - акцент именно на функциях преобразования.Мне кажется, что
mapи т.д. более безопасны, т.к. уменьшают вероятность опечатки.Мне кажется, что самый сложный и важный язык программирования, которым должны овладеть все разрабочики без исключения - это блокнот и ручка. Если разработчик не может выразить словами то, что он собирается делать, то это большая беда. Если я вижу, что разработчик сразу бежит реализовывать задачу, то сразу останавливаю. Все остальное - лишь различные способы выразить то, что разработчик написал на бумаге. А дальше зависит от ситуации: иногда приходится писать и не на любимом языке программирования. Например, мне недавно нужно было на typescript написать плагин. Ничего не имею против typescript, но если бы у меня было выбор, то я выбрал бы Scala.
И я бы не назвал Scala хипстерским языком, все-таки создан был в 2003. И возможно постепенное появление новых языков программирования - это как раз очередная попытка научиться наиболее точно и без ошибок выразить то, что написано ручкой на бумаге.
Кстати, у функционального программирования богатая история.
Безусловно! Все так: надо подробно описывать, как все это выглядит в реальном мире и во что превращается. Я начал этот путь в отдельном разделе. С одной стороны рано давать ссылку, с другой - можно получить конструктивную обратную связь. Что касается IO, то это действительно проблема, т.к. часто разработчики пихают IO в те места, где лучше использовать другие концепции, например, Either, Validated, либо Stream. Это тоже отдельная большая тема, которую нужно обсуждать и как-то прояснять, где IO действительно используется, а где - лучше использовать что-то другое.
Да, согласен! Самая большая проблема - высокий порог входа. Я пытаюсь на Scalabook как-то снизить этот порог входа. Время покажет, удастся ли мне это или нет.
Большая проблема состоит в том, что вроде как с Java на Scala перейти не так сложно, но по факту получается, что разработчики продолжают писать на Java, только используя Scala. Как например, тут. Там от Scala только название языка, а так - чистая Java. Я пытался даже контрибьюить в такие репы, но это бесполезно без подробного объяснения на примерах, в чем плюсы и какие минусы в тех или иных подходах.
Да, все верно: в продуктовой разработке и в реальном мире без побочных эффектов работать нельзя!
Есть несколько способов работы с побочными эффектами в Scala:
Работа с внешними эффектами. Подробно концепция работы с внешними эффектами разобрана тут: Внешние эффекты и ввод/вывод. Там можно изучить концепцию, которая применяется в ФП языках. В Scala есть библиотеки для работы с внешними эффектами: Cats Effect, ZIO и другие.
Работа с внешними эффектами - Кратко. Если же кратко, то рекомендуется из функций с внешними эффектами выделять чистые функции. Таким образом внешние эффекты постепенно вытесняются на первоначальный этап запуска сервиса и самый конечный этап выдачи выходных данных клиенту. Внутренняя часть программы - это чистые функции. Из основных плюсов: они более понятные, поддерживаемые и тестируемые. Для функций с побочными эффектами - например, взаимодействие с базой данных - требуется трудоемкое тестирование: надо поднимать testcontainers, прогонять миграцию, накатывать тестовые данные и т.д. Как показывает практика продуктовой разработки в этом случае количество тестов начинает разрастаться в геометрической прогрессии, что в конечном итоге приводит к тому, что разработчики "забивают" на тесты, игнорируют их и проверка кода в лучшем случае переносится на плечи QA, а в худшем случае и чаще всего - на плечи клиента, что крайне негативно влияет на мнение клиента о качестве продукта. В функциональном же подходе, при выносе чистых функций мы разделяем логику от реализации. Мы можем тестировать чистые функции с помощью юнит-тестов - это гораздо быстрее, чем поднимать инфраструктуру. К тому же тесты растут линейно. Если мы выделим N-чистых функций, то нам часто достаточно 2*N юнит-тестов для проверки позитивного и негативного случаев. А для проверки логики мы можем использовать моканную реализацию БД, что позволяет нам отдельно тестировать логику сервиса, что опять же быстрее, чем поднимать контейнеры и прогонять миграцию. В результате на тестирование самого внешнего эффекта чаще всего достаточно только одного позитивного и одного негативного теста с использованием реальной БД и тестовых данных. Причем эти тесты будут простыми и проверять только взаимодействие с БД. Их легко поддерживать, потому что логику и чистые функции мы будем тестить и дорабатывать в рамках юнит-тестов. Соответственно большая часть ошибок и проблем перехватывается на этапе разработки, т.к. разработчикам легче поддерживать код и покрывать его тестами, а не на стороне QA и уж тем более на стороне клиента. Подробнее о подходе тут - Внешние эффекты и ввод/вывод.
Локальные эффекты и изменяемое состояние можно инкапсулировать: подробнее тут - Локальные эффекты и изменяемое состояние. Причем можно переложить на плечи компилятора проверку того, что никто вовне не имеет доступ к изменяемому состоянию, что повышает безопасность сервиса и исключает большинство проблем при распараллеливании, когда из нескольких потоков пытаются обновить один элемент.
Отдельно стоит упомянуть о работе с потоками, когда нам надо, например, обрабатывать файлы. Подход в функциональном программировании инкапсулирует работу с ресурсами, что избавляет от ошибок, когда разработчик забыл закрыть ресурс, или когда надо обработать ресурс с "почти бесконечным" потоком. В Scala используется библиотека для потоковой обработки FS2. Подробнее о принципах работы с потоковой обработкой в функциональном программировании вот тут - Потоковая обработка и инкрементный ввод-вывод
Понятно, что многое зависит от текущей ситуации и текущего клиента, подходов к разработке ПО довольно много, но, уверен, ознакомиться с подходами в функциональном программировании все же стоит. И попробовать, конечно, для общего развития!
Хотел написать кратко, но получилось "как всегда" :)
Спасибо за обратную связь и за ссылку на chisel - добавил себе в закладки!
Сейчас по планам в ближайшие полгода - это добить раздел по ФП https://scalabook.ru/fp/, а затем сконцентрироваться на библиотеках https://typelevel.org/cats-effect/ и https://fs2.io/#/, т.к. они активно используются в нашей компании и нам требуется актуализировать базу знаний по этим библиотекам. А после этих разделов тогда можно будет подумать о chisel
Спасибо за очередной выпуск "дайджеста"!
Адам
Чингисхан
Романовы