И гораздо лучшей системой типов. Про это, почему-то всё время забывают.
И речь даже не о nullable или type inference, каких-нибудь… банальное наличие bottom type позволяет “делать вещи”, которые в Java в принципе не работают.
Kotlin не может позволить себе реализовать такие вещи как специализацию для дженериков …
Вообще-то может. И более того, на старте это даже, имнип, рассматривалось. Но оно действительно ломает совместимость со стороны Java кода, и - по большому счету - именно поэтому остались только reified type parameters (которые - "чудес не бывает"™ - тоже нельзя из Java вызывать), которые, с одной стороны, требуют явного использования, а с другой, покрывают львиную долю практической потребности в специализации.
… или нормальный петтерн-матчинг, …
?! Если вы про ML-подобный (с deep deconstruction и т.п.) - так от него тоже отказались осознано, а не потому, что “невозможно”. На практике, PM в Kotlin (в гораздо более продвинутой форме) был (и есть) с самых первых версий. Он постоянно развивается, и - насколько я в курсе - даже в самой своей продвинутой форме (на данный момент) не требует никаких фич от JVM… т.е. вполне себе компилируется в “java 8”.
… потому что придется угробить на это ресурсы а потом сделают в Java более оптимально и повториться история с корутинами.
Вы зря сравниваете корутиты Kotlin и виртуальные потоки Java. Они - сильно “про разное”…
“Зарутиль” корутины на dispatcher, который будет работать поверх виртуальных потоков - вообще не проблема. И в этом даже бывает практический смысл. Условный LOOM.Dispatcher - он хоть и не Kotlin SDK (пока), но ничего “такого” в нем там нет… кроме того, что он вас “прибивает гвоздиками” к вполне конкретной Java SDK. А "мы такое не любим"™
А вот добиться от виртуальных потоков семантики continuation - ровно также “просто”, как и от обычных.
Я это к тому, что ваш посыл про “полную зависимость от JVM мира”, он даже в отношении Kotlin JVM не верен. Даже в Kotlin JVM есть вещи, которые, скажем так, не дружат с “миром JVM”. Но они там, есть, т.к. с точки зрения развития языка “это нам нада”(с).
По поводу производительности, можно просто написать Int и попросить кого-нибудь ответить на вопрос где это число будет размещено в памяти. Без похода в байткод не разобраться.
?! А можно по подробнее, об чём речь? Емнип, правила аллокации в Kotlin полностью аналогичны таким же в Java. Зачем в бейткод-то смотреть? Это раз. А два - если таки вдруг надо, то в чём проблема его посмотреть… этож буквально “пара шорткатов”.
Можно сколько угодно рассказывать про безопастность по null и наследующем шаге отстрелить себе ноги в любом месте где есть вызов Java кода.
Это "вы просто не умете их готовить"™ :-) Во-первых, null safety - оно же, в первую очередь, про свой собственный контракт. Во-вторых, Kotlin тебя честно предупреждает об обнаружении т.н. platform-specific types (и кстати, огромный шаг в этом направлении был сделан как раз с выходом KMP). В том числе, и “в любом месте где есть вызов Java кода”. А то, что такого рода предупреждения - по первости - игнорируются в большинстве своем - так это больше от неопытности, имхо.
Также большинство либ рассчитано под Java и не все без приседаний и костылей будет работать.
А можно чуть подробнее про “приседания и костыли”? Реально интересно, чего вы там “такого” находили, что оно было нужно.
Очевидность и понятность языка - хреновая.
Если я правильно понял, о чём речь - это сугубо дело привычки. Лично про себя (я с Kotlin прям с версии 1.0… т.е. лет десять уже), скажу так.
Если писать в Java style (а оно таки можно, даже если вот лично я смысла особого в этом не вижу) - то разницы тут особой и нет.
Если писать в Kotlin style (необходимый минимум идиоматики) - он непривычен на старте, но и только. Уровень “понятности” - именно что - Java с чуть приятным синтаксисом.
Если к этому добавляется полная идиоматика языка - он ещё более непривычен (не непонятен!!!)… особенно, если предыдущий шаг был пропущен. Но после того, как понимаешь, что читается это исключительно “снизу вверх” (без всякого рода разрыва контекста), что объем “лишнего кода” - это minimum minimorum - возвращение к чтению Java исходников (причем даже в лучшем их виде) ничего кроме раздражения (вот лично у меня) не вызывает.
А “возвращаться” приходится гораздо чаще, чем хотелось бы… к сожалению. Все эти бесконечные проверки на null, upper cast’s и "вот это вот всё"™, про что ты уже и забыл - оно, банально, “мылит глаз”. Продираться через это - удовольствие, прямо скажем, на любителя.
Ну и маленький оффтоп ...
Я, при работе с Java/Kotlin в какой-то момент … достаточно давно уже - словил момент, который у меня был (тож очень давно), когда я, работая долго с Erlang, вынужден был периодически переключаться на C#.
Момент - если на пальцах - заключается в следующем… тебя реально начинают пугать ф-ции, которые не помещаются на экране :-)
С Kotlin/Java ощущения, конечно, слабее были. Но прям очень похоже было :-)
В Java, при всех ее минусах, то что написано в коде плюс минус будет в байткоде.
О да. Особенно - на практике :-) Вы не используете annotation processing? Ну там Spring и/или Lombok? Реально? А что вы такое пишете на Java?
Kotlin вам вообще ничего и нигде не гарантирует. Любите читать исходники? Если повезет почитаете не месиво. Если не поведет исходники вы вообще не увидите.
Тут вообще не понятно “про что”. Если вы про то, что “стандартные” java decompiler’ы сильно плохо дружат с Kotlin - то есть такое. Но есть же специализированные инструменты для. Да даже какой-нибудь шикарный Kotlin Explorer уже как лет пять, емнип, есть. Или вы о чём?
Язык который не имеет контроль над основной своей платформой и в следствии чего вечный аутсайдер.
:-) Рискую открыть вам “страшную тайну”, но даже kotlin 2.3 (самая новая, на данный момент версия языка) компилируется в JDK 1.8 без особых ограничений по, собственно, фичам Kotlin’а. Ну да… какие-то платформозависимые (типа @JvmRecords) аннотации становятся не доступны - но и только.
Это я к тому, что самому Kotlin JVM от самой JVM “много не нужно”.
Вы не отвечаете на мой вопрос: я не спрашивал вас о вашем карьерном пути в Erlang’e, я спрашивал: "как часто вы видите применение паттернов ФП (и, если вам угодно, “программирование в функциональном стиле”) в проектах написанных на JavaScript?
Хорошо, давайте “на пальцах”…
Для меня - как для адепта функционального программирования - есть один единственный “паттерн ФП” - это прием ф-ции, как аргумента и/или получение ф-ции, как результата вычисления.
И с этой точки зрения - да, я постоянно вижу “применение паттернов ФП”, в том числе и в JS.
небольшой оффтопик про паттерны ФП
паттерны ФП - они другие
Эта картинка из замечательного доклада Скотта Влашина (Scott Wlashin). Делал он его уж больше десяти лет назад, но оно по прежнему - имхо - актуально.
А вот всё остальное (в том числе, и большинство из перечисленных вами “функциональных фич”) - это именно что стиль - который напрямую к, собственно, функциональному программированию не имеет абсолютно никакого отношения.
И стиль этот - по большому счету - определяется синтаксисом конкретного языка. Какие-то “штуки” - так или иначе - системой типов. Только и всего.
Но - и это важно понимать - что наличие этих “фич”, что их отсутствие - абсолютно никак не сказывается на, собственно, функциональности ЯП. Сказывается это всё - в лучшем случае - на удобстве. И с вот этим вот - лично я, например - не спорю.
Но вы назвали свою публикацию вполне определенно, а не, например, «Почему JS/TS — это боль при программировании в функциональном стиле» :-)
… а еще там нет переменных, циклов и есть иммутабельность (и это, в том числе те признаки, что я указал в статье)
Вот опять. Вы вроде начинаете про одно, а заканчиваете другим, и сами себя запутываете.
Смотрите… важно то, что там нет переменных. А о иммутабельности, я извиняюсь, чего вы писали? Это разве “те признаки”? При том, что мутабельность - и формальном, и в практическом смыслах - там вполне себе есть.
И с циклами - похожая картина. То, что они там просто невозможны, в силу отсутствия и переменных, и присваивания - это одно. Но вы-то о циклах, емнип, упоминаете исключительно в разрезе “ленивости”. Это разве “те признаки”? Для меня - очевидно, что вы “про другое”. Надеюсь, и для вас это теперь очевидно.
т.к. язык и его экосистема сподвигает вас писать циклы …
Я сильно надеюсь, что из того, что я написал выше, вы уже поняли, что всё вами тут перечисленное никакого отношения у функциональности (или не функциональности) ЯП не имеет.
Да, в Эрланг у вас может не быть HKT, но и цикл вы не напишите.
Дались вам эти “циклы”… ну загляните уже “под капот”, например, итераторам в scala, и убедитесь, что “чудес не бывает” ™, и “циклы” там вполне себе есть. Просто, для того, чтоб их “прям ваще” нигде не было, надо несколько большее, чем какая-либо система типов.
Откройте source fp-ts и посмотрите, как они определяют функцию pipe, например.
И? Что такого “ужасного” я должен там увидеть? Собственно, чем “это” принципиально отличается от наличия 100500 каких-нибудь FunctionN в той же Scala? Не… “сутевое” различие-то я понимаю, но у вас - насколько я понял - претензии не к сути, но к форме. Или о чём речь?
У вас нарушается принцип “подстановки” (substitution) и это ломает композицию.
Вот мы дошли до “сладкого”. На всякий случай, уточню - мыж оба говорим об функциональной композиции? Так?
Если таки да - можно для “особо одаренных” (типа меня) раскрыть, что это за принцип такой? Что именно и куда вы “поставляете” при композиции?! Мыж не вычисляем ф-ции, которые “соединяем”. Мы вычисляем именно что их композицию. И результатом этого вычисления является третья ф-ция. Которую мы - пока ещё - не вычисляем. Нет?
Как минимум, вы получаете строгую зависимость от порядка вычислений.
Я либо вообще не понимаю “о чём вы”, либо я сейчас вам открою страшную тайну.
Чтобы не было “строгой зависимости от порядка вычислений” (и соответственно, прям нужна была СП) необходимо call-by-need. А тут (при вычислении композиции) его и рядом нет. Т.е. - во-первых - наличие/отсутствие СП абсолютно не важно. А во-вторых, функциональная композиция - таки задает порядок вычисления результирующей ф-ции.
Если же речь не о функциональной композиции, а о чём-то другом, то лучше таки пояснить.
Пока что из всей дискуссии я так и не понял, что конкретно делает Erlang “недосягаемым ФЯП” в рамках вашего определения через HOF в системе типов.
В рамках “моего определения через HOF” - ничего. Наличие в системе типов HOF - это просто необходимое и достаточное условие, чтоб считать ЯП функциональным.
А “недосягаемым” (для JS и для Scala) его делает модель вычислений, которая позволяет легко и просто получить такую множественную диспетчеризацию, которая на этих языках невозможна в принципе.
На мой взгляд вы очень сильно упрощаете. И ваша последняя строчка, где вы как бы “забираете” часть своего аргумента назад - прямое тому подтверждение.
Ничего я не “забираю”. Я лишь намекаю, что вы можете взять “функциональную чистоту” JS, и сравнить её с аналогичной “чистотой”, например Scala… чисто “для смеха” :-)
Плотность и эргономика функциональных фич в Scala на порядки выше, …
Это каких, например? Наличие “оператора композиции”? Или вы о чём-то другом?
… а систему типов и сравнивать смешно.
А причем тут “функциональная чистота”, и система типов?! “Чистота”, она как раз в гораздо большей степени про модель вычислений, чем про что-то другое. Нет?
Но проблема в том, что то, что не ссылочно-прозрачно крайне плохо композируется.
С какого перепугу-то?!
… сколько вы лично видели проектов в продакшене на JS, где активно используется карирование, частичное применение, монады, изоляция эффектов(то, что для вас всё это не является критерием фп - и я хочу это подчеркнуть двумя толстыми линиями - это ваше личное оценочное суждение а не научный консенсус)?
:-) Ну я весьма значительную часть своей карьеры провел за Erlang’ом… где - внезапно - из всего вами перечисленного - “из коробки” - есть только “изоляция эффектов”. И? Erlang - как ФЯП - недосягаем что для JS, что для Scala. Дальше-то что? Порассуждаем за “а насколько функционален Erlang?”, если в нем “всего этого” нет? :-)
В то же время чисто функциональную Scala я вижу регулярно, в особенности после роста популярности Cats Effect и Zio
Скажем так… есть некоторая разница (с), между функциональным программированием, и программированием в функциональном стиле. Сдается мне - вы говорите про второе, и - местами - путаете “это” с первым.
Я утверждал обратное — что мутабельность по умолчанию ссылочную прозрачность нарушает, а не что иммутабельность её гарантирует, это разные тезисы.
Мой поинт-то в том, что не понятно, какое отношение наличие/отсутствие ссылочной прозрачности имеет к функциональности ЯП?
Haskell ленивый, но TCO там есть и используется.
Ну если под TCO понимать само наличие call-by-need - то да :-) Но call-by-need - это не TCO. И GR - не TCO. Где вы TCO в haskell нашли? Или - а что вы вообще под TCO тогда понимаете?
Интересное определение, но оно ломается на примере, который вы сами привели — Erlang динамически типизирован, никакой системы типов с HOF там нет.
?! В Erlang замечательная система типов. С чего вы взяли, что её (системы типов) там нет?!
… то JS функциональный язык, it’s simple as that.
Ну так и есть. JS функционален ровно в той же степени, в которой функциональна, например, scala. Ибо функциональность - это не про модель вычислений (декларативность/императивность), не про стратегию вычислений (упрощая, ленивость/энергичность) и не про ещё кучу аспектов.
Функциональность - это про вполне определенное свойство системы типов.
Другое дело, что можно начать рассуждать о - так сказать - “чистоте” этой функциональности… :-)
Звучит как ваше оценочное суждение без каких-либо оснований
Ну потому что, то что вы пишете в этих разделах - “делать мне больно мозг” :-) А “приведение оснований” - это будет долго и оффтопик.
Про HKT вы там и сами уже пришли к тому, что “оно лишнее”. С моей точки зрения - странным путем, но пришли. И к тому, что PM - из такой же категории - рано или поздно - придете.
В конце концов, какой-нибудь, common lisp вполне себе живет же без PM на уровне языка :-) Да и само по себе требование exhaustiveness для PM - в разрезе наличия динамической типизации, например - выглядит “странновато”, имхо. Ну или надо будет какое-нибудь Scheme с Clojure и прочими Erlang’ами “лишать звания” :-)
При всём уважении - похоже, у вас у самого какая-то “каша в голове” :(
Имманентный свойством ФЯП (любого - без каких-либо исключений) является исключительно система типов, которая умеет в HOF (а у вас про них вообще нет ничего). Другими словами - без наличия в системе типов функций высшего порядка - ФЯП не бывает.
А всё остальное - в том числе, и всё вами перечисленное - либо ортогонально, собственно, “функциональности”, и является следствием других категорий ЯП. Либо является следствием наличия HOF в системе типов.
Ну и - скажем так, на всякий - про “странности”…
Сама по себе иммутабельность - не есть гарантия ссылочной прозрачности. А ссылочная прозрачность - это про декларативность, а не про функциональность. Имхо, это важно понимать, и различать.
Аналогично с TCO. Оно важно - исключительно - с точки зрения “энергичной” evaluation strategy. Причем тут функциональность?! Зачем вам TCO, если у вас везде, например, call-by-need?
“Ленивость” - aka call-by-need - мало того, что “мимо” (в смысле, это не про функциональность), но нужно же понимать, что оно “конфликтует” с TCO. А вы их в один список ставите. Зачем?!
try/catch — это инструкция, а не выражение, её нельзя композировать …
Какой-нибудь Erlang смотрит на вас с недоумением :-)
А то, что вы пишите “про монады” - это - по большому счёту - про наличие/отсутствие, скажем так, “оператора композиции”. И - опять же - к функциональности, как таковой, оно отношения не имеет.
Про HKT и PM - вы, похоже, не понимаете место этих концепций в ЯП (любом).
Ну и про “гравитацию”…
В JS функциональный стиль — это осознанный выбор, который нужно делать каждый раз заново. Default — императивный подход, и он постоянно притягивает к себе.
Прежде всего отмечу, что, имхо, польза может быть особенно в проектах с большим, сложным внутреннем API, где важно строгое соблюдения контрактов/бизнес-логики.
Я это уже читал, но так и не понял в чём конкретно эта "польза". Другими словами, я не понимаю, каким образом дополнительная связность (хоть её наличие вы и отрицаете) может быть полезна. Особенно, в "проектах с большим, сложным внутреннем API".
Сборок может быть сколько надо, нет ограничений ...
Ну я таки глянул код. Позвольте усомниться в этом вашем утверждении. Если аттрактор и друг это типы из разных сборок, то возникает циклическая зависимость между сборками. Причина - та самая "лишняя" связность, наличие которой вы отрицаете, но которой, тем не менее, нет в канонических вариантах решения.
Собственно, это (наличие циклической зависимости) было очевидно и без кода... но я надеялся, что у вас там она разрешается, например, через генерацию отдельной сборки. Я ошибался.
В разных они сборках или нет, в любом случае, они знают друг о друге ...
При традиционных подходах к - та часть, что в вашем варианте становится аттрактором ничего не знает о "дружественной" части.
Исходная сущность (класс Me) внешне как раз не мутабельная (readonly), а "через интерфейс" предоставляется строго избирательная "проекция" с доступом к внешне скрытым или/и readonly мемберам.
Я это понял :-) Моя реплика была про то, что каноническое решение выглядит с точностью до наоборот. Если хотите, то вопрос в том, почему у вас не так? Почему за основу взят - так сказать - "вывернутый" вариант?
... а правило "три не" в плюсах, хорошее правило ...
Как я понимаю, мы о разных "трех не" говорим. Речь была о том, что "дружба" не наследуется. Не просто так. Очевидно, что - как минимум - в отношении к интерфейсам у вас "всё не так". Я, собственно, поэтому и упомянул явную реализацию.
Помимо основного тела статьи, в «Приложении» сделал много примеров задач с возможным применением, обоснованием зачем.
К сожалению, там нет "обоснования зачем". Там есть "смотри как могу". Любой из рассмотренных примеров замечательно решается без "этого". Собственно, в большинстве случаев, вы сами же об этом и пишите. Поэтому и был мой вопрос про "область ценности".
Про «торчащие нитки» ...
Опять же... если кто-то, что-то "видит" - он имеет полное право за это "дергать". И - скажем так - "прикладная задача проектирования" сводится к "надо сделать так, чтобы каждый видел только то, что ему можно дергать". А вы - буквально - говорите "а давайте на это всё забьем" :-)
internal и nested не дает той гибкости и селективности, как данный модификатор.
Вот ваш пример:
class Me
{
[OnlyYou<IFriend>(..)]
public void SomeMethod() { }
}
interface IFriend
{
void UseMe(Me me);
}
Объясните мне, пожалуйста, чем "это" - в принципе - отличается от?
Мой пример:
class Me
{
private void SomeMethod() { }
interface IFriend
{
void UseMe(Me me) => me.SomeMethod();
}
}
Мне вот видится, что - принципиально - ничем. И, имхо, второй вариант "чище", как минимум, как раз из-за того, что ничего не "торчит". Т.е. с мой точки зрения, всё что вы пытаетесь делать в такого рода вариантах - это заменить вполне традиционную технику т.н. mixin типов, на непонятно что :-(
internal работает «по площадям»
Зато для него есть, например, InternalsVisibleToAttribute, который позволяет вытащить "потроха" в отдельную сборку, и уже там сделать "торчать красиво" :-) В смысле, чтоб "торчало" только то, что нужно. И не приводит, при этом, к циклическим зависимостям.
nested не всегда возможен (а если нужен как nested сразу в нескольких классах? а если нужен «взаимный» nested?) и удобен, и он получает доступ сразу ко всем приватным мемберам outer класса (фактически как в плюсах в варианте не function friend), что не есть хорошо.
Ну вы чего?! Через neasted объявляются агрегаты (сильно упрощая - интерфейсы), которые потом используются для "сборки" того, что вам нужно где-то "во вне". Т.е. если вам прям так нужен "слуга двух господ" - вы вполне можете его получить без особых приседаний. И даже в другой сборке, если надо.
Если говорить за "вообще", то приемов работы с tight coupling - мы знаем не мало. В том смысле, что мы знаем "как есть этих слонов". Проблема - ту которую я вижу - заключается в том, что "дружба" - это как раз то, что к этой самой tight coupling приводит.
Про оценку ценности тут, конечно, пусть каждый сам приценяется ...
Если мембер где-то используется, то эта связь так и так есть. Декларация разрешающая ее тут лишь «документирует» этот факт.
Это не так. И, имхо, это достаточно очевидно. Например, достаточно "представить", что "аттрактор" и "друг" это типы из разных сборок. "Лишняя" связь тут же станет очевидной. Нет?
Представить в кавычках, т.к. сильно не уверен, что "оно" умеет в "такое". Код не смотрел. Но без "такого" ценность (и так не очевидная) этой штуки прям сильно уменьшается, имхо.
Через интерфейс
Вот как раз "через интерфейс" исходная задача решается "с точностью до наоборот". В том смысле, что исходная "сущность" - мутабельная, а через реализацию интерфейса предоставляется readonly проекция. Нет? При таком подходе, "грануляция" доступа - пока отбросив её ценность - органично "схлопывается" до агрегатов. Оно может быть и выглядит громоздко на синтетике, но смотрится вполне себе нормально и логично (отбросив, опять же, всё остальное, что можно "предъявить" такому агрегату).
В C++ весь контроль над друзьями находится на стороне класса ..., что по идее более правильно.
Как бы это уже всё давно разжевано не один раз. "Это" - как раз - не правильно. Механизм "дружбы" в плюсах - это следствие, а не причина, так сказать. И если речь о "дружбе" на уровне классов, то реальной ценности там нет. Если мне не изменяет мой склероз, даже Страуструп предлагал смотреть на "дружбу" исключительно с позиции friend function (например, при реализации операторов).
В отличии от плюсов, не можем сделать другом только для конкретного метода другого класса.
Глубокое имхо... ценность такой "гранулярности" - есть следствие "кривоватого" дизайна. Насколько я понимаю, оно может быть хоть сколько-то ценно при работе с - скажем так - перегруженным агрегатом, в качестве аттрактора. Но даже при этом - мы живём в реальном мире, и не отрицаем этого - я бы сильно подумал над тем, нужно ли мне плодить доп. связность, практически, на ровном месте. Есть более "ровные" способы обеспечить такого рода разграничения. Особенно, если речь исключительно о конкретной сборке.
То что приватно и должно оставаться приватным. Тем более что всякого приватного у класса может быть очень много, и выставлять все это хозяйство на показ (как в плюсах) тот еще «эксгибиционизм».
?! Ну т.е. все "минусы" упомянутые вами ранее - это на самом деле не "минусы"? Так? И мы - внезапно - осознаем что из "C++ дружбы" хоть сколько-то ценно только friend function? Или как?
Пусть даже если так. Но в чём ценность такой "дружбы" для C#? "Дать доступ" же не является само целью? Надеюсь. В "плюсах" для этой "штуки" есть вполне конкретная область - операторы - в которой "оно" прям ценно. В том смысле, что другими механиками языка это "не лечится".
А в C# где вы видите эту "область ценности"? Вопрос вызван тем, что - выглядит так - начали мы с одного, а пришли к другому :-) Т.е. решали одну задачу, а решение (которое получилось) - это решение какой-то совсем другой задачи. Вот и интересно - какой? Там у вас есть некоторые "рассуждения на тему". Но это именно что "рассуждения не тему". Можете ли вы переформулировать их в терминах той задачи, которая сформулирована в начале статьи?
Следующий тип случаев с иерархией, реализован так ...
Хм... имхо, это "немножко" не логично. Логично, имхо, требовать для этого случая явной реализации интерфейса. Во избежание, так сказать. Ну и - если уж всё это навеяно "C++ дружбой" - не стоит забывать про "три не". Они не просто так были сформулированы. А из рассуждений про "как должно быть правильно", складывается стойкое ощущение, что про одно из не вы всё время забываете :-(
А предлагается инструмент лишенный их недостатков — новый модификатор доступа, дополняющий стандартную коллекцию модификаторов.
Ну это как сказать :-) Если оно - как я подозреваю (хочу ошибаться) - работает только в рамках конкретной сборки... лично мне вот вообще не понятна ценность этого "инструмента". И даже если представить, что это такой "заход" в т.н. sealed hierarchy - что могло бы иметь ценность, применительно к C#... оно - всё равно - выглядит сильно "странновато" и "не ровно".
Модификаторы позволяют делать так, что группа классов, не только семантически (в голове у разработчика), но теперь и синтаксически образует целостный концепт, без «торчащих ниток» за которые могут дергать кто угодно.
Когда вы говорите про "торчащие нитки", вы вообще о чём? Эти "нитки" они из сборки чтоль "торчат"? Или откуда? Складывается ощущение что вам кто-то строго запретил пользоваться internal (и его производными) уровнем доступа и использовать neasted иерархии. Это, кстати, относится и к приведенным вами "примерам".
Без более строго сформулированного "зачем" - лично у меня - статья оставляет сильное "хлебное" послевкусие :-)
Там же все это уже можно в последних стандартах (тайпклассы, Comparable, ...)
Есть разное "можно". Например, как нам хорошо известно, в любой параметрический полиморфизм "классы типов" можно (легко и просто) добавить через препроцессинг. Но есть разница (с) с тем, когда классы типов это часть системы типов :-)
хотя меня немного коробит просто от пафоса этого словосочетания: "мощность системы типов"
Зря. Лямбда-куб - как наглядная демонстрация той самой "мощности системы типов" - существует уже лет тридцать как.
Всё, на этом возможности системы типов в Java заканчиваются, вы не можете декларативно добавить каких-то ограничений и статически их проверить.
Ну не всё так печально, конечно. В 8-ке появляются лямбды, и "типа" функциональные типы. Что-то получается через них провернуть. В Java 17 появляются "замкнутые" (sealed) иерархии и - соответственно - какие-то "зайчатки" ADT.
Но это всё всегда упирается в фундамент древней (что первично) и "фанатично" номинативной (что вторично, и - по факту - просто следствие древности) системы типов.
Например, нельзя "просто так" добавить в систему типов bottom type, которого там нет в силу "древности". Точнее он там таки есть, но "другой" :-) А без него, и само по себе "печально"... да и какой-нибудь "вывод типов" работает, мягко говоря, "порой странновато".
получается что Джава это язык с бесконечно мощной системой типов.
С точностью до наоборот. В Java весьма примитивная (в основном, конечно, в силу своей древности) система типов. Достаточно вспомнить, например, как типизируется null. Ну или сложности с введением нормальных функциональных типов и те "костылики", которые возникли в типизации (например, лямбд), после введения "аналогов".
"Плюсик" поставил, но не могу - в который уже раз - не заметить, что "очень не очень" рассказывать про "экспериментальную фичу Kotlin" и не указывать "откуда есть" она взялась. Хотя бы - если "долго, сложно" и т.п. - в виде ссылки на соответствующий ей KEEP.
Иначе - как, например, в этом конкретном случае - действительно может быть не понятно "а зачем козе баян?", а представление о "фиче" будет искажено.
Сознательно "так получилось" или нет - но, имхо, из текста статьи действительно сложно вынести что-то отличное от «DI - вид сбоку» и «зависимости, которые неявно передаются при вызове».
А сontext parameters, имхо, вообще про другое ...
Собственно, "контексты" (термин, в отношении Kotlin, ввёл - если мне не изменяет мой склероз, Александр Нозик) были (и есть) в Kotlin с "начала времен". Никто, если я правильно понимаю, "это" специально не придумывал - оно "само так получилось" на стыке двух других "фич".
Первая "фича", это т.н. member extensions - когда ф-ция (или свойство) расширения одного класса, объявляется в теле другого. Речь о такого рода конструкциях:
class A
class B {
fun A.doSome()
}
Вторая - это т.н. scope functions... в частности, те из них, что позволяют "сменить this". Речь о таком вот, примерно:
val a = A()
val b = B()
with(b) {
a.doSome()
}
В данном случае, doSome - это "особое поведение" класса A, в контексте класса B. Почему так? По большому счёту, потому что нет возможности вызвать этот этот doSome "просто так". Вот такое
val a = A()
val b = B()
a.doSome()
по вполне понятным причинам - просто не скомпилируется.
Т.е. для "вызова" такого doSome нужен вполне себе конкретный лексический контекст.
Данный - скажем так - феномен, получил весьма заметное распространение (в библиотечном коде, в реализациях DSL и т.п.) и был описан лет десять назад.
И, прошу заметить, что тут ни о какой "передачи зависимости" (явной или даже неявной) речи вообще не идёт. Контекст - в этом смысле - конструкт чисто "синтаксический", если так можно выразится.
Собственно, какое-то время спустя, появляется KEEP-259. Весь смысл которого сводится к «дайте возможность использовать чужие классы/интерфейсы как этот самый контекст». Речь, при этом, сугубо о синтаксисе (технический аспект тут действительно тривиален), который бы приводил к тому, что - грубо говоря - "использовать без with/run/etc нельзя". Т.е. что бы "оно" приводило к явному (как в случае с исходным феноменом) использованию scope functions для "выделения" контекста в коде.
Так в языке появляются т.н. context receivers и в таком виде "оно" живет до, емнип, двадцать третьего года. Опять же - хотя в этом KEEP явно фиксируется Injecting use case - который таки можно притянуть как "зависимости, которые неявно передаются при вызове" - но он не является основным и сколь-нибудь важным. Инициатива живет и развивается - в основном - в русле т.н. Code Coloring.
И вот где-то к концу 2023 года мы выходим на, собственно, текущий KEEP - т.н. сontext parameters. Который - суть - есть обобщение опыта использования context receivers. Смысл - по большому счёту - остался прежним. Да - многое переосмыслено, и переработано. Да - некоторые вещи изменились радикально. Но мы всё ещё хотим "того же". Если говорить таки про "зависимости, которые неявно передаются при вызове" - то в этом KEEP Dependency injection use case расписан ещё более явно. Но он всё ещё далеко не главный. Более того, некоторые аспекты этого use case - явно не рекомендуется к использованию.
Mandatory не дает fault tolerance, а конфирмов нет
"fault tolerance" чего оно не дает?! Какое такое особое "fault tolerance" дают RMQ'шные basic.ack?!
Они же сами описывают case'ы, когда сообщение будет "доставлено", но при этом ack выгружено не будет. Их там два, емнип... точно есть "нюансы" с persistent. И, насколько я помню, есть "другие нюансы" в случае, когда сообщение должно бы попадать более чем в одну очередь.
Ну и вариант, когда вам прилетает и basic.return, и этот basic.ack - для одного и того же сообщения - в любом случае, надо уметь "держать в уме", имхо :-)
Вообще, интересно... я точно помню, что раньше, вместе со спеками публиковались отдельные доп. документы: RoI по клиенту/серверу и т.п. Но вот полез на OASIS... и спеки без "тулчейнов", и "допников" не обнаружил :-( Теперь даже в RoI "непотыкаешь" :-(
Если у нас нет подтверждения доставки - мы просто не можем ничего гарантировать
Для ясности... мы оба понимаем, что никакие return и ack/nack от "чудес по сети" ну никак не спасают. Так? Если мы говорим о "девятке" то - по спеке - при обнаружении "чудес", нужно asap:
По возможность пульнуть (куда только можно) channel.flow
Кидать соответствующее исключение, и закрывать (переоткрывать) соответствующие каналы, соединения.
Это означает (ко всему прочему), что надо быть готовыми и к тому, что ни ack, ни nack получен не будет. Я это к тому, что если сама реализация клиента не умеет в обработку "такого", то обрабатывать "такое" придется самим. При реализации outbox, или ещё где-то.
"Стандартный" клиент (я для этого RoI и пытался найти) - если я правильно помню - должен на каждый канал держать отдельный "буфер". Он нужен для того, чтобы при "переоткрытии" канала не начинать "жизнь с нуля". Можно - наверное - раскопать клиента apache qpid... они, емнип, были весьма щепетильны в следовании RoI.
А так - если следовать принципу success is silent, and failure is noisy - достаточно же при получении basic.return помечать соответствующее сообщение в outbox как "нужно отправить". Нет?
С одной стороны, какая-то часть утверждений верная... при этом, другая часть - вызывает недоумение :-(
Метод отправки сообщения basic.publish не имеет возвратной части по протоколу AMQP 0-9-1 ...
Это так, в том смысле, что - условного - basic.publish-ok - по протоколу AMQP - действительно нет.
У каждого такого метода есть возвратная часть, которая говорит some-method-ok.
Это, мягко говоря, не так. Basic.publish тут не уникален - полно в протоколе методов без "возвратной части". Даже в классе basic он такой не один. Даже в множестве клиентских команд.
То есть наш basic.publish работает как fire and forget в семантике at most once.
А вот это вот - вообще не так. Механизмы контроля "этого" были ещё в "восьмерке", остались в "девятке"... и только в "десятке" эта часть была кардинально "пересмотрена" в сторону симметричности обмена. Ну так вся "десятка" такая.
Моя основная идея о причинах такого дизайна протокола AMQP в том, что ...
А зачем гадать?! Если вы таки читали спецификации, про "причины такого дизайна" есть прям в самом её начале. Пункт 2.2.3, если уж совсем точно. Можно почитать.
Тогда в протоколе должна быть другая часть, посвященная гарантированной доставке с подтверждениями. И да, она есть – транзакции.
"Другая часть" - в смысле "гарантированной доставки" - действительно есть. И транзакции есть - но они, как вы сами и говорите, про "другое". А вот "подтверждений" - в части basic.publish - действительно нет. By design.
Как это решили ребята из RabbitMQ
"Ребята из RabbitMQ" решили "подзабить" на спеку, в этой части... о чём им говорили, ещё когда они на "восьмерке" сидели.
Смотрим, что нам говорит "спека":
При обнаружении "сетевых проблем" клиент должен "закрывать" канал или соединение целиком;
Брокер, в случае невозможности обслуживать канал, переводит его в inactive через channel.flow;
В basic.publish есть два "флага", которые отвечают за обработку negative case'ов "доставки" на стороне брокера. Можно глянуть спеку, но суть в том, что при наличие этих флагов, брокер должен "вернуть" basic.return для каждого такого сообщения, в случае наступления этих самых negative case'ов.
Там, конечно, в некоторых местах MAY, а не MUST, но та ситуация, что описывается в доке RMQ как "танцы" с их basic.ack/basic.nack вполне может выглядеть как:
Для всех basic.publish с флагами mandatory/immediate - сформировать соответствующих basic.return и направить его в канал соответствующего клиента;
Направить в этот канал cannel.flow c acticve = 0.
без каких-либо противоречий "спеке". Другое дело, что "внутре у неё неонка" - в смысле, есть различные "особенности реализации", скажем так.
И то, что "ребята из RabbitMQ"- емнип, только с выходом "десятки" - часть её "сессионной" механики попытались адаптировать к - это лучше, конечно, чем ничего. До этого-то эта часть "спеки" ими вообще игнорировалась. При том, что сам по себе basic.return был уже реализован, емнип. Но отсылали они его (да и отсылают) только в тех случаях, когда им это "удобно", насколько я помню. Но, имхо, лучше бы уж целиком на "десятку" - в свое время - пересели... гарантий доставки-то, этот их "костылик" всё равно ведь не дает. О чем они сами и говорят. И да - это особенность RMQ, но не AMQP.
С VMными - всё понятно. Они более менее вписываются по "фазам" + их зависимости разрешаются через maven. Но даже тут есть нюансы...
У kotlin с использованием "плюшек" под maven уже есть "проблемы". Тот же ksp - насколько я в курсе - пока в maven нормально "вписать" не удалось. Оф. рекомендация, емнип, запускайте через exec.
Остальное всё - если я правильно понимаю - это всё на уровне "давайте запускать kotlinc через ant-task из maven". Можно, конечно - но в чём смысл-то?! Тут же нет ни управления зависимостями, ни всего остального... это всё вне maven получается. Нет?
Честно говоря - если брать gradle, то там большинство "таких" плагинов тож строятся по этому принципу (в смысле, "давайте запускать ..."). Но в gradle хоть в принципе возможно затащить "всё это" в билд скрипт, и реально "рулить" этим. Как тож самое (пусть хоть гипотетически) проворачивать в maven - лично мне - вообще не понятно :-(
Мы, когда пытались "это" использовать, знатно об это "постукались". Плюс, была ещё странность со сменой кофнигов... которая "то работает, то не работает".
Справедливости ради у нас на проекте (3M+ LOC, 800+ модулей) своя немного кастомная версия.
Ну мы тож пытались это под себя "допилить". В конце концов, "плюнули и забыли" (tm)
К сожалению, про "самое сладенькое" (Maven Artifact Resolver) в статье только пара строчек. А меж тем - если мне не изменяет мой склероз - избавление от Aether и настраиваемый resolver - декларировались одними из главных целей, которые "четверка" должна будет достичь.
Сделано там, действительно, не мало. Но пока, следить за подвижками в этом плане можно только по фотографиисходникам.
Надеюсь "апачи" таки разродятся статьей на этот счёт, и мы таки узнаем много нового и интересного.
Я дико извиняюсь... но, вы сами-то пробовали "этим" пользоваться? Особенно - в CI/CD окружении? Судя по всему - нет. Иначе бы знали о "небольшом баге", который висит чуть ли не со старта на этой "балалайке". Грубо говоря - если вы сидите на "тройке" - что есть этот "кэш" у вас, что его нет.
FYI. Нет нормального кэширования в maven. By desing нет. Даже в "четверке" - это не предусмотрено. А попытки "прикрутить сбоку" - это (хоть так верти, хоть эдак) именно что "костыли со стразами".
Мы - например - и "своё" пытались делать, и эту "балалайку" прикручивали... не работает оно так, как надо. И - в общем случае - не может работать чисто из-за "некоторых особенностей" самого "движка".
И гораздо лучшей системой типов. Про это, почему-то всё время забывают.
И речь даже не о nullable или type inference, каких-нибудь… банальное наличие bottom type позволяет “делать вещи”, которые в Java в принципе не работают.
Вообще-то может. И более того, на старте это даже, имнип, рассматривалось. Но оно действительно ломает совместимость со стороны Java кода, и - по большому счету - именно поэтому остались только reified type parameters (которые - "чудес не бывает"™ - тоже нельзя из Java вызывать), которые, с одной стороны, требуют явного использования, а с другой, покрывают львиную долю практической потребности в специализации.
?! Если вы про ML-подобный (с deep deconstruction и т.п.) - так от него тоже отказались осознано, а не потому, что “невозможно”. На практике, PM в Kotlin (в гораздо более продвинутой форме) был (и есть) с самых первых версий. Он постоянно развивается, и - насколько я в курсе - даже в самой своей продвинутой форме (на данный момент) не требует никаких фич от JVM… т.е. вполне себе компилируется в “java 8”.
Вы зря сравниваете корутиты Kotlin и виртуальные потоки Java. Они - сильно “про разное”…
“Зарутиль” корутины на dispatcher, который будет работать поверх виртуальных потоков - вообще не проблема. И в этом даже бывает практический смысл. Условный LOOM.Dispatcher - он хоть и не Kotlin SDK (пока), но ничего “такого” в нем там нет… кроме того, что он вас “прибивает гвоздиками” к вполне конкретной Java SDK. А "мы такое не любим"™
А вот добиться от виртуальных потоков семантики continuation - ровно также “просто”, как и от обычных.
Я это к тому, что ваш посыл про “полную зависимость от JVM мира”, он даже в отношении Kotlin JVM не верен. Даже в Kotlin JVM есть вещи, которые, скажем так, не дружат с “миром JVM”. Но они там, есть, т.к. с точки зрения развития языка “это нам нада”(с).
?! А можно по подробнее, об чём речь? Емнип, правила аллокации в Kotlin полностью аналогичны таким же в Java. Зачем в бейткод-то смотреть? Это раз. А два - если таки вдруг надо, то в чём проблема его посмотреть… этож буквально “пара шорткатов”.
Это "вы просто не умете их готовить"™ :-) Во-первых, null safety - оно же, в первую очередь, про свой собственный контракт. Во-вторых, Kotlin тебя честно предупреждает об обнаружении т.н. platform-specific types (и кстати, огромный шаг в этом направлении был сделан как раз с выходом KMP). В том числе, и “в любом месте где есть вызов Java кода”. А то, что такого рода предупреждения - по первости - игнорируются в большинстве своем - так это больше от неопытности, имхо.
А можно чуть подробнее про “приседания и костыли”? Реально интересно, чего вы там “такого” находили, что оно было нужно.
Если я правильно понял, о чём речь - это сугубо дело привычки. Лично про себя (я с Kotlin прям с версии 1.0… т.е. лет десять уже), скажу так.
Если писать в Java style (а оно таки можно, даже если вот лично я смысла особого в этом не вижу) - то разницы тут особой и нет.
Если писать в Kotlin style (необходимый минимум идиоматики) - он непривычен на старте, но и только. Уровень “понятности” - именно что - Java с чуть приятным синтаксисом.
Если к этому добавляется полная идиоматика языка - он ещё более непривычен (не непонятен!!!)… особенно, если предыдущий шаг был пропущен. Но после того, как понимаешь, что читается это исключительно “снизу вверх” (без всякого рода разрыва контекста), что объем “лишнего кода” - это minimum minimorum - возвращение к чтению Java исходников (причем даже в лучшем их виде) ничего кроме раздражения (вот лично у меня) не вызывает.
А “возвращаться” приходится гораздо чаще, чем хотелось бы… к сожалению. Все эти бесконечные проверки на null, upper cast’s и "вот это вот всё"™, про что ты уже и забыл - оно, банально, “мылит глаз”. Продираться через это - удовольствие, прямо скажем, на любителя.
Ну и маленький оффтоп ...
Я, при работе с Java/Kotlin в какой-то момент … достаточно давно уже - словил момент, который у меня был (тож очень давно), когда я, работая долго с Erlang, вынужден был периодически переключаться на C#.
Момент - если на пальцах - заключается в следующем… тебя реально начинают пугать ф-ции, которые не помещаются на экране :-)
С Kotlin/Java ощущения, конечно, слабее были. Но прям очень похоже было :-)
О да. Особенно - на практике :-) Вы не используете annotation processing? Ну там Spring и/или Lombok? Реально? А что вы такое пишете на Java?
Тут вообще не понятно “про что”. Если вы про то, что “стандартные” java decompiler’ы сильно плохо дружат с Kotlin - то есть такое. Но есть же специализированные инструменты для. Да даже какой-нибудь шикарный Kotlin Explorer уже как лет пять, емнип, есть. Или вы о чём?
:-) Рискую открыть вам “страшную тайну”, но даже kotlin 2.3 (самая новая, на данный момент версия языка) компилируется в JDK 1.8 без особых ограничений по, собственно, фичам Kotlin’а. Ну да… какие-то платформозависимые (типа @JvmRecords) аннотации становятся не доступны - но и только.
Это я к тому, что самому Kotlin JVM от самой JVM “много не нужно”.
Хорошо, давайте “на пальцах”…
Для меня - как для адепта функционального программирования - есть один единственный “паттерн ФП” - это прием ф-ции, как аргумента и/или получение ф-ции, как результата вычисления.
И с этой точки зрения - да, я постоянно вижу “применение паттернов ФП”, в том числе и в JS.
небольшой оффтопик про паттерны ФП
Эта картинка из замечательного доклада Скотта Влашина (Scott Wlashin). Делал он его уж больше десяти лет назад, но оно по прежнему - имхо - актуально.
А вот всё остальное (в том числе, и большинство из перечисленных вами “функциональных фич”) - это именно что стиль - который напрямую к, собственно, функциональному программированию не имеет абсолютно никакого отношения.
И стиль этот - по большому счету - определяется синтаксисом конкретного языка. Какие-то “штуки” - так или иначе - системой типов. Только и всего.
Но - и это важно понимать - что наличие этих “фич”, что их отсутствие - абсолютно никак не сказывается на, собственно, функциональности ЯП. Сказывается это всё - в лучшем случае - на удобстве. И с вот этим вот - лично я, например - не спорю.
Но вы назвали свою публикацию вполне определенно, а не, например, «Почему JS/TS — это боль при программировании в функциональном стиле» :-)
Вот опять. Вы вроде начинаете про одно, а заканчиваете другим, и сами себя запутываете.
Смотрите… важно то, что там нет переменных. А о иммутабельности, я извиняюсь, чего вы писали? Это разве “те признаки”? При том, что мутабельность - и формальном, и в практическом смыслах - там вполне себе есть.
И с циклами - похожая картина. То, что они там просто невозможны, в силу отсутствия и переменных, и присваивания - это одно. Но вы-то о циклах, емнип, упоминаете исключительно в разрезе “ленивости”. Это разве “те признаки”? Для меня - очевидно, что вы “про другое”. Надеюсь, и для вас это теперь очевидно.
Я сильно надеюсь, что из того, что я написал выше, вы уже поняли, что всё вами тут перечисленное никакого отношения у функциональности (или не функциональности) ЯП не имеет.
Дались вам эти “циклы”… ну загляните уже “под капот”, например, итераторам в scala, и убедитесь, что “чудес не бывает” ™, и “циклы” там вполне себе есть. Просто, для того, чтоб их “прям ваще” нигде не было, надо несколько большее, чем какая-либо система типов.
И? Что такого “ужасного” я должен там увидеть? Собственно, чем “это” принципиально отличается от наличия 100500 каких-нибудь FunctionN в той же Scala? Не… “сутевое” различие-то я понимаю, но у вас - насколько я понял - претензии не к сути, но к форме. Или о чём речь?
Вот мы дошли до “сладкого”. На всякий случай, уточню - мыж оба говорим об функциональной композиции? Так?
Если таки да - можно для “особо одаренных” (типа меня) раскрыть, что это за принцип такой? Что именно и куда вы “поставляете” при композиции?! Мыж не вычисляем ф-ции, которые “соединяем”. Мы вычисляем именно что их композицию. И результатом этого вычисления является третья ф-ция. Которую мы - пока ещё - не вычисляем. Нет?
Я либо вообще не понимаю “о чём вы”, либо я сейчас вам открою страшную тайну.
Чтобы не было “строгой зависимости от порядка вычислений” (и соответственно, прям нужна была СП) необходимо call-by-need. А тут (при вычислении композиции) его и рядом нет. Т.е. - во-первых - наличие/отсутствие СП абсолютно не важно. А во-вторых, функциональная композиция - таки задает порядок вычисления результирующей ф-ции.
Если же речь не о функциональной композиции, а о чём-то другом, то лучше таки пояснить.
В рамках “моего определения через HOF” - ничего. Наличие в системе типов HOF - это просто необходимое и достаточное условие, чтоб считать ЯП функциональным.
А “недосягаемым” (для JS и для Scala) его делает модель вычислений, которая позволяет легко и просто получить такую множественную диспетчеризацию, которая на этих языках невозможна в принципе.
Ничего я не “забираю”. Я лишь намекаю, что вы можете взять “функциональную чистоту” JS, и сравнить её с аналогичной “чистотой”, например Scala… чисто “для смеха” :-)
Это каких, например? Наличие “оператора композиции”? Или вы о чём-то другом?
А причем тут “функциональная чистота”, и система типов?! “Чистота”, она как раз в гораздо большей степени про модель вычислений, чем про что-то другое. Нет?
С какого перепугу-то?!
:-) Ну я весьма значительную часть своей карьеры провел за Erlang’ом… где - внезапно - из всего вами перечисленного - “из коробки” - есть только “изоляция эффектов”. И? Erlang - как ФЯП - недосягаем что для JS, что для Scala. Дальше-то что? Порассуждаем за “а насколько функционален Erlang?”, если в нем “всего этого” нет? :-)
Скажем так… есть некоторая разница (с), между функциональным программированием, и программированием в функциональном стиле. Сдается мне - вы говорите про второе, и - местами - путаете “это” с первым.
Мой поинт-то в том, что не понятно, какое отношение наличие/отсутствие ссылочной прозрачности имеет к функциональности ЯП?
Ну если под TCO понимать само наличие call-by-need - то да :-) Но call-by-need - это не TCO. И GR - не TCO. Где вы TCO в haskell нашли? Или - а что вы вообще под TCO тогда понимаете?
?! В Erlang замечательная система типов. С чего вы взяли, что её (системы типов) там нет?!
Ну так и есть. JS функционален ровно в той же степени, в которой функциональна, например, scala. Ибо функциональность - это не про модель вычислений (декларативность/императивность), не про стратегию вычислений (упрощая, ленивость/энергичность) и не про ещё кучу аспектов.
Функциональность - это про вполне определенное свойство системы типов.
Другое дело, что можно начать рассуждать о - так сказать - “чистоте” этой функциональности… :-)
Ну потому что, то что вы пишете в этих разделах - “делать мне больно мозг” :-) А “приведение оснований” - это будет долго и оффтопик.
Про HKT вы там и сами уже пришли к тому, что “оно лишнее”. С моей точки зрения - странным путем, но пришли. И к тому, что PM - из такой же категории - рано или поздно - придете.
В конце концов, какой-нибудь, common lisp вполне себе живет же без PM на уровне языка :-) Да и само по себе требование exhaustiveness для PM - в разрезе наличия динамической типизации, например - выглядит “странновато”, имхо. Ну или надо будет какое-нибудь Scheme с Clojure и прочими Erlang’ами “лишать звания” :-)
При всём уважении - похоже, у вас у самого какая-то “каша в голове” :(
Имманентный свойством ФЯП (любого - без каких-либо исключений) является исключительно система типов, которая умеет в HOF (а у вас про них вообще нет ничего). Другими словами - без наличия в системе типов функций высшего порядка - ФЯП не бывает.
А всё остальное - в том числе, и всё вами перечисленное - либо ортогонально, собственно, “функциональности”, и является следствием других категорий ЯП. Либо является следствием наличия HOF в системе типов.
Ну и - скажем так, на всякий - про “странности”…
Сама по себе иммутабельность - не есть гарантия ссылочной прозрачности. А ссылочная прозрачность - это про декларативность, а не про функциональность. Имхо, это важно понимать, и различать.
Аналогично с TCO. Оно важно - исключительно - с точки зрения “энергичной” evaluation strategy. Причем тут функциональность?! Зачем вам TCO, если у вас везде, например, call-by-need?
“Ленивость” - aka call-by-need - мало того, что “мимо” (в смысле, это не про функциональность), но нужно же понимать, что оно “конфликтует” с TCO. А вы их в один список ставите. Зачем?!
Какой-нибудь Erlang смотрит на вас с недоумением :-)
А то, что вы пишите “про монады” - это - по большому счёту - про наличие/отсутствие, скажем так, “оператора композиции”. И - опять же - к функциональности, как таковой, оно отношения не имеет.
Про HKT и PM - вы, похоже, не понимаете место этих концепций в ЯП (любом).
Ну и про “гравитацию”…
Это, имхо, весьма показательно :-)
Я это уже читал, но так и не понял в чём конкретно эта "польза". Другими словами, я не понимаю, каким образом дополнительная связность (хоть её наличие вы и отрицаете) может быть полезна. Особенно, в "проектах с большим, сложным внутреннем API".
Ну я таки глянул код. Позвольте усомниться в этом вашем утверждении. Если аттрактор и друг это типы из разных сборок, то возникает циклическая зависимость между сборками. Причина - та самая "лишняя" связность, наличие которой вы отрицаете, но которой, тем не менее, нет в канонических вариантах решения.
Собственно, это (наличие циклической зависимости) было очевидно и без кода... но я надеялся, что у вас там она разрешается, например, через генерацию отдельной сборки. Я ошибался.
При традиционных подходах к - та часть, что в вашем варианте становится аттрактором ничего не знает о "дружественной" части.
Я это понял :-) Моя реплика была про то, что каноническое решение выглядит с точностью до наоборот. Если хотите, то вопрос в том, почему у вас не так? Почему за основу взят - так сказать - "вывернутый" вариант?
Как я понимаю, мы о разных "трех не" говорим. Речь была о том, что "дружба" не наследуется. Не просто так. Очевидно, что - как минимум - в отношении к интерфейсам у вас "всё не так". Я, собственно, поэтому и упомянул явную реализацию.
К сожалению, там нет "обоснования зачем". Там есть "смотри как могу". Любой из рассмотренных примеров замечательно решается без "этого". Собственно, в большинстве случаев, вы сами же об этом и пишите. Поэтому и был мой вопрос про "область ценности".
Опять же... если кто-то, что-то "видит" - он имеет полное право за это "дергать". И - скажем так - "прикладная задача проектирования" сводится к "надо сделать так, чтобы каждый видел только то, что ему можно дергать". А вы - буквально - говорите "а давайте на это всё забьем" :-)
Вот ваш пример:
Объясните мне, пожалуйста, чем "это" - в принципе - отличается от?
Мой пример:
Мне вот видится, что - принципиально - ничем. И, имхо, второй вариант "чище", как минимум, как раз из-за того, что ничего не "торчит". Т.е. с мой точки зрения, всё что вы пытаетесь делать в такого рода вариантах - это заменить вполне традиционную технику т.н. mixin типов, на непонятно что :-(
Зато для него есть, например, InternalsVisibleToAttribute, который позволяет вытащить "потроха" в отдельную сборку, и уже там сделать "торчать красиво" :-) В смысле, чтоб "торчало" только то, что нужно. И не приводит, при этом, к циклическим зависимостям.
Ну вы чего?! Через neasted объявляются агрегаты (сильно упрощая - интерфейсы), которые потом используются для "сборки" того, что вам нужно где-то "во вне". Т.е. если вам прям так нужен "слуга двух господ" - вы вполне можете его получить без особых приседаний. И даже в другой сборке, если надо.
Если говорить за "вообще", то приемов работы с tight coupling - мы знаем не мало. В том смысле, что мы знаем "как есть этих слонов". Проблема - ту которую я вижу - заключается в том, что "дружба" - это как раз то, что к этой самой tight coupling приводит.
dixi :-)
Доброе...
Начну с середины
Это не так. И, имхо, это достаточно очевидно. Например, достаточно "представить", что "аттрактор" и "друг" это типы из разных сборок. "Лишняя" связь тут же станет очевидной. Нет?
Представить в кавычках, т.к. сильно не уверен, что "оно" умеет в "такое". Код не смотрел. Но без "такого" ценность (и так не очевидная) этой штуки прям сильно уменьшается, имхо.
Вот как раз "через интерфейс" исходная задача решается "с точностью до наоборот". В том смысле, что исходная "сущность" - мутабельная, а через реализацию интерфейса предоставляется readonly проекция. Нет? При таком подходе, "грануляция" доступа - пока отбросив её ценность - органично "схлопывается" до агрегатов. Оно может быть и выглядит громоздко на синтетике, но смотрится вполне себе нормально и логично (отбросив, опять же, всё остальное, что можно "предъявить" такому агрегату).
Как бы это уже всё давно разжевано не один раз. "Это" - как раз - не правильно. Механизм "дружбы" в плюсах - это следствие, а не причина, так сказать. И если речь о "дружбе" на уровне классов, то реальной ценности там нет. Если мне не изменяет мой склероз, даже Страуструп предлагал смотреть на "дружбу" исключительно с позиции friend function (например, при реализации операторов).
Глубокое имхо... ценность такой "гранулярности" - есть следствие "кривоватого" дизайна. Насколько я понимаю, оно может быть хоть сколько-то ценно при работе с - скажем так - перегруженным агрегатом, в качестве аттрактора. Но даже при этом - мы живём в реальном мире, и не отрицаем этого - я бы сильно подумал над тем, нужно ли мне плодить доп. связность, практически, на ровном месте. Есть более "ровные" способы обеспечить такого рода разграничения. Особенно, если речь исключительно о конкретной сборке.
?! Ну т.е. все "минусы" упомянутые вами ранее - это на самом деле не "минусы"? Так? И мы - внезапно - осознаем что из "C++ дружбы" хоть сколько-то ценно только friend function? Или как?
Пусть даже если так. Но в чём ценность такой "дружбы" для C#? "Дать доступ" же не является само целью? Надеюсь. В "плюсах" для этой "штуки" есть вполне конкретная область - операторы - в которой "оно" прям ценно. В том смысле, что другими механиками языка это "не лечится".
А в C# где вы видите эту "область ценности"? Вопрос вызван тем, что - выглядит так - начали мы с одного, а пришли к другому :-) Т.е. решали одну задачу, а решение (которое получилось) - это решение какой-то совсем другой задачи. Вот и интересно - какой? Там у вас есть некоторые "рассуждения на тему". Но это именно что "рассуждения не тему". Можете ли вы переформулировать их в терминах той задачи, которая сформулирована в начале статьи?
Хм... имхо, это "немножко" не логично. Логично, имхо, требовать для этого случая явной реализации интерфейса. Во избежание, так сказать. Ну и - если уж всё это навеяно "C++ дружбой" - не стоит забывать про "три не". Они не просто так были сформулированы. А из рассуждений про "как должно быть правильно", складывается стойкое ощущение, что про одно из не вы всё время забываете :-(
Ну это как сказать :-) Если оно - как я подозреваю (хочу ошибаться) - работает только в рамках конкретной сборки... лично мне вот вообще не понятна ценность этого "инструмента". И даже если представить, что это такой "заход" в т.н. sealed hierarchy - что могло бы иметь ценность, применительно к C#... оно - всё равно - выглядит сильно "странновато" и "не ровно".
Когда вы говорите про "торчащие нитки", вы вообще о чём? Эти "нитки" они из сборки чтоль "торчат"? Или откуда? Складывается ощущение что вам кто-то строго запретил пользоваться internal (и его производными) уровнем доступа и использовать neasted иерархии. Это, кстати, относится и к приведенным вами "примерам".
Без более строго сформулированного "зачем" - лично у меня - статья оставляет сильное "хлебное" послевкусие :-)
Есть разное "можно". Например, как нам хорошо известно, в любой параметрический полиморфизм "классы типов" можно (легко и просто) добавить через препроцессинг. Но есть разница (с) с тем, когда классы типов это часть системы типов :-)
Зря. Лямбда-куб - как наглядная демонстрация той самой "мощности системы типов" - существует уже лет тридцать как.
Ну не всё так печально, конечно. В 8-ке появляются лямбды, и "типа" функциональные типы. Что-то получается через них провернуть. В Java 17 появляются "замкнутые" (sealed) иерархии и - соответственно - какие-то "зайчатки" ADT.
Но это всё всегда упирается в фундамент древней (что первично) и "фанатично" номинативной (что вторично, и - по факту - просто следствие древности) системы типов.
Например, нельзя "просто так" добавить в систему типов bottom type, которого там нет в силу "древности". Точнее он там таки есть, но "другой" :-) А без него, и само по себе "печально"... да и какой-нибудь "вывод типов" работает, мягко говоря, "порой странновато".
С точностью до наоборот. В Java весьма примитивная (в основном, конечно, в силу своей древности) система типов. Достаточно вспомнить, например, как типизируется null. Ну или сложности с введением нормальных функциональных типов и те "костылики", которые возникли в типизации (например, лямбд), после введения "аналогов".
Доброе...
"Плюсик" поставил, но не могу - в который уже раз - не заметить, что "очень не очень" рассказывать про "экспериментальную фичу Kotlin" и не указывать "откуда есть" она взялась. Хотя бы - если "долго, сложно" и т.п. - в виде ссылки на соответствующий ей KEEP.
Иначе - как, например, в этом конкретном случае - действительно может быть не понятно "а зачем козе баян?", а представление о "фиче" будет искажено.
Сознательно "так получилось" или нет - но, имхо, из текста статьи действительно сложно вынести что-то отличное от «DI - вид сбоку» и «зависимости, которые неявно передаются при вызове».
А сontext parameters, имхо, вообще про другое ...
Собственно, "контексты" (термин, в отношении Kotlin, ввёл - если мне не изменяет мой склероз, Александр Нозик) были (и есть) в Kotlin с "начала времен". Никто, если я правильно понимаю, "это" специально не придумывал - оно "само так получилось" на стыке двух других "фич".
Первая "фича", это т.н. member extensions - когда ф-ция (или свойство) расширения одного класса, объявляется в теле другого. Речь о такого рода конструкциях:
Вторая - это т.н. scope functions... в частности, те из них, что позволяют "сменить this". Речь о таком вот, примерно:
В данном случае, doSome - это "особое поведение" класса A, в контексте класса B. Почему так? По большому счёту, потому что нет возможности вызвать этот этот doSome "просто так". Вот такое
по вполне понятным причинам - просто не скомпилируется.
Т.е. для "вызова" такого doSome нужен вполне себе конкретный лексический контекст.
Данный - скажем так - феномен, получил весьма заметное распространение (в библиотечном коде, в реализациях DSL и т.п.) и был описан лет десять назад.
И, прошу заметить, что тут ни о какой "передачи зависимости" (явной или даже неявной) речи вообще не идёт. Контекст - в этом смысле - конструкт чисто "синтаксический", если так можно выразится.
Собственно, какое-то время спустя, появляется KEEP-259. Весь смысл которого сводится к «дайте возможность использовать чужие классы/интерфейсы как этот самый контекст». Речь, при этом, сугубо о синтаксисе (технический аспект тут действительно тривиален), который бы приводил к тому, что - грубо говоря - "использовать без with/run/etc нельзя". Т.е. что бы "оно" приводило к явному (как в случае с исходным феноменом) использованию scope functions для "выделения" контекста в коде.
Так в языке появляются т.н. context receivers и в таком виде "оно" живет до, емнип, двадцать третьего года. Опять же - хотя в этом KEEP явно фиксируется Injecting use case - который таки можно притянуть как "зависимости, которые неявно передаются при вызове" - но он не является основным и сколь-нибудь важным. Инициатива живет и развивается - в основном - в русле т.н. Code Coloring.
И вот где-то к концу 2023 года мы выходим на, собственно, текущий KEEP - т.н. сontext parameters. Который - суть - есть обобщение опыта использования context receivers. Смысл - по большому счёту - остался прежним. Да - многое переосмыслено, и переработано. Да - некоторые вещи изменились радикально. Но мы всё ещё хотим "того же". Если говорить таки про "зависимости, которые неявно передаются при вызове" - то в этом KEEP Dependency injection use case расписан ещё более явно. Но он всё ещё далеко не главный. Более того, некоторые аспекты этого use case - явно не рекомендуется к использованию.
Вот, кстати, "дико плюсую" за "по уму". Сам-то ETF простой как три копейки... но вот дальше.
Отлично получилось, в целом. Осталось реализовать ETF (ну и ещё "койчего") и можно цепляться как external node к.
"fault tolerance" чего оно не дает?! Какое такое особое "fault tolerance" дают RMQ'шные basic.ack?!
Они же сами описывают case'ы, когда сообщение будет "доставлено", но при этом ack выгружено не будет. Их там два, емнип... точно есть "нюансы" с persistent. И, насколько я помню, есть "другие нюансы" в случае, когда сообщение должно бы попадать более чем в одну очередь.
Ну и вариант, когда вам прилетает и basic.return, и этот basic.ack - для одного и того же сообщения - в любом случае, надо уметь "держать в уме", имхо :-)
Вообще, интересно... я точно помню, что раньше, вместе со спеками публиковались отдельные доп. документы: RoI по клиенту/серверу и т.п. Но вот полез на OASIS... и спеки без "тулчейнов", и "допников" не обнаружил :-( Теперь даже в RoI "непотыкаешь" :-(
Для ясности... мы оба понимаем, что никакие return и ack/nack от "чудес по сети" ну никак не спасают. Так? Если мы говорим о "девятке" то - по спеке - при обнаружении "чудес", нужно asap:
По возможность пульнуть (куда только можно) channel.flow
Кидать соответствующее исключение, и закрывать (переоткрывать) соответствующие каналы, соединения.
Это означает (ко всему прочему), что надо быть готовыми и к тому, что ни ack, ни nack получен не будет. Я это к тому, что если сама реализация клиента не умеет в обработку "такого", то обрабатывать "такое" придется самим. При реализации outbox, или ещё где-то.
"Стандартный" клиент (я для этого RoI и пытался найти) - если я правильно помню - должен на каждый канал держать отдельный "буфер". Он нужен для того, чтобы при "переоткрытии" канала не начинать "жизнь с нуля". Можно - наверное - раскопать клиента apache qpid... они, емнип, были весьма щепетильны в следовании RoI.
А так - если следовать принципу success is silent, and failure is noisy - достаточно же при получении basic.return помечать соответствующее сообщение в outbox как "нужно отправить". Нет?
Очень странное впечатление от статьи.
С одной стороны, какая-то часть утверждений верная... при этом, другая часть - вызывает недоумение :-(
Это так, в том смысле, что - условного - basic.publish-ok - по протоколу AMQP - действительно нет.
Это, мягко говоря, не так. Basic.publish тут не уникален - полно в протоколе методов без "возвратной части". Даже в классе basic он такой не один. Даже в множестве клиентских команд.
А вот это вот - вообще не так. Механизмы контроля "этого" были ещё в "восьмерке", остались в "девятке"... и только в "десятке" эта часть была кардинально "пересмотрена" в сторону симметричности обмена. Ну так вся "десятка" такая.
А зачем гадать?! Если вы таки читали спецификации, про "причины такого дизайна" есть прям в самом её начале. Пункт 2.2.3, если уж совсем точно. Можно почитать.
"Другая часть" - в смысле "гарантированной доставки" - действительно есть. И транзакции есть - но они, как вы сами и говорите, про "другое". А вот "подтверждений" - в части basic.publish - действительно нет. By design.
"Ребята из RabbitMQ" решили "подзабить" на спеку, в этой части... о чём им говорили, ещё когда они на "восьмерке" сидели.
Смотрим, что нам говорит "спека":
При обнаружении "сетевых проблем" клиент должен "закрывать" канал или соединение целиком;
Брокер, в случае невозможности обслуживать канал, переводит его в inactive через channel.flow;
В basic.publish есть два "флага", которые отвечают за обработку negative case'ов "доставки" на стороне брокера. Можно глянуть спеку, но суть в том, что при наличие этих флагов, брокер должен "вернуть" basic.return для каждого такого сообщения, в случае наступления этих самых negative case'ов.
Там, конечно, в некоторых местах MAY, а не MUST, но та ситуация, что описывается в доке RMQ как "танцы" с их basic.ack/basic.nack вполне может выглядеть как:
Для всех basic.publish с флагами mandatory/immediate - сформировать соответствующих basic.return и направить его в канал соответствующего клиента;
Направить в этот канал cannel.flow c acticve = 0.
без каких-либо противоречий "спеке". Другое дело, что "внутре у неё неонка" - в смысле, есть различные "особенности реализации", скажем так.
И то, что "ребята из RabbitMQ"- емнип, только с выходом "десятки" - часть её "сессионной" механики попытались адаптировать к - это лучше, конечно, чем ничего. До этого-то эта часть "спеки" ими вообще игнорировалась. При том, что сам по себе basic.return был уже реализован, емнип. Но отсылали они его (да и отсылают) только в тех случаях, когда им это "удобно", насколько я помню. Но, имхо, лучше бы уж целиком на "десятку" - в свое время - пересели... гарантий доставки-то, этот их "костылик" всё равно ведь не дает. О чем они сами и говорят. И да - это особенность RMQ, но не AMQP.
С VMными - всё понятно. Они более менее вписываются по "фазам" + их зависимости разрешаются через maven. Но даже тут есть нюансы...
У kotlin с использованием "плюшек" под maven уже есть "проблемы". Тот же ksp - насколько я в курсе - пока в maven нормально "вписать" не удалось. Оф. рекомендация, емнип, запускайте через exec.
Остальное всё - если я правильно понимаю - это всё на уровне "давайте запускать kotlinc через ant-task из maven". Можно, конечно - но в чём смысл-то?! Тут же нет ни управления зависимостями, ни всего остального... это всё вне maven получается. Нет?
Честно говоря - если брать gradle, то там большинство "таких" плагинов тож строятся по этому принципу (в смысле, "давайте запускать ..."). Но в gradle хоть в принципе возможно затащить "всё это" в билд скрипт, и реально "рулить" этим. Как тож самое (пусть хоть гипотетически) проворачивать в maven - лично мне - вообще не понятно :-(
https://github.com/apache/maven-build-cache-extension/issues/270 - это тот самый "небольшой баг".
Мы, когда пытались "это" использовать, знатно об это "постукались". Плюс, была ещё странность со сменой кофнигов... которая "то работает, то не работает".
Ну мы тож пытались это под себя "допилить". В конце концов, "плюнули и забыли" (tm)
К сожалению, про "самое сладенькое" (Maven Artifact Resolver) в статье только пара строчек. А меж тем - если мне не изменяет мой склероз - избавление от Aether и настраиваемый resolver - декларировались одними из главных целей, которые "четверка" должна будет достичь.
Сделано там, действительно, не мало. Но пока, следить за подвижками в этом плане можно только по
фотографиисходникам.Надеюсь "апачи" таки разродятся статьей на этот счёт, и мы таки узнаем много нового и интересного.
Не только для сборки? Или не только для Java проектов?
Если таки второе, то интересно - как?! Через antrun какой-нибудь? Так-то оно гвоздиками всё прибито к.
Я дико извиняюсь... но, вы сами-то пробовали "этим" пользоваться? Особенно - в CI/CD окружении? Судя по всему - нет. Иначе бы знали о "небольшом баге", который висит чуть ли не со старта на этой "балалайке". Грубо говоря - если вы сидите на "тройке" - что есть этот "кэш" у вас, что его нет.
FYI. Нет нормального кэширования в maven. By desing нет. Даже в "четверке" - это не предусмотрено. А попытки "прикрутить сбоку" - это (хоть так верти, хоть эдак) именно что "костыли со стразами".
Мы - например - и "своё" пытались делать, и эту "балалайку" прикручивали... не работает оно так, как надо. И - в общем случае - не может работать чисто из-за "некоторых особенностей" самого "движка".