All streams
Search
Write a publication
Pull to refresh
0
0
Send message

Очень странно понимается кустарная сборка "на коленке".

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

Ну тут или продолжать сложность определять "на глаз", или вводить метрику.
Если при наличии метрики всё равно приходится "на глаз" определять, то какие цели преследует её введение в рассмотрение? Для чего она вообще такая нужна, если настолько хрупкая?

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

Sample
Sample

Метрика не подходит для современных языков, поддерживающих ФП, так как ФП позволяет программисту реализовывать свои управляющие конструкции. Ниже пример на Kotlin, где представлена пара реализаций для конструкций изоморфных if else, которые "ломают" расчёт сложности для функций test2 и test3.

Вывод: в текущем виде мало кому нужна, так как почти все современные популярные ЯП поддерживают ФП.

Sample
Sample

Отлично! Просто великолепно!
Хабр опустился до тупых репостов.
Всю статью можно смело выпилить, заменив ссылкой на community перевод официальной документации.
Автор, не стыдно, вообще, заниматься репостом?

В данном примере согласен, код императивный.
Правда, посмотрев на него ещё раз, понял, что он императивный как раз потому, что не является чисто функциональным: функция modifyIORef' не является ссылочно прозрачной, так как осуществляет побочное действие по изменению значения переменной. Как следствие, лямбда \x -> modifyIORef' sumVar (+x), так же не является ссылочно прозрачной.

Более того, в случае с IO типом, сам тип возвращаемого значения говорит о том, что функция не чистая, к тому же, не существует safe способов получения из IO чистого значения, то есть, ваш подсчёт суммы, на самом деле, не совсем сумму считает, а порождает IO, в котором будет "обёрнута" сумма.

Если переделать IO на ST, то можно сделать саму функцию sum ссылочно прозрачной, но код её реализации от этого чисто функциональным не станет (используются не чистые функции и изменяемые значения).

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

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

Давайте разберёмся с вашим кодом.
Чтобы получить в Haskell хоть что-то похожее на императивное исполнение, вам понадобилось:

  1. Использовать монады, ведь без них не работает синтаксический сахар do нотации. Если что, подобный синтаксис просто приколочен сбоку в компиляторе, в чистом ФП, базирующемся на лямбда исчислении, никаких вам do.

  2. Использовать IORef, ведь в Haskell нет изменяемых переменных.

  3. Использовать специфическую функцию forM_ для организации чего-то, вроде цикла, ведь в Haskell нет циклов.

  4. Использовать специфическую функцию modifyIORef' для организации присвоения нового значения переменной.

  5. Даже после всего этого, синтаксис оказался довольно далёк от типичного императивного варианта (сравните с кодом на C или Java).

То есть, я бы сказал, что вы реализовали на основе ФП что-то похожее на императивный стиль, хотя и с оговорками.

Вот, кстати, как примерно выглядит ваш код после того, как из него убирается не ФП сахар (do нотация)

sum :: Num a => [a] -> IO a
sum xs =
    newIORef 0 >>=
    \sumVar -> (forM_ xs $ \x -> modifyIORef' sumVar (+x)) >>
    readIORef sumVar

Ну как? Сильно похоже на императивный стиль?

Да, этот код похож на императивный, но таковым не является (do нотация - всего лишь синтаксический сахар поверх функкций >>= и >>).
То есть, ваша функция лишь декларирует, какую композицию побочных эффектов вы хотите, а не то, какие команды будут исполняться при её вызове.
Считать это императивным кодом - большая ошибка, которая может привести к некорректным выводам относительно того, как это вычисляется. Самый простой пример - монадическая функция return. В do нотации выглядит похоже на императивную команду возврата из процедуры, но работает иначе, в частности, функции, идущие на следующих строчках за ней, будут вычисляться, в отличие от команд, идущих на следующих строчках после команды return в императивных языках.

Что же до ST монады, то да, она, вместе с сахаром do нотации, позволяет декларировать чистые функции похоже на императивный код, но лишь похоже.

Конечно же можно. Если что-то на чём-то реализуемо, это вовсе не значит, что нельзя противопоставлять. С чего бы? Вот, например, тот же C вполне реализуем на языке ассемблера, но мы же их противопоставляем, говоря, что в C, в отличие от ассемблера, есть, например, типизация, структурность.

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

Опять же, ваши сходные слова:

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

Нет, нельзя ФП использовать в императивном стиле. Это уже будет не ФП, а использование нескольких парадигм!

Ещё раз, в чистой ФП парадигме есть понятие функций в математическом смысле (ссылочно-прозрачные, без побочных эффектов), и данных в математическом смысле (неизменяемые значения). Более того, данные (булевы значения, числа и прочее) кодируются через те же функции.
Где вы, в математических выражениях увидели последовательность команд, которая присуща императивному подходу? Где вы в мире неизменяемых данных увидели последовательное изменение состояния? Императивные побочные эффекты в ФП парадигме стремятся вытеснить на "периферию" программы, за пределы чисто функционального ядра. В чистых, статически типизированных ФП языках их вообще разметили специальным типом IO монады, Чтобы не смешивать с декларативным функциональным ядром, а их выполнение - вообще вытеснили почти за пределы языковой модели.

Ладно. Я не вижу смысла в дальнейшей дискуссии. Вы слишком вольно пытаетесь толковать строгие определения, за которыми стоит ещё более строгая математика, а у меня нет сил проводить ликбез, в очередной раз.

Могу только посоветовать почитать про классификацию парадигм программирования, и почему они друг другу противопоставляются. Начать можете с той же википедии https://en.wikipedia.org/wiki/Declarative_programming, может поймёте, почему ФП в императивном стиле - абсурд.

В определении я процитировал самую существенную часть.

А с чего это вы решили, что такие части, как "использование команд" и "последовательное изменение состояния" - несущественная часть определения императивной парадигмы?

Я писать, что нельзя ФП противопоставлять императивному программирования, так как ФП легко реализуется на императивных языках.

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

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

Так и не передёргивайте, не меняйте уровень абстракций! Покажите, где в спецификации того же Haskell встречается понятие, эквивалентное понятию команда? Покажите, где в спецификации ленивого ФП языка что-то про последовательность исполнения этих самых команд? Нету? Вот и не надо тогда рассказывать сказки про императивность ФП!

Единственная причина по которой пытаются противопоставлять императивную и функциональную парадигмы, это глобальные состояния. Но глобальные состояния, это возможный, но даже не обязательный признак для императивного программирования, т.к. можно писать stateless программы в императивной парадигме.

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

Тогда как даже ваш пример про Haskell, в котором "IO pipeline, исполнение которого нарочно вынесено "за скобки" в языке", все равно будет является частью программы, без которой не возможное её реальное выполнение. Поэтому даже в чисто функицональных языках вынужденно присутствуют императивные команды.

Нет, в самом языке (его safe части), никаких императивных команд нету. Да и в unsafe части, программе предоставляется лишь функция unsafePerformIO, которую, к тому же, на практике нужно вызывать примерно в 0% случаев. И даже в этом проценте случаев, у вас нет контроля над тем, как именно исполняется IO, это делает компилятор и стандартная библиотека. Но если уж вы так пытаетесь натягивать императивную сову на декларативный глобус, то тогда выходит, что декларативных парадигм вообще не существует: рано или поздно всё это транслируется в набор машинных команд процессора, которые императивны по своей сути. Согласитесь, абсурд получается.

А как вы определяете ФП, простите, что оно у вас может быть императивным? Определение из вики (источника так себе, но для основы пойдёт):

In computer science, imperative programming is a programming paradigm of software that uses statements that change a program's state.

При этом, чистое ФП не содержит таких понятий, как statement и state. Вместо них используются referential transparent expression (function) и immutable (stateless) data.
Как вы, на основе этих понятий, планируете организовать императивное программирование?
В чистых ФП языках, тот же Haskell, даже IO - не реальные операции ввода-вывода, а декларативное описание связывания отдельных IO функций в IO pipeline, исполнение которого нарочно вынесено "за скобки" в языке.

Можно скомбинировать варианты gorun и обохдной:

//usr/bin/env gorun "$0" "$@"; exit "$?"
package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println("Hello", os.Args[1])
    os.Exit(42)
}

У этого варианта код выхода корректный, если запускать ./example.go world, при этом сохраняются остальные преимущества обходного варанта.

В последнем паззлере ошибка: вместо
Function<*>(){}()
должно быть
Function<*>.(){}()
иначе не компилируется.
До рантайм ошибки несовместимости типов не пролезут. Если у функции выведен, а не указан явно тип возвращаемого значения (Unit для функций, декларированных как блок; тип выражения для функций, декларированных как выражения), то это не значит, что его нет.
В любом случае, код, использующий функцию, не скомпилируется, если ожидаемый тип возврата не совместим с выведенным по телу функции.
То же верно и для пулбичных val/var деклараций.
В Хаскеле дурным тоном считается не аннотировать типы публичных деклараций (тех, что экспортируются из модуля). Остальное по желанию.
Да, кстати, в Хаскеле вообще нет различий между лямбдой и именованной функцией, равно как и нет различий между функциональными типами данных и остальными. Более того, так как он тотально ленив, то то, что по типу данных является числом, представляется в памяти как thunk (что-то вроде Supplier в терминах Java). Число вместо него в памяти появляется только тогда, когда понадобилось в других вычислениях.

Раз уж автор попросил в Kotlin Community о конструктивной критике по сути, то она у меня есть.
По пунктам с цитатами из статьи:


Добро пожаловать под кат
В Котлине за любым простым выражением может стоять сколь угодно сложный код из-за всяких умностей вроде перегрузки. Без IDE ничего не поймёшь. А IDE плохо, когда ты едешь в поезде и видишь, что свинговый жабоинтерфейс высасывает из ноутбука батарейку как вампир.

Начать с того, что в чтении/написании Java кода без IDE тоже далеко не уедешь. Что касается самой перегрузки операторов, то в ней особо проблем нет, это всего лишь другой способ объявления методов, который позволяет единообразно писать выражения независимо от типов данных, над которыми эти выражения вычисляются.
Классический пример — BigDecimal в Java:


BigDecimal hundredAndOne = BigDecimal.ONE.add(BigDecimal.TEN.multiply(BigDecimal.TEN));

То же в Kotlin:


val hundredAndOne = BigDecimal.ONE + BigDecimal.TEN ** BigDecimal.TEN

Котлин даёт одинаковый API для коллекций и сиквенсов, из-за чего люди злоупотребляют цепочками map/filter на коллекциях, создавая кучу промежуточных неленивых копий. Стримы в джаве специально введены для различия между ленивой и неленивой коллекцией. Да, есть инспекция в IDE для этого — потому что инспекции призваны исправлять недостатки языков.

В Java, чтобы получить список целых чисел, каждое из которых на 1 больше соответствующего числа из исходного списка нужно будет сделать так:


List<Integer> plusOne = xs.stream().map(x -> x + 1).collect(Collectors.toList());

В Kotlin:


val plusOne = xs.map { it + 1 }

При этом быдлокодеры люди прекрасно в Java делают collect на каждый чих (на прошлом Joker про это был один из докладов) и никакая IDE инспекция им об этом не говорит.


Котлин форсит использование it, что приводит к нечитаемому коду. Что-нибудь типа seq.map { it -> foo(it, 1); }.map { it -> bar(it, 2); }.filter { it -> it.getBaz() > 0; }. Что это вообще было?

Действительно, что это было такое? Если уж использовать it, то не надо стрелочек тогда (рак из точек с запятой в конце каждой строки тоже можно убрать). Надо так:


seq.map { foo(it, 1) }.map { bar(it, 2) }.filter { it.getBaz() > 0 }

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


Цепочки вроде ?.let { foo(it); }?.let { bar(it); } — это вообще ад и должны быть запрещены в декларации о правах человека. И это считается идиоматично, Карл. В отличие от нормального if. Читать такой код невозможно.

Да, с if наверно читаемее:


arg?.let { foo(it) }?.let { bar(it) }

будет равносильно


if (arg != null) {
    val foo = foo(arg)
    if (foo != null) {
        bar(foo)
    } else null
} else null

От интеропа с джавой кровь идёт из глаз. А тут всякие JvmStatic и JvmName, и код превращается в цирк с конями.

Тут соглашусь, использование Kotlin из Java достаточно не удобно просто потому, что очень многих фич Kotlin нет, вот и приходится лишние приседания делать. BTW, обычно Java библиотеки используются из Kotlin, а не в обе стороны. Очень странен проект, где оба языка будут на равных правах присутствовать.


А как вам такое: в Kotlin нет checked exceptions. А в Java-реальности они есть. Отряд специального назначения «Боевые протезы» имеет честь представить новый самоходный костыль @Throws:

Да, в Kotlin выпилили то, что даже в мире Java уже считают антипаттерном (checked exceptions). Для interop сделали аннотацию. В чём проблема?


Автоматические геттеры/сеттеры с добавлением английского слова get и первой буквой проперти в большом регистре (видимо, в локали ENGLISH? Ведь регистр букв системно-зависим) — это страшно.

Чем страшно-то? Геттеры/сеттеры генерируются согласно устоявшимся в Java мире конвенциям.


Экстеншн-методы загрязняют публичный интерфейс такими вещами, о которых автор и подумать боялся.

Экстеншн-методы не загрязняют никаких интерфейсов. По факту это просто статические хэлпер методы, которые вызываются через точку.


Библиотека местами не продумана. Например, reduce.

reduce так работает во всех языках, где он есть. Я верно понял, что и в Haskell (foldr1, foldl1), и в ruby, и в python они не продуманы, если верить автору?


Давайте ещё навалим про библиотеку. Нафига в стандартную библиотеку языка, который поддерживает дата-классы, включили пары?

Во-первых для mapOf(...), да и вообще, для тех же целей, для которых в Java есть Map.Entry


Очень странный момент — возможность не указывать возвращаемый тип метода (особенно публичного).

Тип выводится только для методов, которые объявлены как выражение. Если метод абстрактный и тип не указан (в публичном интерфейсе, например), то он Unit по умолчанию. Если нужен не Unit — придётся в интерфейсе явно указать, так что проблема надуманная, как мне кажется. BTW, конвенции по коду в языках с выведением типов (Haskell, Scala, Kotlin и т.д.) требуют указывать их для публичного API


Резюмирую: всё как-то мимо с критикой в статье. Похоже автор плохо разобрался в объекте своей критики.

Information

Rating
Does not participate
Registered
Activity