Обновить
9
0
Корсаков Артём@fonkost

Scala Tech Lead

Отправить сообщение

Спасибо за пример!

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

Информация

В рейтинге
Не участвует
Откуда
Адлер, Краснодарский край, Россия
Работает в
Зарегистрирован
Активность

Специализация

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