Telegram Web Link
Пока значительная часть Scala-community сходит с ума от монад всех сортов и расцветок, некоторые отмечают недостаток выразительности существующего синтаксиса для работы с ними. Одним из решений является библиотека monadless.io, позволяющая работать вне контекста монады с помощью функции unlift и возвращаться в него с помощью метода lift. Бонусом - интеграция с инфраструктурой библиотек cats, monix и algebird.

Сайт monadless.io содержит достаточное количество примеров, покрывающих наиболее частые случаи использования, а также подробное объяснение того, как работает внутренняя машинерия библиотеки.

http://monadless.io/
Ни для кого не секрет, что механизм implicit-ов в Scala - чрезвычайно мощный инструмент, позволяющий писать легкочитаемый, не перегруженный код. Тем не менее, как и многие инструменты с большим числом возможностей, он может послужить обратной цели, если использовать его неправильно. В дополнение, сложность выведения некоторых implicit-ов в значительной мере затрудняет их восприятие.

Осознаёт сложившуюся ситуацию и лидер Scala-сообщества Мартин Одерски. В новой версии языка (aka Scala 3) он предлагает полностью заменить неявные определения механизмом instance-ов, более простым в обращении и при этом ограничивающим программиста от написания плохого кода. Более подробно о мотивации и синтаксисе новой конструкции языка можно почитать в репозитории компилятора Dotty:

https://github.com/lampepfl/dotty/tree/174b45edcf13c53597c87e46345418d86e95d396/docs/docs/reference/instances
Вдогонку к приятной новости про implicit-ы (как некоторые заметили, очередной шаг в направлении языка Haskell) в Scala 3 было внесено не менее масштабное предложение - полностью исключить null из значений отдельных типов и обособить его в свой собственный Null наряду с уже существующими AnyVal (примитивы) и AnyRef (объекты).

В целом, предложение содержит полную переработку механизма обращения с null-значениями на уровне компилятора. Основная идея заключается в присвоении всем потенциально аnullируемым значениям типа TypeName | Null, в связи с чем их уже нельзя будет передать в методы, принимающие строго тип TypeName. Из интересных особенностей - уточнение типа переменных при сравнении с null на стабильных путях (привет, Kotlin!). При этом авторы также основательно поработали над взаимодействием с кодом предыдущих версий языка Scala и Java-библиотеками, благодаря чему уже существующий код потребует лишь незначительных изменений.

Полное описание предложения можно найти по ссылке:
https://gist.github.com/abeln/9f79774bac111d99b3ae2cb9016a33e6
В качестве одного из основных аргументов в пользу функционального программирования часто приводится абстрактное "оно способствует написанию хорошего кода". Понять, про какой код идёт речь и почему именно он хороший можно на примере обычного http-сервиса.

В классическом рассмотрении такой сервис будет являться в первую очередь сервером, для которого будут определены доступные url и соответствующие им обработчики запросов. Зачастую также заданы (по умолчанию) обработчики серверных ошибок, позволяющие дать ответ на запрос даже при возникновении ошибочной ситуации, а механизмы аутентификации и снятия метрик представлены в виде отдельных модулей-библиотек.

С точки зрения ФП такой сервис будет являться просто функцией Request => Response. Из этого сразу вытекает, что взаимодействовать с таким сервисом можно даже без наличия реального сервера, и, более того, к нему применимы те же рассуждения, что и к обычным функциям. Путём модификации сигнатуры функции сервиса в него можно добавить, к примеру, обработку внутренних исключений, а с помощью комбинации функций можно добавить в сервис метрики, логирование, аутентификацию и т.д. Именно тот факт, что высокоуровневую абстракцию сервиса можно выразить с помощью функции - базовой конструкции языка - приводит к написанию тестируемого, композабельного и понятного кода, который можно назвать хорошим.

Более подробно (с примерами кода) в видео с митапа Scale by the bay:
https://www.youtube.com/watch?v=0oVpLdgZqpE
Существует достаточно много объяснений, что же такое монада, как формальных, так и отсылающих к жизненным аналогиям. В числе последних - буррито. Да, то самое мексиканское блюдо из лепёшки и произвольной начинки.

Проводить подобную параллель пытались далеко не в одной статье, но, пожалуй, один из самых правдопободных вариантов принадлежит Марку Доминусу. Это транслируется в термины Scala следующим образом:

1. Операция pure: всё, что угодно, мы можем завернуть в лепёшку и получить буррито.

2. Операция map: не меняя лепёшки, мы можем поменять начинку и остаться с буррито.

3. Операции flatMap/flatten: если сделать гигантский буррито, состоящий из вложенных в него буррито, то, последовательно развернув внутренние составляющие и скомбинировав начинки, мы вновь получим буррито.

Для функториальной составляющей есть даже иллюстрация:
Если для вас всё это не делает абсолютно никакого смысла, настоятельно рекомендую ознакомиться с книгой Haskell programming, где к концепции монады походят очень обстоятельно и без кулинарных ассоциаций:
Довольно часто в приложениях какой-то конкретный Int на самом деле представляет собой вовсе не произвольное число, а элемент из некоторого диапазона, не совпадающего со множеством всех возможных значений. Аналогичным образом это относится и к строкам, которые должны иметь определённый формат или попросту не быть пустыми.

Естественно, что ограничения подобного рода хочется выразить не только в документации к проекту и именах переменных, но и в системе типов, чтобы обеспечить их проверку компилятором. Некоторые языки (Agda, Idris, Whiley) позволяют создание зависимых типов из коробки, а такие языки как Haskell и Scala обеспечивают их поддержку на уровне библиотек.

Самой популярной библиотекой подобного плана в Scala является Refined, вдохновленная одноименным аналогом из Haskell. Суммарно в библиотеке поддерживается несколько десятков операций над типами. Вот так, к примеру, с её помощью будет записан тип строки, представляющей из себя непустой набор цифр: MatchesRegex[W.`"[0-9]+"`.T]. Несмотря на (порой значительное) увеличение времени компиляции, библиотека однозначно рекомендуется к ознакомлению.
Недавно в канале промелькнула новость о предложении убрать в новой версии Scala слово implicit как таковое. И вот, implicit-ы были полностью убраны из третьей версии языка. В каждом из специфичных случаев использования implicit (которых набралось целых 6 штук) теперь будет использоваться отдельная конструкция.

Помимо этого в стандартную библиотеку был добавлен автоматический вывод type class-ов, что позволит не тянуть в свой проект такую тяжеловесную библиотеку как Shapeless и ускорить время компиляции.

Также появилась возможность писать определения типов/методов/значений на самом верхнем уровне, без упаковки в object/class/trait, в связи с чем конструкция package object потеряла смысл и вскоре будет удалена из языка.

Более детально посмотреть список последних изменений и прочитать инструкцию по установке третьей версии Scala можно здесь:
https://dotty.epfl.ch/blog/2019/03/05/13th-dotty-milestone-release.html
Сегодня понадобилось сделать "typeclass"-аналог натурального преобразования со следующей сигнатурой:

trait LiftF[F[_], G[_]] {
def liftF[A](fa: F[A]): G[A]
}

Естественно, каждый раз писать в контекстных ограничениях что-то вроде F[_]: LiftF[?[_], G]] уже довольно удобно, но хочется иметь возможность делать это максимально лаконично, к примеру, так: F[_]: LiftF.To[G]. В данном случае тип To[G[_]] после применения к G возвращал бы type-лямбду LiftF[?[_], G], то есть, по сути, являлся бы аналогом частично применённого конструктора типов из Haskell.

К сожалению, эта затея не увенчалась успехом - Scala не поддерживает определение "недоприменённых" типов через ключевое слово type - все параметры должны быть вынесены в сигнатуру типа. При этом использование аналога Aux паттерна приводит к ещё более неудобному синтаксису:

trait To[G[_]] {
type From[F[_]] = LiftF[F, G]
}

def func[A, G[_], F[_]: LiftF.To[G]#From](a: F[A]): G[A]

Тем не менее, эта заметка не появилась бы в канале, если бы этим всё и ограничилось. Оказывается, в Scala 3 уже есть возможность записывать частично-применённые типы без каких-либо плагинов и библиотек. Вот так в Dotty будет выглядеть реализация типа To:

type To[G[_]] = [F[_]] => LiftF[F, G]

Таким образом, появилась ещё одна отличная причина ждать/уже сейчас пробовать Dotty.
Изначально планировал, что на месте этого поста будет сравнение двух подходов к реализации полиформизма: композиции и наследования. Но, пока искал материал, наткнулся на гораздо более животрепещущий топик.

Одним из самых острых вопросов для jvm-программистов на сегодняшний день является уменьшение количества аллокаций в системе. Если не придавать значение числу создаваемых объектов, то можно столкнуться с небезызвестными "GC-подвисаниями", когда сборщик мусора забирает всё процессорное время для удаления ненужных объектов.

Естественно, данная проблема не обошла и Scala. Поскольку в языке нет примитивов, количество используемой памяти могло бы возрастать непозволительно быстро. К счастью, "волшебный" trait AnyVal, наследуемый такими типами как Int, Long и т.д., позволяет превратить любой класс с одним value-параметром в обёртку, которая во время исполнения программы не инстанцинируется (все методы такого класса становятся статическими).

На самом деле, это, конечно, работает далеко не всегда. В документации выделяют три случая, когда обёртка (value-класс) всё-таки превращается в полноценный объект:
1. Value-класс рассматривается как объект другого типа.
2. Value-класс помещён в массив
3. Проверка типа value-класса во время исполнения программы (например, паттерн матчинг)

Максимально подробные тесты можно найти по ссылке, однако в качестве простого примера, когда объект-обёртка будет создан (по правилу 1), можно привести следующий код:
object Test {
case class Value(value: Int) extends AnyVal
def identity[A](a: A): A = a

println(identity(Value(1))
}

К счастью (нет, я не специально выбираю такие темы), в Scala 3 появятся Opaque types - конструкция языка, гарантирующая отсутствие обёртки при исполнении программы. На текущий же момент рекомендуется просто с умом использовать AnyVal (или, например, такие конструкции, как типы с тэгами) и прежде всего тестировать, какое реальное влияние оказывает AnyVal на быстродействие в программе.
Был удивлён, что при всём обилии возможностей Scala и нарицательном названии "better Java", всё ещё встречаются случаи, когда в полноценный Scala проект приходится-таки вносить HorribleClass.java. Один из таких случаев - написание runtime-аннотаций.

Scala поддерживает аннотации с помощью абстрактного класса Annotation. Используется этот механизм не очень часто, но встретить можно - например, как инструмент для (де)сериализации JSON. До времени исполнения такие аннотации не доживают и стираются компилятором.

Зачем вообще может понадобиться доступ к аннотациям во время работы программы? К примеру, в тестах: допустим, вы используете модную библиотеку, основанную на JUnit. При этом вы хотите пометить определённые тестовые пакеты (которые представляются в виде классов) как интеграционные, чтобы запускать их отдельно от юнит-тестов. Для этого необходимо использовать аннотации, которые JUnit проанализирует уже после запуска тестов.

Поскольку аннотации Scala после запуска видны не будут, необходимо создавать Java-аналог. Выглядеть он будет примерно так:

@org.scalatest.TagAnnotation
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface IntegrationTest {}

Самое печальное, что это сделано намеренно - в документации отдельно описывается разметка тестовых пакетов Java-аннотациями. Разметку же отдельных тестов можно делать средствами Scala, но при большом их количестве это становится крайне утомительно.

В противовес этому досадному недоразумению в библиотеке specs2 в этом плане всё гораздо более положительно - можно задавать произвольную иерархию тестов, которая к тому же не обязательно должна быть древовидной. Тем не менее (возможно по причинам исторического характера) используется эта библиотека в 7 раз реже (согласно mvnrepository.com).
Наконец-то дошли руки присоединиться к хабра-сообществу: опубликовали мой перевод поста Луки Якобовича "A tale on semirings".

Я выбрал для перевода именно этот пост, поскольку мне очень понравилась наглядность примера - переложение структуры абстрактной алгебры (полукольцо) и связанных с ней законов на код, понятный даже не знакомому со Scala человеку. В дополнение к этому показывается, как можно перенести концепцию полукольца на типы высшего порядка. И всё это с отсылками на классы cats - одной из наиболее используемых в Scala ФП-библиотек.

Надеюсь, вам понравится как мой перевод, так и сам пост Луки. Несмотря на сравнительно большой объём, материал интересный и изложен достаточно подробно.
На днях промелькнула заметка об оптимизациях, проводимых компилятором Haskell.

Обычно в контексте максимальной производительности упоминают C, C++ или какую-нибудь из версий ASM. Это обусловлено тем, что данные языки максимально "близки к железу": позволяют напрямую работать с памятью, выравнивать байтики в структурах и компилируются напрямую в машинный код для целевой платформы.

При всём обилии оптимизаций в компиляторах таких языков, ускорение программы подразумевает неизменность в логике ее работы. В данном же случае выведение логики программы и, как следствие, возможность оптимизации, ограничены обилием низкоуровневых возможностей - компилятор не может однозначно вывести взаимосвязи между частями программы.

Haskell, в свою очередь, гораздо более структурирован и обладает своим набором оптимизаций перед компиляцией в язык C— (да, "си минус минус"), который впоследствии переводится в целевой язык. Внезапно, в некоторых случаях эти оптимизации позволяют превзойти в производительности аналогичную программу на языке (в данном случае, ATS), компилируемом с помощью gcc или clang.

Естественно, вышесказанное не означает, что Haskell "быстрее C" - интересна возможность использования функциональной природы языка для оптимизации самой структуры програмы. Можно надеяться, что подобный функционал появится и в Dotty - в списке возможностей компилятора строке "whole program optimizer" соответствует статус "в процессе".
Обычно в книгах по изучению какого-либо языка типам посвящают одну-две главы или вообще ограничиваются краткими вставками по ходу книги. Для тех, кто давно ждал расширенного руководства по программированию на уровне типов, в этом году появилась книга Thinking with types под авторством Сэнди Магуайра.

На протяжении около 200 с небольшим страниц рассматриваются как уже достаточно популярные темы (типы высшего порядка, частично применённые типы), так и достаточно экзотические - например, вычисление на уровне типов. Практически все примеры приведены на Haskell, но большинство концепций может быть адаптировано и в других языках программирования.
Недавно с удивлением узнал, что в Scala есть продолжения (continuations), аналогичные оным в языках Scheme и Ruby. Добавить их можно с помощью специального плагина и флага компилятора.

Продолжения в каком-то роде представляют из себя функциональный аналог goto, позволяя передавать управление вне текущего блока, а потом снова в него возвращаться. Вот так, например, с их помощью можно реализовать простейший итератор в Scala:

reset {
def iterate() = shift { f: (Int => Int) =>
println("start")
val x = f(0)
println(s"i $x")
x + 1
}
iterate()
iterate()
iterate()
}

Относятся к продолжениям неоднозачно - в JRuby, к примеру, их отказались поддерживать. Незавидная участь, по видимому, ожидает их и в Scala - последнее обновление для соответствующего плагина выходило 2 года назад и даже тогда не было заметно его активного использования в проектах.

Причина забвения проста - чтобы разобраться в коде с их использованием, нужно приложить значительные усилия. К примеру, попробуйте догадаться, что напечатает в консоль вышеприведённый код.
Что выведет код?
anonymous poll

start i0 start i1 start i2 – 5
👍👍👍👍👍👍👍 71%

start start start i0 i1 i2 – 1
👍 14%

Код упадёт с ошибкой StackOverflow – 1
👍 14%

i0 i1 i2 start start start
▫️ 0%

start i0 start i0 start i0
▫️ 0%

Код не скомпилируется
▫️ 0%

👥 7 people voted so far.
Правильный ответ на пятничный вопрос - start start start i0 i1 i2. Сам результат при этом не так интересен, как приводящая к нему логика исполнения программы. Чтобы её увидеть, можно добавить в код дополнительные отладочные выражения. Обратите внимание, что написать println("end") в конце нельзя, так как тип результата блока reset согласован с типом функции f.

val x = reset {
def iterate(n: Int) = shift { f: (Int => Int) =>
println(s"start $n")
val x = f(0)
println(s"got back to $n - got res $x")
println(s"ran $x")
x + 1
}
println("pre-start 0")
iterate(0)
println("pre-start 1")
iterate(1)
println("pre-start 2")
val res = iterate(2)
println("end")
res
}
println(x)

Cоответствующий вывод в консоль:

pre-start 0
start 0
pre-start 1
start 1
pre-start 2
start 2
end
got back to 2 - got res 0
ran 0
got back to 1 - got res 1
ran 1
got back to 0 - got res 2
ran 2
3
2025/07/03 16:50:26
Back to Top
HTML Embed Code: