Pull to refresh

Comments 30

Монада Reader, комонада CoReader как раз для таких вещей и были сделаны. А ещё ваша идея мне напомнила имплиситные функции, о которых Одерски распространялся на недавнем Scala Days: https://www.youtube.com/watch?v=Oij5V7LQJsA&list=PLLMLOC3WM2r5Ei2mnSHCD-ZD04AXovttL&index=1

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

или я вас не понял

Монада Reader, комонада CoReader как раз для таких вещей и были сделаны

Как будут по вашему выглядеть сервисы, если всё вокруг обвешать ридерами?

Этот способ позволяет минимизировать различие между кодом с и без контекста.

Честно мне не попадалось на глаза решения подобной проблемы
> Про имплиситы я вас не понял

Есть такая мысль, вместо имплиситов-аргументов, использовать имплиситы типы. Вместо def doLogic(implicit ctx: Context): Int использовать type MyContext[Int], который является функцией, которая принимает в себя имплиситный аргумент и возвращает соответствующий ему Function0[Int] (каррирование). Это Одерски предлагал в указанном мною видео.

Ещё про имплиситные контексты есть такой забавный трюк: https://github.com/scala-native/scala-native/blob/master/util/src/main/scala/scala/scalanative/util/Scope.scala

> Как будут по вашему выглядеть сервисы, если всё вокруг обвешать ридерами?

Это только поначалу неудобно. Когда вы втягиваетесь в функциональное программирование, становится трудно остановиться. Сначала монады, потом трансформеры, потом Free, потом eff.
Про имплиситную функцию, спасибо за видосик идея очень интересная, реальное решит проблему с контекстами, если весь протокол сервисов будет построен на таком типе функций.

Это только поначалу неудобно. Когда вы втягиваетесь в функциональное программирование, становится трудно остановиться. Сначала монады, потом трансформеры, потом Free, потом eff.


Звучит очень опасно :) Это как бы использовать вилку, чтобы есть суп, только по началу не удобно, вот такая вот ассоциация
Это больше похоже на то, как есть рис вилкой. Поначалу непонятно, зачем вообще нужны столовые приборы, когда и руками получается отлично. Зачем ещё лишние сущности плодить? Потом человек осваивает вилку, и вроде неплохо получается, есть свои удобства.

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

А на самом деле правильнее есть рис, чем его не есть. А чем именно — палочками, вилкой или руками, уже не так принципиально.
Вот с последним выводом, нельзя не согласится
Рисковый вы парень, делать import сats._ Мне вот всегда подобное страшно, каких ещё имплиситных трансформацией принесёт с собой библиотека. Как она будет реагировать с другими обобщёнными библиотеками? Что нужно заимпортировать, чтобы заставить работать примеры с вебсайта? Допустим, примеры работают. Я слегка изменил код, и он снова перестал работать — что нужно импортировать теперь?
Если это самое плохое в примере — ок :) У Cats все хорошо с имплиситам, они предсказуемы. Во всяком случае я пока ни разу не обжигался.
> Если это самое плохое в примере — ок :)

Если честно, я не вчитывался. Поверил на слово, что у вас там и в самом деле HKT и монады. Функциональное программирование обычно обладает свойством, что одну и ту же задачу в нём нельзя решить разными способами. Унификация функционального программирования — это ответ инкапсуляции объектно-ориентированного. Тебе не надо тратить кучу времени на изучение библиотеки, если она инкапсулирована и публикует только несколько полезных для конечных пользователей точек входа. Функциональную библиотеку тоже легко изучить — потому что сделана она может быть только одним способом.

Об этом можно судить по столь нежно любимым всеми функциональщиками коммутативным диаграммам. Там есть пунктирный подвид стрелочек — «существует единственный морфизм». На этом единстве и построена вся теория. Произведение и копроизведение типов так и определяется — через единственность (с точностью до изоморфизмов). Тут можно заметить, что технологии скалы не позволяют никак задать единственность и какое-либо приведение к каноническому виду. Меня, например, всегда огорчает что вывод типов не умеет обрабатывать sealed классы, доказывая что экземпляр класса может быть только одним из его наследников, указанных по списку, и никакого другого не существует. Понятное дело, имплиситы — очень мощный инструмент, и на них можно что-нибудь закостылить на базе HType, но это всё противоестественно.

И вообще, скала всё же скверно приспособлена к функциональному программированию. Буйство функциональных абстракций — это неожиданное применение инструмента, создававшегося для совсем других целей. Скала как язык и в ещё большей степени её тулинг плохо приспособлены для работы с таким количеством имплиситов. Когда я разбираюсь с ООП библиотекой здорового человека — я могу посмотреть в скаладок для конкретного класса, или создать его экземпляр в REPL и проверить, как он работает. Это чем-то напоминает работу в командной строке, все сложности — внутри, а снаружи — достаточно простые команды.

Другое дело создания сумрачного гения в функциональном стиле. REPL бесполезен, скаладок бесполезен — остаётся только читать исходники. Не идёт ни какой речи о сокрытии сложностей — все они закладываются прямо в структуру данных, которыми ты должен обмениваться с библиотекой. Одно дело в библиотеку передать Int, другое дело — монадный трансформер от Int. Ты уже должен понимать как работает внутри библиотека. А из-за имплиситов ты даже представления не имеешь, где искать нужные тебе библиотечные функции. И чёрт с ними, с исходниками, можно и их почитать, но хотелось бы понять, какой именно файл нужен. Имплиситы хорошо работают в уже существующем проекте, когда IDE любезно раскрывает для пользователя всю незаметную глаз машинерию. Но и оно бессильно, когда ты только хочешь задействовать имплиситную библиотеку, или вовсе на бумажке схему бизнес-логики рисуешь.

Поэтому я стараюсь не связываться с имплиситщиной, только если без неё совсем трудно обходиться. Особенно со сторонними библиотеками. Там лежит куча функций, которые мне не нужны, но я вынужден прочитать все исходники, чтобы найти нужную. А ещё спустя несколько лет и десяток версий там что-нибудь изменится, например именование, и мне придётся переписывать свой код и импорты. А я не хочу следить за редкоиспользуемой библиотекой. И если я всё равно читаю чужие исходники, то что мне мешает написать свои собственные? Которые делают примерно тоже самое, но без развесистой клюквы, сугубо в необходимых мне объёмах. Тем более, что все эти монады — вещи чрезвычайно тривиальные и однотипные.

Это как давний спор про номинальные и структурные типы. Что лучше использовать — (Double, Double, Double) или final case class Gas(pressure: Double, temperature: Double, volume: Double). Сторонники ФП предпочитают первый тип, меня же не напрягает написать полностью второй вариант — он оказывается более понятным при последующем чтении.

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

Вот когда в скале появится первоклассная поддержка функциональщины, не случайная — тогда можно будет думать о более тесной интеграции с этой парадигмой. Правда это будет уже другой язык.
Весь этот пример ломается об необходимость делать на фучах recover. Которого нет в Monad и усё, нет ни контекста, нифига.

Если не прав, попровьте, плиз
Для таких случаев есть `MonadError`

А такой вариант с MDC смотрели? Благодаря такому подходу, в логах даже у других либ выводится контекст,


типа

[2017-05-29 15:00:000] [debug] [user-id] [trace-id] our.class.a GET /foo/bar/baz
[2017-05-29 15:00:005] [trace] [user-id] [trace-id] our.class.b Getting things
[2017-05-29 15:00:010] [trace] [user-id] [trace-id] io.getquill.JdbcContext SELECT * FROM Blablabla WHERE id = 'baz'


Не FP, конечно, но для задач логирования этого достаточно.

честно, такое у нас тоже есть, но меня напрягает что на каждом таске происходит одно чтение из тред локали и 2 записи. И эта же идея используется врапере для контекста, кстати

Разработка на Scala как она есть. Вообще нам в половину сервисов надо передавать неявно контекст, а в половину не надо. Поэтому мы не будем использовать implicit (прим. пер. — неявно) и дефолтный аргумент для него, а запилим свой микрофреймворк на 5kloc на шаблонах и с макросами, подвязанный через недокументированное api sbt и без документации, но зато с веб-интерфейсом на локалхосте, и вот оно :)

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

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

Что-то я упустил пасс руками в том месте, где ThreadLocal помог передать контекст в стороннюю библиотеку. Ну то есть понятно, когда она синхронная — это сработает. А если она асинхронная? Когда-то мучался с этим, пытался прикрутить MDC через кастомный диспетчер, как в примере выше к асинхронному сетевому приложению, но так вопрос и не решил: контекст терялся на границе запрос-ответ. То есть, запрос отправляется по сети, MDC сохраняется в ThreadLocal, а потом в этот поток приходит ответ на совершенно другой запрос, и MDC из ThreadLocal привязывается не к тому ответу. Учитывая, что там обрабатывалось до 1к параллельных запросов на пуле из 12 потоков, путаница получалась та еще. Может, существует какое-то рабочее решение этой проблемы?

Есть подозрение, что вы как то неправильно его используете. У нас крутится несколько приложений на подходе похожем на MDC. Для того, чтобы это работало, нужно:
  1. чтобы все операции исполнялись в ExecutorContext'ах, которые реализуют логику по изменению ThreadLocal
  2. по дороге не было ничего, что может потерять контекст. Например, акторы, но есть решение и для них


Количество тасков в контекстах не играет никакой роли. Если опишите поподробнее ситуацию, я скорее всего подскажу как её решить

Ну описал насколько мог подробно, других деталей сейчас не вспомню. И да, приложение было как раз на акторах, решение — на MDCPropagatingDispatcher. Контекст терялся именно на сетевых запросах-ответах, в остальных местах вроде как все работало.


Добавлено: Я просто не уверен, что понимаю, как ThreadLocal может спасти ситуацию, если контекст нужно передать между тредами, а не оставить в текущем.

ситуацию спасает scala.concurrent.ExecutionContext#prepare собственно его дергает Future перед запуском таска. То есть на вызове этого метода создается новый инстанс контекста, в который можно записать что либо. Или который как раз будет при запуске затаска заполнять ThreadLocal нужным значением

Спасибо за подсказку насчет prepare(), будет случай — попробую применить. На данный момент у меня проблема с Netty, к которому я попытался прикрутить злополучный MDC. И опять ничего не получается. По запросу Netty MDC гуглится вот это решение, но оно мало того, что немного криво написано — так еще и совсем не работает. Наверное, автор его для однопоточного режима писал, или для более старой версии Netty, и с условием, что значения MDC выставляются при создании ExecutionContext один раз и больше не меняются (мне нужно, чтобы они подхватывались из текущих актуальных значений в MDC.getCopyOfContextMap() каждый раз при вызове execute(), но это легко поправимо). Но даже с внесенными мной изменениями оно не заработало: я попытался помимо exec() провернуть подобный фокус со всеми вариациями методов submit() и schedule(), а также переопределить метод newChild() который создает дочерние EventLoop (которые являются по сути SingleThreadEventExecutor и реализуют ExecutorService), и у них также переопределить execute(), schedule() и submit() во всех вариациях. Результатат ноль. Это, правда, уже не Scala, а Java, но тоже асинхронная.

Хмм, нашел свой старый код, который пытался прикрутить к Netty пару лет назад. Там действительно используется ExecutionContext#prepare, и оставлен коммент, что все хорошо, пока не нужно взаимодействовать с Java-библиотеками. Так как в Java всякие Executor/ExecutorService не имеют механизма, аналогично скаловскому ExecutionContext#prepare. Для этого у меня там еще несколько вспомогательных классов: обертки для Runnable/Callable, и обертки для Executor/ExecutorService, которые заворачивают Runnable/Callable в их обертки, чтобы вместе с ними передать MDC.getCopyOfContextMap(). А дальше идет коммент, что с Netty все это все равно не работает :) Так как у них там NioEventLoopGroup, который создает дочерние EventLoop, причем реализацию, которая объявлена final и поэтому ее нельзя унаследовать и переопределить нужные методы. И фасад для нее нельзя сделать, так как она в нескольких методах передает this вовне, и фасад отваливается. В общем, внутри scala все красиво, а вот на стыке scala-java подобные нестыковки случаются частенько, к сожалению :(

Ну вообще говоря эту линию между scala и java миром можно сгладить восстановив контекст при возвращении в scala. Примерный псевдокод:
val context = MDC.getCopyOfContextMap()
runMethodThatWillInteractWithJavaAsync().andThen {
  case _ => MDC.setContext(context) // я точно не знаю какой тут должен быть метод
}.map { result => ... //Тут у вас уже опять родной контекст и далее по пути следования
}


Да вы можете потерять часть логов внутри java библиотеки, но это, я считаю, совсем мелочи

В моем случае как раз совсем не мелочи, так как netty пишет свои логи без MDC, и при куче параллельных запросов все превращается в кровавую мешанину. А мне как раз эти логи и нужно привязывать к контексту, чтобы потом разгребать. Я зарепортил этот "баг" в netty, посмотрю, что там на это скажут.

Но нужно для этого всего найти какое-то реальное применение.
Сначала напедалить, а потом думать зачем — всегда так делаю.
Можно хранить в этом контексте всё, что угодно.
Это очень полезный горшок.

Это я к тому, что не совсем понятно, какую именно проблему мы решаем. Если всё это только ради того, чтобы вынести логирование из сервисного метода, то сложно себе представить, что оно того стоит. А если передавать какую-то значимую информацию в этом контексте, то это резко увеличивает неявную связанность между сервисами и лично я бы старался такого избегать до того момента, пока другого варианта просто нет. Можно себе представить, что мы хотим например закешировать полученные из бд сущности, чтобы увеличить производительность, но такой ценой я бы делал это в последнюю очередь.
Логирование — это самый простой и примитивный пример.
Если опять же говорить про веб бекенд, то там можно хранить сессию и пользователя например, но это наверное спорное применение.
Но вот я в статье упомянул, что можно передавать слепок состояния. То есть у тебя есть какие то данные, которые переодически обновляются (например кеш конфигов приложения или справочники). И вот перед тобой стоит задача — гарантировать, что при выполнении одного запроса эти состояния будут одинаковыми во всех сервисах. И вот подобный контекст уже отлично ложится на эту ситуацию. Мы взяли вначале операции сделали snapshot состояния, и сделали всё что нужно с используя его.

Вобщем проблема не разу не подгонялась под решение, решение радилось из-за проблемы
не совсем понимаю, если это данные которые параметризируют выполнение метода, то почему тогда его не передать в качестве параметра? это по крайней мере не будет создавать иллюзию и будет добавлять чистоты методу в функциональном смысле. И как вызывающий поймет, что ему необходимо передать какой-то контекст? Для этого обычно используется явно объявление параметров метода, которые гарантирют заодно, что его без этих параметров не вызовешь на этапе компиляции еще. А так получается, что вроде всех перехитрили: и компилятор и вызывающего этот метод заодно.
Не очень понимаю как я перехитрил компилятор и метод. По поводу использования в сигнатуре метода я писал.
Первое, что может прийти в голову, это передавать контекст через имплиситный аргумент функции.
def foo(bar: Bar)(implicit context: Context)
Но это будет захламлять протокол сервисов.


И как вызывающий поймет, что ему необходимо передать какой-то контекст?


Никто никого не обманывает, если проект пошел на использование этой идеи, то это можно считать контрактом. В точке входа нужно вызвать withContext, больше нигде этого делать не нужно.

Для этого обычно используется явно объявление параметров метода


да, если этих метод полторы штуки, то ок. Но если вам нужно чтобы весь код нуждается в контесте, тоооо будет жутко мазолить глаза. Опять же это решится с ImplicitFunction как и упомяналось выше
Sign up to leave a comment.