POP-lang — воображаемый функциональный язык, основанный на Dependency injection

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Доброе утро, дорогие читатели, в этом выпуске мы рассмотрим, как может выглядеть функциональный язык, основанный на Dependency Injection. На обложке я приводил пример про хипстера в средневековье:

MiddleAges(status, lifespan) = hipster

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

В функциональных языках, как вы знаете, паттерн-матчинг не позволяет подобных вольностей: значения можно матчить либо по структуре, либо по содержанию. Кроме этого, можно использовать очень ограниченный набор проверок - так называемые guards. И этому есть простое объяснение - почему нельзя использовать дорогостоящие вычисления: такие вычисления нужно иметь возможность переиспользовать.

Пусть у нас есть проблема:

case problem:
    PlanA(timeline, benefits):
        # actions
    PlanB(timeline, cost):
        # other actions
    _:
        # give up

Допустим, мы попробовали выбрать план A - и выяснили, что, в данном случае, он неприменим. Попутно мы сделали массу вычислений, некоторые из которых можно было бы использовать и для анализа плана B - и которые теперь пропадут. Именно поэтому сложные вычисления не используются при паттерн матчинге. Мы также их не будем использовать - то есть, конструкцию case трогать не будем. Если что, здесь и дальше я буду использовать псевдокод - чего вы ещё ожидали от статьи о вымышленном языке? () означает кортежи, {} - словари.

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

Вообще, мой личный опыт в функциональном программировании связан с платформой BEAM и языками Erlang и Elixir (поэтому комментарии пишуших на других языках лично для меня интересны вдвойне). Некоторое время назад я был приятно удивлён, наткнувшись на язык gleam - так, что даже написал статью о нём. Если Вы о нём не слышали, gleam - это язык со статической типизацией для BEAM с С-подобным синтаксисом (то есть, он скорее похож на Scala, чем на Haskell).

Забавно, но идея этого поста навеяна оператором try из gleam. Забавно то, что его сделали deprecated в последнем релизе - так что, он, увы, доживает свой последний месяц. Выглядел он так:

fn even_number(x) {
    case x % 2 {
        0 -> Ok(x)
        1 -> Err("something odd")
    }
}

fn main(x: Int) {
    try x = even_number(x)
    // ...
}

// equivalent code
fn main(x: Int) {
    val = even_number(x)
    case val {
        Err(err) -> val
        Ok(x) -> {
            // ...
        }
    }
}

Это уже не псевдокод, а синтаксис gleam. Функция even_number возвращает Ok(x), если x чётное, в противном случае - ошибку. Оператор try действует так, что, в случае чётного числа, мы получаем снова его значение, в случае же нечётного - сама функция main возвращает ошибку.

Другими словами - концепция, хорошо известная разработчикам на Go:

Но вернёмся к нашим баранам. Именно вышеупомянутые ошибки в стиле Go я решил использовать для нашего случая - Dependency Injection. Мы сделаем так: будем делать паттерн-матчинг всего одного выражения - при этом, если матчинг не удался, но есть какие-то результаты, которые могут нам быть интересны, они возвращаются нам в качестве ошибки.

Функции оператора try возьмёт на себя оператор pop:

pop MiddleAges(status) = hipster

pop означает - выплюнуть ошибку, если она есть. В случае, когда нет блока обработки ошибок (как здесь) - то в родительскую функцию. Если такой блок есть, в нём происходит матчинг ошибок:

pop MiddleAges(status) = hipster:
    Err("Coffee is not known in the kingdom"):
        Err("Wrong place, try Turkey or Africa")

# They have coffee
print((hipster, status))

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

Мне кажется, название pop лучше, чем try, потому что после try обычно идёт оптимистичный сценарий и только потом - обработка ошибок. А у нас - наоборот. Кроме того, блок try-catch обычно используется (в том числе, в Erlang) для обработки настоящих исключений (и язык gleam, между прочим, их никак не поддерживает).

Констукция pop может иметь и блок else, который позволяет обрабатывать успехи, а не только ошибки, и в целом, больше соответствует функциональному стилю:

result = pop MiddleAges(status) = hipster:
    err:
        err
    else:
        print((hipster, status))
        Ok(status)

В этом примере result - это Ok() или Err(). По аналогии с try, pop можно также использовать с функциями, возвращающими Ok() или Err():

val = pop my_risky_function()

Теперь перейдём к следущей части. Читатель, возможно, задаётся вопросом, как, собственно, реализовать Dependency Injection - то есть, как сделать MiddleAges функцией. Это сделать несложно. Представим себе такой модуль:

# middle_ages.pop

fn match(hipster):
    (True, hipster)

fn status(hipster):
    ...
    (some_status, hipster)
    
fn lifespan(hipster):
    ...
    (days, hipster)

Пусть этот модуль отвечает за конструкцию MiddleAges. Функция match делает сначала общий матч - определяет, можно ли вообще хипстеру в средние века. В нашем случае - видим, что можно (True), при этом, следующий аргумент (hipster) передаётся в функцию, вычисляющую уже что-нибудь конкретное. Каждая функция отдаёт 2 значения - свой результат и то, что нужно передать в следующую функцию. Опять же, в данном случае - это (неизменяемый) параметр хипстер. Таким образом, все функции (кроме match) принимают на вход одно и то же.

Вы можете возразить, что функции могут зависеть от результатов друг друга, или иметь общие зависимости. И что в приведённом примере они могут дублировать работу друг друга. Это так, но это тоже решается несложно: можно передавать функциям ещё один параметр - контекст, в котором они будут сохранять полезные результаты своей работы:

# middle_ages.pop

fn match(hipster):
    (True, (hipster, {}))

fn status((hipster, ctx)):
    ...
    new_ctx = probably_change(ctx)
    (some_status, (hipster, new_ctx))
    
fn lifespan((hipster, ctx)):
    ...
    new_ctx = probably_change(ctx)
    (days, (hipster, new_ctx))

Не хочу особенно на этом останавливаться - просто поверьте, что это реализуемо, и относительно несложно. Чтобы убедится, что такой способ не содержит особенной магии, нужно, чтобы результаты "паттерн-матчинга" можно было узнать и обычным вызовом функции:

(status, lifespan) = resolve(hipster, match_fn, (status_fn, lifespan_fn))

Каждая из функций - match_fn, status_fn, lifespan_fn - может вернуть как ошибку, так и значение. Ошибки можно сматчить при помощи оператора pop - вы уже видели как.

Это - что касается Dependency Injection. Но, как говорится, мы сделали только два первых шага - давайте теперь нарисуем остальную часть совы. Такой кастомный паттерн-матчинг можно довольно легко встроить в систему типов.

Вот так, например, можно указывать типы параметров в функции:

fn my_function(MyType() = val, Int() = num, x):
    ...

Причём, MyType может быть определён как структурный тип - точно так же, как это делает gleam:

type Cat {
  Cat(name: String, cuteness: Int, age: Int)
}

Концепция структурных типов простая и понятная, и отказываться от неё нет смысла. Кроме этого, тип может использовать паттерн-матчинг а-ля Dependency Injection:

fn my_handler(JwtAuth(user, permissions, info) = request):
    ...

Вообще, если наши матчеры выполняют функцию типов, то должны и называться как типы - что-то вроде Request(), в данном случае. Но, учитывая, что и JwtAuth и рассмотренный ранее MiddleAges - всё-таки, абстрактные сущности, наверно, для них это не так критично.

Подобная запись позволяет использовать как аннотацию типами, так и структурный матчинг значений - одновременно. gleam так не умеет, кроме того, в нём нельзя объявлять несколько функций под одним именем, как это можно в эрланге. Как в примере ниже:

fn my_fun(MyMap() = {height, width}):
    ...
    
fn my_fun("description"):
    "description here"

Типы не должны принимать участия в матчинге нужной функции при вызове - если их объявлено несколько. Должна сматчиться функция, которая сматчилась бы и без аннотации типами. Возможно, какая-то функция сматчится, а затем вычисления Dependency Injection приведут к ошибке. Что ж - в этом случае, вернём ошибку в родительскую функцию.

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

В конце, по традиции, небольшой опрос. Только, большая просьба: голосовать не за название идеи (POP), а за её содержание! Название - я и так знаю, что удачное: и с церковью есть ассоциации, и с эстрадой, и даже, простите, с пятой точкой. Что же касается содержания - я вот думаю, может в хабах по фронтенд-разработке её ещё опубликовать и в хабах, посвящённых Java и Kotlin? Там поклонников Dependency Injection - не то что каждый второй, а каждый второй и третий, как минимум. Ладно - сами найдут.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Опрос
0% Мега идея 0
100% Автор, отсыпь и мне хоть немного 1
Проголосовал 1 пользователь. Воздержался 1 пользователь.
Источник: https://habr.com/ru/post/724892/


Интересные статьи

Интересные статьи

Достижение максимальной выразительности и абстракции домена при сохранении точности протокола актора с помощью библиотеки endless4s Scala.Код, описывающий бизнес-логику, несомненно, является самым цен...
Говоря о разработке сайтов с использованием CMS 1C Bitrix вопрос покрытия тестами поднимается редко. Главная причина в том, что большинство проектов обходится штатным функционалом, который предоставля...
В очередной публикации о медиацентре Kodi поговорим о том, как сделать его еще более универсальным и комфортным. Не будем сравнивать и выбирать между Kodi и Plex - будем использовать их вместе, одновр...
Пролог- Глянь, статью на Хабр подготовил.- Эм... а почему заголовок на английском?- "Предметно-ориентированное проектирование, Гексагональная архитектура портов и адаптеров, Внедрение зав...
В «1С-Битрикс» считают: современный интернет-магазин должен быть визуально привлекательным, адаптированным для просмотра с мобильных устройств и максимально персонализированным с помощью технологии Бо...