Pull to refresh
9
0
Корсаков Артём@fonkost

Scala Tech Lead

Send message

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

А в теории, на основе ленивых вычислений есть техника "прыжков на батуте", на основе которой можно потом построить потоковую обработку.

В общем случае бесконечную последовательность можно определить через метод

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, то два раза посчитается одна и та же последовательность. Проблема легко решается, но надо помнить о ленивых вычислениях.

Мне очень нравится книга Functional Programming in Scala, Second Edition, где подробно рассказывается о ФП и ленивых вычислениях в частности. Я переводил книгу вот тут вот - постарался указать все плюсы и минусы.

Спасибо за уточнение! 👍

Под бесконечной последовательностью я имел в виду, например, ленивое рекурсивное определение:

val primes: LazyList[Int] =
  2 #::
    LazyList.from(3).filter: x =>
      primes.takeWhile(p => p <= math.sqrt(x)).forall(p => x % p != 0)

Определяем список простых чисел как:

1) 2 - простое число, добавляем в primes

2) Начиная с каждого следующего числа проверяем, что оно не делится на все primes, меньше квадрата, и если успех - это следующее простое число.

Это бесконечная ленивая рекурсия.

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

primes.take(10).toList
// List(2, 3, 5, 7, 11, 13, 17, 19, 23, 29)

По идеологическим точно не стоит, т.к. в Scala активно используются map - тоже цикл.

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

Спасибо за важное уточнение!

В ФП обычно используются абстракции 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

Спасибо за очередной выпуск "дайджеста"!

Реализовал в виде дерева (для Рюрика пока не получается, т.к. данных много, чуть позже постараюсь корректно реализовать):
Адам
Чингисхан
Романовы
Очень дельное замечание. Спасибо огромное, переделаю!
Спасибо за замечание, попробую переделать на Wikidata

Information

Rating
Does not participate
Location
Адлер, Краснодарский край, Россия
Works in
Registered
Activity

Specialization

Бэкенд разработчик
Ведущий
Scala
Функциональное программирование