Рубрики
Без рубрики

Напечатана функциональная инъекция зависимости в Python

Первоначально опубликовано в моем блоге: https://sobolevn.me/2020/02/typed-functional-dependents-inized d … Теги с Python, WebDev, начинающими, Джанго.

Продвинутая набора Python (10 частей серии)

Первоначально опубликовано в моем блоге : https://sobolevn.me/2020/02/typed-functional-dependentency-inized

Инъекция зависимости – это противоречивая тема. Есть известные проблемы, хаки и даже целые методологии о том, как работать с DI Frameworks.

Многие люди спросили меня: как вы можете использовать много набранных функциональных концепций вместе с Традиционная объектно-ориентированная зависимость впрыск ?

И этот вопрос имеет большой смысл. Поскольку функциональное программирование – это все о композиции. И впрыск зависимости просто магия. Вы получаете почти случайные объекты, введенные в некоторые места вашего кода. Более того, весь контейнер, который нужно вводить позже, волшебно собран из отдельных частей во время выполнения.

Это приводит к серьезному противоречию между функциональным декларативным стилем и «OMG, где этот класс пришел?»

Сегодня мы собираемся решить эту проблему с хорошим функциональным способом.

Регулярные функции

Представьте, что у вас есть Джанго Игра на базе, где вы награждаете пользователей с точками для каждого угаданного письма в словом (безудержные буквы помечены как ». ):

from django.http import HttpRequest, HttpResponse
from words_app.logic import calculate_points

def view(request: HttpRequest) -> HttpResponse:
    user_word: str = request.POST['word']  # just an example
    points = calculate_points(user_word)
    ...  # later you show the result to user somehow

И вот ваша деловая логика:

# Somewhere in your `words_app/logic.py`:

def calculate_points(word: str) -> int:
    guessed_letters_count = len([letter for letter in word if letter != '.'])
    return _award_points_for_letters(guessed_letters_count)

def _award_points_for_letters(guessed: int) -> int:
    return 0 if guessed < 5 else guessed  # minimum 6 points possible!

Это довольно простое приложение: пользователи пытаются угадать какое-то слово и теммем, мы награждаем очки за каждое угаданное письмо. Но у нас есть порог. Мы награждаем 6 или более очков за 6 или более угаданных букв. Вот и все. У нас есть наш структурный слой на месте и наша красивая чистая логика. Этот код действительно прост для чтения и изменения.

Либо это? Давайте попробуем изменять его, чтобы узнать. Допустим, мы хотим сделать нашу игру более сложной, сделав порог точек настраиваемого. Как мы можем сделать это?

Наивная попытка

Хорошо, давайте сделаем _award_points_for_letters Примите второй аргумент порог: int :

def _award_points_for_letters(guessed: int, threshold: int) -> int:
    return 0 if guessed < threshold else guessed

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

def calculate_points(word: str) -> int:
    # ...
    return _award_points_for_letters(guessed_letters_count)

Исправить это Расчет_Points Функция (и все другие функции верхних звонящих) придется принять порог: int как параметр и передать его на _award_points_for_letters. , вот так:

def calculate_points(word: str, threshold: int) -> int:
    # ...
    return _award_points_for_letters(guessed_letters_count, threshold)

Это также влияет на наши Просмотр .py :

from django.conf import settings
from django.http import HttpRequest, HttpResponse
from words_app.logic import calculate_points

def view(request: HttpRequest) -> HttpResponse:
    user_word: str = request.POST['word']  # just an example
    points = calculate_points(user_word, settings.WORD_THRESHOLD)
    ...  # later you show the result to user somehow

Как мы видим, это выполнимо. Но требуется изменить весь стек вызова для одного параметра. Что приводит нас к некоторым очевидным проблемам:

  • Если наш стек вызова большой, нам придется изменить множество слоев для достижения места, где нам действительно нужна эта ценность
  • Все наши функции и классы будут заражены значениями, которые мы проходим. Это будет иметь несколько эффектов: вы не сможете понять, какие функции и классы действительно нуждаются в передаваемых значениях, и которые просто работают в качестве транспортного слоя (например Calculate_Points в нашем примере). И, конечно, люди начнут использовать эти прошедшие значения, где они нуждаются в них. И это неявно увеличит связь вашего кода и окружающей среды.
  • Было бы трудно сочинить ваши функции и классы вместе. Этот дополнительный параметр всегда будет мешать. Арты ваших функций не будут совпадать.

В целом: это работает, но не масштабируемое решение. Но я бы, вероятно, сделаю это сам для небольшого приложения.

Каркасы

Многие люди Django прямо сейчас могут спросить: почему бы не просто импортировать настройки и использовать его как Джанго Документы учит нас?

Потому что это Уродливый Действительно

На данный момент у нас есть операционная независимая бизнес-логика. Это отличное. Он устойчив к любым структуре, специфическим изменениям. И это также неясно не приносит все сложность структуры с ним. Давайте помним, как django.conf.settings работает:

  1. Нам нужно установить Django_settings_module Env переменная для поиска Модуль настроек
  2. Где-то глубоко внутри рамки django.setup () случится
  3. Джанго импортирую этот модуль
  4. Этот модуль может содержать Настройка-зависимая логика
  5. Настройки также будут получать доступ к среде, файлам, возможно, даже поставщикам облаков для некоторых конкретных значений
  6. Тогда Джанго будет иметь почти неизмеренный объект Singleton, который вы можете импортировать из любого места в вашем коде
  7. Вам придется отправлять настройки в своих тестах, чтобы просто пройти другое значение для вашей функции. Или несколько (и многое другое) значения, если вы сильно полагаетесь на модуль настроек (что, вероятно, будет в любом приложении)

Стоит ли оно того? Может быть. Иногда django.settings достаточно хорош. Возможно, вы можете иметь лишь пара примитивных ценностей, которые должны быть введены. И этот путь позволяет сделать это очень быстро.

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

Итак, как в мире я собираюсь пройти эту проблему, в функцию, если ничего не работает? Параметры слишком шумные, контейнеры слишком волшебные и трудно работать, и глобальные настройки – просто чистое зло в форме синглтона?

Состав

Функциональные программисты – умные люди. Действительно. Они могут сделать буквально все с просто чистыми функциями.

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

from typing import Callable
from typing_extensions import Protocol

class _Deps(Protocol):  # we rely on abstractions, not direct values or types
    WORD_THRESHOLD: int

def calculate_points(word: str) -> Callable[[_Deps], int]:
    guessed_letters_count = len([letter for letter in word if letter != '.'])
    return _award_points_for_letters(guessed_letters_count)

def _award_points_for_letters(guessed: int) -> Callable[[_Deps], int]:
    def factory(deps: _Deps):
        return 0 if guessed < deps.WORD_THRESHOLD else guessed
    return factory

Пожалуйста, обратите внимание, как мы сейчас проходим фабрика Функция на верхний уровень нашего приложения. Кроме того, уделите дополнительное внимание на наше новое _Deps протокол : Это позволяет нам использовать структурные подтягивания для определения необходимой API. Другими словами, все объекты с Word_Threshold int Атрибут пройдет чек (для обеспечения настроек Django Настройки TypeCheck используют Django-stubs ).

Единственное, что осталось, это позвонить нашему возвращению фабрика Функция с самого верха:

from django.conf import settings
from django.http import HttpRequest, HttpResponse
from words_app.logic import calculate_points

def view(request: HttpRequest) -> HttpResponse:
    user_word: str = request.POST['word']  # just an example
    points = calculate_points(user_words)(settings)  # passing the dependencies and calling
    ...  # later you show the result to user somehow

Выглядит легко! Все наши требования удовлетворены:

  1. У нас нет магии, буквально нуль
  2. Все напечатано правильно
  3. Наша логика все еще чистая и не зависит от рамки

Единственная проблема, которую у нас сейчас есть, состоит в том, что наша логика хорошо состоит. Позвольте мне проиллюстрировать мою точку новым требованием. Во время курортного сезона наша игра может предоставить один дополнительный указать на окончательный счет случайным образом. И вот как мы можем адаптировать наш исходный код для удовлетворения новых требований:

def calculate_points(word: str) -> Callable[[_Deps], int]:
    guessed_letters_count = len([letter for letter in word if letter != '.'])
    awarded_points = _award_points_for_letters(guessed_letters_count)
    return _maybe_add_extra_holiday_point(awarded_points)  # won't work :(

def _maybe_add_extra_holiday_point(awarded_points: int) -> int:
    return awarded_points + 1 if random.choice([True, False]) else awarded_points

Но, упс, Награжден_Points имеет тип Callable [[_ DEPS], INT] Это не может быть легко составлено с этой новой функцией. Конечно, мы можем создать новую функцию внутри _maybe_add_extra_holiday_point Только ради композиции:

def _maybe_add_extra_holiday_point(awarded_points: Callable[[_Deps], int]) -> Callable[[_Deps], int]:
    def factory(deps: _Deps) -> int:
        points = awarded_points(deps)
        return points + 1 if random.choice([True, False]) else points
    return factory

Но приятно работать? Я надеюсь, что большинство людей согласятся со мной, что это не так.

Давайте помним, что функциональные программисты являются умными людьми. Они могут сделать буквально все с просто чистыми функциями. И состав помощников. Вот почему они придумали Читатель Монад (или, как мы называем это Додорен, температуру Контейнер Не напугать людей к смерти на Python Land): Это композиция помощника для этой точной ситуации. Давайте обрабатываем наш код еще раз, чтобы увидеть, как это работает:

import random
from typing_extensions import Protocol
from returns.context import RequiresContext

class _Deps(Protocol):  # we rely on abstractions, not direct values or types
    WORD_THRESHOLD: int

def calculate_points(word: str) -> RequiresContext[_Deps, int]:
    guessed_letters_count = len([letter for letter in word if letter != '.'])
    awarded_points = _award_points_for_letters(guessed_letters_count)
    return awarded_points.map(_maybe_add_extra_holiday_point)

def _award_points_for_letters(guessed: int) -> RequiresContext[_Deps, int]:
    def factory(deps: _Deps):
        return 0 if guessed < deps.WORD_THRESHOLD else guessed
    return RequiresContext(factory)

def _maybe_add_extra_holiday_point(awarded_points: int) -> int:
    return awarded_points + 1 if random.choice([True, False]) else awarded_points

Мы изменили возвратный тип функций, и мы также добавили Награжден_points.map (_maybe_add_extra_holiday_point) . Какой еще один способ сказать «Создать Undreccontext Контейнер с этой чистой функцией _maybe_add_extra_holiday_point ». Мы вообще не меняем наш структуру.

Как это работает?

  1. Когда мы называем calculate_points (user_words) На самом деле не начинает ничего делать, это только возвращает ПотребительницаEscontext Контейнер, который нужно вызвать позже
  2. Контейнер достаточно умный, чтобы понять .map метод. Это помнит, что после его исполнения ему нужно будет позвонить _maybe_add_extra_holiday_point. функция
  3. Когда мы добавляем контекст в контейнер в форме calculate_points (user_words) (Настройки) наше Def Factory (депс: _deps) Выполняет и возвращает долгожданное значение
  4. Тогда _maybe_add_extra_holiday_point на самом деле выполняет и возвращает конечное значение

Вот и все. Нет беспорядок, нет магии, без внутренних средств. Но печатать, композицию и простоту.

Прозрачные зависимости

Что, если вы хотите также изменить символ, который указывает на неопределенное письмо (в настоящее время . ), быть настраиваемым? Некоторые пользователи предпочитают Отказ , некоторые _ . Хорошо, мы можем сделать это, не можем ли мы?

Маленькая путаница может произойти с функциональными новичками на этом шаге. Потому что у нас есть только Депс Доступно внутри _award_points_for_letters. а не внутри calculate_points. . И состав снова это ответ. У нас есть особый состав помощника для этого случая: Context.ask () что извлекает зависимости от текущего контекста и позволяет явно использовать его всякий раз, когда мы хотим:

from returns.context import Context, RequiresContext

class _Deps(Protocol):  # we rely on abstractions, not direct values or types
    WORD_THRESSHOLD: int
    UNGUESSED_CHAR: str  # new value!

def calculate_points(word: str) -> RequiresContext[_Deps, int]:
    def factory(deps: _Deps) -> RequiresContext[_Deps, int]:
        guessed_letters_count = len([
            letter for letter in word if letter != deps.UNGUESSED_CHAR
        ])
        awarded_points = _award_points_for_letters(guessed_letters_count)
        return awarded_points.map(_maybe_add_extra_holiday_point)

    return Context[_Deps].ask().bind(factory)

# ...

Здесь две вещи, чтобы упомянуть здесь:

  1. Context.ask () требует явно аннотироваться с _Deps потому что Marpy не может сделать вывод типа здесь
  2. .bind Способ также является утилитой композиции. В отличие от .map который составляет контейнер с чистой функцией, .bind Позволяет нам составить контейнер с помощью функции, которая также возвращает контейнер того же типа

Теперь мы можем поделиться одним и тем же неизменным контекстом только для чтения для всего нашего кода.

Статическая набравка

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

И многие читатели могут уже нашли одну ловушку, которую я оставил в этом примере. Давайте раскрым уродливую правду: _maybe_add_extra_holiday_point не чисто. Мы не сможем проверить его без издевательства на Случайные Отказ И потому что это нечистый, его тип – Callable [[INT], IO [INT]] Отказ

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

А также не забывайте, что некоторые функции могут в конечном итоге с успешным исполнением и могут также потерпеть неудачу. Бросание исключений Внутри вашей бизнес-логики некрасиво. И это также удаляет явный договор от вашей архитектуры. Это варварская практика. Если что-то может пойти не так, мы должны быть готовы к этому. Вот почему многие люди используют Результат Чтобы указать, что вещи могут (и воля!) Идите не так.

Все эти два типа сильно связаны с нашими ПотребительницаEscontext контейнер. Потому что его тип возврата может быть сложно:

  • ПотребительницаEscontext [Envtepe, ReturnType] Указывает, что мы работаем с чистой функцией, которая не может потерпеть неудачу (вроде в нашем примере без Random )
  • Потребительница [Envtepe, результат [ValueType, Errortype]] Указывает, что мы работаем с чистой функцией, которая может потерпеть неудачу (например, когда мы используем / Математический оператор и 0 в качестве разделителя или любых логических сбоев)
  • ДолбежнаeScontext [Envtepe, IO [RETENTTYPE]] Указывает, что мы работаем с нечистые функции, которые не могут потерпеть неудачу (как наш пример с Random )
  • Потребительница Engtepe, IO [Результат [ValueType, Errortype]]] Указывает, что мы работаем с нечистой функцией, которая может потерпеть неудачу (например, http, filesystem или вызовы базы данных)

Это не очень весело сочинять, да? Вот почему мы также отправляем полезные комбинаторы с возвращается нравиться:

Таким образом Возвращает Библиотека берет на себя все правила взлома на себя и обеспечивает хорошие API для наших конечных пользователей.

Ди контейнеров

«Итак, ты говоришь, что я не должен использовать никакие диски вообще?» Можно попросить после прочтения этой статьи. И это действительно интересный вопрос.

Я провел много времени, думая об этом очень теме сам. И мой ответ: вы можете (и, вероятно, следует) использовать контейнерную структуру впрыска зависимостей на рамочном уровне.

И вот почему: в реальных приложениях вашего _Deps Класс с вскоре становится действительно большим. У него было бы много вещей внутри:

  • Репозитории и коммуникации, связанные с базу данных
  • HTTP Services и интеграции API
  • Помогитеры разрешения и аутентификации
  • Кэширующий слой
  • Async задачи и утилиты для работы с ними
  • И, вероятно, больше!

Чтобы обработать все эти вещи, вам нужно импортировать много других вещей. И вам нужно будет как-то создать этот чудовищный объект. Вот где Di Frameworks может помочь вам. Много.

С их магией создание этого контекста может быть намного проще и безопаснее. Вам нужны сложные инструменты для борьбы с реальной сложностью.

Вот почему инструменты, такие как зависимости Требуются для каждой сложной системы: как функциональные, так и императивные.

Заключение

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

  1. Различные приложения требуют разных уровней архитектуры и имеют разные требования к инъекции зависимости
  2. Если вы напуганы от Magic Di контейнеров, используйте напечатанную функциональную композицию: ПотребительницаEscontext Может помочь вам предоставить необходимый контекст с верхнего уровня к самому дну и определить приятную композицию API
  3. При написании кода высокого уровня подумайте о вашей архитектуре и явно определите ваши контракты в качестве типов. Использовать И. и Результат указать возможный сбой и примесь
  4. Используйте композицию помощников, когда под сомнение
  5. Используйте DI-контейнеры на самом верхнем уровне вашего приложения, когда сложность выходит из вашего контроля

Функциональное программирование – это весело и легко! Не стесняйтесь звезда наше репо Если вам понравились концепции выше. Или пойти в Документы чтобы узнать больше новых вещей.

Очень особенное благодаря Николай Фоминый и Артема для рассмотрения этой статьи.

Продвинутая набора Python (10 частей серии)

Оригинал: “https://dev.to/wemake-services/typed-functional-dependency-injection-in-python-4e7b”